ElementUI文档是如何实现的

8,923 阅读4分钟

背景

我们公司有一个自己维护的应用组件库,但是应用组件库的文档不足,同时还不能在线运行栗子,看了看ElementUI的文档还不错,参考(抄袭)ElementUI的实现方案来编写公司内部的组件说明文档。

ElementUI文档的整体思路

把用markdown写的文档拼接成vue文件(这个通过他们自己写的md-loader处理),这个vue文件和我们平时开发项目的vue组件类型(类似<template>...</template><script>export default{}</script>),再通过vue-loader处理。

webpack是如何配置的 (v2.13.2)

首先我们查看ElementUI的package.json文件中的scripts传送门

"scripts": {
 "dev": "... webpack-dev-server --config build/webpack.demo.js ...",
}

通过上图的标注,我们可以知道ElementUI的文档生成的webpack在build/webpack.demo.js里面,webpack.demo.js可以看到webpack是如何处理md文件的,md后缀的文件先通过md-loader处理后,在通过vue-loader处理 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)

首先找到md-loader的入口文件index.js传送门, 里面的关键代码如下:

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 md = require('./config');中的文件传送门, 看这个文件的内容再结合package.json里面的依赖,我们可以得知markdown的解析依赖这些开发包markdown-it(markdown解析器), markdown-it-anchor(锚点插件,用于段落跳转), markdown-it-chain(用于链式调用,参考webpack-chain) , markdown-it-container(自定义包裹元素插件识别remarkdown 语法:::)

config.js

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;

containers.js

// ...
    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 的字符串
    }
// ...    

通过上述的代码功能主要为 把 :::demo xxxx code ::: 转换成 <demo-block> xxx <!--element-demo: code(源码) :element-demo--> code(转义后的代码) </demo-block>

vue demo component是如何生成的

我看md-loader的入口文件index.js传送门里有这么一段代码, let demoComponentContent = genInlineComponentText(html, script);, 这段代码的功能就是生成demo component字符串, 我们可以跳转到util.js看到如下内容:


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; // 返回字符串
}

上面的代码主要使用了@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代码。

// 第一步 
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}
  `;