预计阅读时间: 6分钟
前言
element-ui
的官网是使用 vue
开发的。根据源码中 vue-router
的路由信息,element-ui
的组件文档页引入的并不是一个vue
文件,而是一个 markdown
文件。 element-ui
将 vue
语法写入了 markdown
文件中,通过 markdown
文件生成了我们看到的文档页。
这个过程的实现主要依靠了 element
自研的 md-loader
。
本文将分析 md-loader
是如何做到这一点的。
从webpack入手
我们可以在 build/webpack.demo.js
文件中找到 element-ui
的 webpack
配置,其中对于 markdown
文件的处理如下。
{
test: /\.md$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
{
loader: path.resolve(__dirname, './md-loader/index.js')
}
]
}
根据这段配置,webpack
一旦遇到以 .md
结尾的文件,就会读取文件中的内容,将这些内容传入相应的 loader
中进行处理。由于 loader
的调用顺序是从下往上的,所以会先调用 md-loader
,然后将 md-loader
处理好的内容传入 vue-loader
中 。只要传入 vue-loader
中的内容符合 vue
的单文件组件的规范,就可以当做一个 vue
文件处理。这样一来,我们就将 .md
文件处理为 vue
文件了。扩展开来,只要我们配置好相应的 loader
,不只是 .md
文件,任意的文件都可以通过 vue-loader
进行处理。
我们已经大致了解了文件转换流程,接下来我们可以打开 md-loader
的入口文件( md-loader/index.js
)一起了解 md-loader
具体做了什么工作。
md-loader
在入口文件中,md-loader
主要做了两件事:
-
第一步是将
.md
文件转换为普通的html
格式。 -
第二步是将转换后的
html
代码处理为vue
格式。
关于第一步,md-loader
的实现方式是引入了 markdown-it
包。这个包有在线demo可以使用,我们可以将 markdown
代码复制到其中查看解析结果。在这里我们使用 element-ui
的 button
组件文档作为示例,将该文件复制到在线demo中,即可看到效果。注意右上角的三个标记,分别表示了查看的三种内容:
html
,可以查看markdown
转换后的页面效果。
source
,可以查看markdown
转换后生成的html
代码。
3.debug
,可以查看 markdown
转换后生成的 tokens
数组,后面会用到。
有了 markdown-it
插件的帮助,现在我们已经可以将 .md
文件转换为普通的 html
了。以上的过程如果用代码描述的话就是:
npm i markdown-it
var md = require('markdown-it')();
var result = md.render('button.md中的内容');
没错只有三行 😳
当然,转换后的代码不可能和我们的需求完全一致, md-loader
在转换的过程中也进行了其他的处理。这些配置都放在了同目录下的 config.js
中。
const md = require('./config');
module.exports = function(source) {
const content = md.render(source);
}
这些配置中主要包括标题锚点和自定义代码块。关于锚点,细心的朋友可以发现,当你将鼠标放在 element-ui
文档的标题上时,会出现一个符号标记,这就是锚点了。
锚点作用是点击之后可以跳转到页面的对应位置。虽然 element-ui
的文档中的标题设置了锚点,但是并没有对应的页面大纲。所以只能通过点击标题触发锚点 😳 ( 在 Element Plus
中优化了这个问题 )。
另一个配置是自定义代码块,这是一个很重要的配置。我们可以观察到文档中的有一段内容被 :::demo
标记包裹了起来
::: demo
xxxx
xxxx
:::
很显然这个标记不属于正文里的内容,这里使用了 markdown-it
的一个插件,名叫 markdown-it-container
。这个插件的作用是获取标记内的代码( :::demo )并返回自定义内容。这个过程就类似于 JavaScript
的 replace
方法。
这一段的代码在 build/md-loader/container.js
中。
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>';
}
});
md.use(mdContainer, 'tip');
md.use(mdContainer, 'warning');
};
其中的render
函数返回的 <demo-block>
是 element-ui
中的用于 demo
展示的组件,暂时不用关心。这段代码中最令人费解的部分就是这个 tokens
参数了,这个参数代表的是什么呢?这就要回到我们最开始的在线demo中的 debug
内容了。从中我们可以看到 tokens
是一个数组,里面有一系列的 token
对象,用于描述如何生成 html
标签。token
对象的每一个属性都有各自的意义,如上述代码中的 info
( 代码块中的内容 ),nesting
( 判断标签是否闭合,1
代表标签打开标志,-1
代表标签关闭标志 )。具体可以查看 markdown-it
中文文档,里面还有对 markdown-it
更详尽的介绍。
经过以上的步骤之后,我们终于拿到了一段经过我们转化后的代码了。
这就是 md-loader
的前三行的内容😳
const md = require('./config');
module.exports = function(source) {
const content = md.render(source);
}
没错,整篇文章到现在,我们终于讲完了 md-loader
入口文件的前三行,大致了解了 content
变量里面的内容了,yeah!
不过别着急,万事开头难。踏出第一步,才会有更多可能。
总结
我们现在已经可以将 .md
文件转换为 html
了。下一篇文章中,我们再一起聊聊如何将转换后的 html
代码处理为 vue
格式。
如果这篇文章对你有所帮助,或者有所启发的话,求关注!求点赞!各位的支持和认可,就是我写作的最大动力 😊