• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍

    • 1.Babel 的介绍
    • 2.Babel 的编译流程
    • 3.Babel 的 AST
    • 4.Babel 的 API
    • 5.实战案例:插入函数调用参数
    • 6.JS Parser 的历史
    • 7.traverse 的 path、scope、visitor
    • 8.Generator 和 SourceMap 的奥秘
    • 9.Code- Frame 和代码高亮原理
    • 10.Babel 插件和 preset
    • 11.Babel 插件的单元测试
    • 12.Babel 的内置功能(上)
    • 13.Babel 的内置功能(下)
    • 14.Babel 配置的原理
    • 15.工具介绍:VSCode Debugger 的使用
    • 16.实战案例:自动埋点
    • 17.实战案例: 自动国际化
    • 18.实战案例:自动生成 API 文档
    • 19.实战案例: Linter
    • 20.实战案例: 类型检查
    • 21.实战案例: 压缩混淆
    • 22.实战案例: JS 解释器
    • 23.实战案例: 模块遍历
    • 24.Babel Macros
    • 25.如何调试 Babel 源码?
    • 26.手写 Babel:思路篇
    • 27.手写 Babel: parser 篇
    • 28.手写 Babel: traverse 篇
    • 29.手写 Babel: traverse -- path篇
    • 30.手写 Babel: traverse -- scope篇
    • 31.手写 Babel: generator篇
    • 32.手写 Babel: core篇
    • 33.手写 Babel: cli篇
    • 34.手写 Babel: 总结
    • 35.小册总结
    • 36.加餐:会了 babel 插件,就会写 prettier 插件

如果我们提供了一个 sdk 给别人用,那么要把有哪些 api、都有什么参数等等信息写到文档中,并且每次改代码都要同步更新下文档。这件事情很繁琐,靠人来维护也不靠谱,可不可以自动生成呢?

api 文档的生成也是根据源码信息来的,有哪些函数、类、都有啥参数、参数是什么类型,这些都在源码里面,而更多信息一般都会写在注释里。我们可以通过 babel 取到这些信息,那么自然可以自动生成文档。

思路分析

比如这样一段代码:

/**
 * say 你好
 * @param name 名字
 */
function sayHi(name: string, age: number, a: boolean) {
    console.log(`hi, ${name}`);
    return `hi, ${name}`;
}

/**
 * 类测试
 */
class Guang {
    name: string; // name 属性
    constructor(name: string) {
        this.name = name;
    }

    /**
     * 方法测试
     */
    sayHi(): string {
        return `hi, I'm ${this.name}`;
    }
}

我们要处理 FunctionDeclaration 节点和 ClassDelcaration 节点:

可以通过 AST 拿到各种信息,比如:

FunctionDelcaration:

  • 函数名: path.get('id').toString()
  • 参数: path.get('params')
  • 返回值类型: path.get('returnType').getTypeAnnotation()
  • 注释信息:path.node.leadingComments

注释可以使用 doctrine 来 parse,支持 @xxx 的解析

ClassDeclaration:

  • 类名:path.get('id').toString()
  • 方法:travese ClassMethod 节点取信息(包括 constructor 和 method)
  • 属性: traverse ClassProperty 节点取信息
  • 注释信息: path.node.leadingComments

有了这些信息之后,就可以打印成文档了,打印就是拼接字符串的过程,可以支持 markdown、html、json 等格式。

我们来写下代码。

代码实现

首先搭一个插件的基本结构:

const { declare } = require('@babel/helper-plugin-utils');

const autoDocumentPlugin = declare((api, options, dirname) => {
    api.assertVersion(7);

    return {
        pre(file) {
            file.set('docs', []);
        },
        visitor: {
            FunctionDeclaration(path, state) {
            },
            ClassDeclaration (path, state) {
            }
         },
         post(file) {
            const docs = file.get('docs');
        }
    }
}

我们在全局的 file 对象中放一个 docs 的数组,用于收集信息。

FunctionDeclaration 的处理

就像前面说的,通过 AST 可以拿到函数的各种信息:

FunctionDeclaration(path, state) {
    const docs = state.file.get('docs');
    docs.push({
        type: 'function',
        name: path.get('id').toString(),
        params: path.get('params').map(paramPath=> {
            return {
                name: paramPath.toString(),
                type: resolveType(paramPath.getTypeAnnotation())
            }
        }),
        return: resolveType(path.get('returnType').getTypeAnnotation()),
        doc: path.node.leadingComments && parseComment(path.node.leadingComments[0].value)
    });
    state.file.set('docs', docs);
},

其中要注意的有两点:

  • path.getTypeAnnotation() 取到的类型需要做进一步的处理,比如把 TSStringKeyword 换成 string,这样更易读
function resolveType(tsType) {
    const typeAnnotation = tsType.typeAnnotation;
    if (!typeAnnotation) {
        return;
    }
    switch (typeAnnotation.type) {
        case "TSStringKeyword":
            return "string";
        case "TSNumberKeyword":
            return "number";
        case "TSBooleanKeyword":
            return "boolean";
    }
}
  • 注释信息用 doctrine 来 parse,可以解析注释里的 @xxx 信息
const doctrine = require("doctrine");

function parseComment(commentStr) {
    if (!commentStr) {
        return;
    }
    return doctrine.parse(commentStr, {
        unwrap: true,
    });
}

ClassDeclaration 的处理

ClassDeclaration 的处理复杂一些,要分别提取 constructor、method、properties 的信息。

首先,收集 class 的整体信息

ClassDeclaration (path, state) {
    const docs = state.file.get('docs');
    const classInfo = {
        type: 'class',
        name: path.get('id').toString(),
        constructorInfo: {},
        methodsInfo: [],
        propertiesInfo: []
    };
    if (path.node.leadingComments) {
        classInfo.doc = parseComment(path.node.leadingComments[0].value);
    }
    docs.push(classInfo);
    state.file.set('docs', docs);
}

然后遍历 ClassProperty 和 ClassMethod 并提取信息

path.traverse({
    ClassProperty(path) {
        classInfo.propertiesInfo.push({
            name: path.get("key").toString(),
            type: resolveType(path.getTypeAnnotation()),
            doc: [path.node.leadingComments, path.node.trailingComments]
                .filter(Boolean)
                .map((comment) => {
                    return parseComment(comment.value);
                })
                .filter(Boolean),
        });
    },
    ClassMethod(path) {
        if (path.node.kind === "constructor") {
            classInfo.constructorInfo = {
                params: path.get("params").map((paramPath) => {
                    return {
                        name: paramPath.toString(),
                        type: resolveType(paramPath.getTypeAnnotation()),
                        doc: parseComment(path.node.leadingComments[0].value),
                    };
                }),
            };
        } else {
            classInfo.methodsInfo.push({
                name: path.get("key").toString(),
                doc: parseComment(path.node.leadingComments[0].value),
                params: path.get("params").map((paramPath) => {
                    return {
                        name: paramPath.toString(),
                        type: resolveType(paramPath.getTypeAnnotation()),
                    };
                }),
                return: resolveType(path.getTypeAnnotation()),
            });
        }
    },
});

这样处理完之后,在 post 阶段就能拿到所有的信息了,之后就是文档的生成

文档生成

文档生成其实就是对象打印的过程,我们可以通过插件的参数传入 format,然后用不同的 renderer 来渲染,之后写入 docs 目录。

post(file) {
    const docs = file.get('docs');
    const res = generate(docs, options.format);
    fse.ensureDirSync(options.outputDir);
    fse.writeFileSync(path.join(options.outputDir, 'docs' + res.ext), res.content);
}

renderer 其实就是拼接字符串,我们实现一下 markdown 的(比较简单的实现,大家如果有需求需要继续完善 renderer 和信息的提取,其实这里也可以用模版引擎来做,更易于维护)

function 和 class 分别拼接不同的字符串

module.exports = function (docs) {
    let str = "";

    docs.forEach((doc) => {
        if (doc.type === "function") {
            str += "##" + doc.name + "\n";
            str += doc.doc.description + "\n";
            if (doc.doc.tags) {
                doc.doc.tags.forEach((tag) => {
                    str += tag.name + ": " + tag.description + "\n";
                });
            }
            str += ">" + doc.name + "(";
            if (doc.params) {
                str += doc.params
                    .map((param) => {
                        return param.name + ": " + param.type;
                    })
                    .join(", ");
            }
            str += ")\n";
            str += "#### Parameters:\n";
            if (doc.params) {
                str += doc.params
                    .map((param) => {
                        return "-" + param.name + "(" + param.type + ")";
                    })
                    .join("\n");
            }
            str += "\n";
        } else if (doc.type === "class") {
            str += "##" + doc.name + "\n";
            str += doc.doc.description + "\n";
            if (doc.doc.tags) {
                doc.doc.tags.forEach((tag) => {
                    str += tag.name + ": " + tag.description + "\n";
                });
            }
            str += "> new " + doc.name + "(";
            if (doc.params) {
                str += doc.params
                    .map((param) => {
                        return param.name + ": " + param.type;
                    })
                    .join(", ");
            }
            str += ")\n";
            str += "#### Properties:\n";
            if (doc.propertiesInfo) {
                doc.propertiesInfo.forEach((param) => {
                    str += "-" + param.name + ":" + param.type + "\n";
                });
            }
            str += "#### Methods:\n";
            if (doc.methodsInfo) {
                doc.methodsInfo.forEach((param) => {
                    str += "-" + param.name + "\n";
                });
            }
            str += "\n";
        }
        str += "\n";
    });
    return str;
};

这样我们就完成了 api 文档的自动生成。

测试

处理的代码为:

生成的文档:

可以继续完善,比如生成这样的 html 文档:

总结

这一节我们梳理了自动生成 api 文档的实现思路,如果对外提供 sdk 的话,那么自动文档生成是个刚需,不然每次都要人工同步改。

自动文档生成主要是信息的提取和渲染两部分,提取源码信息我们只需要分别处理 ClassDeclaration、FunctionDeclaration 或其他节点,然后从 ast 取出名字、注释等信息,之后通过 renderer 拼接成不同的字符串。

其实这种工具的应用有很多的,各种语言都有。

(代码在这里,建议 git clone 下来通过 node 跑一下)

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
17.实战案例: 自动国际化
Next
19.实战案例: Linter