最近打算重写本身组件库的官网,在写展现组件部分的时候遇到了一个问题,全部组件的功能展现都在一个.vue文件里面写的话,会很麻烦。若是只用简单的md就能够转成须要的页面而且有代码高亮、demo展现框和页面样式,那该多好。vue
module: {
rules: [
//.....
{
test: /\.md$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
{
loader: path.resolve(__dirname, './md-loader/index.js')
}
]
},
]
},
复制代码
目录 | 大致功能 |
---|---|
index.js | 入口文件 |
config.js | markdown-it的配置文件 |
containers.js | render添加自定义输出配置 |
fence | 修改fence渲染策略 |
util | 一些处理解析md数据的函数 |
先看看demowebpack
//demo.md
## Table 表格
用于展现多条结构相似的数据,可对数据进行排序、筛选、对比或其余自定义操做。
### 基础表格
基础的表格展现用法。
:::demo 当`el-table`元素中注入`data`对象数组后,在`el-table-column`中用`prop`属性来对应对象中的键名便可填入数据,用`label`属性来定义表格的列名。可使用`width`属性来定义列宽。
```html
<template>
<el-table
:data="tableData"
style="width: 100%">
<el-table-column
prop="date"
label="日期"
width="180">
</el-table-column>
<el-table-column
prop="name"
label="姓名"
width="180">
</el-table-column>
<el-table-column
prop="address"
label="地址">
</el-table-column>
</el-table>
</template>
<script>
export default {
data() {
return {
tableData: [{
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}
}
}
}
</script>
:::
(```)
复制代码
把.md文件解析出来包成一个.vue。git
代码展现和实例展现都须要在一个卡片里面,卡片能够展开关闭github
一套code两个用途,一个是做为示例展现,一个是做为示例代码(代码高亮),也就是只写一套代码就够了 web
好了,下面就是一步步搞懂element的md-loader是如何作到这些的数组
首先须要安装这两个依赖bash
yarn add markdown-it-chain markdown-it-anchor -D
复制代码
config.js对markdown-it
作配置markdown
//config.js
const Config = require('markdown-it-chain');
const anchorPlugin = require('markdown-it-anchor');//给页眉添加锚点
const config = new Config();
config
.options.html(true).end()
.plugin('anchor').use(anchorPlugin, [
{
level: 2,
slugify: slugify,
permalink: true,
permalinkBefore: true
}
]).end()
const md = config.toMd();
module.exports = md;
复制代码
用markdown-it-anchor
给页眉添加锚点ide
markdown-it-chain
的链式配置参考文档
如今在index.js
里面引入config.js
const md = require('./config');
module.exports = function(source) {
const content = md.render(source) //获得来自.md的解析出来的数据,请记住这个content
//....
}
复制代码
既然是包装成.vue,那么最终输出的确定是和日常见到的如出一辙吧
//大概是这样
<template>
</template>
<script>
export default {
}
</script>
复制代码
因而修改index.js
module.exports = function(source) {
const content = md.render(source) //获得来自.md的解析出来的数据
//....
let script = `
<script>
export default {
name: 'component-doc',
}
</script>`
//script标签
//输出这个template和script合并的字符串
return `
<template>
<section class="content element-doc"> //template
</section>
</template>`
${pageScript};
}
复制代码
如今须要考虑一个问题,一个md里面有不少相似的demo,最终输出的确定是一个vue对象,那么这些demo就必须包装成一个个组件。
很明显这里不能用模板建立组件,因而须要用到渲染函数的形式。
须要用到的插件
名字 | 大致功能 |
---|---|
vue-template-compiler | template转为render函数(做为配置项) |
component-compiler-utils | 编译Vue单文件组件的工具 |
(须要知道的)vue-loader 会借助 component-compiler-utils 的工具编译Vue单文件组件
如今在util.js
里面引入他们
const { compileTemplate } = require('@vue/component-compiler-utils');
const compiler = require('vue-template-compiler');
复制代码
在源码里面这个功能在util.js
里面的genInlineComponentText
函数完成。(这个函数挺复杂和冗长的,只能拆分说明)
function genInlineComponentText(template, script) {}
复制代码
首先这个函数接受两个参数 template
和script
,他们由各自对应的处理函数处理的, 数据来源即是一开始就解析出来的content
function stripScript(content) {
const result = content.match(/<(script)>([\s\S]+)<\/\1>/);
return result && result[2] ? result[2].trim() : '';
} //只输出带有script标签的
function stripTemplate(content) {
content = content.trim();
if (!content) return content;
return content.replace(/<(script|style)[\s\S]+<\/\1>/g, '').trim();
}//过滤清空掉script和style,输出天然是template
复制代码
再来请点开上面table里面工具的github文档说明,对应里面的options配置,反正照着文档来就行
const options = {
source: `<div>${template}</div>`,
filename: 'inline-component',
compiler // 这个compiler便是vue-template-compiler
}
复制代码
利用上面引入的compileTemplate
编译
const compiled = compileTemplate(options)
复制代码
若是有则抛出编译过程当中的报错和警告
if (compiled.tips && compiled.tips.length) {
compiled.tips.forEach(tip => {
console.warn(tip);
});
}
// errors
if (compiled.errors && compiled.errors.length) {
console.error(
//.....
);
}
复制代码
最后拿到编译后的code
let demoComponentContent = `${compiled.code}`
复制代码
如今处理script
script = script.trim() //去掉两边空格
if (script) {
script = script.replace(/export\s+default/, 'const democomponentExport =')
} else {
script = 'const democomponentExport = {}';
}
复制代码
这部分是把字符串里面的export default
替换为const democomponentExport =
便于后面的解构赋值
最后return出去
demoComponentContent = `(function() {
${demoComponentContent} // 跑了一遍
${script}
return {
render, //下面有说
staticRenderFns, //这里不须要关注这个
...democomponentExport //解构出上面的对象的属性
}
})()`
return demoComponentContent;
复制代码
上面的render
实际上在函数里面的${demoComponentContent}
跑了一遍的时候就已经赋值了 能够看看这里demo编译出来compiled.code
的结果
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h //能够把下面return的_c当作是$createElement
return _c(
"div", // 一个 HTML 标签名
[//.....] // 子节点数组 ,实际上也是由由$createElement构建而成
)
}
var staticRenderFns = []
render._withStripped = true
复制代码
那么如今就很明了了,从新看回以前的index.js
let script = ` <script> export default {
name: 'component-doc',
components: {
'demo-components':(function() {
var render = function() {
//.....
return {
render,
staticRenderFns,
...democomponentExport
}
})()
}
}
</script>
`
复制代码
关于render._withStripped = true
若是未定义,则不会被get
所拦截,这意味着在访问不存在的值后不会抛错。 这里使用了@component-compiler-utils
,因此是自动加上的,能够不用关心。
回顾一下上面作了什么
markdown-it
解析.md里面的数据template
和script
,经过插件编译成render Functioon
,利用他建立组件。如今的问题是一个content
里面包含着全部的demo的数据,如何分辨并对每一个demo作以上的操做,并且components
里面的组件名字是不能重名的,如何解决这一点。
首先须要下载依赖
yarn add markdown-it-container -D
复制代码
直接看源码
文档示例
element源码
const mdContainer = require('markdown-it-container');
module.exports = md => {
md.use(mdContainer, 'demo', {
validate(params) {
return params.trim().match(/^demo\s*(.*)$/);
},
render(tokens, idx) {
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
if (tokens[idx].nesting === 1) {
const description = m && m.length > 1 ? m[1] : '';
const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : '';
return `<demo-block>
${description ? `<div>${md.render(description)}</div>` : ''}
<!--element-demo: ${content}:element-demo-->
`;
}
return '</demo-block>';
}
})
};
复制代码
须要了解的,下面的是一个块级容器,用(:::符号包裹),这个插件能够自定义这个块级所渲染的东西
::: demo 我是description(1)
*demo*(2)
:::
复制代码
tokens
是一个数组,里面是块级容器里面全部md代码的code,按照必定规则分割,例如tokens[idx].type === 'fence'
意味着被```包裹的东西
块级容器默认返回的是:::
符号包裹的内容,也就是说即便是写在同一行的我是description
默认是不会在content
里面的,这里render
所返回的就是插在content
返回值里面的,至因而在包裹内容的前仍是后,取决于页初仍是页尾,也就是tokens[idx].nesting
是否等于1,这一点在文档的Examples能够知道。
因而如今这段代码的功能就很明显了,页初添加<demo-block>
,页尾添加</ demo-block>
,组成一个 <demo-block />
组件, <demo-block/>
是一个全局注册的组件,是示例展现用的,后面会提到。
在这个demo-block里面就是三样东西
${description} //(1) description即为demo的开头说明,请返回demo.md查看
<!--element-demo: ${content}:element-demo--> //(2) 展现组件 前面和后面的即是标记,在index.js会根据这个标记找到这段内容
${content} // (3) 展现的代码 这个东西会在fence.js文件里面作渲染覆盖,修改它的标签内容
复制代码
每一个demo都由这三样东西构成,请记住这个结构。
const startTag = '<!--element-demo:';
const startTagLen = startTag.length;
const endTag = ':element-demo-->';
const endTagLen = endTag.length;
let componenetsString = '';
let id = 0; // demo 的 id
let output = []; // 输出的内容
let start = 0; // 字符串开始位置
let commentStart = content.indexOf(startTag);
let commentEnd = content.indexOf(endTag, commentStart + startTagLen);
while (commentStart !== -1 && commentEnd !== -1) {
output.push(content.slice(start, commentStart));
const commentContent = content.slice(commentStart + startTagLen, commentEnd);
const html = stripTemplate(commentContent);
const script = stripScript(commentContent);
let demoComponentContent = genInlineComponentText(html, script);
const demoComponentName = `element-demo${id}`;
output.push(`<template slot="source"><${demoComponentName} /></template>`);
componenetsString += `${JSON.stringify(demoComponentName)}: ${demoComponentContent},`;
//这里start会是前一个标记结尾的地方,常规说来就是前一个demo的代码展现的开头
id++;
start = commentEnd + endTagLen;
commentStart = content.indexOf(startTag, start);
commentEnd = content.indexOf(endTag, commentStart + startTagLen);
}
output.push(content.slice(start)) //相当重要的一步,后面会讲
复制代码
源码这一段有点多,首先须要搞懂各个变量的做用
demoComponentName
与id
,命名每一个demo组件名字用的,id会在while每一次循环+1,这样子组件从第一个到最后一个都不会重名。
output
是一个数组,最终会拼接放入输出vue文件的template
里面,也就是这一页的HTML咯。 从上面代码能够看到,对output
进行操做的只有三个地方,while
循环开头会把description
推动去,而后是推入展现组件
字符串,
output.push(`<template slot="source"><${demoComponentName} /></template>`)
复制代码
这里面的demoComponentName
会在最终输出的类vue对象字符串里面进行局部注册,也就是最上面提到的。
而后就是循环结束后会再push一次,这一步相当重要,下面是讲解
let content = `
Description1 //description
Component1 // 展现组件
componentCode1//展现代码
Description2
Component2
componentCode2
Description3
Component3
componentCode3
`
复制代码
content
就是上面这样的结构,那么output
实际上就是经历了如下过程
1·第一次循环
output.push(Description1)
output.push(Component1)
2.第二次循环
output.push(componentCode1)
output.push(Description2)
output.push(Component2)
3.第三次循环
output.push(componentCode2)
output.push(Description3)
output.push(Component3)
4.循环结束
也就是说循环结束后,componentCode3
是没有推入output
的,而componentCode3
包含 </ demo-block>
,这样子在最后拼接的时候,HTML结构是有问题的。
demoComponentContent
前面有讲过是返回的render Function
,componenetsString
的结构相似下面的代码`componentName1:(renderFn1)(),componentName2:(renderFn2)()`
复制代码
最终在script
的代码就是
script = `<script>
export default {
name: 'component-doc',
components: {
component1:(function() {*render1* })(),
component2:(function() {*render2* })(),
component3:(function() {*render3* })(),
}
}
</script>`;
复制代码
而后index.js
里就能够返回这个啦,
return `
<template>
<section class="content element-doc">
${output.join('')}
</section>
</template>
${script}
`;
复制代码
最后输出的大体代码就是
<h3>我是demo1</h3>
<template slot="source"><demo1/></template>
<template slot="highlight"><pre v-pre><code class="html">${md.utils.escapeHtml(content)}</code></pre></template>
//第三个其实就是展现代码,在fence.js里面对其作了修改,以此对应具名插槽。
复制代码
fence.js
的操做大体就和上面我写的注释是同样的,主要代码以下
if (token.info === 'html' && isInDemoContainer) {
return `<template slot="highlight"><pre v-pre><code class="html">${md.utils.escapeHtml(token.content)}</code></pre></template>`;
}
复制代码
经过覆盖修改默认输出,添加<template slot="highlight">
便于在demo-block
分发内容。
<template slot="source"><${demoComponentName} /></template> //展现组件
复制代码
打开Element的源码,在example=>components里面能够找到demo-block.vue
, 里面有这样的代码
<div class="source">
<slot name="source"></slot>
</div>
复制代码
在 template 上使用特殊的 slot 特性,能够将内容从父级传给具名插槽 . 这里面description
是默认的插槽,展现代码也是具名可是由于须要代码高亮就复杂了一点。
至此,Element的md-loader
的大部分代码都及功能都看完了,感谢Element团队贡献出的源码,让我获益良多。
但愿能对你理解源码有帮助,若是有什么不对的地方,欢迎批评指正。