You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

401 lines
15 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 26文档如何给你的组件库设计一个可交互式文档
你好,我是大圣。
在我们实现了组件库核心的组件内容之后我们就需要提供一个可交互的组件文档给用户使用和学习了。这个文档页面主要包含组件的描述组件Demo示例的展示、描述和代码并且每个组件都应该有详细的参数文档。
现在,我们从上述文档页包含的信息来梳理一下我们的需求。我们需要用最简洁的语法来写页面,还需要用最简洁的语法来展示 Demo + 源代码 + 示例描述。那么从语法上来说,首选就是 Markdown 无疑了,因为它既简洁又强大。
那在我们正式开始设计文档之前,我们还需要对齐一下。如果要展示 Demo 和源码的话,为了能更高效且低成本的维护,我们会把一个示例的 Demo + 源码 + 示例描述放到一个文件里,尽量多的去复用,这样可以减少需要维护的代码。而做示例展示的话,本质上可以说是跟 Markdown 的转译一致,都是 Markdown -> HTML只是转译的规则我们需要拓展一下。接下来我们就正式开始了。
## VuePress
首先我们需要一个能基于Markdown构建文档的工具我推荐VuePress。它是Vue官网团队维护的在线技术文档工具样式和Vue的官方文档保持一致。
VuePress内置了Markdown的扩展写文档的时候就是用Markdown语法进行渲染的。最让人省心的是它可以直接在Markdown里面使用Vue组件这就意味着我们可以直接在Markdown中写上一个个的组件库的使用代码就可以直接展示运行效果了。
我们可以在项目中执行下面的代码安装VuePress的最新版本
```bash
yarn add -D vuepress@next
```
然后我们新建docs目录作为文档目录新建docs/README.md文件作为文档的首页。除了Markdown之外我们可以直接使用VuePress的语法扩展对组件进行渲染。
```yaml
---
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配置详情介绍这样我们就拥有了下面的首页样式
![图片](https://static001.geekbang.org/resource/image/d8/ba/d8cd69f08b0932a670c74c80ef6699ba.png?wh=1920x1785)
然后我们进入docs/.vuepress/目录下新建文件config.js这是这个网站的配置页面。下面的代码我们配置了logo和导航navbar页面顶部右侧就会有首页和安装两个导航。
```javascript
module.exports = {
themeConfig:{
title:"Element3",
description:"vuepress搭建的Element3文档",
logo:"/element3.svg",
navbar:[
{
link:"/",
text:"首页"
},{
link:"/install",
text:"安装"
},
]
}
}
```
然后我们创建docs/install.md文件点击顶部导航之后就会显示install.md的信息。我们在文稿中就可以直接写上介绍Element3如何安装的文档了下面的文稿就是Element3的安装使用说明。
```markdown
## 安装
### 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已经成功渲染为在线文档了并且代码也自带了高亮显示。
![图片](https://static001.geekbang.org/resource/image/09/c3/09d16ddc1080b4d8d4b04b934bdyy3c3.png?wh=1920x1655)
然后我们需要在这个文档系统中支持Element3首先执行下面的代码安装Element3
```bash
npm i element3 -D
```
然后在项目根目录下的docs/.vuepress文件夹中新建文件clientAppEnhance.js这是VuerPress的客户端扩展文件。我们导入了defineClientAppEnhance来返回客户端的扩展配置。这个函数中会传递Vue的实例App以及路由配置Router我们使用app.use来全局注册Element3组件就可以直接在Markdown中使用Element3的组件了。
```javascript
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组件演示效果。
```markdown
## Button 按钮
常用的操作按钮。
### 基础用法
基础的按钮用法。
<el-button type="primary">
按钮
</el-button>
```html
<el-button type="primary">
按钮
</el-button>
```
```
然后进入docs/.vuepress/config.js中新增侧边栏sidebar的配置之后就可以看到下图的效果了。
```javascript
sidebar:[
{
text:'安装',
link:'/install'
},
{
text:'按钮',
link:'/button'
},
]
```
![图片](https://static001.geekbang.org/resource/image/d2/6d/d267f5d33cc9652c145d737f8f4dc96d.png?wh=1920x846)
这样我们就基于VuePress支持了Element3组件库的文档功能剩下的就是给每个组件写好文档即可。
但是这样的话el-button的源码就写了两次如果我们想更好地定制组件库文档就需要自己解析Markdown文件在内部支持Vue组件的效果显示和源码展示也就相当于定制了一个自己的VuePress。
```markdown
:::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官网的渲染效果。
![图片](https://static001.geekbang.org/resource/image/85/c6/85c611ff85c7d69773e41c546f4eb3c6.png?wh=1920x1462)
那么接下来我们就看看如何定制,具体操作一下。
## 解析Markdown
我们需要自己实现一个Markdown-loader对Markdown语法进行扩展。
Element3中使用Markdown-it进行Markdown语法的解析和扩展。Markdown-it导出一个函数这个函数可以把Markdown语法解析为HTML标签。这里我们需要做的就是解析出Markdown中的demo语法渲染其中的Vue组件并且同时能把源码也显示在组件下方这样就完成了扩展任务。
Element3中对Markdown的扩展源码都可以在[GitHub](https://github.com/hug-sun/element3/tree/master/packages/md-loader/src)上看到。
下面的代码就是全部解析的逻辑首先我们使用md.render把Markdown渲染成为HTML并且获取内部demo子组件在获取了demo组件内部的代码之后调用genInlineComponentText把组件通过Vue的compiler解析成待执行的代码这一步就是模拟了Vue组件解析的过程然后使用script标签包裹编译之后的Vue组件最后再把组件的源码放在后面demo组件的解析就完成了。
```javascript
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渲染的结果渲染在浏览器上。
```javascript
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中源码演示的效果。
```javascript
<!-- 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相比于我们实现的文档有什么特色呢
欢迎你在评论区分享你的看法,也欢迎你把这节课的内容分享给你的同事和朋友们,我们下一讲再见!