分析 Element 的 Markdown loader : md-loader(1)

2,462 阅读2分钟

预计阅读时间: 6分钟

前言

element-ui 的官网是使用 vue 开发的。根据源码中 vue-router 的路由信息,element-ui 的组件文档页引入的并不是一个vue文件,而是一个 markdown 文件。 element-uivue 语法写入了 markdown 文件中,通过 markdown 文件生成了我们看到的文档页。

这个过程的实现主要依靠了 element 自研的 md-loader

屏幕快照 2021-04-12 上午10.44.57.png

本文将分析 md-loader 是如何做到这一点的。

从webpack入手

我们可以在 build/webpack.demo.js 文件中找到 element-uiwebpack 配置,其中对于 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-loader 主要做了两件事:

  1. 第一步是将 .md 文件转换为普通的 html 格式。

  2. 第二步是将转换后的 html 代码处理为 vue 格式。

关于第一步,md-loader 的实现方式是引入了 markdown-it 包。这个包有在线demo可以使用,我们可以将 markdown 代码复制到其中查看解析结果。在这里我们使用 element-uibutton 组件文档作为示例,将该文件复制到在线demo中,即可看到效果。注意右上角的三个标记,分别表示了查看的三种内容:

  1. html,可以查看 markdown 转换后的页面效果。

WX20210318-163248.png

  1. source,可以查看 markdown 转换后生成的 html 代码。

屏幕快照 2021-03-19 下午2.02.01.png

3.debug,可以查看 markdown 转换后生成的 tokens 数组,后面会用到。

屏幕快照 2021-03-19 下午2.02.10.png

有了 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 文档的标题上时,会出现一个符号标记,这就是锚点了。

1616479556702.jpg

锚点作用是点击之后可以跳转到页面的对应位置。虽然 element-ui 的文档中的标题设置了锚点,但是并没有对应的页面大纲。所以只能通过点击标题触发锚点 😳 ( 在 Element Plus 中优化了这个问题 )。

另一个配置是自定义代码块,这是一个很重要的配置。我们可以观察到文档中的有一段内容被 :::demo 标记包裹了起来

::: demo
xxxx
xxxx
:::

很显然这个标记不属于正文里的内容,这里使用了 markdown-it 的一个插件,名叫 markdown-it-container。这个插件的作用是获取标记内的代码( :::demo )并返回自定义内容。这个过程就类似于 JavaScriptreplace 方法。

这一段的代码在 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 格式。

如果这篇文章对你有所帮助,或者有所启发的话,求关注!求点赞!各位的支持和认可,就是我写作的最大动力 😊