阅读 228

组件库文档系统的设计思路(数据库设计文档)

组件库现在基本上是每个公司前端团队的标配了,组件库的开发也是老生常谈的话题了。今天的内容不介绍组件是如何开发的,而是介绍如何搭建组件库文档展示平台,方便组内同事查阅组件相关信息。组件库文档展示平台基于vue3开发,vite构建,rollup打包。组件库最终展示效果如下图:

image.png

1. 项目特点

  • 项目基于vue3开发,vite打包。

  • 项目文档统一用markdown文件编写。

  • 项目markdown文档的样式可随意定制。

  • 项目markdown文件支持vue源码查看,源码执行,源码复制。

  • 组件内容包括:组件源码,组件样式,组件ts文件,入口文件等。

  • 组件文档中仅需开发者编写测试demo即可。

  • 组件文档中的基础信息根据源码自动生成,包括组件标题,描述,props, events, methods, slots等。

  • 项目路由表根据组件库目录自动生成,无需手动额外配置。

  • 项目导航目录根据路由表自动生成,无需手额外配置。

  • 组件支持按需加载或全量引入。

2. 项目结构

项目主要包括三大块内容:packagesdocsdist

  • packages目录放包的源文件:组件源代码,一个完整的组件内容包括:index.ts, index.vue, types.ts, index.less。

  • docs目录放文档内容:包括开发编写的组件demo,以及其他非组件相关文档内容。

  • dist目录放组件打包后的文档:经过doc-loader插件处理后自动生成组件的基础描述文档,包括组件name, desc, props, events, methods, slots等内容;然后和开发者编写的demo文档合并生成完整的组件文档,并输出到dist目录里。

组件文档平台.png

根据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


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐