基础建设/业务/自动化接口

125 阅读3分钟

背景

我们前端写http请求时,一般都是去找服务端要接口文档,然后根据接口文档在文件上写入关于的相关信息。这个过程中,文件内容有很多是重复劳作的。

事实上可以写一个脚本,根据文档地址里面的接口内容,自动生成常见的前端模板,例如接口请求表格表单

目标

服务端同学给前端同学接口文档,通过脚本实现文件内容自动生成。

效果如下:

image.png

image.png

调研

1、一开始我的想法是做一个爬虫脚本,通过爬这个接口网页的关键信息,比如路径。来实现自动化的功能。 后面通过与其他研发人员沟通,对于yapi这个网站,它们是提供对外的api接口的,只需要接入对应的token发送请求即可,这就少了一步爬虫的操作。

2、在实际操作中,服务端同学可能会陆续给新的接口,故此,不直接生成文件了,而是在终端打印出接口文档内容,用户可直接复制粘贴。这样一来避免了文件被覆盖,二来不用固定死文件位置。

获取yapi文档中项目对应的token: image.png

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);
    })

}

资料

yapi接口文档