本文为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
独有的配置项,配置项类型根本就是两种,一种是属性,一种是钩子函数,插件的次要逻辑都在钩子函数里,Rollup
和Vite
提供了构建和编译时各个机会的钩子,插件能够依据性能抉择对应的钩子。
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
匹配到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.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
标签之前的内容都用类名为card
的div
包裹起来,目标是为了在页面上显示一个个块的成果:
最初一行是给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"><!-- 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.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
的插件就介绍到这里,咱们下一篇再见,拜拜~