本文为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.tsimport 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.jsfunction 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.jsfunction markdownToVue(source, options) {    const { source: vueSource, imports, components } = extractComponents(source)    // ...}

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

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

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

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

// varlet-markdown-vite-plugin/index.jsfunction 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```vueimport 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.jsconst 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.jsconst 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.jsfunction 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.jsfunction 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.jsfunction markdownToVue(source, options) {    // ...    templateString = injectCodeExample(templateString)}

又调用了injectCodeExample办法:

// varlet-markdown-vite-plugin/index.jsfunction 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.jsfunction 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.jsfunction 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的插件就介绍到这里,咱们下一篇再见,拜拜~