关于前端:Markdown文件居然也可以直接作为Vue路由组件

35次阅读

共计 7170 个字符,预计需要花费 18 分钟才能阅读完成。

本文为 Varlet 组件库源码主题浏览系列第五篇,读完本文你能够理解到如何通过编写一个 Vite 插件来反对应用 md 文件间接作为路由组件。

之前 [文档站点的搭建]() 里咱们介绍了路由的动静生成逻辑,其中说到了文档是应用 Markdown 格局编写的,并且还间接在路由文件里应用 md 文件作为路由组件:

路由就是门路到组件的映射,这个组件显然指的是 Vue 组件,Vue组件是一个蕴含特定选项的 JavaScript 对象,咱们平时开发个别应用的是 Vue 单文件,单文件最终也会被编译成选项对象,这个工作是 @vitejs/plugin-vue 做的,显然这个插件并不能解决 Markdown 文件,那么最终也就无奈生成正确的 Vue 组件。

解决办法就是编写一个 Vite 插件,指定在 @vitejs/plugin-vue 插件之前调用,将 .md 文件的内容转换为 Vue 单文件的格局,而后配置 @vitejs/plugin-vue 插件,让它顺便也解决一下扩大名为 .md 的文件,因为曾经转换成 Vue 单文件的语法格局了,所以它能够失常解决,接下来从源码角度来具体理解一下。

Vite 配置

之前的文章里具体介绍了启动服务时的 Vite 配置,这里只看一下波及到的插件局部:

// varlet-cli/src/config/vite.config.ts
import vue from '@vitejs/plugin-vue'
import md from '@varlet/markdown-vite-plugin'

export function getDevConfig(varletConfig: Record<string, any>): InlineConfig {
    return {
        plugins: [
            vue({include: [/\.vue$/, /\.md$/],// vue 插件默认只解决.vue 文件,通过该参数配置让其也解决一下.md 文件
            }),
            md({style: get(varletConfig, 'highlight.style') }),// 应用 md 文件转换插件,应用插件时能够传入参数
        ]
    }
}

markdown-vite-plugin 插件

插件代码在 varlet-markdown-vite-plugin 目录,一个 Vite 插件就是一个函数,接管应用时传入的参数,最终返回一个对象。Vite插件扩大了 Rollup 的接口,并且带有一些 Vite 独有的配置项,配置项类型根本就是两种,一种是属性,一种是钩子函数,插件的次要逻辑都在钩子函数里,RollupVite提供了构建和编译时各个机会的钩子,插件能够依据性能抉择对应的钩子。

Vite插件文档:插件 API。

Rollup插件文档:plugin-development。

// varlet-markdown-vite-plugin/index.js
function VarletMarkdownVitePlugin(options) {
  return {
    name: 'varlet-markdown-vite-plugin',// 插件名称
    enforce: 'pre',// 插件调用程序
    // Rollup 钩子,转换文件内容
    transform(source, id) {if (!/\.md$/.test(id)) {return}
      try {return markdownToVue(source, options)
      } catch (e) {this.error(e)
        return ''
      }
    },
    // Vite 钩子,用于热更新
    async handleHotUpdate(ctx) {if (!/\.md$/.test(ctx.file)) return
      const readSource = ctx.read
      ctx.read = async function () {return markdownToVue(await readSource(), options)
      }
    },
  }
}
module.exports = VarletMarkdownVitePlugin

以上就是这个插件的函数,返回了一个对象,name属性为插件的名称,必填,用于信息和谬误输入时的提醒;enforce用于指定钩子的调用程序:

vue插件没有指定,所以 md 插件会在其之前调用,保障到它这里 .md 文件的内容曾经转换结束。

接下来配置了两个钩子函数,咱们具体来看。

md 文件内容转换

transform 是 Rollup 提供的构建阶段的钩子,能够在这个钩子内转换文件的内容,先判断文件后缀是否是 .md,不是的话就不进行解决,是的话调用了markdownToVue 办法:

// varlet-markdown-vite-plugin/index.js
function markdownToVue(source, options) {const { source: vueSource, imports, components} = extractComponents(source)
    // ...
}

反对在 md 文件中引入 Vue 组件

source即文件内容,进来先调用了 extractComponents 办法,这个办法是干嘛的呢,是用来反对在 md 文件里引入 Vue 组件的,比方布局组件中的 Row 组件的文档:

引入了 Responsive.vue 组件,最终在页面上的渲染成果如下:

晓得了它的作用后咱们再来看一下实现:

// varlet-markdown-vite-plugin/index.js
function extractComponents(source) {const componentRE = /import (.+) from ['"].+['"]/
  const importRE = /import .+ from ['"].+['"]/g
  const vueRE = /```vue((.|\r|\n)*?)```/g
  const imports = []
  const components = []
  
  // 替换 ```vue....``` 的内容
  source = source.replace(vueRE, (_, p1) => {
    // 解析出 import 语句列表
    const partImports = p1.match(importRE)
    const partComponents = partImports?.map((importer) => {
      // 去除换行符
      importer = importer.replace(/(\n|\r)/g, '')
      // 解析出导入的组件名
      const component = importer.replace(componentRE, '$1')
      // 收集导入语句及导入的组件
      !imports.includes(importer) && imports.push(importer)
      !components.includes(component) && components.push(component)
      // 返回应用组件的字符串
      return `<${kebabCase(component)} />`
    })
    return partComponents ? `<div class="varlet-component-preview">${partComponents.join('\n')}</div>` : ''
  })
  return {
    imports,
    components,
    source,
  }
}

以后面的为例,source为:

xxx

​```vue
import BasicExample from '../example/Responsive.vue'
​```

xxx

匹配到 vueREp1 为:

import BasicExample from '../example/Responsive.vue'

应用 importRE 正则匹配后能够失去 partImports 数组:

[`import BasicExample from '../example/Responsive.vue'`]

遍历这个数组,而后解析出 componentBasicExample,将导入语句及组件名称收集起来,而后拼接模板字符串为:

<div class="varlet-component-preview">
    <basic-example />
</div>

最初这个模板字符串会替换掉 sourcevueRE匹配到的内容。

代码高亮

让咱们持续回到 markdownToVue 办法:

// varlet-markdown-vite-plugin/index.js
const markdown = require('markdown-it')

function markdownToVue(source, options) {
    // ...
    const md = markdown({
        html: true,// 容许存在 html 标签,这是必要的,因为后面解决 Vue 组件最初会生成 html 标签
        typographer: true,// 容许替换一些特殊字符,https://github.com/markdown-it/markdown-it/blob/master/lib/rules_core/replacements.js
        highlight: (str, lang) => highlight(str, lang, options.style),// 代码高亮,str 为要高亮的代码,lang 为语言品种
    })
}

应用 markdown-it 解析 markdown,并且应用了highlight 属性自定义代码语法高亮:

// varlet-markdown-vite-plugin/index.js
const hljs = require('highlight.js')

function highlight(str, lang, style) {
  let link = ''

  if (style) {link = '<link class="hljs-style"rel="stylesheet"href="' + style + '"/>'}

  if (lang && hljs.getLanguage(lang)) {
    return (
      '<pre class="hljs"><code>' +
      link +
      hljs.highlight(str, { language: lang, ignoreIllegals: true}).value +
      '</code></pre>'
    )
  }

  return ''
}

代码高亮应用的是 highlight.js,最开始应用 md 插件时咱们传入了参数:

{style: get(varletConfig, 'highlight.style') }

这个用于设置 highlight.js 的主题,一个主题就是一个 css 文件,highlight.js内置了十分多的主题:

默认配置如下:

所以当指定了主题的话会创立一个 link 标签来加载对应的主题款式,否则就应用默认的,默认主题定义在 /site/pc/Layout.vue 组件内:

这么做的益处是能够应用 css 变量,当页面切换暗黑模式时代码主题也能够跟着变动。

解决生成的 html

持续看 markdownToVue 办法:

// varlet-markdown-vite-plugin/index.js
function markdownToVue(source, options) {
    // ...
    let templateString = htmlWrapper(md.render(vueSource))
      templateString = templateString.replace(/process.env/g, '<span>process.env</span>')
}

调用 render 办法将 markdown 编译成 html,而后调用了htmlWrapper 办法:

// varlet-markdown-vite-plugin/index.js
function htmlWrapper(html) {const hGroup = html.replace(/<h3/g, ':::<h3').replace(/<h2/g, ':::<h2').split(':::')
  const cardGroup = hGroup
    .map((fragment) => (fragment.includes('<h3') ? `<div class="card">${fragment}</div>` : fragment))
    .join('')
  return cardGroup.replace(/<code>/g, '<code v-pre>')
}

前两行做的事件就是把 h3 标签之后,h2标签之前的内容都用类名为 carddiv包裹起来,目标是为了在页面上显示一个个块的成果:

最初一行是给 code 标签增加了一个 v-pre 指令,这个指令用来跳过该元素及其所有子元素的编译,因为文档的代码示例难免会波及到 Vue 模板语法的示例,如果不跳过,间接就被编译了。

引入代码块组件

持续 markdownToVue 办法:

// varlet-markdown-vite-plugin/index.js
function markdownToVue(source, options) {
    // ...
    templateString = injectCodeExample(templateString)
}

又调用了 injectCodeExample 办法:

// varlet-markdown-vite-plugin/index.js
function injectCodeExample(source) {const codeRE = /(<pre class="hljs">(.|\r|\n)*?<\/pre>)/g

  return source.replace(codeRE, (str) => {
    const flags = [
      '// playground-ignore\n',
      '<span class="hljs-meta">#</span><span class="bash"> playground-ignore</span>\n',
      '<span class="hljs-comment">// playground-ignore</span>\n',
      '<span class="hljs-comment">/* playground-ignore */</span>\n',
      '<span class="hljs-comment">&lt;!-- playground-ignore --&gt;</span>\n',
    ]

    const attr = flags.some((flag) => str.includes(flag)) ? 'playground-ignore' : ''str = flags.reduce((str, flag) => str.replace(flag,''), str)

    // 引入 var-site-code-example 组件
    return `<var-site-code-example ${attr}>${str}</var-site-code-example>`
  })
}

Varlet提供了在线 playground 的性能:

能够间接从文档的代码块进行跳转:

但不是所有代码块都须要,比方:

所以就通过在文档上减少一个正文来注明疏忽:

injectCodeExample办法就会查看是否存在这个标记,存在的话就给 var-site-code-example 组件传递一个不显示这个跳转按钮的属性,var-site-code-example组件的门路为 /site/components/code-example/CodeExample.vue,用来提供代码块的开展收起、复制、跳转playground 的性能。

组装 Vue 单文件的格局

最初就是依照 Vue 单文件的格局进行拼接了:

// varlet-markdown-vite-plugin/index.js
function markdownToVue(source, options) {
    // ...
    return `
        <template>
            <div class="varlet-site-doc">${templateString}</div>
        </template>

        <script>
            ${imports.join('\n')}

            export default {
              components: {${components.join(',')}
              }
            }
        </script>
      `
}

把转换失去的 html 内容增加到 template 标签内,把解析出的组件导入语句增加到 script 标签内,并且进行注册,转换成这种格局后,后续 vue 插件就能够失常解决了。

热更新

除了 transform 钩子,还应用到了 handleHotUpdate 钩子,这个钩子是 Vite 提供的,用来执行自定义的热更新解决,这个钩子接管一个上下文对象:

file是发生变化的文件,read是读取这个文件内容的办法,varlet-markdown-vite-plugin插件重写了这个办法:

// varlet-markdown-vite-plugin/index.js
function VarletMarkdownVitePlugin(options) {
  return {async handleHotUpdate(ctx) {if (!/\.md$/.test(ctx.file)) return

      const readSource = ctx.read
      ctx.read = async function () {return markdownToVue(await readSource(), options)
      }
    },
  }
}

目标和后面一样,就是把 markdown 语法转换成 Vue 单文件语法,vue插件也应用了这个钩子和 read 办法:

同样因为这个插件是在 vue 插件之前调用的,所以到了 vue 插件应用的就是被转换的 read 办法,就能在热更新时顺利解决 .md 文件。

解决 markdown 的插件就介绍到这里,咱们下一篇再见,拜拜~

正文完
 0