引言
前后端分离大致是这样的
- 后端:控制层 / 业务层 / 数据操作层
- 前端:控制层 / 视图层
前后端的控制层,实际上就是前后端接口的对接
前后端分离,实现了更好地解耦合,但也引入了接口对接的过程,这个过程常常是繁琐,容易产生错误的
于是引入了api接口文档,来解决这个事情,如果有一份事先约定好的接口文档,双方都按照这个来,就能实现完美的对接。(但这通常很难实现,无法预先知道需要什么接口,接口的参数,是一个反复修改的过程)
现在后端广泛采用swagger技术,能够在开发时,就能生成接口文档,并能便捷地测试接口
对前端来说,就可以根据后端项目的swagger文档,来设计前端的控制层,也就是通常的根目录下的api
文件夹,将对接口的请求封装为功能函数(也是为了与视图层解耦)
// user.ts
export const getUserList = () => request.get('/user/list')
但其实,一个api函数也就是对应的一个后端接口,已经有了接口文档,为什么不能直接生成前端的控制层?
前端控制层函数看似简单,其实做到类型完备,函数提示清晰(参数类型,返回值类型,各种注释),是一个十分繁琐的过程
所以,just relax,这个过程交由swagger-typescript-api
来完成吧
swagger-typescript-api
我并不是讲swagger-typescript-api
教程,可以去github上看它的所有用法,我只是讲述一下我是如何使用它的
我的项目并不是一个大型前后端分离项目,仅仅是作为练手,用前后端分离的方式自己开发。如果适用于您,您可以往下看
swagger-typescript-api
有两种使用方式:命令行 & node脚本程序
优缺点显然:前者方便,后者易定制
我将以命令行的方式
进入我的前端项目中,在shell中输入
npx swagger-typescript-api -p http://localhost:8080/v2/api-docs?group=Manager -o ./src/api --axios --modular --module-name-index 1 --single-http-client
http://localhost:8080/v2/api-docs?group=Manager
我的swagger api文档地址-o ./src/api
将生成的文件输出到src下的api目录下--axios
采用axios客户端,默认fetch--modular
分离http client
,data constracts
, 和routes
,否则只会生成一个大文件http client
这里就是axios客户端,对其进行了一定的封装data constracts
api接口中,用到的参数,或者返回值类型
--module-name-index 1
分离routes
,意思是按api路径.split('/')[1]
拆分接口文件
比如我有两个controller,UserController和DishController,访问UserController下的api,都是以
/admin/user
开头的,而访问DishController下的api,是以/admin/dish
开头,所以这样做后,也就是按照后端的controller分离api接口文件了
--single-http-client
意为只有一个http客户端,稍后解释
于是在api文件夹下,生成了
这里swagger-typescript-api
替我生成了除API.ts外的所有文件
如果直接使用的话,还是不太方便,因为每个controller都是一个http客户端
意味着我需要这么调用接口
new Category().getCategoryList()
new Dish().addDish()
当然,最重要的是,我们还需要对axios进行配置!
- 比如添加baseUrl,当然它生成的http客户端默认为localhost:8080,但我们通常都会配置为环境变量,以便切换不同环境下的后端
- 比如添加请求拦截器,向后端请求自动携带token认证信息
- 比如添加响应拦截器,对产生的http错误,进行捕获和反馈(如show error message,告知unauthorized)
如果没有设置--single-http-client
,产生的controller是这样的
class Employee<SecurityDataType = unknown> extends HttpClient<SecurityDataType>{...}
这样,你需要为每个controller的http客户端进行相同的配置,so dity!!!
但是,设置之后,产生controller是这样的
class Employee<SecurityDataType = unknown> {http: HttpClient<SecurityDataType>;constructor(http: HttpClient<SecurityDataType>) {this.http = http;}...
}
可以看到,前者是继承,后者是组合,也叫委派
但是,我们仍然需要为每个controller委派相同的http-client,所以我引入了API.ts来解决这个问题(这只是一个简单的示例)
class API<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {public category = new Category(this);public common = new Common(this);public dish = new Dish(this);public employee = new Employee(this);
}export const api = new API({paramsSerializer: (params) => qs.stringify(params, { indices: false }),baseURL: import.meta.env.VITE_APP_API_URL,
});api.instance.interceptors.request.use((config) => {if (getToken()) {config.headers["token"] = getToken();}return config;},(error) => {console.log(error);Promise.reject(error);}
);api.instance.interceptors.response.use((res) => {const code = res.data.code;const msg = res.data.msg || "系统未知错误,请反馈给管理员";if (res.request.responseType === "blob" ||res.request.responseType === "arraybuffer") {return res;}if (code !== 1) {message.error(msg);return Promise.reject(new Error(msg));} else {return res;}},(error) => {console.log("err" + error);let { message: msg } = error;if (msg === "Network Error") {msg = "后端接口连接异常";} else if (msg.includes("timeout")) {msg = "系统接口请求超时";} else if (msg.includes("Request failed with status code")) {// 获得异常http状态码const statusCode = +msg.substr(msg.length - 3);if (statusCode === 401) {Modal.confirm({title: "系统提示",content: "登录状态已过期,请重新登录",okText: "确定",onOk() {removeToken();location.href = "/";},});return Promise.reject("无效的会话,或者会话已过期,请重新登录。");}msg = "系统接口" + statusCode + "异常";}message.error(msg);return Promise.reject(error);}
);
进行封装后,我们可以更为优雅地调用api
api.category.getCategoryList()