组件库文档系统的设计思路(数据库设计文档)
组件库现在基本上是每个公司前端团队的标配了,组件库的开发也是老生常谈的话题了。今天的内容不介绍组件是如何开发的,而是介绍如何搭建组件库文档展示平台,方便组内同事查阅组件相关信息。组件库文档展示平台基于vue3开发,vite构建,rollup打包。组件库最终展示效果如下图:
1. 项目特点
项目基于vue3开发,vite打包。
项目文档统一用markdown文件编写。
项目markdown文档的样式可随意定制。
项目markdown文件支持vue源码查看,源码执行,源码复制。
组件内容包括:组件源码,组件样式,组件ts文件,入口文件等。
组件文档中仅需开发者编写测试demo即可。
组件文档中的基础信息根据源码自动生成,包括组件标题,描述,props, events, methods, slots等。
项目路由表根据组件库目录自动生成,无需手动额外配置。
项目导航目录根据路由表自动生成,无需手额外配置。
组件支持按需加载或全量引入。
2. 项目结构
项目主要包括三大块内容:packages
,docs
,dist
。
packages目录放包的源文件:组件源代码,一个完整的组件内容包括:index.ts, index.vue, types.ts, index.less。
docs目录放文档内容:包括开发编写的组件demo,以及其他非组件相关文档内容。
dist目录放组件打包后的文档:经过doc-loader插件处理后自动生成组件的基础描述文档,包括组件name, desc, props, events, methods, slots等内容;然后和开发者编写的demo文档合并生成完整的组件文档,并输出到dist目录里。
根据dist目录的组件文档和原始的非组件文档合并,自动生成项目路由表和侧边栏导航目录。
3. doc-loader插件
组件文档基础信息提取用到了doc-loader插件,doc-loader插件主要是将组件源代码转换成AST,通过遍历AST来提取组件关键信息。
3.1 使用方法
根目录下新增配置文件,doc.config.js,内容如下:
const fs = require("fs-extra"); const path = require("path"); const DocLoader = require("@htfed/doc-loader"); new DocLoader({ entry: path.resolve(__dirname, "../src"), // 定义入口文件,也就是组件库原始目录 output: path.resolve(__dirname, "../dist"), // 定义输出目录,也就是组件文档输出路径 scriptCompileOptions: {}, // js解析选项,用于@babel/parser parse方法传入 typeScriptCompileOptions: {}, // ts解析选项,用于@babel/parser parse方法传入 templateCompileOptions: {}, // template解析选项,用于@vue/compiler-core baseCompile方法传入 outputFileExtension: ".md", // 定义输出文件的格式,支持.md, .json beforeOutputFileHook: (options) => { // 文件输出之前的钩子函数 const { entry: fileEntry, mdContent } = options; const fileName = fileEntry.match(/(\w+)\\index\.vue$/)[1]; const fullFileName = path.resolve( __dirname, `../docs/components/ui/${fileName}.md` ); const newMdContent = fs.existsSync(fullFileName) && fs.readFileSync(fullFileName, "utf8"); return { fileName, fileContent: newMdContent ? `${newMdContent}\n####\n${mdContent}` : mdContent, }; }, outputMdFileOptions: {}, });复制代码
3.2 实现逻辑
doc-loader插件主要处理两块内容:源码解析
,内容渲染
。
源码解析逻辑如下:
接收传入的参数:包括文件原始目录,打包输出文件,解析参数等等。
清空打包目录。
开始解析目标文档。
读取目标文档(组件)的源码内容,并通过正则读取.vue文件中的template, js, ts内容。如果组件定义了ts文件,通过读取ts文件来获取ts内容。
调用@babel/parser里的parse方法将js内容解析成组件AST(抽象语法树),再调用@babel/traverse遍历组件AST,通过调用CallExpression()获取组件events事件定义;ExportDefaultDeclaration()方法获取组件desc描述信息,name组件名称,props组件属性定义等;VariableDeclarator()方法获取组件methods方法定义。同时通过调用回调函数将这些基本信息保存到componentInfo中。
调用@babel/parser里的parse方法将ts内容解析成组件AST(抽象语法树),再调用@babel/traverse遍历组件AST,调用TSPropertySignature()来获取ts文件的interface接口定义和types类型定义,从而提取组件的props定义。同时通过调用回调函数将这些基本信息保存到componentInfo.props中。
调用@vue/compiler-core提供的baseCompile方法,或是@vue/compiler-sfc的compile方法来编译模板template内容,生成templateAst,遍历templateAst内容,定义slot()方法提取template中的slot插槽和slot标签上的相关属性。同时通过调用回调函数将这些基本信息保存到componentInfo.slots中。
比如template编译代码如下:
// 遍历模板AST const traverserTemplateAst = (ast, visitor = {}) => { function traverseNode(node, parent) { visitor.enter && visitor.enter(node, parent); visitor[node.tag] && visitor[node.tag](node, parent); node.children && traverseArray(node.children, node); visitor.exit && visitor.exit(node, parent); } function traverseArray(array, parent) { array.forEach((child) => { traverseNode(child, parent); }); } traverseNode(ast, null); }; // template模板内容编译 const compileTemplate = (templateStr, options = {}, callback = () => {}) => { if (!templateStr) return; const { ast } = baseCompile(templateStr, { ...defaultTemplateCompileOptions, ...options, }); // 遍历模板ast traverserTemplateAst(ast, { // 插槽标签 slot(node, parent) { // 提取所有的插槽slot const index = parent.children.findIndex((item) => item === node); let desc = defaultText; let name = defaultText; if (index > 0) { // 查询是否有插槽的注释标签 // @vue/compiler-core 里的parseComment方法,type: 3 /* COMMENT */ const tag = parent.children[index - 1]; if (tag.type === 3) { desc = tag.content.trim(); } } // 获取插槽name名称 <slot name="header"></slot> 获取值"header" if (node.props && node.props.length) { const targetProp = node.props.filter( (prop) => prop.type === 6 && prop.name === "name" )[0]; targetProp && (name = targetProp.value.content); } // 执行回调,将值保存到componentInfo.slots中 callback({ type: "slots", key: name, content: { name, desc, }, }); }, }); };复制代码
内容渲染逻辑如下:
组件源码解析完成后,基础信息保存在componentInfo中,key为文件路径,读取内容,并根据内容生成.md或是.json文件。
内容渲染分为.md和.json两种格式。
.md文件就是遍历componentInfo数据,根据数据类型生成md格式的字符串,比如标题用 ### 表示等。最终生成符合md文件格式的字符串内容。
如果初始参数中定义了beforeOutputFileHook函数,则执行beforeOutputFileHook(),该方法用于在文档输出前对数据做补充处理。(此处我们是读取开发者定义的组件demo内容,与上面生成的md字符串合并生成新的文档数据)
最终调用fs.writeFile将数据写入,生成文件,输出到打包目录里。
核心代码如下:
render() { const mdArr = []; Object.keys(this.parserResult).forEach((key) => { const content = this.parserResult[key]; if (content) { switch (key) { case "name": mdArr.push( ...this.onRenderTitle({ content, isNewLine: false, weight: 2, }) ); break; case "desc": mdArr.push(content); break; case "props": if (this.options[key]) { // props数据 mdArr.push( ...this.onRenderContent({ key, content, option: this.options[key], }) ); // tsProps数据 mdArr.push( ...this.onRenderContent({ key: "tsProps", content: this.onFilterTsProps( content, this.parserResult.tsProps ), option: this.options.props, // 公用props的配置数据 }) ); } break; case "slots": case "events": case "methods": this.options[key] && mdArr.push( ...this.onRenderContent({ key, content, option: this.options[key], }) ); break; default: break; } } }); return mdArr.join("\n"); }复制代码
4. 项目预览
打包目录文档生成后,遍历文档目录,和项目本身非组件文档目录,结合自动生成路由表,并生成左侧菜单数据。
import { Docs, Doc } from "../types"; function load({ fileEntry, fileExtension, callback }: any) { if (!fileEntry) return; Object.keys(fileEntry).reduce((total: Doc[], i: any) => { if (!fileExtension || i.endsWith(fileExtension)) { const fileArr = i.split("/"); const fileName = fileArr[fileArr.length - 1]; const nameArr = fileName.split("."); const name = nameArr[nameArr.length - 2]; const options: Doc = { name, fileName, fileExtension, filePath: i, fileContent: fileEntry[i], }; total.push(options); callback && callback(options); } return total; }, []); } const docs: Docs[] = []; // 除了组件之外的根目录文档,比如introduce.md const componentDocs = import.meta.globEager("../components/*.md"); // 打包生成的组件文档 const disDocs = import.meta.globEager("../../dist/*.md"); load({ fileEntry: { ...componentDocs, ...disDocs, }, fileExtension: ".md", callback: (options: Doc) => { docs.push({ name: options.name, path: options.name, meta: { title: options.fileContent?.default?.$vd?.toc[0]?.content || options.name, }, component: options.fileContent.default, }); }, }); export default docs;复制代码
每次组件有更新,需要执行下 node doc.config.js 命令生成新的文档内容,启动项目即可预览。
5. 写在最后
组件文档平台的核心就是利用babel插件将源码生成AST,遍历AST来提取文档信息。现在这个方案可能还会有很多不足点,后面在组件完善中再慢慢优化,如果大家有更好的想法和建议,欢迎留言补充。
作者:邹R-ainna
链接:https://juejin.cn/post/7057804183105175565