下面开始讲解一下具体的实现。
Markdown 编译
使用 markdown-it-container 来转换自定义容器。
module.exports = md => {
md.use(require('markdown-it-container'), 'demo', {
// ....
render(tokens, idx) {
if (tokens[idx].nesting === 1) {
return `<demo-block><div>${md.render(description)}</div>`;
}
return '</demo-block>';
}
});
};
自定义容器 demo 就被转成了 demo-block 组件。
针对代码块(fence),markdown-it 有默认的渲染逻辑。当代码块在 demo 容器内要做一下特殊处理。
const defaultRender = md.renderer.rules.fence;
// 覆盖默认渲染规则
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
// ...
if (tokens[idx].info === 'html' && isInDemoContainer) {
return `<template slot="highlight">
<pre v-pre>
<code class="html">...</code>
</pre>
</template>`;
}
return defaultRender(tokens, idx, options, env, self);
};
v-pre 是 Vue 自带的指令,用来显示原始 Mustache 标签。考虑到代码片段会包含 Mustache 标签,使用该指令来跳过对 code 的编译。
设置占位符
现在,我们已经完成了从 Markdown 到 HTML 的转换。还缺少点功能,demo 中的代码片段没有渲染。
要渲染代码片段,关注以下两点:
如何渲染
组件的位置
在 Vue 中,可以使用一个普通的 JavaScript 对象来定义组件。把代码片段转化成一个对象,之后在父元素中注册一下即可,问题 1 就解决了。
再看问题 2。代码区域即是组件要显示的位置。在 markdown-it 编译代码片段前,我们还需要把代码复制一份(上文中提到了代码既要显示还要渲染),创建一个占位符,用来放置在下一步才注册的组件。
md.use(require('markdown-it-container'), 'demo', {
render(tokens, idx) {
if (tokens[idx].nesting === 1) {
return `<demo-block>
...
__START__${code}__END__
`;
}
return '</demo-block>';
}
});
START__${code}__END 就是占位符。之后要把占位符的内容进行编译并替换为组件。
这里的做法类似于宏替换。
代码片段转换成组件
代码片段的 script 原本就是导出对象。把 template 转换成 render 函数,再将 script 与 render 函数合并,这样就把代码片段转换成组件。
vue-template-compiler 正好是我们所需要的。先调用 Vue.compile 方法,查看编译后的效果。
//const res = Vue.compile('<div>demo</div>')
// res.render
function anonymous() {
with(this) {
return _c('div',[_v("demo")])
}
}
// res.staticRenderFns
[]
render 函数中包含了 with 语句。with 语句是不建议使用,知乎上有关于这个问题的讨论。在该问题的回答中,Vue 作者尤雨溪提到了 vue-loader 是把 with 给去掉了。先看一下 vue-loader 是如何编译 template 的。核心逻辑如下:
const { compileTemplate } = require('@vue/component-compiler-utils');
const compiler = require('vue-template-compiler');
const finalOptions = {
source: <div>${template}</div>
,
compiler
};
const compiled = compileTemplate(finalOptions);
compiled 包含了编译之后的结果,compiled.code 即是我们想要的内容。
// compiled.code
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _vm._m(0)
}
var staticRenderFns = [
// ....
]
render._withStripped = true
使用 @vue/component-compiler-utils 与 vue-template-compiler 相配合就完成对 template 的编译。接下来把 render,staticRenderFns 与 script 合并成一个对象。使用自执行函数将合成后的结果返回,这样就获得了组件内容。
script = script.replace(/export\s+default/, 'const demoComponentExport =');
const demoComponentContent = `(function() {
${compiled.code}
${script}
return {
render,
staticRenderFns,
...demoComponentExport
}
})()`
编译组件完成后,就剩批量注册组件以及占位符替换了。最后这两步本质上是字符串拼接,不再赘述。
自定义 loader 的核心代码大致如下:
module.exports = function(source) {
const content = md.render(source);
let componentsString = ''; // 局部注册组件的内容
let output = []; // 输出的内容
// 对 content 进行处理,拼接处理 template 与 script 内容
return `
<template>
<section class="content element-doc">
${output.join('')}
</section>
</template>
<script>
export default {
name: 'component-doc',
components: {
${componentsString}
}
}
</script>
`;
};
核心的转换逻辑到这里讲解完毕,完整的代码点击这里。
总结
本文介绍了 Element 文档站中 md-loader 的实现。由于 Markdown 支持自定义容器,因此我们不仅仅可以在文档中写 Vue 组件,当然更可以写 React,比如 docz。大家可以结合自己的业务场景发掘出更多的用法,希望本文能给你带来一些的启发。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。