趁storybook還沒支持vue3 來擼個本身的md-loader

簡述:爲了保證各人羣的觀感,本文一共分三大塊,分別對應了詳細的分析+開發過程,只有代碼版本以及避坑大賞,能夠自行跳轉各取所需(畢竟詳細版本實在太長了)。css

詳細版本

element3做爲組件庫,其核心做用就是讓用戶能夠根據文檔正確使用組件,因此文檔天然也應是重點之一。html

既然是文檔,那麼做爲能夠有組織與高效的快速構建UI組件的storybook必然是做爲首選。可是好巧不巧storybook如今並沒有法用於vue3的文檔構建,我收到兩種說法但基本都是保證在三月份以前是能夠支持的。因此就趁着storybook還不支持,再來聊聊實現文檔的另外一套方案,也是當前還在使用的方案。vue

經過markdown-it編寫md-loader實現對md文件的解析。node

那咱們不妨本身搞一個element3裏面真實在使用的md-loader。不過在開始編寫md-loader以前咱們確定先要知道markdown-it的使用方式。webpack

markdown-it

markdown-it自己只是用於解析md語法,其導出一個函數,該函數既能夠做爲構造函數經過new進行建立實例,也能夠直接做爲普通函數調用並返回實例。建立出的實例包含一個叫作render的方法,這個方法就是markdown-it的核心方法。該方法可將markdown語法解析爲html標籤並使其能夠在頁面中正常渲染。git

// markdown-it/lib/index.js
function MarkdownIt(presetName, options) {
  if (!(this instanceof MarkdownIt)) {
    return new MarkdownIt(presetName, options);
  }
  ...
}

const Markdown = require('markdown-it')
const md = Markdown() // const md = new Markdown()效果相同
const content = md.render('## 這是一個二級標題')
console.log(content) // <h2>這是一個二級標題</h2>
複製代碼

可是咱們是要用於文檔使用,就顯得太不夠用了。因此在這裏咱們要再瞭解兩個插件。github

markdown-it-chain

首先是markdown-it-chain,這個插件是一個輔助插件。其做用等效於webpack-chain,也就是讓markdown-it支持鏈式操做。web

// 引入markdown-it-chain模塊,該模塊導出一個用於建立配置的構造函數。
const Config = require('markdown-it-chain')

// 經過new進行實例化獲得配置實例
const config = new Config()

// 將配置結構改成鏈式操做
// 全部API調用時都將追蹤到被存儲的配置並將其改變
config
  // 做用於new Markdown時的options配置
  // Ref: https://markdown-it.github.io/markdown-it/#MarkdownIt.new
  .options
    .html(true) // 等同於 .set('html', true)
    .linkify(true)
    .end()

  // 做用於'plugins'
  .plugin('toc')
    // 第一個參數是插件模塊,能夠是一個函數
    // 第二個參數是該插件所接收的參數數組
    .use(require('markdown-it-table-of-contents'), [{
      includeLevel: [2, 3]
    }])
    // 和JQuery中的.end()相似
    .end()

  .plugin('anchor')
    .use(require('markdown-it-anchor'), [{
      permalink: true,
      permalinkBefore: true,
      permalinkSymbol: '$'
    }])
    // 在toc以前接受插件
    .before('toc')

// 使用上面的配置建立Markdown實例
const md = config.toMd()
md.render('[[TOC]] \n # h1 \n ## h2 \n ## h3 ')
複製代碼

markdown-it-container

第二個插件能夠說是md-loader的第一個重點了。這個插件是markdown-it-container,其用於建立可被markdown-it解析的自定義塊級容器。ajax

::: warning
*here be dragons*
:::
複製代碼

這就是一個塊級容器,若是咱們沒有給予它一個渲染器,那麼它會被默認解析成下面這樣vue-router

<div class="warning">
<em>here be dragons</em>
</div>
複製代碼

不過這麼說確定仍是沒有辦法理解這是什麼意思,因此咱們上代碼實例。

const md = require('markdown-it')();

md.use(require('markdown-it-container'), 'spoiler', {
	// validate爲校驗方法 須要返回布爾值 爲true時則校驗成功
  validate: function(params) {
    // params爲:::後面的內容 能夠理解爲:::後面的內容均爲參數
    return params.trim().match(/^spoiler\s+(.*)$/);
  },
  // 渲染函數 根據返回值進行渲染
  render: function (tokens, index) {
    // token數組 包含全部解析出來的token 大體分爲起始標籤、結束標籤和內容 它長下面這樣
    /* [ Token { type: 'container_spoiler_open', tag: 'div', attrs: null, map: [ 0, 2 ], nesting: 1, level: 0, children: null, content: '', markup: ':::', info: ' spoiler click me', meta: null, block: true, hidden: false }, Token { type: 'paragraph_open', tag: 'p', attrs: null, map: [ 1, 2 ], nesting: 1, level: 1, children: null, content: '', markup: '', info: '', meta: null, block: true, hidden: false }, Token { type: 'inline', tag: '', attrs: null, map: [ 1, 2 ], nesting: 0, level: 2, children: [ [Token], [Token], [Token] ], content: '*content*', markup: '', info: '', meta: null, block: true, hidden: false }, Token { type: 'paragraph_close', tag: 'p', attrs: null, map: null, nesting: -1, level: 1, children: null, content: '', markup: '', info: '', meta: null, block: true, hidden: false }, Token { type: 'container_spoiler_close', tag: 'div', attrs: null, map: null, nesting: -1, level: 0, children: null, content: '', markup: ':::', info: '', meta: null, block: true, hidden: false } ] */
    console.log(tokens, 'tokens')
    // 當前token對應下標 只會是塊起始與結束標籤對應下標
    console.log(index, 'index')
    /* 匹配結果: [ 'demo click me', 'click me', index: 0, input: 'demo click me', groups: undefined ] */
    const m = tokens[index].info.trim().match(/^spoiler\s+(.*)$/);

    if (tokens[index].nesting === 1) {
      // opening tag
      return '<details><summary>' + md.utils.escapeHtml(m[1]) + '</summary>\n';
    } else {
      // closing tag
      return '</details>\n';
    }
  }
});

console.log(md.render('::: spoiler click me\n*content*\n:::\n'));

// 輸出:
// <details><summary>click me</summary>
// <p><em>content</em></p>
// </details>
複製代碼

正式開始

接下來咱們建立一個markdown-loader文件夾,進入目錄執行yarn init初始化package.json文件,同時再建立一個包含index.jsconfig.js文件的src目錄

├── src
│ ├── config.js
│ └── index.js
└── package.json
複製代碼

接下來咱們在config.js文件當中書寫配置文件並將生成的md實例經過module.exports導出

// src/config.js
const Config = require('markdown-it-chain')

const config = new Config()

config
  .options
    .html(true)
    .end()

const md = config.toMd()

module.exports = md

複製代碼

而後咱們在index.js中 導入md實例而且調用render方法先嚐試渲染一個二級標題試試

// src/index.js
const md = require('./config.js')

console.log(md.render('## 二級標題'))
複製代碼

結果咱們居然收穫了一個報錯?

$ node src/index
/Users/zhangyuxuan/Desktop/for github/markdown-loader/node_modules/markdown-it-chain/src/index.js:38
    return plugins.reduce((md, { plugin, args }) => md.use(plugin, ...args), md)
                   ^

TypeError: Cannot read property 'reduce' of undefined
    at MarkdownItChain.toMd (/Users/zhangyuxuan/Desktop/for github/markdown-loader/node_modules/markdown-it-chain/src/index.js:38:20)
    at Object.<anonymous> (/Users/zhangyuxuan/Desktop/for github/markdown-loader/src/config.js:9:19)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Module.require (internal/modules/cjs/loader.js:952:19)
    at require (internal/modules/cjs/helpers.js:88:18)
    at Object.<anonymous> (/Users/zhangyuxuan/Desktop/for github/markdown-loader/src/index.js:1:12)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
複製代碼

看到這個報錯讓我無語了一陣,由於我明確我沒有什麼使用上的問題纔對。沒辦法,我只好追到源碼裏面去。通過個人一陣探索,我迷茫了。我確實找到了bug的緣由,但也就是由於我知道了緣由才真正致使了個人迷茫。

還記得前面說markdown-it-chain的使用的時候,是包含config.plugin('toc')這樣一段配置插件的代碼。我萬萬沒想到,這個插件是必需要有的。由於源代碼裏面並無配置不傳入插件的配置,那麼咱們就有了兩條路,要麼改源碼,要麼加上插件,很顯然咱們要選擇最簡單的那條路。加個插件不就行了,原本咱們也是要使用markdown-it-container的。

// src/config.js
const Config = require('markdown-it-chain')

const config = new Config()

config
	.options
		.html(true)
		.end()
	.plugin('containers')
		.use(mdContainer, ['warning'])

const md = config.toMd()

module.exports = md
複製代碼

這樣就配置好了,可是你品,咱們咋麼可能用一個warning就能完成咱們的文檔展現。因此咱們如今要開始很重要的一步,咱們在src目錄下新建一個containers.js文件,咱們將會在這裏自定義咱們真正須要的塊級容器。

編寫containers.js

由於咱們後續是要將containers做爲插件直接傳入plugin.use()中,因此這裏咱們須要經過module.exports直接導出一個函數,函數接收md實例做爲參數。由於後續咱們還須要保留warning和tip兩種塊級容器,因此記得調用md.use將兩種塊級容器掛載。

// src/containers.js
const mdContainer = require('markdown-it-container')

module.exports = function(md) {
	// 這裏保留warning和tip 這兩個文檔裏面隨時可能會用到
  md.use(mdContainer, 'warning')
  md.use(mdContainer, 'tip')
}
複製代碼

同時咱們還要進行對咱們來講最重要的demo容器的編寫。首先咱們在validator方法中須要校驗的是demo字段這個是已經明確的,不過應該會有小夥伴以爲以前的判斷方法是會複雜一些,咱們其實能夠直接使用RegExp.prototype.test方法進行判斷就行了,而且test方法自己返回的就是布爾值。接下來咱們就只須要把目光聚焦在render函數的編寫就好。

// src/containers.js
const mdContainer = require('markdown-it-container')

module.exports = function(md) {
  md.use(mdContainer, 'demo', {
    validator(params) {
      return /^demo\s*(.*)$/.test(params)
    }
  })
	// 這裏保留warning和tip 這兩個文檔裏面隨時可能會用到
  md.use(mdContainer, 'warning')
  md.use(mdContainer, 'tip')
}
複製代碼

根據上面的實例咱們能夠知道,咱們能夠經過render函數中的匹配返回結果m[1]拿到demo後面的內容,那麼咱們就能夠把這段文字做爲當前demo的描述。咱們先來進行起始標籤的編寫,上面的示例中咱們已經知道,其實標籤的判斷方法就是來判斷token[index].nesting === 1。因此首先咱們加上這個判斷,並在其中聲明一個description常量,這就是咱們上面所提到的demo的描述。咱們須要判斷咱們是否成功匹配到了demo,若是匹配成功而且他的第1位存在,咱們就是用m[1]做爲描述,不然取空。因此咱們的description應該是這樣的:const description = m?.[1] || ''

// src/containers.js
const mdContainer = require('markdown-it-container')

module.exports = function(md) {
  md.use(mdContainer, 'demo', {
    validator(params) {
      return /^demo\s*(.*)$/.test(params)
    },
    render(tokens, index) {
      const m = tokens[index].info.trim().match(/^demo\s+(.*)$/)
      if (tokens[index].nesting === 1) {
        const description = m?.[1] || ''
      }
    }
  })
	// 這裏保留warning和tip 這兩個文檔裏面隨時可能會用到
  md.use(mdContainer, 'warning')
  md.use(mdContainer, 'tip')
}
複製代碼

有了描述,咱們天然是須要把它渲染出來,可是咱們還須要思考一個問題。在真實文檔中的demo是會被實際渲染成組件的,因此最終咱們要真是渲染出一個vue模板才能夠,那麼咱們在render渲染的標籤上就要作些手腳。咱們先用一個div標籤讓他做爲自定義標籤正常的渲染出來,而後在其內部,添加一個div,在div當中展現咱們的描述信息。

// src/containers.js
const mdContainer = require('markdown-it-container')

module.exports = function(md) {
  md.use(mdContainer, 'demo', {
    validator(params) {
      return /^demo\s*(.*)$/.test(params)
    },
    render(tokens, index) {
      const m = tokens[index].info.trim().match(/^demo\s+(.*)$/)
      if (tokens[index].nesting === 1) {
        const description = m?.[1] || ''
        return ` <div> <div>${md.render(description)}</div> `
      }
    }
  })
	// 這裏保留warning和tip 這兩個文檔裏面隨時可能會用到
  md.use(mdContainer, 'warning')
  md.use(mdContainer, 'tip')
}
複製代碼

如今咱們有了起始標籤,只須要在簡單的返回一個結束標籤就能夠看一下渲染結果了。

// src/containers.js
const mdContainer = require('markdown-it-container')

module.exports = function(md) {
  md.use(mdContainer, 'demo', {
    validator(params) {
      return /^demo\s*(.*)$/.test(params)
    },
    render(tokens, index) {
      const m = tokens[index].info.trim().match(/^demo\s+(.*)$/)
      if (tokens[index].nesting === 1) {
        const description = m?.[1] || ''
        return ` <div> <div>${md.render(description)}</div> `
      }
      return `</div>`
    }
  })
	// 這裏保留warning和tip 這兩個文檔裏面隨時可能會用到
  md.use(mdContainer, 'warning')
  md.use(mdContainer, 'tip')
}

console.log(md.render('::: demo click me\n*content*\n:::\n'))
// 輸出結果
// <div>
// <div><p>click me</p></div>
// <p><em>content</em></p>
// </div>
複製代碼

渲染真實文檔內容

先別急,雖然如今已經能夠渲染出這部份內容了,可是若是咱們用一個真實的md文檔當中的代碼來試一試呢。

## Button 按鈕

經常使用的操做按鈕。

### 基礎用法

基礎的按鈕用法。

:::demo 使用`type``plain``round``circle`屬性來定義 Button 的樣式。

​```html <template> <el-row> <el-button>默認按鈕</el-button> <el-button type="primary">主要按鈕</el-button> <el-button type="success">成功按鈕</el-button> <el-button type="info">信息按鈕</el-button> <el-button type="warning">警告按鈕</el-button> <el-button type="danger">危險按鈕</el-button> </el-row> <el-row> <el-button plain>樸素按鈕</el-button> <el-button type="primary" plain>主要按鈕</el-button> <el-button type="success" plain>成功按鈕</el-button> <el-button type="info" plain>信息按鈕</el-button> <el-button type="warning" plain>警告按鈕</el-button> <el-button type="danger" plain>危險按鈕</el-button> </el-row> <el-row> <el-button round>圓角按鈕</el-button> <el-button type="primary" round>主要按鈕</el-button> <el-button type="success" round>成功按鈕</el-button> <el-button type="info" round>信息按鈕</el-button> <el-button type="warning" round>警告按鈕</el-button> <el-button type="danger" round>危險按鈕</el-button> </el-row> <el-row> <el-button icon="el-icon-search" circle></el-button> <el-button type="primary" icon="el-icon-edit" circle></el-button> <el-button type="success" icon="el-icon-check" circle></el-button> <el-button type="info" icon="el-icon-message" circle></el-button> <el-button type="warning" icon="el-icon-star-off" circle></el-button> <el-button type="danger" icon="el-icon-delete" circle></el-button> </el-row> </template> ​```

:::
複製代碼

咱們仍是來打印經過md.render渲染上面這段md的結果

<demo-block>
  <div><p>使用<code>type</code><code>plain</code><code>round</code><code>circle</code>屬性來定義 Button 的樣式。</p></div>
  <pre><code class="language-html">&lt;template&gt; &lt;el-row&gt; &lt;el-button&gt;默認按鈕&lt;/el-button&gt; &lt;el-button type=&quot;primary&quot;&gt;主要按鈕&lt;/el-button&gt; &lt;el-button type=&quot;success&quot;&gt;成功按鈕&lt;/el-button&gt; &lt;el-button type=&quot;info&quot;&gt;信息按鈕&lt;/el-button&gt; &lt;el-button type=&quot;warning&quot;&gt;警告按鈕&lt;/el-button&gt; &lt;el-button type=&quot;danger&quot;&gt;危險按鈕&lt;/el-button&gt; &lt;/el-row&gt; &lt;el-row&gt; &lt;el-button plain&gt;樸素按鈕&lt;/el-button&gt; &lt;el-button type=&quot;primary&quot; plain&gt;主要按鈕&lt;/el-button&gt; &lt;el-button type=&quot;success&quot; plain&gt;成功按鈕&lt;/el-button&gt; &lt;el-button type=&quot;info&quot; plain&gt;信息按鈕&lt;/el-button&gt; &lt;el-button type=&quot;warning&quot; plain&gt;警告按鈕&lt;/el-button&gt; &lt;el-button type=&quot;danger&quot; plain&gt;危險按鈕&lt;/el-button&gt; &lt;/el-row&gt; &lt;el-row&gt; &lt;el-button round&gt;圓角按鈕&lt;/el-button&gt; &lt;el-button type=&quot;primary&quot; round&gt;主要按鈕&lt;/el-button&gt; &lt;el-button type=&quot;success&quot; round&gt;成功按鈕&lt;/el-button&gt; &lt;el-button type=&quot;info&quot; round&gt;信息按鈕&lt;/el-button&gt; &lt;el-button type=&quot;warning&quot; round&gt;警告按鈕&lt;/el-button&gt; &lt;el-button type=&quot;danger&quot; round&gt;危險按鈕&lt;/el-button&gt; &lt;/el-row&gt; &lt;el-row&gt; &lt;el-button icon=&quot;el-icon-search&quot; circle&gt;&lt;/el-button&gt; &lt;el-button type=&quot;primary&quot; icon=&quot;el-icon-edit&quot; circle&gt;&lt;/el-button&gt; &lt;el-button type=&quot;success&quot; icon=&quot;el-icon-check&quot; circle&gt;&lt;/el-button&gt; &lt;el-button type=&quot;info&quot; icon=&quot;el-icon-message&quot; circle&gt;&lt;/el-button&gt; &lt;el-button type=&quot;warning&quot; icon=&quot;el-icon-star-off&quot; circle&gt;&lt;/el-button&gt; &lt;el-button type=&quot;danger&quot; icon=&quot;el-icon-delete&quot; circle&gt;&lt;/el-button&gt; &lt;/el-row&gt; &lt;/template&gt; </code></pre>
</demo-block>
複製代碼

不能說離譜吧,反正是毫無頭緒。就算咱們大概能猜到他是把標籤和引號所有經過轉移字符串進行了替換,也沒有辦法分析出來什麼。

因此接下來咱們要作的事情,就是把loader用起來,用到vue項目裏面去。不過由於storybook就快支持vue3了,我們也不搞什麼複雜的東西了。直接建立一個vue項目,而後把咱們的md-loader包丟進去。以後再在vue.config.js文件裏面配置一下使用loader就行了。

// vue.config.js
const path = require('path')

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('md2vue')
      .test(/\.md$/)
      .use('vue-loader')
      .loader('vue-loader')
      .end()
      .use('md-loader')
      .loader(path.resolve(__dirname, 'src/md-loader/index.js'))
      .end()
  }
}
複製代碼

接下來咱們只須要再稍微配置一下路由,把咱們的md文件渲染到頁面上便可,像這樣

// router/index
import { createRouter, createWebHashHistory } from 'vue-router'

const Button = () => import('../docs/button.md')

const routes = [
  {
    path: '/',
    name: 'button',
    component: Button
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router
複製代碼

哦,忽然發現忘了一件事,咱們還須要在index.js裏面正確導出咱們的loader才能夠,很簡單的

// src/index.js
module.exports = source => {
  return `<template> ${md.render(source)} </template>`
}
複製代碼

如今沒問題了,咱們只須要把vue項目啓動,就能夠看到md渲染出來的結果了。

什麼?你報錯了?是否是vue-loader忘了安裝?你安裝了?仍是報錯?

那,報錯信息是否是parseComponent is not defined?這塊很是噁心人,vue-loader版本必定要是16.0.0以上的,不然就會出現這個錯誤,而且安裝的時候默認會是15.9.6版本(就使人迷惑的一波)。如今,是否是能夠訪問到頁面了。

image-20210225110213533.png

看見這一坨,令我欣慰的是他確實渲染成功了,可是令我頭痛的是這也長得太難看了點。因此咱們如今的目標很明確,有三點:第一,咱們要把代碼高亮;第二,咱們要讓demo做爲組件能夠展現出來;第三,給這個頁面加點樣式,就像element3似的那種收縮。

代碼高亮 highlight.js

咱們先來處理代碼高亮的問題,這個相對比較好解決,就是使用highlight.js來實現就行了,markdown-it 自己也是支持經過它來實現代碼高亮的

// src/config.js
const hljs = require('highlight.js')
const highlight = (str, lang) => {
  if (!lang || !hljs.getLanguage(lang)) {
    return `<pre><code class="hljs">${str}</code></pre>`
  }
  const html = hljs.highlight(lang, str, true).value
  return `<pre><code class="hljs language-${lang}">${html}</code></pre>`
}

config.options
  .html(true)
  .highlight(highlight)
  .end()
  .plugin('containers')
  .use(containers)
  .end()

// public/index.html
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/default.min.css'>
複製代碼

這個就是highlight.js的用法,須要關注的其實也就是hljs.getLanguagehljs.highlight。前者是獲取語言種類,也就是當前高亮代碼是哪一種語言,後者就是對代碼進行高亮處理了。咱們如今再來看一下效果,可能,你會發現沒有效果。具體是什麼緣由我也還不太清楚,可是玩這個loader的時候,最好在package.json裏面加個自定義的腳本"clean": "rm -rf node_modules && yarn cache clean",刪掉依賴清除緩存而後從新安裝一下依賴,大多問題就都解決了。

image-20210225114809343.png

看完這個效果,我屬實也是不太淡定,咱們仍是把public/index.html裏面的樣式刪掉後面咱們本身改吧。怎麼改呢?偷個雞咯,加上<link rel="stylesheet" href="//shadow.elemecdn.com/npm/highlight.js@9.3.0/styles/color-brewer.css"/>,瞬間好看多了。

這樣第一步暫時算是完成了,咱們繼續搞第二步,給demo塊顯示出來。達成這個目的咱們須要先改動一下以前container.js的內容,不過改動並不大,只是把渲染的div改爲一個demo-block組件,後續不少內容咱們都須要在該組件中去編寫。

// src/containers.js render
if (tokens[index].nesting === 1) {
  const description = m?.[1] || ''
  return `<demo-block> <div>${md.render(description)}</div> `
}
return `</demo-block>`
複製代碼

搞個demo-block

暫時先跳出咱們的md-loader,由於接下來咱們要寫vue組件啦,不過我認爲這並非你們關心的點,因此我在這兒直接放出組件的代碼。後續使用到哪一個地方的時候我會加以描述的。

<!-- DemoBlock.vue -->
<template>
  <div class="demo-block">
    <div class="source">
      <slot name="source"></slot>
    </div>
    <div class="meta" ref="meta">
      <div class="description" v-if="$slots.default">
        <slot></slot>
      </div>
      <div class="highlight">
        <slot name="highlight"></slot>
      </div>
    </div>
    <div
      class="demo-block-control"
      ref="control"
      @click="isExpanded = !isExpanded"
    >
      <span>{{ controlText }}</span>
    </div>
  </div>
</template>

<script>
import { ref, computed, watchEffect, onMounted } from 'vue'
export default {
  setup() {
    const meta = ref(null)
    const isExpanded = ref(false)
    const controlText = computed(() =>
      isExpanded.value ? '隱藏代碼' : '顯示代碼'
    )
    const codeAreaHeight = computed(() =>
      [...meta.value.children].reduce((t, i) => i.offsetHeight + t, 56)
    )
    onMounted(() => {
      watchEffect(() => {
        meta.value.style.height = isExpanded.value
          ? `${codeAreaHeight.value}px`
          : '0'
      })
    })

    return {
      meta,
      isExpanded,
      controlText
    }
  }
}
</script>

<style lang="scss">
.demo-block {
  border: solid 1px #ebebeb;
  border-radius: 3px;
  transition: 0.2s;

  &.hover {
    box-shadow: 0 0 8px 0 rgba(232, 237, 250, 0.6),
      0 2px 4px 0 rgba(232, 237, 250, 0.5);
  }

  code {
    font-family: Menlo, Monaco, Consolas, Courier, monospace;
  }

  .demo-button {
    float: right;
  }

  .source {
    padding: 24px;
  }

  .meta {
    background-color: #fafafa;
    border-top: solid 1px #eaeefb;
    overflow: hidden;
    transition: height 0.2s;
  }

  .description {
    padding: 20px;
    box-sizing: border-box;
    border: solid 1px #ebebeb;
    border-radius: 3px;
    font-size: 14px;
    line-height: 22px;
    color: #666;
    word-break: break-word;
    margin: 10px;
    background-color: #fff;

    p {
      margin: 0;
      line-height: 26px;
    }

    code {
      color: #5e6d82;
      background-color: #e6effb;
      margin: 0 4px;
      display: inline-block;
      padding: 1px 5px;
      font-size: 12px;
      border-radius: 3px;
      height: 18px;
      line-height: 18px;
    }
  }

  .highlight {
    pre {
      margin: 0;
    }

    code.hljs {
      margin: 0;
      border: none;
      max-height: none;
      border-radius: 0;

      &::before {
        content: none;
      }
    }
  }

  .demo-block-control {
    border-top: solid 1px #eaeefb;
    height: 44px;
    box-sizing: border-box;
    background-color: #fff;
    border-bottom-left-radius: 4px;
    border-bottom-right-radius: 4px;
    text-align: center;
    margin-top: -1px;
    color: #d3dce6;
    cursor: pointer;
    position: relative;

    i {
      font-size: 16px;
      line-height: 44px;
      transition: 0.3s;
      &.hovering {
        transform: translateX(-40px);
      }
    }

    > span {
      position: absolute;
      transform: translateX(-30px);
      font-size: 14px;
      line-height: 44px;
      transition: 0.3s;
      display: inline-block;
    }

    &:hover {
      color: #409eff;
      background-color: #f9fafc;
    }

    & .text-slide-enter,
    & .text-slide-leave-active {
      opacity: 0;
      transform: translateX(10px);
    }

    .control-button {
      line-height: 26px;
      position: absolute;
      top: 0;
      right: 0;
      font-size: 14px;
      padding-left: 5px;
      padding-right: 25px;
    }
  }
} 
.hljs {
  line-height: 1.8;
  font-family: Menlo, Monaco, Consolas, Courier, monospace;
  font-size: 12px;
  padding: 18px 24px;
  background-color: #fafafa;
  border: solid 1px #eaeefb;
  margin-bottom: 25px;
  border-radius: 4px;
  -webkit-font-smoothing: auto;
}
</style>
複製代碼

記得在main.jsDemoBlock註冊爲組件。這時候咱們再來看一下效果。

image-20210304143446684.png

很明顯如今就剩下兩個問題了。第一,上面的source也就是咱們會真實被渲染出來的demo尚未;第二,description展現的很好,可是下面代碼並無渲染出來。那麼咱們如今就搞定這兩點,先來搞定代碼展現,畢竟這個比較簡單嘛。

說到代碼展現,咱們須要回到demo-block組件看一下,這裏放了一個具名插槽highlight,這個插槽就是爲了後續渲染展現代碼使用的。因此咱們能夠先明確一點,就是咱們須要在展現代碼外面,加上對應的template #highlight

替換fence渲染規則

要作到這點仍是簡單的,還記得以前咱們打印tokens的時候見過一個type: fencetoken麼?fence直譯柵格,其實就是md語法的```也就是代碼塊,咱們其實就是要修改它的渲染規則。你說巧不巧,在markdown-it中暴露出了修改方法md.renderer.rules.fence。因此咱們只要有md實例就能夠進行修改了。

那咱們找個簡單的途徑,搞個函數傳參進去不就行了。

// fence.js 覆蓋默認的 fence 渲染策略
module.exports = md => {

}

複製代碼

接下來咱們就開始編寫覆蓋渲染的邏輯,首先咱們要了解的是md.renderer.rules.fence是一個函數,他一共須要接受五個參數tokens, idx, options, env, slf。第一二個參數你們應該已經很熟悉了,第三個參數應該也有些許印象,它其實就是md的options配置。從第四個參數應該會較爲陌生,env,這貨我也不知道是幹嗎的,由於fence裏面根本用不上它,咱們要用的是slf,可是函數嘛,你也懂。這最後一個參數slf其實就是renderer實例。

看到這兒是否是還挺迷茫的,其實我想告訴你,這五個參數咱們只須要關心前兩個,由於後三個參數是咱們爲了相對簡單,要保留原有的渲染邏輯而必須傳進去的參數(有沒有很絕望,反正這三個我們是暫時用不上了)。

// fence.js 覆蓋默認的 fence 渲染策略
module.exports = md => {
	const defaultRender = md.renderer.rules.fence
  md.renderer.rules.fence = (tokens, idx, options, env, self) => {
    
  }
}
複製代碼

這部分渲染邏輯改寫其實蠻簡單的,咱們只須要判斷一下咱們當前這個fence是不是在一個自定義塊容器當中,因此咱們只須要獲取一下當前index前一位來判斷一下就行了

// fence.js 覆蓋默認的 fence 渲染策略
module.exports = md => {
	const defaultRender = md.renderer.rules.fence
  md.renderer.rules.fence = (tokens, idx, options, env, self) => {
    const token = tokens[idx]
    // 判斷該 fence 是否在 :::demo 內
    const prevToken = tokens[idx - 1]
    // 前面提過nesting === 1爲起始標籤,若是同時符合demo的正則匹配代表它是咱們的目標fence
    const isInDemoContainer =
      prevToken &&
      prevToken.nesting === 1 &&
      /^demo\s*(.*)$/.test(prevToken.info)
  }
}
複製代碼

最後咱們只須要再判斷一下咱們的目標fence是否爲html語言的就行了,咱們就能夠對原有內容進行改寫了,最後記得調用咱們的defaultRender渲染一下

// fence.js 覆蓋默認的 fence 渲染策略
module.exports = md => {
	const defaultRender = md.renderer.rules.fence
  md.renderer.rules.fence = (tokens, idx, options, env, self) => {
    const token = tokens[idx]
    // 判斷該 fence 是否在 :::demo 內
    const prevToken = tokens[idx - 1]
    // 前面提過nesting === 1爲起始標籤,若是同時符合demo的正則匹配代表它是咱們的目標fence
    const isInDemoContainer =
      prevToken &&
      prevToken.nesting === 1 &&
      /^demo\s*(.*)$/.test(prevToken.info)
  }
  if (token.info === 'html' && isInDemoContainer) {
    return `<template #highlight><pre v-pre><code class='html'>${md.utils.escapeHtml( token.content )}</code></pre></template>`
  }
  return defaultRender(tokens, idx, options, env, self)
}
複製代碼

接下來只須要在md-loader/src/config.js裏面調用就行了

// md-loader/src/config.js
const overwriteFenceRule = require('./fence')

...

const md = config.toMd()
overwriteFenceRule(md)
module.exports = md
複製代碼

蜜汁bug

若是你正確的按照前面的步驟走到這裏,你必定會發現以前添加的highligh消失了,代碼恢復了最醜的模樣。說實話這個點其實我還蠻迷惑的,由於我極其不解究竟是怎麼回事,我甚至扒到了源碼裏面看了下

// markdown-it/lib/renderer.js
if (options.highlight) {
  highlighted = options.highlight(token.content, langName, langAttrs) || escapeHtml(token.content);
} else {
  highlighted = escapeHtml(token.content);
}
複製代碼

這段代碼在不替換fence渲染邏輯的狀況下一定會被執行,這也就是以前代碼高亮變得很是好看的緣由。

但是替換了fence渲染邏輯以後,這段代碼好似沒法執行了,甚至不論怎麼通關斷點調試都沒法證實此段代碼有被執行。沒辦法,咱們只能換個方式進行處理了。highligh.js做爲一個代碼高亮的插件,它是一個至關完善的插件。最完善的點,就是他對多種環境均有支持,因此,咱們不妨用在vue組件內直接經過highligh進行處理

// md-loader/src/index.js
module.exports = source => {
  const content = md.render(source)
  return ` <template> <section class='content element-doc'> ${content} </section> </template> <script> import hljs from 'highlight.js' export default { mounted(){ this.$nextTick(()=>{ const blocks = document.querySelectorAll('pre code:not(.hljs)') Array.prototype.forEach.call(blocks, hljs.highlightBlock) }) } } </script> `
}
複製代碼

好了,如今它又回到原來漂漂亮亮的樣子了,不過這段代碼,後面咱們會改的,稍後再說。

接近尾聲

如今咱們就差最後一個最麻煩的步驟,咱們就成功啦!這最難實現的也就是demo塊中將組件真實渲染出來。

咱們先來打印一下咱們的content

<h2>Button 按鈕</h2>
<p>經常使用的操做按鈕。</p>
<h3>基礎用法</h3>
<p>基礎的按鈕用法。</p>
<demo-block>
  <div>
    <p>使用
      <code>type</code><code>plain</code><code>round</code><code>circle</code>
      屬性來定義 Button 的樣式。
    </p>
  </div>
  <template #highlight>
    <pre v-pre>
    	<code class='html'>
        &lt;template&gt;
          &lt;el-row&gt;
            &lt;el-button&gt;默認按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;primary&quot;&gt;主要按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;success&quot;&gt;成功按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;info&quot;&gt;信息按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;warning&quot;&gt;警告按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;danger&quot;&gt;危險按鈕&lt;/el-button&gt;
          &lt;/el-row&gt;

          &lt;el-row&gt;
            &lt;el-button plain&gt;樸素按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;primary&quot; plain&gt;主要按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;success&quot; plain&gt;成功按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;info&quot; plain&gt;信息按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;warning&quot; plain&gt;警告按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;danger&quot; plain&gt;危險按鈕&lt;/el-button&gt;
          &lt;/el-row&gt;

          &lt;el-row&gt;
            &lt;el-button round&gt;圓角按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;primary&quot; round&gt;主要按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;success&quot; round&gt;成功按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;info&quot; round&gt;信息按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;warning&quot; round&gt;警告按鈕&lt;/el-button&gt;
            &lt;el-button type=&quot;danger&quot; round&gt;危險按鈕&lt;/el-button&gt;
          &lt;/el-row&gt;

          &lt;el-row&gt;
            &lt;el-button icon=&quot;el-icon-search&quot; circle&gt;&lt;/el-button&gt;
            &lt;el-button type=&quot;primary&quot; icon=&quot;el-icon-edit&quot; circle&gt;&lt;/el-button&gt;
            &lt;el-button type=&quot;success&quot; icon=&quot;el-icon-check&quot; circle&gt;&lt;/el-button&gt;
            &lt;el-button type=&quot;info&quot; icon=&quot;el-icon-message&quot; circle&gt;&lt;/el-button&gt;
            &lt;el-button type=&quot;warning&quot; icon=&quot;el-icon-star-off&quot; circle&gt;&lt;/el-button&gt;
            &lt;el-button type=&quot;danger&quot; icon=&quot;el-icon-delete&quot; circle&gt;&lt;/el-button&gt;
          &lt;/el-row&gt;
        &lt;/template&gt;
			</code>
    </pre>
  </template>
</demo-block>
複製代碼

emmmm,我敢說就靠上面這個東西寫渲染要麻煩死哦,那咱們來給本身省點事兒怎麼樣?若是咱們能想辦法搞一份沒有通過處理的template,而且咱們給它一個特定的標識,咱們是否是就能省不少事呢?

按照這樣的思路,回到咱們的md-loader/src/containers.js裏面,回憶一下咱們以前打印過的tokens。當nesting===1的時候,他的下一個token是否是就是type爲fence的那一個?

// md-loader/src/containers.js
render(tokens, idx) {
  const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/)
  if (tokens[idx].nesting === 1) {
    const description = m?.[1] || ''
    // console.log(description, 'description')
    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>`
}
複製代碼

有了標記之後咱們再回到index.js裏面完成對代碼的解析。如今咱們就要用上以前咱們添加的標記了

// md-loader/src/index.js
const md = require('./config.js')

module.exports = source => {
  // 聲明標記的開始與結束以及長度 後續咱們要使用它來對代碼進行切割
	const commentStart = '<!--element-demo:'
  const commentStartLen = commentStart.length
  const commentEnd = ':element-demo-->'
  const commentEndLen = commentEnd.length
  // 有了起始和結束的標誌 咱們就能夠拿到真實代碼的起始位置與結束位置了
  let demoStart = content.indexOf(commentStart)
  let demoEnd = content.indexOf(commentEnd, demoStart + commentStartLen)
  // 根據起始與結束位置獲取到真實組件代碼部分
  const componentContent = content.slice(demoStart + commentStartLen, demoEnd)
}
複製代碼

剝離模板(template)與腳本(script)

拿到組件代碼以後咱們仍是先暫停再分析一波。咱們有了代碼,怎麼才能讓他渲染出來呢?並且代碼是有可能包含script標籤也有可能沒有的(好比簡單組件示例當中並不須要包含各類響應式數據,單純的進行了展現),那麼假設咱們對templatescript進行一個拆分的話,再丟進index.js中的那個大模板裏面,是否是有點可行?爲此,咱們再搞出來一個utils.js專門用來寫剝離templatescript`的方法

// md-loader/src/utils.js
const stripTemplate = content => {
  // 先對content的先後空格處理一下,以避免後面有什麼影響
  content = content.trim()
  // 若是處理空格以後爲空,直接把這貨返回出去就行了
  if (!content) {
    return content
  }
  // 由於這裏是剝離template,因此咱們直接把script以及style這些無用的標籤去掉
  content = content.replace(/<(script|style)>[\s\S]+<\/1>/g, '').trim()
  // 接下來就是匹配咱們想要的部分
  const res = content.match(/<(template)\s*>([\s\S]+)<\/\1>/)
  // 咱們確定是不想要template標籤的,因此在這裏判斷一下是否匹配到了,若是匹配到的話再對結果去一下空格並返回,不然依然是直接把content返回出去
  return res ? res[2]?.trim() : content
}

const stripScript = content => {
  // 這部分就簡單了,其實就是上面的翻版,咱們只要script就行了
  const res = content.match(/<(script)\s*>([\s\S]+)<\/\1>/)
  return res ? res[2]?.trim() : ''
}
複製代碼

如今咱們就能夠再咱們的index.js當中對content進行處理了,不過咱們拿到的script標籤內容如今仍是export default {}的形式,而且可能會包含import ... from ...。這個形式並不利於咱們用到index.js當中導出的模板去,因此咱們順便處理一下

// md-loader/src/index.js
const md = require('./config.js')
const { stripTemplate, stripScript } = require('./utils.js')

module.exports = source => {
  // 聲明標記的開始與結束以及長度 後續咱們要使用它來對代碼進行切割
	const commentStart = '<!--element-demo:'
  const commentStartLen = commentStart.length
  const commentEnd = ':element-demo-->'
  const commentEndLen = commentEnd.length
  // 有了起始和結束的標誌 咱們就能夠拿到真實代碼的起始位置與結束位置了
  let demoStart = content.indexOf(commentStart)
  let demoEnd = content.indexOf(commentEnd, demoStart + commentStartLen)
  // 根據起始與結束位置獲取到真實組件代碼部分
  const componentContent = content.slice(demoStart + commentStartLen, demoEnd)
  const template = stripTemplate(componentContent)
  let script = stripScript(componentContent)
	script = script.trim()
  if (script) {
    script = script
    	// 將export default 轉成聲明一個變量進行保存
      .replace(/export\s+default/, 'const demoComponentExport =')
      .replace(/import ([,{}\w\s]+) from (['"\w]+)/g, (match, p1, p2) => {
        if (p2 === "'vue'") {
          // 因爲全局使用的vue爲大寫的Vue,因此這裏須要專門處理一下
          return `const ${p1} = Vue`
        }
      })
  } else {
    // 若script標籤內部爲空,直接聲明一個空對象便可
    script = 'const demoComponentExport = {}'
  }
}
複製代碼

如今咱們須要的代碼剝離出來了,可是咱們還須要放回去,並且本來的代碼咱們已經不須要了。因此這裏咱們經過聲明一個output數組,用它來存放咱們真正要輸出出去的內容

// md-loader/src/index.js
const md = require('./config.js')
const { stripTemplate, stripScript } = require('./utils.js')

module.exports = source => {
  // 聲明標記的開始與結束以及長度 後續咱們要使用它來對代碼進行切割
	const commentStart = '<!--element-demo:'
  const commentStartLen = commentStart.length
  const commentEnd = ':element-demo-->'
  const commentEndLen = commentEnd.length
  // 有了起始和結束的標誌 咱們就能夠拿到真實代碼的起始位置與結束位置了
  let demoStart = content.indexOf(commentStart)
  let demoEnd = content.indexOf(commentEnd, demoStart + commentStartLen)
  // 輸出內容
  const output = []
  // 把咱們標記以前的內容push到output中
  output.push(content.slice(0, demoStart))
  // 根據起始與結束位置獲取到真實組件代碼部分
  const componentContent = content.slice(demoStart + commentStartLen, demoEnd)
  const template = stripTemplate(componentContent)
  let script = stripScript(componentContent)
  // 在這裏咱們把剝離出來的template放到以前預留的source插槽中後也push進去
  output.push(`<template #source>${template}</template>`)
  // 同時把標記內容以後的部分也push進去
  output.push(content.slice(demoEnd + commentEndLen))
	script = script.trim()
  if (script) {
    script = script
    	// 將export defalut 轉成聲明一個變量進行保存
      .replace(/export\s+default/, 'const demoComponentExport =')
      .replace(/import ([,{}\w\s]+) from (['"\w]+)/g, (match, p1, p2) => {
        if (p2 === "'vue'") {
          // 因爲全局使用的vue爲大寫的Vue,因此這裏須要專門處理一下
          return `const ${p1} = Vue`
        }
      })
  } else {
    // 若script標籤內部爲空,直接聲明一個空對象便可
    script = 'const demoComponentExport = {}'
  }
  // 在咱們默認導出的對象當中把script的內容展開進去,同時把template當中的content替換爲咱們的output進行輸出
  return ` <template> <section class='content element-doc'> ${output.join('')} </section> </template> <script> import hljs from 'highlight.js' export default { mounted(){ this.$nextTick(()=>{ const blocks = document.querySelectorAll('pre code:not(.hljs)') Array.prototype.forEach.call(blocks, hljs.highlightBlock) }) }, ...script } </script> `
}
複製代碼

好了,咱們成功渲染了,也就是說咱們完成了。可是終歸仍是須要優化一下的,爲何?不要忘了咱們如今才只有一個block,因此就只有一個標記出來的組件內容,若是咱們使用真正的button.md

## Button 按鈕

經常使用的操做按鈕。

### 基礎用法

基礎的按鈕用法。

:::demo 使用`type``plain``round``circle`屬性來定義 Button 的樣式。

```html <template> <el-row> <el-button>默認按鈕</el-button> <el-button type="primary">主要按鈕</el-button> <el-button type="success">成功按鈕</el-button> <el-button type="info">信息按鈕</el-button> <el-button type="warning">警告按鈕</el-button> <el-button type="danger">危險按鈕</el-button> </el-row> <el-row> <el-button plain>樸素按鈕</el-button> <el-button type="primary" plain>主要按鈕</el-button> <el-button type="success" plain>成功按鈕</el-button> <el-button type="info" plain>信息按鈕</el-button> <el-button type="warning" plain>警告按鈕</el-button> <el-button type="danger" plain>危險按鈕</el-button> </el-row> <el-row> <el-button round>圓角按鈕</el-button> <el-button type="primary" round>主要按鈕</el-button> <el-button type="success" round>成功按鈕</el-button> <el-button type="info" round>信息按鈕</el-button> <el-button type="warning" round>警告按鈕</el-button> <el-button type="danger" round>危險按鈕</el-button> </el-row> <el-row> <el-button icon="el-icon-search" circle></el-button> <el-button type="primary" icon="el-icon-edit" circle></el-button> <el-button type="success" icon="el-icon-check" circle></el-button> <el-button type="info" icon="el-icon-message" circle></el-button> <el-button type="warning" icon="el-icon-star-off" circle></el-button> <el-button type="danger" icon="el-icon-delete" circle></el-button> </el-row> </template> ```

:::

### 禁用狀態

按鈕不可用狀態。

:::demo 你可使用`disabled`屬性來定義按鈕是否可用,它接受一個`Boolean`值。

```html <el-row> <el-button disabled>默認按鈕</el-button> <el-button type="primary" disabled>主要按鈕</el-button> <el-button type="success" disabled>成功按鈕</el-button> <el-button type="info" disabled>信息按鈕</el-button> <el-button type="warning" disabled>警告按鈕</el-button> <el-button type="danger" disabled>危險按鈕</el-button> </el-row> <el-row> <el-button plain disabled>樸素按鈕</el-button> <el-button type="primary" plain disabled>主要按鈕</el-button> <el-button type="success" plain disabled>成功按鈕</el-button> <el-button type="info" plain disabled>信息按鈕</el-button> <el-button type="warning" plain disabled>警告按鈕</el-button> <el-button type="danger" plain disabled>危險按鈕</el-button> </el-row> ```

:::

### 文字按鈕

沒有邊框和背景色的按鈕。

:::demo

```html <el-button type="text">文字按鈕</el-button> <el-button type="text" disabled>文字按鈕</el-button> ```

:::

### 圖標按鈕

帶圖標的按鈕可加強辨識度(有文字)或節省空間(無文字)。

:::demo 設置`icon`屬性便可,icon 的列表能夠參考 Element3 的 icon 組件,也能夠設置在文字右邊的 icon ,只要使用`i`標籤便可,可使用自定義圖標。

```html <el-button type="primary" icon="el-icon-edit"></el-button> <el-button type="primary" icon="el-icon-share"></el-button> <el-button type="primary" icon="el-icon-delete"></el-button> <el-button type="primary" icon="el-icon-search">搜索</el-button> <el-button type="primary" >上傳<i class="el-icon-upload el-icon--right"></i ></el-button> ```

:::

### 按鈕組

以按鈕組的方式出現,經常使用於多項相似操做。

:::demo 使用`<el-button-group>`標籤來嵌套你的按鈕。

```html <el-button-group> <el-button type="primary" icon="el-icon-arrow-left">上一頁</el-button> <el-button type="primary" >下一頁<i class="el-icon-arrow-right el-icon--right"></i ></el-button> </el-button-group> <el-button-group> <el-button type="primary" icon="el-icon-edit"></el-button> <el-button type="primary" icon="el-icon-share"></el-button> <el-button type="primary" icon="el-icon-delete"></el-button> </el-button-group> ```

:::

### 加載中

點擊按鈕後進行數據加載操做,在按鈕上顯示加載狀態。

:::demo 要設置爲 loading 狀態,只要設置`loading`屬性爲`true`便可。

```html <el-button type="primary" :loading="true">加載中</el-button> ```

:::

### 不一樣尺寸

Button 組件提供除了默認值之外的三種尺寸,能夠在不一樣場景下選擇合適的按鈕尺寸。

:::demo 額外的尺寸:`medium``small``mini`,經過設置`size`屬性來配置它們。

```html <el-row> <el-button>默認按鈕</el-button> <el-button size="medium">中等按鈕</el-button> <el-button size="small">小型按鈕</el-button> <el-button size="mini">超小按鈕</el-button> </el-row> <el-row> <el-button round>默認按鈕</el-button> <el-button size="medium" round>中等按鈕</el-button> <el-button size="small" round>小型按鈕</el-button> <el-button size="mini" round>超小按鈕</el-button> </el-row> ```

:::

### Attributes

| 參數        | 說明           | 類型    | 可選值                                             | 默認值 |
| ----------- | -------------- | ------- | -------------------------------------------------- | ------ |
| size        | 尺寸           | string  | medium / small / mini                              | —      |
| type        | 類型           | string  | primary / success / warning / danger / info / text | —      |
| plain       | 是否樸素按鈕   | boolean | —                                                  | false  |
| round       | 是否圓角按鈕   | boolean | —                                                  | false  |
| circle      | 是否圓形按鈕   | boolean | —                                                  | false  |
| loading     | 是否加載中狀態 | boolean | —                                                  | false  |
| disabled    | 是否禁用狀態   | boolean | —                                                  | false  |
| icon        | 圖標類名       | string  | —                                                  | —      |
| autofocus   | 是否默認聚焦   | boolean | —                                                  | false  |
| native-type | 原生 type 屬性 | string  | button / submit / reset                            | button |
複製代碼

如今這個量級可就不是在開玩笑了,咱們天然得想點對策處理一波

最後的優化

咱們確定能想到用循環來處理這部分邏輯,可是咱們循環的是誰呢?咱們仔細品一下,以前咱們有一個demoStartdemoEnd做爲組件起始與結束位置對不對?那麼,若是找不到的時候這個值會是-1,咱們只須要找到二者均爲-1的狀況,這個時候必定是再也不包含咱們須要處理的組件邏輯的對不?因此我單獨拿這一部分邏輯出來經過循環搞一下

// md-loader/src/index.js
...
// output確定依然仍是在循環以外
const output = []
// 這裏須要一個start起始index,下面說明爲何須要他
let start = 0
// 由於有多個,後續循環的時候確定須要去改變他的值了,這裏改爲let先
let demoStart = content.indexOf(commentStart)
let demoEnd = content.indexOf(commentEnd, demoStart + commentStartLen)
while(demoStart !== -1 && demoEnd !== -1) {
	// 由於slice是不會改變原字符串的,因此咱們在這裏須要持續改變start及demoStart來保證咱們一直切割的都是從頭/上一個組件結束到下一個組件開始以前的無需處理代碼部分
  output.push(content.slice(start, demoStart))
  // 獲取組件代碼部分確定也要挪進來 兩個剝離方法天然是同理
  const componentContent = content.slice(demoStart + commentStartLen, demoEnd)
  const template = stripTemplate(componentContent)
  let script = stripScript(componentContent)
}
複製代碼

好嘞,當咱們作到script的時候就發現了問題了,咱們總歸不能把這麼多script標籤累加到一個字符串當中吧,那下面簡直就是無法寫了。因此咱們要搞一個操做,就是弄出來一個提取真實組件的方法。該方法直接返回一個能夠做爲組件使用的字符串,而後呢?咱們把這些組件註冊進去不就行了

// md-loader/src/utils.js
// 這裏咱們要返回組件代碼,那template和script是必不可少的
const getRealComponentCode = (template, script) => {
  // 把咱們以前處理script標籤的代碼全都挪進這裏
  script = script.trim()
  if (script) {
    script = script
      // 將export defalut 轉成聲明一個變量進行保存
      .replace(/export\s+default/, 'const demoComponentExport =')
      .replace(/import ([,{}\w\s]+) from (['"\w]+)/g, (match, p1, p2) => {
        if (p2 === "'vue'") {
          // 因爲全局使用的vue爲大寫的Vue,因此這裏須要專門處理一下
          return `const ${p1} = Vue`
        }
      })
  } else {
    // 若script標籤內部爲空,直接聲明一個空對象便可
    script = 'const demoComponentExport = {}'
  }
  // 這裏咱們返回一個自執行函數就行了,後續引入進去的時候會幫咱們把該返回的東西返回出去,若是想問我爲何不用對象,那仔細看代碼你就懂了
  return `(function() { // 不會忘了咱們的script內容是用來聲明變量的吧,搞個對象可咋返回喲 ${script} return { template: \`${template}\`, ...demoComponentExport } })()`
}
複製代碼

好了,如今咱們繼續修改index.js的內容,咱們如今是能夠拿到真實的組件代碼了,那咱們下一步努力天然是把這一堆組件通通註冊到實例當中去

// md-loader/src/index.js
...
// output確定依然仍是在循環以外
const output = []
// 這裏須要一個start起始index,下面說明爲何須要他
let start = 0
// 這裏聲明一個字符串,咱們一會就把註冊用的內容保存到這裏
let componentsString = ''
// 順便聲明一個id,咱們既然有多個組件天然就要區分一下名稱
let id = 0
// 由於有多個,後續循環的時候確定須要去改變他的值了,這裏改爲let先
let demoStart = content.indexOf(commentStart)
let demoEnd = content.indexOf(commentEnd, demoStart + commentStartLen)
while(demoStart !== -1 && demoEnd !== -1) {
	// 由於slice是不會改變原字符串的,因此咱們在這裏須要持續改變start及demoStart來保證咱們一直切割的都是從頭/上一個組件結束到下一個組件開始以前的無需處理代碼部分
  output.push(content.slice(start, demoStart))
  // 獲取組件代碼部分確定也要挪進來 兩個剝離方法天然是同理
  const componentContent = content.slice(demoStart + commentStartLen, demoEnd)
  const template = stripTemplate(componentContent)
  // 由於修改已經放到方法裏了,這裏換成const聲明
  const script = stripScript(componentContent)
  const demoComponent = getRealComponentCode(template, script)
  // 這裏聲明一個名字,經過id進行變化
  const demoComponentName = `element-demo-${id}`
  // 這裏記得push一個自定義組件進去哦
  output.push(`<template #source><${demoComponentName}/></template>`)
  // 這裏纔是真正用來註冊的地方 裏面的demoComponentName須要經過JSON.stringify處理一下,否則後續會識別不了的
  componentsString += ` ${JSON.stringify(demoComponentName)}: ${demoComponent}, `
  // 都搞完以後記得把start那些挨個處理一下
  id++
  start = demoEnd + commentEndLen
  demoStart = content.indexOf(commentStart, start)
  demoEnd = content.indexOf(commentEnd, demoStart + commentStartLen)
}
// 如今咱們的script須要拎出來處理一下了,畢竟要註冊組件了
// pageScript用來存儲後面真實輸出的script標籤
let pageScript = ''
// 若是咱們是有組件的
if (componentsString) {
  // 不要忘記把hljs和vue都引入一下
  pageScript = `<script> import hljs from 'highlight.js' import * as Vue from 'vue' export default { name: 'component-doc', components: { ${componentsString} }, mounted() { // 這裏也不要忘了咱們以前的高亮處理 this.$nextTick(()=>{ const blocks = document.querySelectorAll('pre code:not(.hljs)') Array.prototype.forEach.call(blocks, hljs.highlightBlock) }) } } </script>`
// 這種狀況就是隻有script標籤,基本上就等同是沒有組件代碼的 因此咱們只須要用以前的剝離script方法處理一下就好
} else if (content.indexOf('<script>') === 0) {
  pageScript = stripScript(content)
}
// 這裏是真的真的不要忘記 循環完了之後還有好多東西沒有push到output中呢
output.push(content.slice(start))

return ` <template> <section class="content element-doc"> ${output.join('')} </section> </template> ${pageScript} `
複製代碼

讓咱們跑起來!(指代碼)

開不開心!是否是看見警告而後渲染不出來!(別打我)

當頭一棒

咱們看警告就知道實際上是vue的運行時的問題,這塊咱們就再也不用template去處理了,畢竟vue3當中分包分的仍是很開的,咱們直接安裝@vue/compiler-dom,經過vue3本身的compiler幫咱們拿到render函數就行了

// md-loader/src/utils.js
const compiler = require('@vue/compiler-dom')

// 這裏咱們要返回組件代碼,那template和script是必不可少的
const getRealComponentCode = (template, script) => {
  // 後面這個配置參數就是根據module/function模式不一樣區切換不一樣的語句的,咱們能夠不用過多考慮
  const compiled = compiler.compile(template, { prefixIdentifiers: true })
  // 在這裏咱們把本來的return給替換掉 code中會包含一個render方法,咱們後面在返回的iife當中直接插入進去讓他執行,拿到render放進return的對象中就行了
  const code = compiled.code.replace(/return\s+/, '')
  // 把咱們以前處理script標籤的代碼全都挪進這裏
  script = script.trim()
  if (script) {
    script = script
      // 將export defalut 轉成聲明一個變量進行保存
      .replace(/export\s+default/, 'const demoComponentExport =')
      .replace(/import ([,{}\w\s]+) from (['"\w]+)/g, (match, p1, p2) => {
        if (p2 === "'vue'") {
          // 因爲全局使用的vue爲大寫的Vue,因此這裏須要專門處理一下
          return `const ${p1} = Vue`
        }
      })
  } else {
    // 若script標籤內部爲空,直接聲明一個空對象便可
    script = 'const demoComponentExport = {}'
  }
  // 這裏咱們返回一個自執行函數就行了,後續引入進去的時候會幫咱們把該返回的東西返回出去,若是想問我爲何不用對象,那仔細看代碼你就懂了
  return `(function() { ${code} // 不會忘了咱們的script內容是用來聲明變量的吧,搞個對象可咋返回喲 ${script} return { ...demoComponentExport, render } })()`
}
複製代碼

這將是咱們最後一次重啓項目了,沒錯,咱們完成了!剩下還有一些樣式問題只要在文檔的那個vue項目中去編寫就行了~

歡慶時刻

咱們終於結束啦!給本身鼓鼓掌吧(呱唧呱唧)

代碼GKD

這部分就是純純的代碼了,供給伸手黨直接拿走還有後續回顧找代碼的人用

md-loader/src/index.js

const { stripTemplate, stripScript, getRealComponentCode } = require('./util')

const md = require('./config.js')
console.log(md.render)
module.exports = source => {
  let content = md.render(source)

  const commentStart = '<!--element-demo:'
  const commentStartLen = commentStart.length
  const commentEnd = ':element-demo-->'
  const commentEndLen = commentEnd.length

  const output = []
  let start = 0
  let id = 0
  let componentsString = ''

  let demoStart = content.indexOf(commentStart)
  let demoEnd = content.indexOf(commentEnd, demoStart + commentStartLen)

  while (demoStart !== -1 && demoEnd !== -1) {
    output.push(content.slice(start, demoStart))

    const componentContent = content.slice(demoStart + commentStartLen, demoEnd)
    const template = stripTemplate(componentContent)
    const script = stripScript(componentContent)

    const demoComponent = getRealComponentCode(template, script)
    const demoComponentName = `element-demo-${id}`
    output.push(`<template #source><${demoComponentName}/></template>`)
    componentsString += `${JSON.stringify( demoComponentName )}: ${demoComponent},`

    id++
    start = demoEnd + commentEndLen
    demoStart = content.indexOf(commentStart, start)
    demoEnd = content.indexOf(commentEnd, demoStart + commentStartLen)
  }
  let pageScript = ''
  if (componentsString) {
    pageScript = `<script> import hljs from 'highlight.js' import * as Vue from "vue" export default { name: 'component-doc', components: { ${componentsString} } } </script>`
  } else if (content.indexOf('<script>') === 0) {
    pageScript = stripScript(content)
  }
  output.push(content.slice(start))

  return ` <template> <section class="content element-doc"> ${output.join('')} </section> </template> ${pageScript} `
}
複製代碼

md-loader/src/config.js

const Config = require('markdown-it-chain')
const containers = require('./containers')
const hljs = require('highlight.js')
const overwriteFenceRule = require('./fence')

const config = new Config()

const highlight = (str, lang) => {
  if (!lang || !hljs.getLanguage(lang)) {
    return `<pre><code class="hljs">${str}</code></pre>`
  }
  const html = hljs.highlight(lang, str, true).value
  return `<pre><code class="hljs language-${lang}">${html}</code></pre>`
}

config.options
  .html(true)
  .highlight(highlight)
  .end()
  .plugin('containers')
  .use(containers)
  .end()

const md = config.toMd()
overwriteFenceRule(md)
module.exports = md
複製代碼

md-loader/src/containers.js

const mdContainer = require('markdown-it-container')

module.exports = md => {
  md.use(mdContainer, 'demo', {
    validate(params) {
      return /^demo\s*(.*)$/.test(params)
    },
    render(tokens, idx) {
      const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/)
      if (tokens[idx].nesting === 1) {
        const description = m?.[1] || ''
        // console.log(description, 'description')
        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')
}
複製代碼

md-loader/src/fence.js

// 覆蓋默認的 fence 渲染策略
module.exports = md => {
  const defaultRender = md.renderer.rules.fence
  md.renderer.rules.fence = (tokens, idx, options, env, self) => {
    const token = tokens[idx]
    // 判斷該 fence 是否在 :::demo 內
    const prevToken = tokens[idx - 1]
    const isInDemoContainer =
      prevToken &&
      prevToken.nesting === 1 &&
      /^demo\s*(.*)$/.test(prevToken.info)
    if (token.info === 'html' && isInDemoContainer) {
      return `<template #highlight><pre v-pre><code class='html'>${md.utils.escapeHtml( token.content )}</code></pre></template>`
    }
    return defaultRender(tokens, idx, options, env, self)
  }
}
複製代碼

md-loader/src/util.js

const compiler = require('@vue/compiler-dom')

const stripTemplate = content => {
  content = content.trim()
  if (!content) {
    return content
  }
  content = content.replace(/<(script|style)>[\s\S]+<\/1>/g, '').trim()
  const res = content.match(/<(template)\s*>([\s\S]+)<\/\1>/)
  return res ? res[2]?.trim() : content
}

const stripScript = content => {
  const res = content.match(/<(script)\s*>([\s\S]+)<\/\1>/)
  return res && res[2] ? res[2].trim() : ''
}

const getRealComponentCode = (template, script) => {
  const compiled = compiler.compile(template, { prefixIdentifiers: true })
  let code = compiled.code.replace(/return\s+/, '')

  script = script.trim()
  if (script) {
    script = script
      .replace(/export\s+default/, 'const demoComponentExport =')
      .replace(/import ([,{}\w\s]+) from (['"\w]+)/g, (match, p1, p2) => {
        if (p2 === "'vue'") {
          return `const ${p1} = Vue`
        }
      })
  } else {
    script = 'const demoComponentExport = {}'
  }

  code = `(function() { ${code} ${script} return { mounted(){ this.$nextTick(()=>{ const blocks = document.querySelectorAll('pre code:not(.hljs)') Array.prototype.forEach.call(blocks, hljs.highlightBlock) }) }, render, ...demoComponentExport } })()`
  return code
}

module.exports = {
  stripScript,
  stripTemplate,
  getRealComponentCode
}
複製代碼

vue項目/demo-block.vue

<template>
  <div class="demo-block">
    <div class="source">
      <slot name="source"></slot>
    </div>
    <div class="meta" ref="meta">
      <div class="description" v-if="$slots.default">
        <slot></slot>
      </div>
      <div class="highlight">
        <slot name="highlight"></slot>
      </div>
    </div>
    <div
      class="demo-block-control"
      ref="control"
      @click="isExpanded = !isExpanded"
    >
      <span>{{ controlText }}</span>
    </div>
  </div>
</template>

<script>
import { ref, computed, watchEffect, onMounted } from 'vue'
export default {
  setup() {
    const meta = ref(null)
    const isExpanded = ref(false)
    const controlText = computed(() =>
      isExpanded.value ? '隱藏代碼' : '顯示代碼'
    )
    const codeAreaHeight = computed(() =>
      [...meta.value.children].reduce((t, i) => i.offsetHeight + t, 56)
    )
    onMounted(() => {
      watchEffect(() => {
        meta.value.style.height = isExpanded.value
          ? `${codeAreaHeight.value}px`
          : '0'
      })
    })

    return {
      meta,
      isExpanded,
      controlText
    }
  }
}
</script>

<style lang="scss">
.demo-block {
  border: solid 1px #ebebeb;
  border-radius: 3px;
  transition: 0.2s;

  &.hover {
    box-shadow: 0 0 8px 0 rgba(232, 237, 250, 0.6),
      0 2px 4px 0 rgba(232, 237, 250, 0.5);
  }

  code {
    font-family: Menlo, Monaco, Consolas, Courier, monospace;
  }

  .demo-button {
    float: right;
  }

  .source {
    padding: 24px;
  }

  .meta {
    background-color: #fafafa;
    border-top: solid 1px #eaeefb;
    overflow: hidden;
    transition: height 0.2s;
  }

  .description {
    padding: 20px;
    box-sizing: border-box;
    border: solid 1px #ebebeb;
    border-radius: 3px;
    font-size: 14px;
    line-height: 22px;
    color: #666;
    word-break: break-word;
    margin: 10px;
    background-color: #fff;

    p {
      margin: 0;
      line-height: 26px;
    }

    code {
      color: #5e6d82;
      background-color: #e6effb;
      margin: 0 4px;
      display: inline-block;
      padding: 1px 5px;
      font-size: 12px;
      border-radius: 3px;
      height: 18px;
      line-height: 18px;
    }
  }

  .highlight {
    pre {
      margin: 0;
    }

    code.hljs {
      margin: 0;
      border: none;
      max-height: none;
      border-radius: 0;

      &::before {
        content: none;
      }
    }
  }

  .demo-block-control {
    border-top: solid 1px #eaeefb;
    height: 44px;
    box-sizing: border-box;
    background-color: #fff;
    border-bottom-left-radius: 4px;
    border-bottom-right-radius: 4px;
    text-align: center;
    margin-top: -1px;
    color: #d3dce6;
    cursor: pointer;
    position: relative;

    i {
      font-size: 16px;
      line-height: 44px;
      transition: 0.3s;
      &.hovering {
        transform: translateX(-40px);
      }
    }

    > span {
      position: absolute;
      transform: translateX(-30px);
      font-size: 14px;
      line-height: 44px;
      transition: 0.3s;
      display: inline-block;
    }

    &:hover {
      color: #409eff;
      background-color: #f9fafc;
    }

    & .text-slide-enter,
    & .text-slide-leave-active {
      opacity: 0;
      transform: translateX(10px);
    }

    .control-button {
      line-height: 26px;
      position: absolute;
      top: 0;
      right: 0;
      font-size: 14px;
      padding-left: 5px;
      padding-right: 25px;
    }
  }
} 
.hljs {
  line-height: 1.8;
  font-family: Menlo, Monaco, Consolas, Courier, monospace;
  font-size: 12px;
  padding: 18px 24px;
  background-color: #fafafa;
  border: solid 1px #eaeefb;
  margin-bottom: 25px;
  border-radius: 4px;
  -webkit-font-smoothing: auto;
}
</style>
複製代碼

避坑大全

說是大全,其實我也只能列舉出來我踩過的坑,因此後續若是有朋友也遇到了某些坑歡迎聯繫我進行補充哈~

  1. markdown-it-chain插件必需調用plugin()傳入插件,不然必定會報錯,這個坑說來也好解決,由於一是插件確定是要用的,否則不必用chain這個插件;二是稍微改改源碼就行了,因此說是坑,也只是學習或者說寫教程的時候纔會遇到的一個坑罷了。
  2. vue-loader必定要是16.0.0版本以上,這個倒也是不能徹底算坑。可是我經過yarn add vue-loader -D安裝的時候發現默認就是15.9.6版本的,因此仍是寫在這裏以防萬一,這個坑會出現的bug很好判斷,parseComponent is not defined基本上控制檯提示前面這個報錯,就極大機率是你的loader版本有問題,確認一下就行了。
  3. 第三個就真實是個坑了,說實話對於loader的運行機制我還不算很清晰,因此這個問題我只能說會遇到,也知道怎麼能解決,可是個人解決方法會比較麻煩。那麼這個坑是啥呢?寫loader的時候,常常性發現修改以後沒有效果。而且你會發現,重裝依賴,沒有用;清緩存,沒用;強制刷新,也沒啥用。有用的方法是什麼?是刪掉依賴清緩存再重裝,對,你須要完整走完這一複雜的流程,否則真就沒用你敢信?
相關文章
相關標籤/搜索