当前位置: 首页 > Web前端 > JavaScript

Markdown文件也可以直接用作Vue路由组件?

时间:2023-03-26 22:54:28 JavaScript

本文是Varlet组件库源码主题阅读系列的第五篇。看完本文,你可以学习如何编写一个Vite插件,支持直接使用md文件作为路由组件。在[BuildingDocumentationSite]()中,我们介绍了路由的动态生成逻辑。里面提到文档是用Markdown格式写的,在路由文件中直接使用md文件作为路由组件:routing就是组件的路由。映射,这个组件显然是指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.tsimportvuefrom'@vitejs/plugin-vue'importmdfrom'@varlet/markdown-vite-plugin'exportfunctiongetDevConfig(varletConfig:Record):InlineConfig{return{plugins:[vue({include:[/\.vue$/,/\.md$/],//仅限vue插件默认处理.vue文件,通过这个参数配置,也可以处理.md文件}),md({style:get(varletConfig,'highlight.style')}),//使用md文件转换插件-in,使用插件时可以传入参数]}}markdown-vite-plugin插件代码在varlet-markdown-vite-plugin目录下,Vite插件是接收参数的函数在使用过程中传入,最终返回一个对象。Vite插件扩展了Rollup的接口,并且有一些Vite特有的配置项。配置项基本上有两种,一种是属性,一种是钩子函数。插件的主要逻辑在钩子函数中。Rollup和Vite在构建和编译的各个时刻都提供了hook,插件可以根据功能选择对应的hook。Vite插件文档:插件API。汇总插件文档:插件开发。//varlet-markdown-vite-plugin/index.jsfunctionVarletMarkdownVitePlugin(options){return{name:'varlet-markdown-vite-plugin',//插件名称enforce:'pre',//插件调用顺序//Rollup钩子,转换文件内容transform(source,id){if(!/\.md$/.test(id)){return}try{returnmarkdownToVue(source,options)}catch(e){this.error(e)return''}},//Vitehook热更新asynchandleHotUpdate(ctx){if(!/\.md$/.test(ctx.file))returnconstreadSource=ctx.readctx.read=asyncfunction(){returnmarkdownToVue(awaitreadSource(),options)}},}}module.exports=VarletMarkdownVitePlugin上面是这个插件的函数,返回一个对象,name属性是插件的名字,就是必需的,用于错误输出的信息和提示;enforce用于指定hooks的调用顺序:没有指定vue插件,所以md插件会在它之前被调用,以确保.md文件的内容已经转换为它。接下来配置了两个钩子函数,我们详细看一下。md文件内容转换transform是Rollup在构建阶段提供的一个hook。您可以在此挂钩中转换文件的内容。首先判断文件后缀是否为.md。如果不是,请不要处理它。如果是,调用markdownToVue方法://varlet-markdown-vite-plugin/index.jsfunctionmarkdownToVue(source,options){const{source:vueSource,imports,components}=extractComponents(source)//...}supports导入md文件中的vue组件源,即文件内容,进来先调用extractComponents方法。这个方法是做什么用的?用于支持在md文件中引入Vue组件,比如布局组件中Row组件的文档:引入了Responsive.vue组件,最终在页面上的渲染效果如下:知道了它的功能,让我们来看看实现://varlet-markdown-vite-plugin/index.jsfunctionextractComponents(source){constcomponentRE=/import(.+)from['"].+['"]/constimportRE=/import.+from['"].+['"]/gconstvueRE=/```vue((.|\r|\n)*?)```/gconstimports=[]constcomponents=[]//替换```vue....```的内容source=source.replace(vueRE,(_,p1)=>{//解析出导入语句列表constpartImports=p1.match(importRE)constpartComponents=partImports?.map((importer)=>{//移除换行符importer=importer.replace(/(\n|\r)/g,'')//解析导入的组件名称constcomponent=importer.replace(componentRE,'$1')//收集导入语句和导入的组件!imports.includes(importer)&&imports.push(importer)!components.includes(component)&&components.push(component)//使用组件返回字符串return`<${kebabCase(component)}/>`})返回partComponents?`${partComponents.join('\n')}

`:''})return{imports,components,source,}}拿前面的例子来说,来源是:xxx```vueimportBasicExamplefrom'../example/Responsive.vue'```xxx匹配vueRE,p1是:importBasicExamplefrom'../example/Responsive.vue'使用importRE正则后匹配,可以得到partImports数组:[`importBasicExamplefrom'../example/Responsive.vue'`]遍历这个数组,然后解析出组件为BasicExample,收集i??mport语句和组件名,然后拼接templatestringinto:
最后的templatestring将替换源中vueRE匹配的内容代码高亮让我们回到markdownToVue方法://varlet-markdown-vite-plugin/index.jsconstmarkdown=require('markdown-it')functionmarkdownToVue(source,options){//...constmd=markdown({html:true,//允许使用html标签,这是必须的,因为Vue组件之前会进行处理,最终生成html标签typographer:true,//允许替换一些特殊字符,https://github.com/markdown-it/markdown-it/blob/master/lib/rules_core/replacements.jshighlight:(str,lang)=>highlight(str,lang,options.style),//代码高亮,str是要highlighted代码,lang为语言类型})}使用markdown-it解析markdown,使用highlight属性自定义代码语法高亮://varlet-markdown-vite-plugin/index.jsconsthljs=require('highlight.js')functionhighlight(str,lang,style){letlink=''if(style){link=''}if(lang&&hljs.getLanguage(lang)){return(''+link+hljs.highlight(str,{语言:lang,忽略Illegals: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.jsfunctionmarkdownToVue(source,options){//...lettemplateString=htmlWrapper(md.render(vueSource))模板字符串=模板字符串。replace(/process.env/g,'process.env')}调用render方法将markdown编译成html,然后调用htmlWrapper方法://varlet-markdown-vite-plugin/指数。jsfunctionhtmlWrapper(html){consthGroup=html.replace(/

(fragment.includes('${fragment}

`:fragment)).join('')returncardGroup.replace(//g,'')}前两行是对h3标签之后h2之前的内容使用类名卡片tagdiv包裹的目的是为了在页面上显示每个block的效果:最后一行是在代码标签中添加v-pre指令,用于跳过元素及其所有子元素的编译,因为文档代码示例中难免会涉及到Vue模板语法的示例,如果不跳过,直接编译。引入代码块继续markdownToVue方法://varlet-markdown-vite-plugin/index.jsfunctionmarkdownToVue(source,options){//...templateString=injectCodeExample(templateString)}又调用了injectCodeExample方法://varlet-markdown-vite-plugin/index.jsfunctioninjectCodeExample(source){constcodeRE=/((.|\r|\n)*?<\/pre>)/greturnsource.replace复制代码(codeRE,(str)=>{constflags=['//playground-ignore\n','#playground-ignore\n','//playground-ignore\n','/*playground-ignore*/\n','\n',]constattr=flags.some((flag)=>str.includes(flag))?'playground-ignore':''str=flags.reduce((str,flag)=>str.replace(flag,''),str)//导入var-site-code-example组件return`${str}`})}varlet提供在线游乐场的功能:可以直接从文档的代码块跳转:但并不是所有的代码块都是必须的,例如:sojust传入在文档中添加注释表示忽略:injectCodeExample方法会检查这个flag是否存在,如果存在则传递一个不显示跳转按钮的属性给var-site-code-example组件,var-site-code-example组件的路径为/site/components/code-example/CodeExample.vue,用于提供代码块展开收起、复制、跳转到playground的功能拼装Vue单文件的格式最后根据Vue单文件的格式进行拼接://varlet-markdown-vite-plugin/index.jsfunctionmarkdownToVue(source,options){//...return``}将转换后的html内容添加到模板标签中,将解析后的组件导入语句添加到script标签中,并注册。转换成这种格式后,后续的vue插件就可以正常使用了。除了transformhook,热更新还使用了vite提供的handleHotUpdatehook来进行自定义的热更新处理。这个钩子接收一个context对象:file是改变后的文件,read是读取文件内容的方法,varlet-markdown-vite-plugin插件重写了这个方法://varlet-markdown-vite-plugin/index.htmljsfunctionVarletMarkdownVitePlugin(options){return{asynchandleHotUpdate(ctx){if(!/\.md$/.test(ctx.file))returnconstreadSource=ctx.readctx.read=asyncfunction(){返回markdownToVue(awaitreadSource(),options)}},}}目的和之前一样,就是把markdown语法转换成vue单文件语法,vue插件也是用这个hook和read的方法:也是因为该插件在vue插件之前调用,所以vue插件使用转换读取方式时,可以顺利热更新处理.md文件。处理markdown的插件介绍到此结束。下篇文章见,再见~