趁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的时候,常常性发现修改以后没有效果。而且你会发现,重装依赖,没有用;清缓存,没用;强制刷新,也没啥用。有用的方法是什么?是删掉依赖清缓存再重装,对,你须要完整走完这一复杂的流程,否则真就没用你敢信?
相关文章
相关标签/搜索