1. 前言
Yike-Design组件库项目自7月1日正式开源已近一月
在尝试和挑战中收获知识,在碰撞和交流中收获友谊,这也是参与开源项目最棒的意义~
今天开始,我会在社区中分享自己在组件库开发过程中的收获,期望这能对大家有所帮助,也希望能够通过文档的形式让大家都能够理解我的开发思路,以便后续的共同维护
当然,我目前还只是一个 积极参与者,认知有限,水平有限,我随时期待来自你的指导和建议
此前,shaka
好兄弟已经在之前的文章中对文档的使用和编写规范进行了大致说明
那么这篇文章将为大家带来Yike-Design组件库关于多样例
文档部分基于Markdown-It
的实现方法,并将逐步拆解其实现路径,让我们开始吧
2. 实现效果
大家可以先看一下最终的实现效果 ⬇⬇⬇
3.背景
技术的实现路径取决于技术的应用场景,我们的组件库文档的设计主要有以下需求
- 文档与组件库分离
- 单个组件需要存在多个Demo实例
- 每个Demo之间状态隔离、数据隔离
- 对于开发者而言,API、说明文档的编写要尽可能高效、简洁
- 文档要尽可能美观
显然,通过Markdown
能够高效地编写这类文档,VitePress和markdown-it等插件也已经完整地支持渲染组件,本文将讲述基于Markdown-it这个工具实现组件库文档的过程
4. 实现路径
Markdown-It
先来认识一下Markdown-It
这个工具吧,它能够把md文件转换为html,作为一个强大的工具,VitePress底层也采用它进行渲染
我们可以逐步体验它的能力
- 安装依赖 bash
css
npm i -g markdown-it
- 测试解析
javascript
import md from 'markdown-it';
const parse_demo = `
## 这里是一份文档
### 这里应该变成H3`;
console.log(md().render(parse_demo));
最终输出的产物便是
- vite.config.ts 引入插件
至此,我们常规的markdown都可以通过markdown-it进行解析了,下面我们尝试在vite项目中使用插件解析md文件,支持直接添加到路由中
javascript
const developRoutes: Array<RouteRecordRaw> = [
{
path: '/develop',
name: 'develop',
component: () => import('../CONTRIBUTING.md'),
},
];
- 自定义插件
新建一个vite-plugin-md.mjs
文件用来配置我们的自定义插件,我们使用最基本的transform解析能力
javascript
import MarkdownIt from 'markdown-it'
export default () => ({
name: 'vitePluginMarkdown',
// src为文件内容,id为当前文件的路径
transform(src, id) {
// 匹配.md后缀的文件进行解析
if (id.endsWith('.md')) {
const markdownIt = MarkdownIt({
html: true,
xhtmlOut: false,
})
// 解析之后的html文档需要在外层包裹<template>根结点
return {
code: `<template>${markdownIt.render(src)}</template>`,
map: null,
}
}
},
})
- 修改vite的配置文件
在vite.config.ts
中引入这个插件
javascript
import vitePluginMarkdown from './plugins/vite-plugin-md.mjs';
const vuePlugin = createVuePlugin({ include: [/\.vue$/, /\.md$/] });
// 配置可编译 .vue 与 .md 文件
export default defineConfig({
plugins: [
vitePluginMarkdown(),
vuePlugin,
],
});
💐💐💐 此时,你就可以直接通过对应的路由访问到md文档了
添加并解析Vue组件
现在可以开始加入我们的Demo组件解析了,我们先准备一下需要解析的文档和对应的组件
yk-button是我们已经全局注册
的组件,他能够很轻松地被渲染出来
bash
### 这里是文件解析
<yk-button>测试</yk-button>
解析自定义组件(.vue)
但是 ,我们期望的是将的多个
包含demo样例的自定义模版
添加进md文档中进行解析,全都在一个doc.md文件里维护demo样例显然是不合适的
因此,我们需要在md文档中引入编写好的demo文件进行渲染,如下
而自定义组件想要被正常解析需要提前引入,让我们去自定义插件里实现一下~
vite-plugin-md.mjs
javascript
export default () => ({
name: 'vitePluginMarkdown',
transform(src, id) {
...
...
return {
code: `
<script setup>import ParsePrimary from './parse-primary.vue';</script>
<template>${markdownIt.render(src)}</template>`,
map: null,
}
}
},
})
此时,我们的Demo样例组件就可以被渲染出来了
走到这一步,我们的组件库文档已经初见端倪,我们已经可以通过不断地编写demo文件,并在md文件中引入即可,当然,我们需要在插件中把对应的组件引入进来,下一步,便是实现每个Demo的源码复制
和使用说明
了
通过(Snippet.vue)组件实现单个Demo的规范化样式
Snippet
组件是我们的Demo容器,主要包含了四个部分的内容
- Title demo的标题
- Desc demo的说明
- Demo demo的渲染
- Code demo的示例源代码
其中,title和code部分通过props
传入,desc和demo部分则提供插槽
直接由Markdown-It进行渲染,当然,具体这个容器需要长啥样,大家根据需要自己编写代码即可
html
<template>
<div class="case-card">
<!-- id 用于锚点定位 -->
<yk-title :id="title" :level="3">{{ title }}</yk-title>
<slot name="desc"></slot>
<div class="container">
<slot name="demo"></slot>
</div>
<yk-space class="space" :size="8">
<div v-show="showCode" class="icons" @click="onCopy">
<yk-icon name="yk-kaobei"></yk-icon>
</div>
<div class="icons" :class="{ select: showCode }" @click="clickShow">
<yk-icon name="yk-daima"></yk-icon>
</div>
</yk-space>
<div v-show="showCode" ref="codes" class="codes">
<pre class="hljs"><code v-html="html"></code></pre>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
title: {
type: String,
default: '标题',
},
code: {
type: String,
default: '',
},
})
const html = hljs.highlightAuto(decodeURIComponent(props.code)).value
</script>
至此,我们只需要在插件中渲染yk-snippet
这个组件然后把四个组成部分丢给他,便可完成整个文档库的渲染
代码拆解
约定Demo文件格式
对于一个Demo而言,我们只关心那四个部分,我们需要在编写的时候进行规范化以便在脚本侧提取想要的内容,第一行为Title,第二行为Desc,第三行则是期望引入的Demo文件,源码部分我们将直接根据这个路径去读取文件内容
markdown
:::snippet
按钮类型 type
按钮有三种类型:`主按钮` 、`次按钮` 、`线框按钮` 。主按钮在同一个操作区域建议最多出现一次。
<ButtonPrimary/>
:::
匹配Demo块并解析对应内容
javascript
const snippetPattern = /:::snippet\s+(.*?)\s+:::/gs
//匹配md文件中所有的snippet块
const matches = src.matchAll(snippetPattern)
for (const match of matches) {
// parse three lines in snippet block
// 提取三行内容
const [title, desc, demoName] = match[1].split('\n')
// match demo Vue components
// 去除括号和斜杠,直接得到Demo组件名称,如ButtonPrimary
const tagPattern = /<(\w+)\/>/
const demoTagName = demoName.match(tagPattern)[1] // <ButtonPrimary/> -> ButtonPrimary
const demoComponentName = camelToDashCase(demoTagName).replace(
/([a-zA-Z])([A-Z])/g,
'$1-$2',
) // ButtonPrimary -> button-primary
// 获取源码
const demoCode = fetchDemoCode(demoComponentName, id)
// 根据组件和md文件的相对路径添加依赖
const importLine = `import ${demoTagName} from './${demoComponentName}.vue';\n`
importContent += importLine
// 调用demo容器组件,将demoCode和title作为props传入
// Demo 将直接渲染组件实例,作为插槽传入
// Desc 用MrkdownIt解析为html,作为插槽传入
const caseCardContent = `<yk-snippet title="${title}" code="${encodeURIComponent(
demoCode,
)}" >
<template v-slot:demo>${demoName}</template>
<template v-slot:desc>${markdownIt.render(desc)}</template>
</yk-snippet>
`
// 替换原来待渲染的内容
src = src.replace(match[0], caseCardContent) // html render
工具方法
需要两个工具方法
javascript
// 驼峰命名转短横线
export function camelToDashCase(str) {
return str.replace(/([a-zA-Z])([A-Z])/g, '$1-$2').toLowerCase()
}
// fetch demo source code by relative path
// 根据Demo组件文件名和当前md文档的绝对路径,读取demo组件源代码
export function fetchDemoCode(componentName, id) {
const targetFile = `${componentName}.vue`
const absolutePath = path.resolve(path.dirname(id), targetFile)
try {
const content = fs.readFileSync(absolutePath, 'utf-8')
return content
} catch (error) {
return ''
}
}
返回渲染内容
将所有的snippet块均处理为yk-snippet
容器进行渲染后,我们返回对应的script和template部分即可,这边外层包了一个带类名的div,主要是为了添加额外的自定义样式
javascript
return {
code: `
<script setup>
${importContent}
</script>
<template>
<div class='yk-demo-doc'>
${markdownIt.render(src)}
</div>
</template>`,
map: null,
}
额外工作~
当然,如果不想每次都引入yk-snippet这个容器组件的话,可以外main.ts里注册一下
javascript
import Snippet from './components/Snippet.vue';
app.component('YkSnippet', Snippet);
5. 自定义代码块
当然,为了应对Icon这种需要直接渲染demo的场景,我们也提供了pure代码块
markdown
:::pure
<IconPlanarity/>
:::
6.结束
其实我的实现类似于markdown-it-container
的对于块
的处理,有需要的话也完全可以通过定义其他标志符号采取不同的渲染逻辑,目前看,可能替换并渲染的方案不是最优雅的,如果有意见和建议也欢迎随时提出~
后续可能会针对项目中npm run new
脚本和目前我着手实现的Message
和Upload
组件输出文档,对于阅读体验和行文逻辑有建议的小伙伴可以提出意见,期待和你们共同进步!!!