Element的markdown-loader源码解析

Element md-loader源码地址html

为什么要抄md-loader

最近打算重写本身组件库的官网,在写展现组件部分的时候遇到了一个问题,全部组件的功能展现都在一个.vue文件里面写的话,会很麻烦。若是只用简单的md就能够转成须要的页面而且有代码高亮、demo展现框和页面样式,那该多好。vue

转换逻辑

事先修改webpack配置

module: {
        rules: [
            //.....
            {
                test: /\.md$/,
                use: [
                    {
                        loader: 'vue-loader',
                        options: {
                            compilerOptions: {
                                preserveWhitespace: false
                            }
                        }
                    },
                    {
                        loader: path.resolve(__dirname, './md-loader/index.js')
                    }
                ]
            },
        ]
    },
复制代码

Element md-loader 目录

目录 大致功能
index.js 入口文件
config.js markdown-it的配置文件
containers.js render添加自定义输出配置
fence 修改fence渲染策略
util 一些处理解析md数据的函数

md-loader须要完成的功能

先看看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>
 
:::
(```)
复制代码
  • 功能1

把.md文件解析出来包成一个.vue。git

  • 功能2

代码展现和实例展现都须要在一个卡片里面,卡片能够展开关闭github

  • 功能3

一套code两个用途,一个是做为示例展现,一个是做为示例代码(代码高亮),也就是只写一套代码就够了 web

  • 功能4 锚点

好了,下面就是一步步搞懂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) {}
复制代码

首先这个函数接受两个参数 templatescript,他们由各自对应的处理函数处理的, 数据来源即是一开始就解析出来的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里面的数据
  • 抽离templatescript,经过插件编译成render Functioon,利用他建立组件。

如今的问题是一个content里面包含着全部的demo的数据,如何分辨并对每一个demo作以上的操做,并且components里面的组件名字是不能重名的,如何解决这一点。

给每一个Demo打上‘标记’

首先须要下载依赖

yarn add markdown-it-container -D
复制代码

markdown-it-container地址

直接看源码

文档示例

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)) //相当重要的一步,后面会讲
复制代码

源码这一段有点多,首先须要搞懂各个变量的做用

  • demoComponentNameid,命名每一个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 FunctioncomponenetsString的结构相似下面的代码
`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是默认的插槽,展现代码也是具名可是由于须要代码高亮就复杂了一点。

至此,Elementmd-loader的大部分代码都及功能都看完了,感谢Element团队贡献出的源码,让我获益良多。

但愿能对你理解源码有帮助,若是有什么不对的地方,欢迎批评指正。

相关文章
相关标签/搜索