lalalazero / zeroview

手写一套基于 Vue 2.6 的 UI 组件库
https://lalalazero.github.io/zeroview/
0 stars 0 forks source link

从 elementui 源码分析组件示例是怎么写的 #13

Open lalalazero opened 4 years ago

lalalazero commented 4 years ago

背景

轮子的示例一般要展示组件自身和相应的代码高亮。比如像这样: image 在我自己写轮子官网的过程中,也遇到了这样的需求。以下记录了自己学习的过程。

思路1:手动转义 < > 符号

手写的意思就是,button 组件写一个 button-example.vue,里面会用到我写的 z-view-button.vue 组件,还会有对应的示例。遇到的第一个问题是示例代码如果不转义 < > 符号,就会被 vue 编译,视为 z-view-button 组件。

<template>
  <div class="button-example">
    <z-view-button>button</z-view-button>
    <div>
      <p>button 代码示例</p>
      <z-view-button>button</z-view-button>
    </div>
  </div>
</template>
<script>
import zViewButton from "./button.vue";
export default {
  name: 'buttonExample',
  components: {
    zViewButton
  }
};
</script>
<style lang="scss" scoped>
</style>

我也尝试过用 v-pre 也并没有用

<pre v-pre>
          <code>
              <z-view-button>button</z-view-button>
          </code>
</pre>

只有明确的转义才会成功。

<div class="code">&lt;z-view-button&gt;button&lt;/z-view-button&gt;</div>

image

很明显,思路1这样的办法很挫。不可能每个示例代码里面的 < > 我都去转义,而且还要再另外做代码高亮。

思路2: hightlight.js 自动转义 < >和代码高亮

把示例代码写成一个字符串,然后在 button-example.vue 组件 mounted 之后,首先用 highlight.js 把字符串的 < > 转义,然后再语法高亮。代码类似:

// template
<div class="code">
        <pre>
          <code>
          </code>
        </pre>
</div>
// script
import hljs from "highlight.js";
import "highlight.js/styles/github.css";
mounted() {
    const codeStr = `
      <z-view-button>button</z-view-button>
   `;
    let highlight = hljs.highlight("html", codeStr).value;
    this.$el.querySelector("pre code").innerHTML = highlight;
  }

实际渲染 image

这样勉强可以达到想要的效果,但是感觉也很蠢。于是我去看了 elementui 的做法

思路3:elementui 的做法—markdown文件转vue组件

elementui 的做法是组件的示例用法都用 markdown 来写,解析这个 markdown 文件输出是一个 vue 组件,里面包含轮子和相应的代码示例。这个过程就在 md-loader/index.js 文件中。

lalalazero commented 4 years ago

markdown 文件转 vue 组件

elementui 的手写 markdown loader

首先,对于使用 webpack 的项目,需要对 .md 结尾的 markdown 文件配 loader,elementui 是这么配置的 /build/webpack.demo.js

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

所以可以找到它手写的 markdown 文件的 loader,第一次看的时候我并看不懂,所以我尝试自己抄他的然后写一个 loader。

我的手写一个 markdown loader

因为我的项目是基于 vue-cli 工具的,所以我的配置不是在 webpack.config.js 里面,但是也差不多。我找了下 vue-cli 的文档,大概是下面这样配置的:

// vue.config.js
module.exports = {
  // ... 省略其他
  chainWebpack: config => {
    config.module
      .rule('markdown')
      .test(/\.md$/)
      .use('vue-loader')
      .loader('vue-loader').tap(options => {
        return Object.assign({}, options, {
          compilerOptions: {
            preserveWhitespace: false
          }
        })
      })
      .end()

    config.module
      .rule('markdown')
      .test(/\.md$/).use(require.resolve('./md-loader.js'))
      .loader(require.resolve('./md-loader.js'))
      .end()
  }
}

配好了之后(这里有个小知识点:require.resovle 和 require 的区别),在 md-loader.js 里面写这样的内容:

module.exports = function(){
  return `<template><div>hello world</div></template>` // 这就是一个最简单的 .vue 组件
}

然后 App.vue 里面随便写一下

<template>
  <div id='app'>
       <button-example />
  </div>
</template>
<script>
export default {
  components: {
   'button-example': () => import("../docs/button.md") // 随便一个 .md 文件
  }
}
</script>

运行之后,页面效果是这样的 image 因为在我们配置的 md-loader.js 里面,写死了返回一个 .vue 组件,内容就是 hello world。 到这一步,说明我们的 .md 文件的 loader 配置成功了。下一步就是把 markdown 文件的内容实际的渲染出来。

lalalazero commented 4 years ago

渲染 markdown 文件具体内容

一个具体的介绍 button 组件的 markdown 文件的内容大概如下:

# 一级标题:button 组件
## 二级标题: 基础用法
::: demo 组件代码示例
```html
<z-view-button>按钮文字</z-view-button>
```
:::

注意里面花里胡哨的符号

 # ## ::: ``` 

elementui 用到了 markdown-it markdown-it-container markdown-it-container 这几个库。他们的基础用法就是把 # 一级标题 渲染成 <h1>一级标题</h1> 根据官网例子和 elementui 代码,以下的符号都有特殊含义

::: 符号定义了一个 container,``` 定义一个 code fence (代码片段)

这里 elementui 重写了 markdown-it-container 的 container 和 fence 规则,把 :::demo 这样的一个块变成了一个 组件返回 image

并且对于里面的 code fence 变成了这样返回 image

因此仿照 elementui ,我是这样写 md-loader.js 的。

lalalazero commented 4 years ago

定义 container

function setCustomContainer(md) {
  md.use(mdContainer, 'demo', {
    validate(params) {
      return params.trim().match(/^demo\s+(.*)$/);
    },
    render(tokens, idx) {
      const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
      if (tokens[idx].nesting === 1) {
        const description = m && m.length > 1 ? m[1] : '';
        let str1 = description ? `<template slot="description">${md.render(description)}</template>` : `<template slot="description">description</template>`
        const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : 'content';
        return `<demo-block>
        ${str1}
       <!-- zViewDemo ${content} zViewDemo -->
        `;
      }
      return '</demo-block>';
    }
  })

}

上面这个方法会把以下的 markdown 内容转成 <demo-block /> 组件输出


:::demo 组件代码示例
```html
<z-view-button>button</z-view-button>
```
:::

经过转换之后输出是这样

<demo-block>
<template slot='description>组件代码示例</template>
<!-- zViewDemo <z-view-button>button</z-view-button> zViewDemo -->
</demo-block>

至于中间为什么要标记 <!-- zViewDemo 下面会讲到

重写 fence

function overWriteFenceRule(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 && prevToken.info.trim().match(/^demo\s*(.*)$/);
    if (token.info === 'html' && isInDemoContainer) {
      return `<template slot="highlight"><pre v-pre><code class="html">${md.utils.escapeHtml(token.content)}</code></pre></template>`;
    }
    return defaultRender(tokens, idx, options, env, self);
  };
}

前面我们说 fence 就是一个 container 里面的代码块,也就是

:::demo
```html
这里是 fence
```

通过重写 fence 方法,我们对返回的内容用一个 <template slot='highlight'><pre v-pre><code></code>这里是转义后的 fence 内容</pre></temolate>包裹起来 这里有两个作用:

  1. 转义后的内容就不会被识别为组件了,用来做组件示例代码
  2. 引入 <pre><code> 标签是为了 highlight.js 插件做代码高亮

一个 container 就是一个 demo-block 组件

我们编写组件示例无非就是两部分:

  1. 组件示例本身
  2. 组件示例代码 通过重写 fence 规则已经把第2部分做到了,那么第1部分怎么做。到目前为止,markdown 的渲染输出应该是这样

一个 .md 文件就是一个大的 vue 组件

静态显示组件和示例代码

如果有 js 怎么办

如果有 style

多个 container

还有其他内容