Vue3项目如何通过自定义的Vite Plugin实现路由加载md文件

722 阅读4分钟

上一篇简单的对Vite插件进行了实现,这篇文章一起去处理对md文件的处理

上一篇:从Naive UI项目上学习Vite Plugin的使用

项目准备

可以直接拉取这里的代码 - 代码仓库

好了,如果你是选择了doc-1.0-init分支 我们需要对项目的路由、路由文件进行处理,并且安装好对应的依赖文件。

pages

src下新建一个pages页面,里面存放一个md文件,供vue-router加载,内容我们摘抄Naive UI随便一个组件库的文档说明,比如他的Scrollbar,可以直接从这里进去复制。

router

// routes.js 存放路由数据
const commonRoutes = [
  {
    path: "/md",
    name: "md",
    component: () => import("../pages/index.md"),
  },
];

const routes = [...commonRoutes];

export default routes;

// index.js  存放路由实例,已经路由相关的处理,暴露一个setupRouter
// 完整代码
import { createRouter, createWebHistory } from "vue-router";
import routes from "./routes";

const router = createRouter({
  history: createWebHistory(),
  routes,
});

const setupRouter = (app) => {
  app.use(router);
};

export default setupRouter;

main.js和app.vue

<!-- app.vue直接引入router-view标签即可,删除其他 -->
<template>
  <router-view />
</template>
// main 文件引入路由的setupRouter函数,然后将app实例传入
import { createApp } from "vue";
import App from "./App.vue";
import setupRouter from "./router";

const app = createApp(App);

setupRouter(app);

app.mount("#app");

解析md文件

安装插件

  • highlight.js 实现相关的代码高亮,本文可能用处不大,但是顺便安装了
  • marked md文件解析工具
  • naive-ui 因为是参照naive,所以直接用它的组件进行自定义
  • unplugin-vue-components 自动按需加载naive组件
pnpm i highlight.js -S
pnpm i marked naive-ui unplugin-vue-components -D

按照naive官网的说明,将naive进行按需配置,修改我们的./plugins/index.js,在creatMyPlugin函数最终返回的数组中,加入

import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'

return [
   myPlugin,
   vue({
     include: [/\.vue$/, /\.md$/]
   }),
   Components({
     // 默认是没有对md文件进行处理,所以需要加上,上面的同理
     include: [/\.vue$/, /\.md$/],
     resolvers: [NaiveUiResolver()],
   }),
   Inspect(),
 ];

自定义marked的render

我们需要重新定义渲染标签,来实现样式上的统一,并且考虑到项目文档中会有代码展示,所以要配合highlight进行处理。

首先考虑的是,我们需要分析自定义哪些标签,对于一个组件库的文档网站,table是必须的,然后就是各种标题heading,对于组件文档中,还会有代码文本,所以我们这里定义为codespanblockquote,然后就是其他常规的listhrparagraphlinklist等等。我随便找了个Naive UI的组件文档,解析后如下: image.png 其中有个比较特殊的一个点在于第五条数据,type: 'code' , lang: 'demo',这是我们自定义的语言块,需要单独对其中的内容去处理,并解析同目录下的文件,收集成组件的方式进行加载。为了避免报错,无法进行下一步,我们可以删除```demo ```这一部分的内容,或者随便添加一段代码,测试代码高亮,比如添加这一段 ``` js const a = 1 ```。 然后开始我们的自定义改造 新建文件./plugins/utils/md-renderer.js,详细代码如下(这里直接取Naive UI的自定义render):

const hljs = require('highlight.js')
const { marked } = require('marked')

function createRenderer (wrapCodeWithCard = true) {
  const renderer = new marked.Renderer()
  const overrides = {
    table (header, body) {
      if (body) body = '<tbody>' + body + '</tbody>'
      return (
        '<div class="md-table-wrapper"><n-table single-column class="md-table">\n' +
        '<thead>\n' +
        header +
        '</thead>\n' +
        body +
        '</n-table>\n' +
        '</div>'
      )
    },

    tablerow (content) {
      return '<tr>\n' + content + '</tr>\n'
    },

    tablecell (content, flags) {
      const type = flags.header ? 'th' : 'td'
      const tag = flags.align
        ? '<' + type + ' align="' + flags.align + '">'
        : '<' + type + '>'
      return tag + content + '</' + type + '>\n'
    },

    code: (code, language) => {
      //这里需要解释一下,code是会识别md文件中的 ```language ```
      if (language.startsWith('__')) {
        language = language.replace('__', '')
      }
      const isLanguageValid = !!(language && hljs.getLanguage(language))
      if (!isLanguageValid) {
        throw new Error(
          `MdRendererError: ${language} is not valid for code - ${code}`
        )
      }
      const highlighted = hljs.highlight(code, { language }).value
      const content = `<n-code><pre v-pre>${highlighted}</pre></n-code>`
      return wrapCodeWithCard
        ? `<n-card embedded :bordered="false" class="md-card" content-style="padding: 0;">
            <n-scrollbar x-scrollable content-style="padding: 16px;">
              ${content}
            </n-scrollbar>
          </n-card>`
        : content
    },
    heading: (text, level) => {
      const id = text.replace(/ /g, '-')
      return `<n-h${level} id="${id}">${text}</n-h${level}>`
    },
    blockquote: (quote) => {
      return `<n-blockquote>${quote}</n-blockquote>`
    },
    hr: () => '<n-hr />',
    paragraph: (text) => {
      return `<n-p>${text}</n-p>`
    },
    link (href, title, text) {
      if (/^(http:|https:)/.test(href)) {
        return `<n-a href="${href}" target="_blank">${text}</n-a>`
      }
      return `<router-link to="${href}" #="{ navigate, href }" custom><n-a :href="href" @click="navigate">${text}</n-a></router-link>`
    },
    list (body, ordered, start) {
      const type = ordered ? 'n-ol' : 'n-ul'
      const startatt = ordered && start !== 1 ? ' start="' + start + '"' : ''
      return `<${type}${startatt}>\n` + body + `</${type}>\n`
    },
    listitem (text) {
      return `<n-li>${text}</n-li>`
    },
    codespan (code) {
      return `<n-text code>${code}</n-text>`
    },
    strong (text) {
      return `<n-text strong>${text}</n-text>`
    },
    checkbox (checked) {
      return `<n-checkbox :checked="${checked}" style="vertical-align: -2px; margin-right: 8px;" />`
    }
  }

  Object.keys(overrides).forEach((key) => {
    renderer[key] = overrides[key]
  })
  return renderer
}

module.exports = createRenderer

marked工具解析文本

./plugins目录下新建文件mdToVue.js。这个文件主要就是用来解析md的格式,并输出合规的vue格式的代码,生成对应的templatescriptstyle

首先在这里我们先要理解一个很重要的点。我们现在的工作,是一种约定性的解析。 比如```demo ``` ## API

image.png 这就是一种约定性的代码块,后续的工作里,我们就去寻找解析后type='code'并且 lang='code'的代码,获取里面的内容,并根据换行符去将所有xxx.vue处理成数组,然后处理成xxx.demo.vue格式,并匹配同路径下的同名文件,然后进一步解析。如果我们将其中任何一步更改,就会导致文档解析的失败。好了,废话不多说,开干~

首先将请求文件的文本进行词法分析(marked.lexer),它会将文本内容解析成一个对象数组,对象包含type、raw、depth、text,我们需要借助这种处理好的数组进行下一步处理。比如下面的代码:

#  heading

就会被解析成这样:

[{
  type: "heading",
  raw: "  # heading\n\n",
  depth: 1,
  text: "heading",
  tokens: [
    {
      type: "text",
      raw: "heading",
      text: "heading"
    }
  ]
}]

获取了分析后的数据,我们就大有可为了。比如获取所有的三级标题,获取demo代码块的内容,并处理成组件等等。

md的内容分析后,紧接着进行数据解析(marked.parser),将数据处理成html元素,代码如下:

const docMainTemplate = marked.parser(tokens, {
  gfm: true, // 启用github风格的markdown
  renderer: mdRenderer,
});

放在<template></template>标签中,然后返回出去,就大功告成,我们就可以在我们的页面里,看到和Naive UI文档很像的页面了

image.png 你也可以通过Inspect来查看解析后的代码:

image.png

最后贴上mdToVue的完整代码:

import { marked } from "marked";
import createRenderer from "./utils/md-renderer";
const mdRenderer = createRenderer();

const mdToVue = (code) => {
  const tokens = marked.lexer(code);
  const docMainTemplate = marked.parser(tokens, {
    gfm: true,
    renderer: mdRenderer,
  });
  const docTemplate = `<template>
  ${docMainTemplate}
</template>
`;
  return docTemplate;
};

export default mdToVue;

结语

这篇简单的md解析就结束了,下一篇,就开始完整的实现Naive UI组件库文档的搭建和简单的组件编写。先写这两篇文章的目的,主要是为了大家能先有个实现的概念,避免直接一波输出,晕头转向的看不下去。好了,不废话了,好好学习,天天向上,每天进步一点点,一年后我也是大佬~