|
|
|
|
# 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相比于我们实现的文档有什么特色呢?
|
|
|
|
|
|
|
|
|
|
欢迎你在评论区分享你的看法,也欢迎你把这节课的内容分享给你的同事和朋友们,我们下一讲再见!
|
|
|
|
|
|