你好,我是大圣。

在我们实现了组件库核心的组件内容之后,我们就需要提供一个可交互的组件文档给用户使用和学习了。这个文档页面主要包含组件的描述,组件Demo示例的展示、描述和代码,并且每个组件都应该有详细的参数文档。

现在,我们从上述文档页包含的信息来梳理一下我们的需求。我们需要用最简洁的语法来写页面,还需要用最简洁的语法来展示 Demo + 源代码 + 示例描述。那么从语法上来说,首选就是 Markdown 无疑了,因为它既简洁又强大。

那在我们正式开始设计文档之前,我们还需要对齐一下。如果要展示 Demo 和源码的话,为了能更高效且低成本的维护,我们会把一个示例的 Demo + 源码 + 示例描述放到一个文件里,尽量多的去复用,这样可以减少需要维护的代码。而做示例展示的话,本质上可以说是跟 Markdown 的转译一致,都是 Markdown -> HTML,只是转译的规则我们需要拓展一下。接下来我们就正式开始了。

VuePress

首先我们需要一个能基于Markdown构建文档的工具,我推荐VuePress。它是Vue官网团队维护的在线技术文档工具,样式和Vue的官方文档保持一致。

VuePress内置了Markdown的扩展,写文档的时候就是用Markdown语法进行渲染的。最让人省心的是,它可以直接在Markdown里面使用Vue组件,这就意味着我们可以直接在Markdown中写上一个个的组件库的使用代码,就可以直接展示运行效果了。

我们可以在项目中执行下面的代码安装VuePress的最新版本:

yarn add -D vuepress@next

然后我们新建docs目录作为文档目录,新建docs/README.md文件作为文档的首页。除了Markdown之外,我们可以直接使用VuePress的语法扩展对组件进行渲染。


---
home: true
heroImage: /theme.png
title: 网站快速成型工具
tagline: 一套为开发者、设计师和产品经理准备的基于 Vue 3 的桌面端组件库
heroText: 网站快速成型工具
actions:
  - text: 快速上手
    link: /install
    type: primary
  - text: 项目简介
    link: /button
    type: secondary
features:
  - title: 简洁至上
    details: 以 Markdown 为中心的项目结构,以最少的配置帮助你专注于写作。
  - title: Vue 驱动
    details: 享受 Vue 的开发体验,可以在 Markdown 中使用 Vue 组件,又可以使用 Vue 来开发自定义主题。
  - title: 高性能
    details: VuePress 会为每个页面预渲染生成静态的 HTML,同时,每个页面被加载的时候,将作为 SPA 运行。
footer: powdered by vuepress and me
---
# 额外的信息



我们在README.md中输入上面的内容,通过title配置网站的标题、actions配置快捷链接、features配置详情介绍,这样我们就拥有了下面的首页样式:

图片

然后我们进入docs/.vuepress/目录下,新建文件config.js,这是这个网站的配置页面。下面的代码我们配置了logo和导航navbar,页面顶部右侧就会有首页和安装两个导航。

module.exports = {
  themeConfig:{
    title:"Element3",
    description:"vuepress搭建的Element3文档",
    logo:"/element3.svg",
    navbar:[
      {
        link:"/",
        text:"首页"
      },{
        link:"/install",
        text:"安装"
      },
    ]

  }
}

然后我们创建docs/install.md文件,点击顶部导航之后,就会显示install.md的信息。我们在文稿中就可以直接写上介绍Element3如何安装的文档了,下面的文稿就是Element3的安装使用说明。

## 安装
### npm 安装
推荐使用 npm 的方式安装,它能更好地和 [webpack](https://webpack.js.org/) 打包工具配合使用。
```shell
npm i element3 -S
```
### CDN
目前可以通过 [unpkg.com/element3](https://unpkg.com/element3) 获取到最新版本的资源,在页面上引入 js 和 css 文件即可开始使用。
```html
<!-- 引入样式 -->
<link
  rel="stylesheet"
  href="https://unpkg.com/element3/lib/theme-chalk/index.css"
/>
<!-- 引入组件库 -->
<script src="https://unpkg.com/element3"></script>
```
:::tip
我们建议使用 CDN 引入 Element3 的用户在链接地址上锁定版本,以免将来 Element3 升级时受到非兼容性更新的影响。锁定版本的方法请查看 [unpkg.com](https://unpkg.com)。
:::
### Hello world

通过 CDN 的方式我们可以很容易地使用 Element3 写出一个 Hello world 页面。[在线演示](https://codepen.io/imjustaman/pen/abZajYg)

<iframe height="265" style="width: 100%;" scrolling="no" title="Element3 Demo" src="https://codepen.io/imjustaman/embed/abZajYg?height=265&theme-id=light&default-tab=html,result" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">
  See the Pen <a href='https://codepen.io/imjustaman/pen/abZajYg'>Element3 Demo</a> by ImJustAMan
  (<a href='https://codepen.io/imjustaman'>@imjustaman</a>) on <a href='https://codepen.io'>CodePen</a>.
</iframe>
如果是通过 npm 安装,并希望配合 webpack 使用,请阅读下一节:[快速上手](/#/zh-CN/component/quickstart)。

然后我们在浏览器里点击安装后,就会看到下图的页面显示。Markdown已经成功渲染为在线文档了,并且代码也自带了高亮显示。

图片

然后我们需要在这个文档系统中支持Element3,首先执行下面的代码安装Element3:

npm i element3 -D

然后在项目根目录下的docs/.vuepress文件夹中新建文件clientAppEnhance.js,这是VuerPress的客户端扩展文件。我们导入了defineClientAppEnhance来返回客户端的扩展配置。这个函数中会传递Vue的实例App以及路由配置Router,我们使用app.use来全局注册Element3组件,就可以直接在Markdown中使用Element3的组件了。


import { defineClientAppEnhance } from '@vuepress/client'

import element3 from 'element3'

export default defineClientAppEnhance(({ app, router, siteData }) => {
  app.use(element3)
})

这样VuePress就内置了Element3。我们在docs下面新建button.md文件,可以直接在Markdown中使用Element3的组件进行演示。下面的文稿中我们直接使用了el-button组件演示效果。

## Button 按钮

常用的操作按钮。
### 基础用法
基础的按钮用法。

<el-button type="primary">
按钮
</el-button>

```html
<el-button type="primary">
按钮
</el-button>
```

然后进入docs/.vuepress/config.js中,新增侧边栏sidebar的配置之后,就可以看到下图的效果了。

    sidebar:[
      {
        text:'安装',
        link:'/install'
      },
      {
        text:'按钮',
        link:'/button'
      },
    ]

图片

这样我们就基于VuePress支持了Element3组件库的文档功能,剩下的就是给每个组件写好文档即可。

但是这样的话,el-button的源码就写了两次,如果我们想更好地定制组件库文档,就需要自己解析Markdown文件,在内部支持Vue组件的效果显示和源码展示,也就相当于定制了一个自己的VuePress。

:::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>
</template>
```

:::

它能直接使用下面的:::demo语法,在标记内部代码的同时,显示渲染效果和源码,也就是下图Element3官网的渲染效果。

图片

那么接下来我们就看看如何定制,具体操作一下。

解析Markdown

我们需要自己实现一个Markdown-loader,对Markdown语法进行扩展。

Element3中使用Markdown-it进行Markdown语法的解析和扩展。Markdown-it导出一个函数,这个函数可以把Markdown语法解析为HTML标签。这里我们需要做的就是解析出Markdown中的demo语法,渲染其中的Vue组件,并且同时能把源码也显示在组件下方,这样就完成了扩展任务。

Element3中对Markdown的扩展源码都可以在GitHub上看到。

下面的代码就是全部解析的逻辑:首先我们使用md.render把Markdown渲染成为HTML,并且获取内部demo子组件;在获取了demo组件内部的代码之后,调用genInlineComponentText,把组件通过Vue的compiler解析成待执行的代码,这一步就是模拟了Vue组件解析的过程;然后使用script标签包裹编译之后的Vue组件;最后再把组件的源码放在后面,demo组件的解析就完成了。

const { stripScript, stripTemplate, genInlineComponentText } = require('./util')
	const md = require('./config')
	
	module.exports = function (source) {
	  const content = md.render(source)
	
	  const startTag = '<!--element-demo:'
	  const startTagLen = startTag.length
	  const endTag = ':element-demo-->'
	  const endTagLen = endTag.length
	
	  let componenetsString = ''
	  let id = 0 // demo 的 id
	  const output = [] // 输出的内容
	  let start = 0 // 字符串开始位置
	
	  let commentStart = content.indexOf(startTag)
	  let commentEnd = content.indexOf(endTag, commentStart + startTagLen)
	  while (commentStart !== -1 && commentEnd !== -1) {
	    output.push(content.slice(start, commentStart))
	
	    const commentContent = content.slice(commentStart + startTagLen, commentEnd)
	    const html = stripTemplate(commentContent)
	    const script = stripScript(commentContent)
	
	    const demoComponentContent = genInlineComponentText(html, script)
	
	    const demoComponentName = `element-demo${id}`
	    output.push(`<template #source><${demoComponentName} /></template>`)
	    componenetsString += `${JSON.stringify(
	      demoComponentName
	    )}: ${demoComponentContent},`
	
	    // 重新计算下一次的位置
	    id++
	    start = commentEnd + endTagLen
	    commentStart = content.indexOf(startTag, start)
	    commentEnd = content.indexOf(endTag, commentStart + startTagLen)
	  }
	
	  // 仅允许在 demo 不存在时,才可以在 Markdown 中写 script 标签
	  // todo: 优化这段逻辑
	  let pageScript = ''
	  if (componenetsString) {
	    pageScript = `<script>
	      import hljs from 'highlight.js'
	      import * as Vue from "vue"
	      export default {
	        name: 'component-doc',
	        components: {
	          ${componenetsString}
	        }
	      }
	    </script>`
	  } else if (content.indexOf('<script>') === 0) {
	    // 硬编码,有待改善
	    start = content.indexOf('</script>') + '</script>'.length
	    pageScript = content.slice(0, start)
	  }
	
	  output.push(content.slice(start))
	  return `
	    <template>
	      <section class="content element-doc">
	        ${output.join('')}
	      </section>
	    </template>
	    ${pageScript}
	  `
	}

然后我们还要把渲染出来的Vue组件整体封装成为demo-block组件。在下面的代码中,我们使用扩展Markdown的render函数,内部使用demo-block组件,把Markdown渲染的结果渲染在浏览器上。

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

module.exports = (md) => {
  md.use(mdContainer, 'demo', {
    validate(params) {
      return params.trim().match(/^demo\s*(.*)$/)
    },
    render(tokens, idx) {
      const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/)
      if (tokens[idx].nesting === 1) {
        const description = m && m.length > 1 ? m[1] : ''
        const content =
          tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : ''
        return `<demo-block>
        ${description ? `<div>${md.render(description)}</div>` : ''}
        <!--element-demo: ${content}:element-demo-->
        `
      }
      return '</demo-block>'
    }
  })

  md.use(mdContainer, 'tip')
  md.use(mdContainer, 'warning')
}

然后我们就实现了demo-block组件。接下来我们新建DemoBlock.vue,在下面的代码中我们通过slot实现了组件的渲染结果和源码高亮的效果,至此我们就成功了实现了Markdown中源码演示的效果。

<!-- 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>

总结

我们来总结一下今天学到的内容。

首先我们使用Vue官网文档的构建工具VuePress来搭建组件库文档,VuePress提供了很好的上手体验,Markdown中可以直接注册使用Vue组件,我们在.vuepress中可以扩展对Element3的支持。

如果我们定制需求更多一些,就需要自己解析Markdown并且实现对Vue组件的支持了,我们可以使用Markdown-it插件解析,支持Vue组件和代码高亮,这也是现在Element3文档的渲染方式。

思考题

最后留给你一道思考题:现在很多组件库开始尝试使用Storybook来搭建组件库的文档,那么这个Storybook相比于我们实现的文档有什么特色呢?

欢迎你在评论区分享你的看法,也欢迎你把这节课的内容分享给你的同事和朋友们,我们下一讲再见!