后端问:作为前端如何实现自动生成接口文档

技术博客 (121) 2023-11-11 09:01:01

前言,众所周知,后端的接口文档一般使用 swagger/openapi,而其他的一般接口文档也基本是使用 openapi 来进行解析的。例如 redoc(20.8k star) 和 knife4j(3.5k star) 等.

作为一个通用的解决方案,前端社区自然也有对应的实现。

如何实现 swagger 文档界面

swagger-ui-express 是一个 express 插件,可以在 express 中实现 swagger-ui。然后传入 openapi 来渲染接口文档。

const express = require("express");
const app = express();
const swaggerUi = require("swagger-ui-express");
const swaggerDocument = require("./swagger.json");

var options = {
  explorer: true,
};

app.use(
  "/api-docs",
  swaggerUi.serve,
  swaggerUi.setup(swaggerDocument, options)
);

从上面的示例可以看到 swagger.json 即为接口文档的描述数据。在这种情况下我们需要准备已经有的 json 数据方可显示文档。

如何生成文档描述数据

我们可以安排 swagger-jsdoc 这个库,来实现在代码中写接口注释,即可生成文档数据。

// Initialize swagger-jsdoc -> returns validated swagger spec in json format
const swaggerSpec = swaggerJSDoc(options);

app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));

注释示例:

/** * @openapi * /: * get: * description: Welcome to swagger-jsdoc! * responses: * 200: * description: Returns a mysterious string. */
app.get("/", (req, res) => {
  res.send("Hello World!");
});

文档不想写,注释不想写,就想生成接口文档

在之前的示例中,虽然我们可以通过写 jsdoc 形式的注释来实现也写接口文档。但问题是,我都能在注释里把文档写清楚了,就不叫“生成文档”了,而叫收集文档片段,而且在代码里写文档,似乎也不太优雅。

要实现注释写不想写,就能实现接口文档的生成,我们首先要让某个工具能理解我们代码的意图。如何理解?

  • 方法 1 通过人工智能去理解代码意图然后生成文档
  • 方法 2 通过代码钩子或埋点获取意图生成文档

很显然,方法 1 不在我们的实现范畴,因为它涉及大模型、数据安全、本地化等各种问题。而方法 2 则是我们能去研究的方案,例如我们使用 vue 开发项目时,vue-devtool 插件就能看到 vue 相关的状态,这就是在 vue 的实现中有为插件开放获取状态的接口。如果没有开放,也可以通过拦截、代理等方法获取运行状态。

分析接口意图

对于原生的 express 产生的接口,我们可以通过 app.routes app._router.stack express.Router() router.stack 等 api 获取到当前所使用的接口。但我们这里使用 mockm,接口是直接声明在对象中的,就更简单了许多。

api: {
  "/a": 111,
  "/b": 222,
  'get /name': `张三`,
  '/status/:code' (req, res) {
    res.json({statusCode: req.params.code})
  },
},

观察上面的 api 对象,例如 "/a": 111 实现的接口是无论通过什么方法请求 /a 这个接口,都返回数字 111,而 'get /name': '张三', 则是通过 get 请求 /name 时返回字符串 张三

以下是转换 api 为 openApi json 的实现:

toOpenApi(serverRouterList) {
  const mapObj = {
    all(item) {
      return [
        `delete`,
        `get`,
        `patch`,
        `post`,
        `put`,
      ].map(method => {
        return {
          ...item,
          method,
          tags: item.tags || [`all-method`],
        }
      })
    },
  }
  serverRouterList = serverRouterList.map(item => {
    const fn = mapObj[item.method]
    return fn ? fn(item) : [item]
  }).flat()
  return serverRouterList
}

后端问:作为前端如何实现自动生成接口文档 (https://mushiming.com/) 技术博客 第1张

serverRouterList 是格式化 api 后面的数据,包含 method route action alias 等,其中要生成接口文档,只需要有 method route 就足够了。

一个只包含此接口的 openApi json 生成后如下:

{
  "openapi": "3.0.0",
  "servers": [
    {
      "url": "/"
    }
  ],
  "tags": [],
  "paths": {
    "/a": {
      "get": {
        "parameters": [],
        "responses": {}
      }
    }
  }
}

看起来是简单吧?

然后把它交给 swagger-ui 即可渲染出文档,展现和使用都没有问题:

后端问:作为前端如何实现自动生成接口文档 (https://mushiming.com/) 技术博客 第2张

如何为接口分组、添加字段说明、数据验证等功能?

接口分组比较简单,直接为接口设置标签就行。由于在 mockm 中可以直接写字面量即可生成 api,但我们要为 api 添加额外说明的时候,就不能简单的使用字面量了。

为了兼容 mockm 的已有实现,我们扩展一个叫 side 的函数,用于收集额外信息和返回字面量。


"post /api/login": side({
  tags: [`admin`],
  summary: `根据用户名获取 token`,
  schema: {
    body: joi.object({
      username: joi.string()
        .default(`李蕾`)
        .required()
        .description(`用户名`),
    }).description(`用户信息`),
  },
  action (req, res) {
    const { username } = req.body
    res.json({
      status: 200,
      message: `欢迎 ${username}, 登录成功`,
      token: `tokentoken`,
    });
  }
}),

可以看到,我们在 side 函数中,为 api 进行了分组,并且为 api 添加了描述,然后为 api 的每个字段添加了说明,是否必填,数字类型是什么等。

这时候可能有一个疑问,在前面提到了使用注释编写接口文档信息,而这里则是使用代码编写接口信息,他们之前的区别在哪里?

  • 区别 1 使用注释编写时没有语法提示、代码完成、代码高亮等功能,十分不方便
  • 区别 2 使用注释编写时,实际上是在写原始 openapi yaml 格式的数据,这要求你对 openapi 规范有相当的了解和熟悉
  • 区别 3 使用代码编写时,顺便可以完成业务上的其他功能,而注释和业务是割裂的,我们都知道业务更新但注释没更新的那种噩梦

观察上面的代码,我们使用 joi 来进行数据描述,与此同时 joi 也可以为我们实现数据的校验。

以下是文档生成效果,可以看到数据描述、默认值(示例值)、类型、是否必填等都可以正常显示:

后端问:作为前端如何实现自动生成接口文档 (https://mushiming.com/) 技术博客 第3张

以下是接口校验功能的效果,我们在接口中声明了 username 为 string,但我们尝试传入数字 111,结果按预期运行,自动提示了哪个字段错误了,为什么错:

后端问:作为前端如何实现自动生成接口文档 (https://mushiming.com/) 技术博客 第4张

功能插件化

对于使用 mockm 的群体中,有一些朋友可能并不想去真的用它来实现 api ,更别说还要为 api 生成文档,所以把此功能抽离为插件,当有一天 mockm 的功能繁复时,可以拆分为插件包单独在其他仓库进行维护:

module.exports = async (util) => {
  const joi = await util.tool.generate.initPackge(`joi`);

  return {
    plugin: [util.plugin.validate, util.plugin.apiDoc],
  };
};

游乐场

安装和初始化 mockm

yarn add mockm
npx mm --config

编写接口

在 mm.config.js 中录入以下信息

module.exports = async (util) => {
  const joi = await util.tool.generate.initPackge(`joi`);

  return {
    plugin: [util.plugin.validate, util.plugin.apiDoc],
    api: {
      "post /api/login": util.side({
        tags: [`admin`],
        summary: `根据用户名获取 token`,
        schema: {
          body: joi
            .object({
              username: joi
                .string()
                .default(`李蕾`)
                .required()
                .description(`用户名`),
            })
            .description(`用户信息`),
        },
        async action(req, res) {
          const { username } = req.body;
          res.json({
            status: 200,
            message: `欢迎 ${username}, 登录成功`,
            token: `tokentoken`,
          });
        },
      }),
    },
  };
};

查看接口文档

浏览器打开 http://127.0.0.1:9000/doc 即可

后端问:作为前端如何实现自动生成接口文档 (https://mushiming.com/) 技术博客 第5张

一个基于 express 的框架。它可以快速生成 api 以及创造数据,开箱即用,便于部署。

github 地址:github.com/wll8/mockm,求 star 。

THE END

发表回复