vue+element实现word文档(转成markdown了)带目录预览

 

目录

背景

思考过程

word转markdown

用node的express框架搭建服务器

展示md文件到页面上

提取目录

引入element树

纯前端展示

完整目录结构


背景

当前所做项目(vue-cli+element)有一个需求,做一个帮助文档,把word文档在页面中展示出来,要求能目录跳转。

思考过程

拿到这个需求,感觉这个难点在于目录跳转,word就算读取也全是字,无法识别哪里是目录,然后去提取目录和做定位。年前做项目时,因为项目中公共组件比较多,就想写个使用说明,专门去学过markdown的语法,在markdown中,1个到6个#号分别对应html标签中h1-h6,如果能把word转成markdown,不就可以根据#提取出一个目录了吗?所以要做的第一部就是把word转成markdown。

word转markdown

word转markdown的首先想的是找找有没在线工具,不用下载直接用还是比较舒服的。在网上倒是找到了一个,但是说实话,效果不太好。之前玩游戏要自己做一个筛选页面,因为excel看着实在太费劲了,需要把ecexl表格转JSON数据,然后再处理成组件需要的格式,那会也是找了好多在线的都不好用,最后用的是一个excel自带的插件,名字叫excel to json,还是非常好用的。对了,那个筛选页面我扔到码云上了,已发布到网上,点击这里查看,上面那个筛选组件(我叫选项卡)是我手写的,有需要的可以联系我。所以最后考虑word有没自带的插件能把它转成一个md文件,最后还真找到了一个插件 Writage 转md文件效果特别好,会把word文档里的图片提取到一个文件夹里,打开md文件,所有图片均能正常显示,15000+字的文档只有一处错误,手动改了。Writage 安装好之后,最上面就能看到。

安装好之后点文件,另存为,选择存放位置,选择格式,这里格式一定要选md。

vue+element实现word文档(转成markdown了)带目录预览_第1张图片

然后你就可以在之前选择的位置找到它的md格式版,同时还会有一个目录,用来存放文档中所有涉及到的图片,至于help.js是干什么的后面会解释。

 到这里有两种选择,一种是md文件和media文件夹都放在服务器上,通过请求后端接口来展示,另一种是都放在前端,纯前端展示。这里先说第一种,都放在服务器上,通过接口请求md内容,然后再做展示。

用node的express框架搭建服务器

这里的代码啥意思就不说了,直接上代码

// 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"));



终端启动服务器,后面是目录结构

vue+element实现word文档(转成markdown了)带目录预览_第2张图片

后端服务写好了,现在让我们来写前端代码,因为是要做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文件到页面上

把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 ,需要手动全局替换目录地址,可以另外打开一个页面能成功看到图片,然后再做全局修改,这里我是这么做的。

vue+element实现word文档(转成markdown了)带目录预览_第3张图片

vue+element实现word文档(转成markdown了)带目录预览_第4张图片

接下来就是提取目录,然后目录跟页面内容一一对应起来。说实话,刚开始到这确实没有思路,直到看到了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树,做渲染

引入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目录得用其他方式进行展示。目录文字超长这里我做了两种处理,折行显示或超出部分显示省略号。






最终效果如下:

vue+element实现word文档(转成markdown了)带目录预览_第5张图片

完整目录结构

别问我为啥还有个2,因为本身使用的文档保密,所以废了好大劲重新找了一个。如果本文章帮助到你了,记得回来加个关注哦!博主前端菜鸟一个,目前只会vue,如果有实现不了的需求,可以在下面进行评论,我有时间了会尝试去实现。

vue+element实现word文档(转成markdown了)带目录预览_第6张图片

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(需求实现,vue.js,node.js,前端)