写点什么

利用 Babel 自动生成“Attribute”文档

作者:小鑫同学
  • 2022-10-19
    北京
  • 本文字数:3534 字

    阅读完需:约 1 分钟

利用Babel自动生成“Attribute”文档

1. 前言

大家好,我是小鑫同学。一位从事过 Android 开发混合开发,现在长期从事前端开发的编程爱好者,我觉得在编程之路上最重要的是知识的分享,所谓三人行必有我师。所以我开始在社区持续输出我所了解到、学习到、工作中遇到的各种编程知识,欢迎有想法、有同感的伙伴加我fe-xiaoxin微信交流~


利用 Babel 自动解析源码属性上的注释生成对应 Markdown 文档,这个场景的应用主要包括在组件库文档对组件属性的介绍中,这一篇就通过编写一个 Babel 插件来实现这个功能~

2. 开发自动生成属性文档插件

2.1 生成 Babel 插件模板:

  • 2.1.1 创建babel-plugin-auto-attr-doc文件夹;

  • 2.1.2 安装npm i -g yo generator-babel-plugin-x

  • 2.1.3 在新建目录下执行 yo babel-plugin-x:v7-ts


生成的插件模板如下:


babel-plugin-auto-attr-doc  ├─ lib                      │  └─ index.js              ├─ src                      │  └─ index.ts              ├─ __tests__                │  ├─ fixtures              │  │  └─ example            │  │     ├─ actual.ts       │  │     └─ expected.ts     │  └─ index.js              ├─ package-lock.json        ├─ package.json             ├─ README.md                └─ tsconfig.json            
复制代码

2.2 转换思路详解:


  • 2.2.1 转换过程:利用 Babel Typescript 脚本解析为 AST,通过对 AST 结构分析抽离对应的注释部分,再拼接 Markdown 表格风格的语法;

  • 2.2.2 源码要求:我们应该将组件涉及到对外提供的属性统一到对应的types.ts文件管理,分别导出对应的type字段;

  • 2.2.3 注释要求:分别定义字段描述、类型、可选项、默认值 4 项,由于解析器关键词冲突原因,我们应该尽量避免;


/**  * @cDescribe 类型  * @cType string  * @cOptions   * @cDefault   */ export type IType = "primary" | "success" | "warning" | "danger" | "info";  /**  * @cDescribe 图标组件  * @cType string  * @cOptions   * @cDefault   */ export type IIcon = string;  /**  * @cDescribe 是否为朴素按钮  * @cType boolean  * @cOptions   * @cDefault false  */ export type IPlain = boolean;
复制代码


  • 2.2.4 Markdown 表格:展示组件的属性、描述、类型、可选值和默认值这几项;


2.3 单元测试用例:

  1. 准备插件待解析源码文件source-code.ts

  2. 准备实际生成 MD 后应该显示的内容文件actual.md


| 属性名 | 说明 | 类型 | 可选值  | 默认值 || ------ | ---- | ---- | ----- | ----- || type | 类型 | string |  |  || icon | 图标组件 | string |  |  || plain | 是否为朴素按钮 | boolean |  | false |
复制代码


  1. 调整单元测试文件读取:


it(`should ${caseName.split("-").join(" ")}`, () => {  const actualPath = path.join(fixtureDir, "source-code.ts");
// 对源码进行加载解析 transformFileSync(actualPath);
// 读取我们准备好的md文件 const actual = fs .readFileSync(path.join(fixtureDir, "actual.md")) .toString();
// 读取插件解析生成的md文件 const expected = fs .readFileSync(path.join(fixtureDir, "api-doc.md")) .toString();
// diff const diff = diffChars(actual, expected); diff.length > 1 && _print(diff); expect(diff.length).toBe(1);});
复制代码

2.4 AST 分析详解:

  • 通过在AST explorer的源码分析,我们在 Babel 中可以通过遍历ExportNamedDeclaration(命名导出声明);

  • leadingComments数组中可以取出所有注释文本的集合,在 Babel 处理时我们需要依次处理每一块注释后增加标记来避免重复处理;

  • (path.node.declaration as t.TypeAlias).id.name中取属性名称;


将注释文本通过 doctrine 模块解析为对象后和属性名合并对转换 Markdown 所需要的所有数据~


2.5 插件开发过程:

2.5.1 定义 Comment、ApiTable 类型对象:

type Comment =  | {      describe: string;      type: any;      options?: any;      default?: any;    }  | undefined;
复制代码


type ApiTable = {  attributeName: any;  attributeDescribe: any;  attributeType: any;  attributeOptions: any;  attributeDefault: any;};
复制代码

2.5.2 插件主逻辑分析:

  1. pre:初始化存放 apidoc 容器,避免在存放时找不到容器;

  2. visitor:解析源码并获取组织 MD 内容数据暂存到 apidoc 中;

  3. post:取出所有的 apidoc 内容解析并输出到本地文件中;


export default declare(  (api: BabelAPI, options: Record<string, any>, dirname: string) => {    api.assertVersion(7);
return { name: "auto-attr-doc", pre(this: PluginPass, file: BabelFile) { this.set("api-doc", []); }, visitor: { ExportNamedDeclaration( path: NodePath<t.ExportNamedDeclaration>, state: PluginPass ) { const apidoc = state.get("api-doc"); // 处理 path.node.leadingComments 中未处理的数据后塞到apidoc中 state.set("api-doc", apidoc); }, }, post(this: PluginPass, file: BabelFile) { const apidoc = this.get("api-doc"); const output = generateMD(apidoc); const root = path.parse(file.opts.filename || "./").dir; fs.writeFileSync(path.join(root, "api-doc.md"), output, { encoding: "utf-8", }); }, } as PluginObj<PluginPass>; });
复制代码

2.5.3 主逻辑实现:

  1. leadingComments数组会在依次访问ExportNamedDeclaration时不停增加,我们在处理掉当前索引的对象后增加一个处理过的标记skip,下次循环直接跳过;

  2. 通过parseComment函数解析后的对象可以通过tags数组获取到所有的注释项目,通过对应的title得到对应description内容;

  3. 在往 apidoc 存放数据时需要处理属性名称符合一定的规则,并将apidoc对象存放到原容器中;


{  ExportNamedDeclaration(    path: NodePath<t.ExportNamedDeclaration>,    state: PluginPass  ) {    const apidoc = state.get("api-doc");    let _comment: Comment = undefined;    path.node.leadingComments?.forEach((comment) => {      if (!Reflect.has(comment, "skip")) {        const tags = parseComment(comment.value)?.tags;        _comment = {          describe:            tags?.find((v) => v.title === "cDescribe")?.description || "",          type: tags?.find((v) => v.title === "cType")?.description || "",          options:            tags?.find((v) => v.title === "cOptions")?.description || "",          default:            tags?.find((v) => v.title === "cDefault")?.description || "",        };        Reflect.set(comment, "skip", true);      }    });    apidoc.push({      attributeName: (path.node.declaration as t.TypeAlias).id.name.substr(1).toLocaleLowerCase(),      attributeDescribe: _comment!.describe,      attributeType: _comment!.type,      attributeOptions: _comment!.options,      attributeDefault: _comment!.default,    } as ApiTable);    state.set("api-doc", apidoc);  },}
复制代码

2.5.4 注释解析函数:

const parseComment = (comment: string) => {  if (!comment) {    return;  }  return doctrine.parse(comment, {    unwrap: true,  });};
复制代码

2.5.5 Markdown 表格拼装:

const generateMD = (apidoc: Array<ApiTable>) => {  let raw = `| 属性名 | 说明 | 类型 | 可选值  | 默认值 |\n| ------ | ---- | ---- | ----- | ----- |\n`;  apidoc.forEach((item) => {    raw += `| ${item.attributeName} | ${item.attributeDescribe} | ${item.attributeType} | ${item.attributeOptions} | ${item.attributeDefault} |\n`;  });  return raw;};
复制代码

2.5.6 生成结果展示~

3. 总结

插件生成目前基本功能完成,注释解析可以通过 Babel 的插件选项来定义作为一个扩展方向,MD 文件的生成可以通过对应工具转换,更多的输出文件类型也可以作为扩展方向,欢迎喜欢玩转 Babel 的小伙伴一起交流交流~


已推送至 GitHub,欢迎克隆演示:git clone https://github.com/OSpoon/awesome-examples.git


如果看完觉得有收获,欢迎点赞、评论、分享支持一下。你的支持和肯定,是我坚持写作的动力~最后可以关注我 @小鑫同学。欢迎点此扫码加我微信fe-xiaoxin交流,共同进步(还可以帮你 fix🐛)~


发布于: 刚刚阅读数: 3
用户头像

小鑫同学

关注

⚡InfoQ签约作者 2018-12-10 加入

⚡来【IT200.cn】找我

评论

发布
暂无评论
利用Babel自动生成“Attribute”文档_前端_小鑫同学_InfoQ写作社区