如何将 Vue 3 SFC 混入至 Markdown 文档中
如何将 Vue 3 SFC 混入至 Markdown 文档中
Vue 3 的诞生给前端领域注入了一管强劲的新鲜血液,与之一同而来的有打包工具 Vite,以及基于 Vite 全新打造的静态页面生成框架 —— VitePress。
VitePress 引入了一个非常有意思的功能 —— 在 Markdown 里,你可以直接使用 Vue,在 Markdown 内编写 Vue 组件,并最终通过 Vue 渲染到文章的对应位置。
这个特性相当实用,它能极大地增强 Markdown 文档的互动性和表现力,可惜的是它仅局限于 VitePress,很多并没有使用 VitePress 的项目并没有办法使用这项非常有意思的特性。
我一度想在自己的博客项目内引入这个功能,于是我创造了一个库,它允许你在非 VitePress 的 Vue 3 项目内,渲染带有 Vue 3 SFC 的 Markdown 文档。
使用示例和 Demo 在文章末尾。
思路
首先我不考虑 SSR 以及编写一个 Webpack / Vite 插件,在编译阶段去实现这件事,这些实际上和在 Markdown 里混入 Vue SFC 并不直接相关,这会给走通这条路带来一些额外的负担。同时我也不打算从0开始重新撸一个 Vue SFC 编译器或是 Markdown 编译器,重复造轮子很没必要,而且维护成本极高。
我打算用一种非常直接的方式实现这件事 —— 让这件事在运行时上跑通,只依赖 Vue SFC 和 Markdown 的编译。
Vue 官方提供了一个名为 @vue/compiler-sfc
的包,利用它,我们可以非常便利地对一个 Vue SFC 进行编译,而且这个编译可以发生在用户的浏览器上(当然,把这个包带到运行时毫无疑问会造成打包产物体积的大幅上升)。
我们可以在 Markdown 格式的基础上定义一个特别的 tag,在对 Markdown 进行编译之前,我们可以利用正则提取出 tag 内包装的内容,将其送入 @vue/compiler-sfc
进行编译,同时,为了给后续挂载 Vue SFC 提供锚点,这个特别的 tag 可以整个替换为一个带有随机 id 的 div
,这个 id 与 tag 内的 SFC 代码对应。后续我们可以直接挂载 SFC 到这个 div 上,这样它就会正确地出现在文章内的正确位置。
SFC 编译后,我们得到的编译产物理论上还是文本,我们需要执行它,这我考虑直接用 eval 将其激活、运行,这个地方需要设计一套机制,使编译产物在 eval 之后,我能便利地获取到相关内容。
最后,一个 SFC 在编译后也解不开对 Vue 本身的依赖,所以要使其正常工作,我需要调用项目本身就存在的 Vue 去挂载编译后的 SFC。这里我们需要设计一个方法让 Vue 能够挂载经由 @vue/compiler-sfc 得到的编译产物。
如果上面这个流程可以走通,那这个项目的可行性就得到了验证。
实现
编译 Vue SFC
@vue/compiler-sfc
这个包本身的文档并不丰富,使得我花了一番时间去了解它的工作方式,期间也参考了一些其他编译 Vue SFC 相关的项目。
要编译一个 Vue SFC,首先我需要将其拆分为 template、script、style 三部分,分别将其送入 @vue/compiler-sfc
的 compileTemplate
、compileScript
以及 compileStyle
这三个方法。这三个方法各自会返回一份编译产物,前两者为函数文本,后者为样式文本。
其中样式文本是很好处理的,我们只需要将其作为 style tag 插入即可。
不过这里在实现上有一个小坑,对于 scoped,编译器确实对样式加上了 scopeId,但是这个 id 是你预先指定的 id,它会和 Vue 实际挂载 SFC 时给 template 分配的 id 不一致,这一问题的解法后面会提到。
对于 template 和 script 编译后返回的函数文本,对于它俩的处理会比较复杂,而且还涉及到对 import 的处理。
这里我用了一个巧法解决了这个问题。
函数包装
template 部分的编译产物是 render
函数,这个函数本身带有对 Vue 一些方法的引用,引用的方式是 import
。
script 编译之后其实和原本 Vue SFC 内写的 script 部分没什么区别,这一部分如果加上了 TS、JSX 等特性会变得更复杂,但我们暂时先不考虑这些。就单纯的 JavaScript 编写的 script 块来说,我们需要处理的只有 import
语句。
根据上文提到的思路,这一块的重中之重就是两个点:
- 处理
import
。 - 激活函数,使其可以被利用。
对于前者,我选择把函数本身进行二次包装,包装为一个入参为 context
的函数,返回编译后的函数。我们在这一层包装里处理 import
。
context 的存在可以让我们把外部的依赖传递到函数内,因而通过它,函数内能够拿到外部源自于项目的 Vue 以及其他依赖。
这样一来,我们只要将 import
语句转化成变量定义与赋值就行了。
举个例子,我们有一个这样的 import
:
import { h } from 'vue';
我们转化它为:
const h = context.vue.h;
这样我们就成功拿到了项目包含的 Vue 导出的 h
方法。函数体内的编译产物函数也就能利用到它了。
这一步转换完全可以用正则实现,只需要做一些简单的字符串处理即可,相关实现代码在下面的链接中,比较长,这里就不贴出来了。
markvue/parser.ts at main · backrunner/markvue (github.com)
为了使这段代码在 eval
后可以拿到,代码还需要做一些处理,把包装好的函数挂到全局,这样这些方法就可以很简单地访问到了。
组件重组
Vue SFC 最核心的是 script 块,这一部分的结构在编译前后是基本没有差异的。
而 template 块编译之后其实就是 Vue 组件里你可以自己定义、编写的那个 render
函数。
基于这一层原理,在经过上述步骤拿到编译后的 script 块和函数之后,只要把这两个部分重新拼在一起,就能重组出一个可以被 createApp
执行的 Vue 组件,进而就可以对它进行挂载。
这要注意的是,重组后组件的 __scopeId
需要手动指定,否则 Vue 会给它分配一个随机的 id,导致 scoped style 失效。
使用示例和 Demo
我把这个库命名为 MarkVue,它主要作用于浏览器运行时,结合 Marked 提供在 Markdown 里使用 Vue 的体验。
除了 Vue SFC,我还额外做了一个 Vue 组件插槽的能力,你可以用特别的 tag 将某一个 Vue 组件挂载到文章的对应位置,这个实现很简单,我就不再细说了。
使用示例见项目的 Readme:
backrunner/markvue: Allows you mix Vue SFC or component into Markdown (github.com)
Demo:
喜欢这篇文章、喜欢这个库,不妨给我点个star~,十分感谢你的支持。
展望
这个库目前还只是在浏览器里实现了这一层能力,从打包产物体积这个角度来说这肯定不是最优雅的方案,这个库未来还可以向 Vite 插件的方向进一步升级,提前编译好所有的内容,避免给客户端带来压力。
如果你对这个方向感兴趣,欢迎 fork 我的项目或者基于我的思路进行更深入、更进一步的开发~,开源社区有共创才会变得更好。
转载自:https://juejin.cn/post/7073861541182832670