你好,我是大圣。
在我们实现了组件库核心的组件内容之后,我们就需要提供一个可交互的组件文档给用户使用和学习了。这个文档页面主要包含组件的描述,组件Demo示例的展示、描述和代码,并且每个组件都应该有详细的参数文档。
现在,我们从上述文档页包含的信息来梳理一下我们的需求。我们需要用最简洁的语法来写页面,还需要用最简洁的语法来展示 Demo + 源代码 + 示例描述。那么从语法上来说,首选就是 Markdown 无疑了,因为它既简洁又强大。
那在我们正式开始设计文档之前,我们还需要对齐一下。如果要展示 Demo 和源码的话,为了能更高效且低成本的维护,我们会把一个示例的 Demo + 源码 + 示例描述放到一个文件里,尽量多的去复用,这样可以减少需要维护的代码。而做示例展示的话,本质上可以说是跟 Markdown 的转译一致,都是 Markdown -> HTML,只是转译的规则我们需要拓展一下。接下来我们就正式开始了。
首先我们需要一个能基于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-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相比于我们实现的文档有什么特色呢?
欢迎你在评论区分享你的看法,也欢迎你把这节课的内容分享给你的同事和朋友们,我们下一讲再见!