目录
背景
思考过程
word转markdown
用node的express框架搭建服务器
展示md文件到页面上
提取目录
引入element树
纯前端展示
完整目录结构
当前所做项目(vue-cli+element)有一个需求,做一个帮助文档,把word文档在页面中展示出来,要求能目录跳转。
拿到这个需求,感觉这个难点在于目录跳转,word就算读取也全是字,无法识别哪里是目录,然后去提取目录和做定位。年前做项目时,因为项目中公共组件比较多,就想写个使用说明,专门去学过markdown的语法,在markdown中,1个到6个#号分别对应html标签中h1-h6,如果能把word转成markdown,不就可以根据#提取出一个目录了吗?所以要做的第一部就是把word转成markdown。
word转markdown的首先想的是找找有没在线工具,不用下载直接用还是比较舒服的。在网上倒是找到了一个,但是说实话,效果不太好。之前玩游戏要自己做一个筛选页面,因为excel看着实在太费劲了,需要把ecexl表格转JSON数据,然后再处理成组件需要的格式,那会也是找了好多在线的都不好用,最后用的是一个excel自带的插件,名字叫excel to json,还是非常好用的。对了,那个筛选页面我扔到码云上了,已发布到网上,点击这里查看,上面那个筛选组件(我叫选项卡)是我手写的,有需要的可以联系我。所以最后考虑word有没自带的插件能把它转成一个md文件,最后还真找到了一个插件 Writage 转md文件效果特别好,会把word文档里的图片提取到一个文件夹里,打开md文件,所有图片均能正常显示,15000+字的文档只有一处错误,手动改了。Writage 安装好之后,最上面就能看到。
安装好之后点文件,另存为,选择存放位置,选择格式,这里格式一定要选md。
然后你就可以在之前选择的位置找到它的md格式版,同时还会有一个目录,用来存放文档中所有涉及到的图片,至于help.js是干什么的后面会解释。
到这里有两种选择,一种是md文件和media文件夹都放在服务器上,通过请求后端接口来展示,另一种是都放在前端,纯前端展示。这里先说第一种,都放在服务器上,通过接口请求md内容,然后再做展示。
这里的代码啥意思就不说了,直接上代码
// express-static得用npm下载 npm i express-static --save
const express = require("express");
const expressStatic = require("express-static");
const fs = require('fs');
var server = express();
server.listen(5566);
console.log('服务器启动成功地址是: localhost:5566');
// 配置请求头
server.all('*', function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*")
res.header("Access-Control-Allow-Headers", "X-Requested-With")
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")
res.header("X-Powered-By", ' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8")
next()
});
// 读取help.md里的内容并返回给前端
server.use("/getHelp", function (req, res) {
// 同步读取文件
let helpData = fs.readFileSync("./document/help.md");
res.send(helpData);
});
// 静态文件目录
server.use(expressStatic("./document"));
终端启动服务器,后面是目录结构
后端服务写好了,现在让我们来写前端代码,因为是要做demo,所以直接在app.vue里写,这里用的是axios(npm i axios -S)进行的请求。
mehtods里
getHelp() {
this.$axios
.get("http://localhost:5566/getHelp")
.then((res) => {
console.log(res.data);
})
.catch((err) => {
console.log(err);
});
},
created里调用这个方法,我们就拿到了help点md里的内容。接下来考虑怎么把md格式的内容展示到页面上。
把md文件展示到页面上我们需要借助一个插件 marked.js 这个插件可以把md格式转换成html,我们就可以直接通过插值表达式或者v-html去渲染到页面上了,用法也很简单
npm i marked --save 然后再引入使用就可以了,我这里是在main里引入,然后绑定到全局了
import marked from "marked";
Vue.prototype.$marked = marked;
对getHelp方法进行改造
getHelp() {
this.$axios
.get("http://localhost:5566/getHelp")
.then((res) => {
// 原始md文件内容
this.helpData = res.data;
// 转换后的html
this.help = this.$marked(res.data);
})
.catch((err) => {
console.log(err);
});
},
然后我们之间在页面上去渲染help属性就好了。这里有个坑是图片的,因为用Writage转md后,所有图片的引入链接全是 media/943f402e73aecc2c08ffaeb88de41720.png ,需要手动全局替换目录地址,可以另外打开一个页面能成功看到图片,然后再做全局修改,这里我是这么做的。
接下来就是提取目录,然后目录跟页面内容一一对应起来。说实话,刚开始到这确实没有思路,直到看到了md转换成html后的内容,发现会自动给每个h标签添加一个id,其id值为标题内容,但是稍微有点坑,坑后面再说。这样的话,我们可以提取出来一个目录结构,然后通过锚点(创建一个a标签,href值为#标题名,点击这个a标签)跳转到对应的部分。这个目录我用的是element的树结构,所以先从原始md文件内容提取出来一个符合其树结构的数据格式的目录吧。
我这里卡的比较久,可能是因为js基础不够扎实。因为md中,1个#到6个#是一一对应的,所以并且是按顺序的,所以我们一定可以通过代码来生成我们需要的目录。这里直接放代码(注释全写代码里了),你们如果有其他更好的方式可以分享出来,让我学习学习。
// 获取目录树
getTitle() {
// 提取出所有#和后面标题,放到一个数组中
let titleList = this.helpData.match(/#+.+/g);
// 把每个标题出现次数统计出来,必须保证标题唯一,全部使用1.1.1.1这是一个好办法
// 因为marked会把#后面的内容添加为id,如果标题重复,锚点跳转会只跳第一个
let count = titleList.reduce((obj, name) => {
if (name in obj) {
obj[name]++;
} else {
obj[name] = 1;
}
return obj;
}, {});
// 删除只出现了一次的
for (let item in count) {
if (count[item] == 1) {
delete count[item];
}
}
// console.log(count);
// 能保证标题唯一的话,可以不用加这行代码
titleList = titleList.map((item, index) => (item = index + "$" + item));
// console.log(titleList);
// 最后一个各级别的标题
let nowLabel = [];
// 生成的标题
let title = [];
// 当前对象
let cur;
for (let item of titleList) {
// 当前目录级别
let level = item.match(/#+/)[0].length;
// 其父级目录文字
let label = nowLabel[level - 2];
// 更新当前目录文字
nowLabel[level - 1] = item;
// console.log(item, nowLabel, "===============");
if (level == 1) {
cur = {
level: level,
label: item,
value: item,
children: [],
};
title.push(cur);
} else {
let obj = this.getObj(label, cur, level);
// console.log(obj);
// 把当前目录添加到其父级目录对象的children里
obj.children.push({
level: level,
label: item,
value: item,
children: [],
});
}
}
// 这里是去除标题里的#和空格(如果上面没添加$和索引的话)
this.titleData = this.removeTitleData(title);
},
// 该方法根据传入参数,返回其父级目录对象
// 当前目录父级目录,当前一级目录对象,当前目录级别
getObj(label, cur, level) {
if (level - 1 > cur.level) {
for (let item of cur.children) {
let res = this.getObj(label, item, level);
if (res) {
return res;
} else {
continue;
}
}
} else {
if (cur.label == label) {
return cur;
} else {
return false;
}
}
},
// 去除目录中无用的部分
removeTitleData(title) {
return title.map((item) => {
// 这里因为标题里添加$和索引了,如果没有添加,可以不需要这行
item.label = item.label.split("$")[1];
item.label = item.label.replace(/#+\s+/, "");
if (item.children.length > 0) {
item.children = this.removeTitleData(item.children);
}
return item;
});
},
gethelp方法里调用方法获取目录
getHelp() {
this.$axios
.get("http://localhost:5566/getHelp")
.then((res) => {
// 原始md文件内容
this.helpData = res.data;
// 转换后的html
this.help = this.$marked(res.data);
// 获取目录
this.getTitle();
})
.catch((err) => {
console.log(err);
});
},
data里是这样的
data() {
return {
// md原始数据
helpData: "",
// md转成html标签后的数据
help: "",
// 目录数据
titleData: [],
// 目录树配置
defaultProps: {
children: "children",
label: "label",
},
// 是否使用本地数据
isLocalFile: true,
};
},
接下来引用element树,做渲染
树节点点击toTitle方法
// 目录跳转的方法
toTitle(data) {
// console.log(data);
// 新建一个a标签用来做跳转
let a = document.createElement("a");
// 这里就是marked自动添加id的坑 1.2.1.2会变成 1212 ,把点会去掉
// 第二个坑是会把空格转成 "-"
console.log(data.label.replace(" ", "-").replace(/[.]{1}/g, ""));
// 设置锚点,这里直接用label是因为我重新手动设置id为内容了
a.href = "#" + decodeURI(data.label);
// 触发a的点击事件进行跳转
a.click();
},
重新手动设置id的方法
// 手动修改所有h标签的id
resetId() {
for (let i = 1; i < 7; i++) {
let h = document.getElementsByTagName("h" + i);
for (let j = 0; j < h.length; j++) {
h[j].setAttribute("id", h[j].innerHTML);
}
// console.log(h);
}
},
getHelp方法里调用
getHelp() {
this.$axios
.get("http://localhost:5566/getHelp")
.then((res) => {
this.helpData = res.data;
this.help = this.$marked(res.data);
this.getTitle();
// 必须在页面渲染完后再执行这个方法,不然会获取不到
this.$nextTick(() => {
this.resetId();
});
})
.catch((err) => {
console.log(err);
});
},
还有其他可以优化的地方,比如:树结构标题超长可以换行或者省略号展示,加载一次后把目录数据存在本地等,这里就不赘述了。接下来我们来讨论另一个,把md文档和对应图片放在本地,纯前端做展示。
纯前端展示其实跟接口返回是一样的,这里我们得把md数据放在一个js文件中导出,然后在app中引入,这里图片地址同样得改,需要注意的是,media文件夹必须放在static里才能正常展示,其他地方没试。
// 修改完图片地址后,直接把md文件的内容全粘到反引号里就可以了
let data = ` `
export default data;
app.vue 完整代码如下,我是把使用本地资源还是接口数据写了一个变量做切换,电脑还是app显示也做了一个变量(就是把目录给隐藏了),app目录得用其他方式进行展示。目录文字超长这里我做了两种处理,折行显示或超出部分显示省略号。
最终效果如下:
别问我为啥还有个2,因为本身使用的文档保密,所以废了好大劲重新找了一个。如果本文章帮助到你了,记得回来加个关注哦!博主前端菜鸟一个,目前只会vue,如果有实现不了的需求,可以在下面进行评论,我有时间了会尝试去实现。