共计 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
独有的配置项,配置项类型根本就是两种,一种是属性,一种是钩子函数,插件的次要逻辑都在钩子函数里,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 ? `<div class="varlet-component-preview">${partComponents.join('\n')}</div>` : ''
})
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
,将导入语句及组件名称收集起来,而后拼接模板字符串为:
<div class="varlet-component-preview">
<basic-example />
</div>
最初这个模板字符串会替换掉 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 = '<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
标签之前的内容都用类名为 card
的div
包裹起来,目标是为了在页面上显示一个个块的成果:
最初一行是给 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"><!-- playground-ignore --></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
的插件就介绍到这里,咱们下一篇再见,拜拜~