背景
我们前端写http请求时,一般都是去找服务端要接口文档,然后根据接口文档在文件上写入关于的相关信息。这个过程中,文件内容有很多是重复劳作的。
事实上可以写一个脚本,根据文档地址里面的接口内容,自动生成常见的前端模板,例如接口请求、表格、表单。
目标
服务端同学给前端同学接口文档,通过脚本实现文件内容自动生成。
效果如下:
调研
1、一开始我的想法是做一个爬虫脚本,通过爬这个接口网页的关键信息,比如路径。来实现自动化的功能。
后面通过与其他研发人员沟通,对于yapi这个网站,它们是提供对外的api接口的,只需要接入对应的token发送请求即可,这就少了一步爬虫的操作。
2、在实际操作中,服务端同学可能会陆续给新的接口,故此,不直接生成文件了,而是在终端打印出接口文档内容,用户可直接复制粘贴。这样一来避免了文件被覆盖,二来不用固定死文件位置。
获取yapi文档中项目对应的token:
yaapi开发接口: yapi文档对外的开放接口文档 注意,它的域名跟服务端同学给你的接口文档网址的域名是一致的。
比如服务端同学给你的网址是:yapi.bbb.cn
那么开放api的域名就是yapi.bbb.cn
因为我是前端工程师,服务端的语言选择的是node.js来进行接口的请求与文件读写操作。
设计方案
sequenceDiagram
用户-->>终端: 输入activity api
终端-->>用户: 提供交互选择
用户-->>终端: 输入服务端同学提供的接口网页地址
终端-->>node脚手架: 传入接口网页地址
node脚手架-->>node脚手架: 解析网页地址获取分类接口列表的id
node脚手架-->>yapi服务端: 获取id类下的接口列表
yapi服务端-->>node脚手架: 拿到接口文档里面的所有接口的数据data
node脚手架-->>node脚手架: 根据data进行接口解析并打印出接口文档
完整代码
require('module-alias/register') // 引入别名路径包,因为tsc不支持编译别名路径,故此需要这个包进行路径转换。
import axios from "axios";
import log from "@/utils/BaseLog";
const inquirer = require('inquirer') //弹出交互选项,询问用户要创建的项目需要哪些功能
const token = ""; // token获取地址 https://yapi.xxx.cn/project/xxx/setting
const instance = axios.create({
baseURL: 'https://yapi.xxx.cn/',
timeout: 1000,
});
// 获取某个分类下接口列表 https://hellosean1025.github.io/yapi/openapi.html
interface ListItem {
edit_uid: number,
status: string,
api_opened: boolean,
tag: Array<unknown>,
_id: number,
method: string, // 'POST'
catid: number,
title: string,
path: string, // '/api_service/v1/go/boss/seckill/goods/add',
project_id: number,
uid: number,
add_time: number
}
interface InterfaceListCatResponseData {
count: number,
total: number,
list: Array<ListItem>
}
interface InterfaceListCatResponse {
data: {
errcode: number,
errmsg: string,
data: InterfaceListCatResponseData,
}
}
function interfaceListCat(address: string): Promise<InterfaceListCatResponse> {
let cat_number = address.split('/').pop() as string;
let catid = cat_number.split("_").pop();
return instance.request({
url: '/api/interface/list_cat',
method: 'GET', // default
params: {
catid,
token,
}
})
}
// 根据分类接口获取对应的模板数据。
function getApiTemplateByInterfaceListCat(interfaceListCatResponse: InterfaceListCatResponse) {
let templateBody = '';
let templateFooter = '';
interfaceListCatResponse.data.data.list.forEach((item) => {
let type = item.method == 'POST' ? 'data' : 'params';
let arrays = item.path.split('/');
let index = arrays.indexOf("boss"); // boss后台接口
if (index < 0) {
index = arrays.indexOf("activity"); // 活动接口
}
let functionName = arrays.slice(index + 1).join("_");
let body = `
// ${item.title} https://yapi.xxx.cn/project/${item.project_id}/interface/api/${item._id}
function ${functionName}(${type}) {
return request({
url:"${item.path}",
method:"${item.method}",
${type}
})
}
`
let footer = ` ${functionName},\n`
templateBody += body;
templateFooter += footer;
})
templateFooter = `
export default {
${templateFooter}
}
`
return templateBody + templateFooter;
}
// 获取接口数据(有详细接口数据定义文档) https://hellosean1025.github.io/yapi/openapi.html
interface Row {
[keyName: string]: {
type: string, // 属性类型 eg:number/string
description: string // 对属性的描述 eg:商品名称
}
}
// 根据row生成vueEleTable模板
function getVueEleTableTemplate(row: Row): string {
let tableTemplate = '';
let rowKeys = Reflect.ownKeys(row) as Array<string>;
rowKeys.forEach((key) => {
let text = '';
let type = row[key].type;
let prop = key;
let label = row[key].description ? row[key].description : '';
switch (type) {
case 'string':
if (/img/.test(key)) {
text = `
<el-table-column align="center" label="${label}">
<template slot-scope="{ row }">
<el-image
v-if="row.${prop}"
:src="row.${prop}"
:preview-src-list="[row.${prop}]"
fit="contain"
></el-image>
</template>
</el-table-column>`
} else {
text = `
<el-table-column align="center" prop="${prop}" label="${label}"/>`
}
break;
case 'number':
if (/id/.test(key)) {
text = `
<el-table-column align="center" prop="${prop}" label="${label}" width="100"/>`
}
break;
default:
break;
}
if (text == '') {
text = `
<el-table-column align="center" prop="${prop}" label="${label}" >
<template slot-scope="{ row }"></template>
</el-table-column>`
}
tableTemplate += text
})
tableTemplate = `
<el-table
:data="tableData"
border
fit
highlight-current-row
size="small"
style="width: 100%"
v-loading="loading"
>
${tableTemplate}
</el-table-column>
`
return tableTemplate;
}
interface Params {
name: string, // eg:change_type
example: string, // eg:privileges
desc: string, // eg:兑换类型(privileges、goods、others)
}
// 根据row生成vueEleForm模板
function getVueEleFormTemplate(reqQuery: Array<Params>): string {
let eleFormTemplate = '';
reqQuery.forEach(param => {
if (/page/.test(param.name)) return;
let desc = param.desc ? param.desc : '';
let text = `
<el-form-item label="${desc}">
<el-input
v-model="queryForm.${param.name}"
placeholder="${desc}"
:clearable="true"
/>
</el-form-item>`
eleFormTemplate += text
})
eleFormTemplate = `
<el-form
:inline="true"
@keyup.enter.native="submitForm('form')"
:model="queryForm"
:rules="ruleForm"
ref="form"
>
${eleFormTemplate}
<el-form-item>
<el-form-item>
<el-button type="success" icon="el-icon-search" @click="submitForm('form')">查询</el-button>
</el-form-item>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onHandleAdd">新增</el-button>
</el-form-item>
</el-form>`
return eleFormTemplate;
}
interface InterfaceGetResponseData {
[keyName: string]: any,
req_query: Array<Params>,
res_body: string, // 响应的body
}
interface InterfaceGetResponse {
data: {
errcode: number,
errmsg: string,
data: InterfaceGetResponseData,
}
}
function interface_get(address: string): Promise<InterfaceGetResponse> {
let id = address.split('/').pop();
return instance.request({
url: '/api/interface/get',
method: 'GET', // default
params: {
id,
token,
}
})
}
// 根据分类接口获取对应的模板数据。
function getApiTemplateByInterfaceGet(interfaceGetResponse: InterfaceGetResponse) {
let vueTemplate: { form: string, table: Array<string> } = {
form: '',
table: [],
}
let { data } = interfaceGetResponse;
vueTemplate.form = getVueEleFormTemplate(data.data.req_query);
let resBody = JSON.parse(data.data.res_body).properties.data.properties;
let resMsgKeys = Reflect.ownKeys(resBody);
resMsgKeys.forEach((key) => {
if (resBody[key].type == 'array') {
vueTemplate.table.push(getVueEleTableTemplate(resBody[key].items.properties));
}
})
return vueTemplate;
}
// 根据分类接口文档,自动生成前端js api文档。
export async function autoWriteApiListCatTemplate() {
let promptList = [
{
type: "input",
message: "请填写接口文档的地址:",
name: "address",
validate: function (val: string) {
if (val.trim().length == 0) {
log.error('\n不允许为空');
return false;
}
let reg = /^https:\/\/yapi.xxx.cn\/project\/\d+\/interface\/api\/cat_\d+$/g;
if (reg.test(val) == false) {
log.error(`\n当前输入的地址不是类级别的接口,请查看是否输入错误`);
log.error(`举例:https://yapi.xxx.cn/project/1687/interface/api/cat_14938`);
return false;
}
return true;
},
default: '',
},
]
let obj = (await inquirer.prompt(promptList));
let address = obj.address.trim();
let fileContent = getApiTemplateByInterfaceListCat(await interfaceListCat(address));
log.success("api文档=================")
log.primary(fileContent);
}
// 根据单独接口文档,自动生成vue饿了吗模板
export async function autoWriteApiTemplate() {
let promptList = [
{
type: "input",
message: "请填写接口文档的地址:",
name: "address",
validate: function (val: string) {
if (val.trim().length == 0) {
log.error('\n不允许为空');
return false;
}
let reg = /^https:\/\/yapi.xxx.cn\/project\/\d+\/interface\/api\/\d+$/g;
if (reg.test(val) == false) {
log.error(`\n当前输入的地址不是单独的接口,请查看是否输入错误`);
log.error(`举例:https://yapi.xxx.cn/project/1687/interface/api/21693`);
return false;
}
return true;
},
default: '',
},
]
let obj = (await inquirer.prompt(promptList));
let address = obj.address.trim();
let template = getApiTemplateByInterfaceGet(await interface_get(address));
log.success('接口vue模版自动生成成功,文件内容如下:');
log.success('form==================================');
log.primary(template.form);
template.table.forEach((item) => {
log.success('table==================================');
log.primary(item);
})
}