背景
最近接手了一个任务,要对旧的项目做一次重构升级,后端会新建一个后端服务,用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