前端自动生成后端API请求实践

背景

最近接手了一个任务,要对旧的项目做一次重构升级,后端会新建一个后端服务,用java重写原先PHP的代码,这个过程中前端需要配合后端的改造做一些调整,趁这个机会对原先前端的api维护方式和调用方式做一次调整,方便后续维护和调用后端api。

目标

  • 配合后端将旧的接口迁移到新的接口
  • 调整前端的API声明和使用方式,方便后续维护和调用

现存问题

当前后端的api托管采用了yapi这种方式,即后端会将接口声明,参数等在yapi上面维护,类似一个api预览的网页。在原先旧的一些前端项目里面,前端开发对api的使用概况为下面几个步骤

  • 在yapi上查找需要用到的api
  • api目录下新建一个文件或者用已有的文件,在上面声明这个接口
  • 根据请求的path声明一个接口,并复制粘贴对应请求的url和请求方式(get post等)
  • api/index.js中引入这个文件,然后用 export default的方式将所有的api暴露出去
  • 在对应的业务代码中引入 import Api from '@/api',通过Api对象去调用对应的接口,例如 Api.setting.getApplicationList()

api/index.js如下

javascript 复制代码
import * as auth from './auth';
import * as application from './application';
import * as interfaces from './interfaces';
import * as system from './system';
import * as log from './log';
import * as operation from './operation';

export default {
    auth,
    application,
    interfaces,
    system,
    log,
    operation,
}

上面的这种方式存在一些问题

  • 需要手动去维护声明这些api,每次后端有新增接口或者调整接口url之类的,前端都需要去yapi上面复制粘贴,然后声明一个新的方法才可以调用
  • 不同的同事可能在不同的文件中声明同一个接口,导致接口声明有重复,不利于维护
  • export default的方式不方便通过编辑器点击跳转到具体的接口声明,也没有接口的说明,代码提示也比较弱,纯粹靠前端手动去维护很难维护
  • 代码中基本上都是用then的方式来调用接口,而不是 await async这种方式,不是很优雅,例如下面这样
ini 复制代码
Api.setting.getApplicationList().then(res => {})
  • 前端声明api的方式经常没有编写注释,也没办法看类型,如果是typescript的项目的话还得自己手动声明类型,存在比较多的工作量,使用编辑器调用的时候也不是很方便

改造思路

针对上面的问题,我们可以通过下面一些方式来解决现存的问题

  • 使用 yapi-to-typescript这个工具,配置好我们的项目之后,每次都根据后端维护的yapi自动生成前端调用的api,不再手动维护
  • 调用的时候直接从对应的文件中import,不再合到同一个入口再export出来
  • 调整为 await async这种方式,然后在eslint中增加校验

改造步骤

如果是新的项目,就直接直接使用yapi-to-typescript这个工具来接入生成api即可,如果是旧的项目根据实际情况调整,如果有类似笔者这种大的重构的机会的话可以考虑接入这个工具。

接入ytt

第一步就是安装这个包,按自己项目使用的工具来安装即可

css 复制代码
npm i yapi-to-typescript --save-dev

创建配置文件,在项目根目录下创建ytt.config.js这个文件,这里笔者放一下自己的配置文件作为参考,笔者是vue的项目,用js的模式,如果是使用typescript的话要调整下配置

javascript 复制代码
import { defineConfig } from 'yapi-to-typescript';
import { replace, dropRight } from 'lodash';

/** 平台的请求url前缀 */
const PLATFORM_PREFIX = 'open';
const PLATFORM_URL_PREFIX = `/${PLATFORM_PREFIX}`;
// 生成出来的请求名有可能和关键字重复,这里维护一个黑名单,命中的话调整下命名
const REQUEST_NAME_BLACK_LIST = ['delete'];

/**
 * 将请求路径的第二级作为类别(/open/sys-auth/xxx => sys-auth)
 * @param {string} path 请求的路径
 * @returns 分类名称
 */
function getCategoryName(path) {
    const pathArray = path.split('/');
    const findResult = pathArray.find(item => {
        return item && item !== PLATFORM_PREFIX;
    });
    return findResult || 'other';
}

/**
 * 获取类别的路径前缀
 * @param {string} path 请求的路径
 * @returns 类别的路径前缀 例如/open/sys-auth
 */
function getCategoryPerfix(path) {
    const pathArray = path.split('/');
    const findIndex = pathArray.findIndex(item => {
        return item && item !== PLATFORM_PREFIX;
    });
    if (findIndex !== -1) {
        const newList = dropRight(pathArray, pathArray.length - findIndex - 1);
        return newList.join('/');
    } else {
        return PLATFORM_URL_PREFIX;
    }
}

export default defineConfig([
    {
        serverUrl: 'http://1.1.1.1:3000',
        typesOnly: false,
        target: 'javascript',
        reactHooks: {
            enabled: false
        },
        prodEnvName: 'production',
        outputFilePath: (interfaceInfo, changeCase) => {
            const secondPath = getCategoryName(interfaceInfo.path); // 获取第二段 path
            const fileName = changeCase.camelCase(`/${secondPath}`);
            return `src/top/api/${fileName}.js`;
        },
        requestFunctionFilePath: 'src/top/libs/request.js',
        dataKey: 'data',
        projects: [
            {
                token: '这里换成自己的token即可',
                id: 110,
                categories: [
                    {
                        id: 0,
                        getRequestFunctionName(interfaceInfo, changeCase) {
                            // 以接口第三级的path及后面的路径生成请求函数名
                            const prefix = getCategoryPerfix(interfaceInfo.path);
                            const requestName = changeCase.camelCase(replace(interfaceInfo.path, prefix, ''));
                            return REQUEST_NAME_BLACK_LIST.includes(requestName) ? `${requestName}Fn` : requestName;
                        }
                    }
                ]
            }
        ]
    }
]);

简单走读下这个配置文件,具体更多的配置和详细说明可以看官方文档

  • serverUrl:这个就是自己的那个yapi的地址,比如是私有部署的,就换成自己内部的域名即可
  • typesOnly:是否只生成类型,这里笔者是用的js,所以设置为false
  • target:设置下自己的生成的目标文件类型,笔者是用的javascript,配置即可
  • outputFilePath:这个是设置生成的js放置的位置和生成的文件名,笔者这里根据请求的路径的第二段作为文件名,因为笔者接触的项目api请求路径第一段都是一样的,所以选择了第二段作为文件名
  • requestFunctionFilePath: 这个是我们声明的一个请求方法,例如我们用axios创建的一个请求方法,这个会用于生成代码
  • projects: 这个是配置要接入的项目的信息
    • token: 用户的token,可以从页面中的模块获取到
    • id: 项目的id,可以从url中获取到,用于明确具体那个项目
    • categories 分类信息
      • id: 0 代表所有
      • getRequestFunctionName: 生成请求方法的名称,这里笔者是用请求的第三级的path作为请求的方法名,然后转为驼峰式

在这个位置获取token

打开yapi进入对应的项目之后,从url中获取项目的id

增加运行命令

为了使用方便,我们在package.json中增加多一个命令,用于运行ytt生成我们的api

json 复制代码
"scripts": {
    "yapi": "npx ytt"
}

我们试下运行,npm run yapi

运行成功之后,我们看下我们指定的目录下生成文件的情况,可以看到这时候已经生成文件成功,我们可以直接引用xx.js在需要的地方进行调用了

看下产物,例如上面的admin.js,可以看到这里的request会根据我们的配置然后引入,要保证这个request的文件是有的,然后生成的代码中就可以用这个请求的方法,把参数透传到我们写好的request方法中

对应的request.js

这里笔者贴一下自己的request方法,就是常见的用axios封装的一个方法

javascript 复制代码
import axios from 'axios';
import { Message } from 'element-ui';
import { i18n } from '@/lang/index';

const axiosInstance = axios.create({
    baseURL: process.env.VUE_APP_API_BASE,
    withCredentials: true, // 跨域请求时发送cookie
    headers: {
        'Content-Type': 'application/json'
    },
    timeout: 1000 * 120,
    accept: '*/*'
});

axiosInstance.interceptors.request.use(
    config => {
        // ...
        return config;
    },
    error => {
        return Promise.reject(error);
    }
);

axiosInstance.interceptors.response.use(
    res => {
        // TODO: 这里可以根据状态码返回指定的提示
        const { code, data } = res.data;
        const message = res.data.message || res.data.msg;
        if (code === 200) {
            return getResponseByConfig(data, res.config, res.headers);
        } else {
            Message({
                message: i18n.t('ohsOdvVGjSglf02V2kHz8'),
                type: 'error',
                duration: 3 * 1000
            });
            return Promise.reject(message);
        }
    },
    error => {
        return Promise.reject(error?.response?.data);
    }
);

export default async function request(payload, options) {
    // 配置参数选项
    const requestOptions = {
        method: payload.method,
        url: payload.path,
        data: payload.data
    };
    const res = await axiosInstance.request(Object.assign(requestOptions, options));
    return res;
}

function getResponseByConfig(data, config, headers) {
    if (config.returnHeader) {
        return { headers, data };
    } else {
        return data;
    }
}

调用改造

如上所述,之前是手动声明的,然后异步请求结果全部用then,这里我们就是对代码进行改造,如果是新的项目直接用即可,例如我们现在要发起/open/admin/get-list这个接口的请求,我们就是引入admin文件即可,用里面的getList方法

javascript 复制代码
import { getList } from '@/api/admin';

async init() {
    const res = await getList();
    ...
}

增加lint的检验

笔者使用的是eslint,这里就是调整下eslint的配置,让项目不要使用then,而是用await async这种方式,这里如果是typescript的话用下下面这个规则即可,这里不再赘述

swift 复制代码
@typescript-eslint/await-thenable
相关推荐
ekskef_sef27 分钟前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6411 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻1 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云1 小时前
npm淘宝镜像
前端·npm·node.js
dz88i81 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr1 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook
程序员_三木2 小时前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
顾平安3 小时前
Promise/A+ 规范 - 中文版本
前端
聚名网3 小时前
域名和服务器是什么?域名和服务器是什么关系?
服务器·前端
桃园码工3 小时前
4-Gin HTML 模板渲染 --[Gin 框架入门精讲与实战案例]
前端·html·gin·模板渲染