本文为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): 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
独有的配置项,配置项类型基本就是两种,一种是属性,一种是钩子函数,插件的主要逻辑都在钩子函数里,Rollup
和Vite
提供了构建和编译时各个时机的钩子,插件可以根据功能选择对应的钩子。
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 ? `${partComponents.join('\n')}` : ''
})
return {
imports,
components,
source,
}
}
以前面的为例,source
为:
xxx
```vue
import BasicExample from '../example/Responsive.vue'
```
xxx
匹配到vueRE
,p1
为:
import BasicExample from '../example/Responsive.vue'
使用importRE
正则匹配后可以得到partImports
数组:
[
`import BasicExample from '../example/Responsive.vue'`
]
遍历这个数组,然后解析出component
为BasicExample
,将导入语句及组件名称收集起来,然后拼接模板字符串为:
最后这个模板字符串会替换掉source
内vueRE
匹配到的内容。
代码高亮
让我们继续回到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 = ''
}
if (lang && hljs.getLanguage(lang)) {
return (
'' +
link +
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
'
'
)
}
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, 'process.env')
}
调用render
方法将markdown
编译成html
,然后调用了htmlWrapper
方法:
// varlet-markdown-vite-plugin/index.js
function htmlWrapper(html) {
const hGroup = html.replace(/ (fragment.includes('${fragment}