我们公司有一个自己维护的应用组件库,但是应用组件库的文档不足,同时还不能在线运行栗子,看了看ElementUI的文档还不错,参考(抄袭)ElementUI的实现方案来编写公司内部的组件说明文档。 把用markdown写的文档拼接成vue文件(这个通过他们自己写的md-loader处理),这个vue文件和我们平时开发项目的vue组件类型(类似<template>...</template><script>export default{}</script>),再通过vue-loader处理。 首先我们查看ElementUI的package.json文件中的scripts传送门 通过上图的标注,我们可以知道ElementUI的文档生成的webpack在build/webpack.demo.js里面,webpack.demo.js可以看到webpack是如何处理md文件的,md后缀的文件先通过md-loader处理后,再通过vue-loader处理 webpack.demo.js传送门 首先找到md-loader的入口文件index.js传送门, 里面的关键代码如下: 首先找到上述代码const md = require('./config');中的文件传送门, 看这个文件的内容再结合package.json里面的依赖,我们可以得知markdown的解析依赖这些开发包markdown-it(markdown解析器), markdown-it-anchor(锚点插件,用于段落跳转), markdown-it-chain(用于链式调用,参考webpack-chain) , markdown-it-container(自定义包裹元素插件识别remarkdown 语法:::) config.js containers.js 通过上述的代码功能主要为 把 :::demo xxxx code ::: 转换成 <demo-block> xxx <!--element-demo: code(源码) :element-demo--> code(转义后的代码) </demo-block> 我看md-loader的入口文件index.js传送门里有这么一段代码, let demoComponentContent = genInlineComponentText(html, script);, 这段代码的功能就是生成demo component字符串, 我们可以跳转到util.js看到如下内容: 上面的代码主要使用了@vue/component-compiler-utils vue-template-compiler来编译template模板,把template模板转成render函数创建dom语法, 然后拼接demo component字符串。 结合markdown文件如何解析与vue demo代码标记和vue demo component是如何生成的这两段的内容再重新看一遍md-loader的入口文件index.js传送门可以发现程序的流程为:markdown => html => 提取( <!--element-demo: code(源码) :element-demo-->)中的vue代码 => 编译里面的vue代码,返回demoComponentContent字符串 => 把html代码与demoComponentContent拼接起来组成新的vue代码。背景
ElementUI文档的整体思路
webpack是如何配置的 (v2.13.2)
"scripts": {
"dev": "... webpack-dev-server --config build/webpack.demo.js ...",
}
// ...
{
test: /\.md$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
{
loader: path.resolve(__dirname, './md-loader/index.js')
}
]
},
// ...
md-loader是如何工作的 (v2.13.2)
const {
stripScript, // 提取script
stripTemplate, // 提取template
genInlineComponentText // 生成行内组件代码
} = require('./util');
const md = require('./config'); // markdown编译器, 下面的 ‘markdown文件如何解析与vue demo代码标记’详细讲解
module.exports = function(source) {
const content = md.render(source); // 经过md编译好的代码
// content里面vue在线栗子的代码被 <!--element-demo: ...vue code... :element-demo-->
// ...
// 循环提取出<!--element-demo: ...vue code... :element-demo-->里的 vue code
while(...){
var commentContent = content.slice(commentStart + startTagLen, commentEnd); // vue code
const html = stripTemplate(commentContent); // 提取html
const script = stripScript(commentContent); // 提取js
let demoComponentContent = genInlineComponentText(html, script); // 把html, 和js组成组件代码.
const demoComponentName = `element-demo${id}`; // demo组件名
output.push(`<template slot="source"><${demoComponentName} /></template>`); // 插入组件
componenetsString += `${JSON.stringify(demoComponentName)}: ${demoComponentContent},`; // 追加组件字符串
}
if (componenetsString) { // 这是script字符串
pageScript = `<script>
export default {
name: 'component-doc',
components: {
${componenetsString}
}
}
</script>`;
}
return `
<template>
<section class="content element-doc">
${output.join('')} // 这是md转的html
</section>
</template>
${pageScript}
`;
};
markdown文件如何解析与vue demo代码标记
const Config = require('markdown-it-chain');
const containers = require('./containers'); // 里面使用markdown-it-container把语法为:::info => <!--element-demo: ...vue code... :element-demo-->
// ...
const config = new Config();
// ... 一些配置, 配置container/anchor
const md = config.toMd();
module.exports = md;
// ...
render(tokens, idx) {
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/); // elementUI文档里面,vue demo都是用:::demo vue code :::包裹着
if (tokens[idx].nesting === 1) { // 匹配到 :::demo xxxx 替换成return 的字符串
const description = m && m.length > 1 ? m[1] : '';
const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : ''; // content是vue code代码
return `<demo-block>
${description ? `<div>${md.render(description)}</div>` : ''}
<!--element-demo: ${content}:element-demo-->
`;
}
return '</demo-block>'; // 匹配到 :::, 替换成return 的字符串
}
// ...
vue demo component是如何生成的
function genInlineComponentText(template, script) {
// ...
const compiled = compileTemplate(finalOptions);
// ...
let demoComponentContent = `
${compiled.code}
`; // 模板代码 转成vue使用$createElement生成html的render函数
// ...
demoComponentContent = `(function() {
${demoComponentContent}
${script}
return {
render,
staticRenderFns,
...democomponentExport
}
})()`;
return demoComponentContent; // 返回字符串
}
总结
// 第一步
const content = md.render(source); // 经过md编译好的代码, 对应markdown文件如何解析与vue demo代码标记
// 第二步
while(...){ // 对应 vue demo component是如何生成的
// ...
let demoComponentContent = genInlineComponentText(html, script); // 把html, 和js组成组件代码.
// ...
}
// 第三步
// ....
return `
<template>
<section class="content element-doc">
${output.join('')} // 这是md转的html
</section>
</template>
${pageScript}
`;