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團隊貢獻出的源碼,讓我獲益良多。

但願能對你理解源碼有幫助,若是有什麼不對的地方,歡迎批評指正。

相關文章
相關標籤/搜索