Markdown文件居然也可以直接作为Vue路由组件?

简介: 本文为Varlet组件库源码主题阅读系列第五篇,读完本文你可以了解到如何通过编写一个`Vite`插件来支持使用`md`文件直接作为路由组件。
本文为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文件转换插件,使用插件时可以传入参数
        ]
    }
}
AI 代码解读

markdown-vite-plugin插件

插件代码在varlet-markdown-vite-plugin目录,一个Vite插件就是一个函数,接收使用时传入的参数,最终返回一个对象。Vite插件扩展了Rollup的接口,并且带有一些 Vite 独有的配置项,配置项类型基本就是两种,一种是属性,一种是钩子函数,插件的主要逻辑都在钩子函数里,RollupVite提供了构建和编译时各个时机的钩子,插件可以根据功能选择对应的钩子。

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
AI 代码解读

以上就是这个插件的函数,返回了一个对象,name属性为插件的名称,必填,用于信息和错误输出时的提示;enforce用于指定钩子的调用顺序:

vue插件没有指定,所以md插件会在其之前调用,保证到它这里.md文件的内容已经转换完毕。

接下来配置了两个钩子函数,我们详细来看。

md文件内容转换

transformRollup提供的构建阶段的钩子,可以在这个钩子内转换文件的内容,先判断文件后缀是否是.md,不是的话就不进行处理,是的话调用了markdownToVue方法:

// varlet-markdown-vite-plugin/index.js
function markdownToVue(source, options) {
    const { source: vueSource, imports, components } = extractComponents(source)
    // ...
}
AI 代码解读

支持在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,
  }
}
AI 代码解读

以前面的为例,source为:

xxx

​```vue
import BasicExample from '../example/Responsive.vue'
​```

xxx
AI 代码解读

匹配到vueREp1为:

import BasicExample from '../example/Responsive.vue'
AI 代码解读

使用importRE正则匹配后可以得到partImports数组:

[
    `import BasicExample from '../example/Responsive.vue'`
]
AI 代码解读

遍历这个数组,然后解析出componentBasicExample,将导入语句及组件名称收集起来,然后拼接模板字符串为:

<div class="varlet-component-preview">
    <basic-example />
</div>
AI 代码解读

最后这个模板字符串会替换掉sourcevueRE匹配到的内容。

代码高亮

让我们继续回到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为语言种类
    })
}
AI 代码解读

使用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 ''
}
AI 代码解读

代码高亮使用的是highlight.js,最开始使用md插件时我们传入了参数:

{ style: get(varletConfig, 'highlight.style') }
AI 代码解读

这个用于设置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>')
}
AI 代码解读

调用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>')
}
AI 代码解读

前两行做的事情就是把h3标签之后,h2标签之前的内容都用类名为carddiv包裹起来,目的是为了在页面上显示一个个块的效果:

最后一行是给code标签添加了一个v-pre指令,这个指令用来跳过该元素及其所有子元素的编译,因为文档的代码示例难免会涉及到Vue模板语法的示例,如果不跳过,直接就被编译了。

引入代码块组件

继续markdownToVue方法:

// varlet-markdown-vite-plugin/index.js
function markdownToVue(source, options) {
    // ...
    templateString = injectCodeExample(templateString)
}
AI 代码解读

又调用了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">&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>`
  })
}
AI 代码解读

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>
      `
}
AI 代码解读

把转换得到的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)
      }
    },
  }
}
AI 代码解读

目的和前面一样,就是把markdown语法转换成Vue单文件语法,vue插件也使用了这个钩子和read方法:

同样因为这个插件是在vue插件之前调用的,所以到了vue插件使用的就是被转换的read方法,就能在热更新时顺利处理.md文件。

处理markdown的插件就介绍到这里,我们下一篇再见,拜拜~

目录
打赏
0
0
0
0
15
分享
相关文章
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
224 0
|
3月前
|
vue实现任务周期cron表达式选择组件
vue实现任务周期cron表达式选择组件
389 4
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
91 1
如何高效实现 vue 文件批量下载及相关操作技巧
在Vue项目中,实现文件批量下载是常见需求。例如文档管理系统或图片库应用中,用户可能需要一次性下载多个文件。本文介绍了三种技术方案:1) 使用`file-saver`和`jszip`插件在前端打包文件为ZIP并下载;2) 借助后端接口完成文件压缩与传输;3) 使用`StreamSaver`解决大文件下载问题。同时,通过在线教育平台的实例详细说明了前后端的具体实现步骤,帮助开发者根据项目需求选择合适方案。
116 0
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
53 0
Vue 文件批量下载组件封装完整使用方法及优化方案解析
本文详细介绍了批量下载功能的技术实现与组件封装方案。主要包括两种实现方式:**前端打包方案(基于file-saver和jszip)** 和 **后端打包方案**。前者通过前端直接将文件打包为ZIP下载,适合小文件场景;后者由后端生成ZIP文件流返回,适用于大文件或大量文件下载。同时,提供了可复用的Vue组件`BatchDownload`,支持进度条、失败提示等功能。此外,还扩展了下载进度监控和断点续传等高级功能,并针对跨域、性能优化及用户体验改进提出了建议。可根据实际需求选择合适方案并快速集成到项目中。
204 17
Vue 表情包输入组件的实现代码:支持自定义表情库、快捷键发送和输入框联动的聊天表情解决方案
本文详细介绍了在 Vue 项目中实现一个功能完善、交互友好的表情包输入组件的方法,并提供了具体的应用实例。组件设计包含表情分类展示、响应式布局、与输入框的交互及样式定制等功能。通过核心技术实现,如将表情插入输入框光标位置和点击外部关闭选择器,确保用户体验流畅。同时探讨了性能优化策略,如懒加载和虚拟滚动,以及扩展性方案,如自定义主题和国际化支持。最终,展示了如何在聊天界面中集成该组件,为用户提供丰富的表情输入体验。
163 8
基于 ant-design-vue 和 Vue 3 封装的功能强大的表格组件
VTable 是一个基于 ant-design-vue 和 Vue 3 的多功能表格组件,支持列自定义、排序、本地化存储、行选择等功能。它继承了 Ant-Design-Vue Table 的所有特性并加以扩展,提供开箱即用的高性能体验。示例包括基础表格、可选择表格和自定义列渲染等。
219 6
Zerox:AI驱动的万能OCR工具,精准识别复杂布局并输出Markdown格式,支持PDF、DOCX、图片等多种文件格式
Zerox 是一款开源的本地化高精度OCR工具,基于GPT-4o-mini模型,支持PDF、DOCX、图片等多种格式文件,能够零样本识别复杂布局文档,输出Markdown格式结果。
551 4
Zerox:AI驱动的万能OCR工具,精准识别复杂布局并输出Markdown格式,支持PDF、DOCX、图片等多种文件格式
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等