theme: cyanosis
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情
在container的具体实现中,我们知道经过container.js的处理之后,得到了一个HTML字符串,那么接下来,我们的工作就是处理字符串,将其渲染为Vue代码。
extendMarkdown: md => {
const id = setInterval(() => {
const render = md.render;
if (typeof render.call(md, '') === 'object') {
md.render = (...args) => {
let result = render.call(md, ...args);
// result.html 就是经过container.js处理的代码
const { template, script, style } = renderDemoBlock(result.html);
result.html = template;
result.dataBlockString = `${script}\n${style}\n${result.dataBlockString}`;
return result;
}
clearInterval(id);
}
}, 10);
}
由上述代码片段可知,主要的字符串处理工作就是在 renderDemoBlock 函数中完成的,下面我们就一起探究这个函数具体完成了那些事情。
renderDemoBlock 函数解析
点击 renderDemoBlock 查看源文件
还原代码块中的代码
还记得上篇文章中说,代码块中的代码是以注释的形式存放在HTMl中,因此,第一步就是将注释代码还原为正常代码。
<demo-block>
<template slot="demo">
<!--pre-render-demo:<template>
<div class="red-center-text">
<p>{{ message }}</p>
<input v-model="message" placeholder="Input something..."/>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue'
}
}
}
</script>
<style>
.red-center-text {
color: #ff7875;
text-align: center;
}
</style>
:pre-render-demo-->
</template>
</demo-block>
通过查找开始标志和结束标志在字符串中的位置,将注释代码截取出来
const startTag = '<!--pre-render-demo:';
const startTagLen = startTag.length;
const endTag = ':pre-render-demo-->';
const endTagLen = endTag.length;
...
...
let commentStart = content.indexOf(startTag);
let commentEnd = content.indexOf(endTag, commentStart + startTagLen);
const commentContent = content.slice(commentStart + startTagLen, commentEnd);
这里有一个不常用的知识点
str.indexOf(target)
可以获得目标字符串在 str
第一次出现的位置 ,它还可以传入第二个参数,用来限制从哪一个下标开始查找
const str = `hello,world`
const idx = str.indexOf('o') // 4
const idx1 = str.indexOf('o', 5) // 6
经过上述步骤就得到了代码块中的代码:
<template>
<div class="red-center-text">
<p>{{ message }}</p>
<input v-model="message" placeholder="Input something..."/>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue'
}
}
}
</script>
<style>
.red-center-text {
color: #ff7875;
text-align: center;
}
</style>
到了这一步,我们其实就得到了一个完整的Vue SFC (Single File Component)文件。接下来,就需要编译该文件。
编译 Vue SFC文件
首先我们要先了解一下 Vue SFC 文件最终会被编译为什么样子
在 Vue2 中,我们可以通过如下方式定义一个组件:
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script>
// js 对象
const profileInstance = {
data: function () {
return {
msg: 'hello'
}
},
render: function (createElement) {
return createElement('p', 'hello')
}
}
const Profile = Vue.extend(profileInstance)
new Profile().$mount('#app')
</script>
这种形式其实就是 SFC 编译后的形式,我们需要将 vue 文件编译后成 js 对象的形式
分离template、script、style
由于 template、script、style需要不同的处理,我们需要将这三者从字符串中分离出来。
// 提取 script
function stripScript(content) {
const result = content.match(/<(script)>([\s\S]+)<\/>/);
return result && result[2] ? result[2].trim() : '';
}
// 提取style
function stripStyle(content) {
const result = content.match(/<(style)\s*>([\s\S]+)<\/>/);
return result && result[2] ? result[2].trim() : '';
}
// 提取 template
// 编写例子时不一定有 template。所以采取的方案是剔除其他的内容
function stripTemplate(content) {
content = content.trim();
if (!content) {
return content;
}
return content.replace(/<(script|style)[\s\S]+<\/>/g, '').trim();
}
解析 template
从上文 js 对象中可以看出,HTML是通过一个render函数渲染出来的。因此,解析 template 的过程其实就是将 template 转换为 render 渲染函数的过程。
template 转换为 render 渲染函数是一个非常复杂的过程,需要经历如下阶段:
但幸运的是,Vue 团队已经帮我们完成了这项工作。
const { compileTemplate } = require('@vue/component-compiler-utils');
const compiler = require('vue-template-compiler');
const finalOptions = {
source: `<div>${template}</div>`,
filename: 'inline-component',
compiler
};
const compiled = compileTemplate(finalOptions);
// compiled.code 就是render渲染函数
编译后的结果:
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
[
[
_c("div", { staticClass: "red-center-text" }, [
_c("p", [_vm._v(_vm._s(_vm.message))]),
_vm._v(" "),
_c("input", {
directives: [
{
name: "model",
rawName: "v-model",
value: _vm.message,
expression: "message"
}
],
attrs: { placeholder: "Input something..." },
domProps: { value: _vm.message },
on: {
input: function($event) {
if ($event.target.composing) {
return
}
_vm.message = $event.target.value
}
}
})
])
]
],
2
)
}
var staticRenderFns = []
render._withStripped = true
合并 script 和 template
script 和 template 在一块才是一个完整的 Vue 对象,因此,需要将二者合并起来
script = script.trim();
if (script) {
script = script.replace(/export\s+default/, 'const democomponentExport =');
} else {
script = 'const democomponentExport = {}';
}
demoComponentContent = `(function() {
${demoComponentContent}
${script}
return {
render,
staticRenderFns,
...democomponentExport
}
})()`;
demoComponentContent
就是完整的 Vue 对象了,此时,经过打包等处理之后已经可以渲染到页面上了
style 此时并不需要处理,所有的style 都会交由 style-loader 进行处理
插槽代码替换
写到这里,千万不要忘记了我们以上处理的 Vue 代码都是通过 slot 嵌入到 demo-block 组件中的,如上所示
所以,我们还需要将编译处理过的 Vue 对象放入 <demo-block slot='demo'></demo-block>
中
这里,其实是通过构建组件的方法完成的。
const demoComponentName = `render-demo-${id}`; // 示例代码组件名称
// 将子组件存入到templateArr中
templateArr.push(`<template><${demoComponentName} /></template>`);
componenetsString += `${JSON.stringify(demoComponentName)}:${demoComponentContent},`;
pageScript = `<script>
export default {
name: 'component-doc',
components: {
${componenetsString}
}
}
</script>`;
每个 demo
代码块都有一个 id
, 所以可以根据 id
创建一个独一无二的组件名称。
pageScript
是整个 markdown
文件对应的 Vue
对象,将示例代码组件作为子组件在 components
属性中进行注册,就可以通过 demoComponentName
使用子组件了。
<demo-block slot='demo'>
<!--这里id仅为示意,应该为数字 -->
<template>
<render-demo-id></render-demo-id>
</template>
</demo-block>
那么,这是如何进行替换的呢?
在处理传入 renderDemoBlock 的HTML 字符串时,首先会创建一个 templateArr
数组用来存储模版输出内容。
在还原注释代码一步中,会得到注释代码的起始位置和结束位置。
在处理 template 和 script 之前,首先会根据 commentStart
将注释代码之前的代码存储到 templateArr
中
templateAArr.push(content.slice(start, commentStart))
在 template
和 script
处理结束后,会根据 commentEnd
将注释代码之后的代码存储到 templateArr
中
templateArr.push(content.slice(commentEnd))
这样就将原本的注释代码,替换为了编译后的Vue子组件
到了这里我们就实现了HTML字符串解析的全过程。随着整个 markdown 文件 对应 Vue 对象被渲染,作为其子组件的 demo 代码也会被正常渲染,从而展示在界面上。
总结
本篇文章,详解的介绍了如何处理HTML字符串,如何处理代码块中的代码。
但以上所介绍的都是以单个 markdown 文件为基础,多个文件的情况并没有说明。其实,原理是相同的,只是多个文件需要循环处理 HTML 字符串
vuepress-plugin-dmeo-container
源码解析系列到此就结束了,希望可以帮助到使用该插件的其他同学!
暂无评论内容