不要轻视简单,简单意味着坚固。
-- 『三体』
为什么需要接口定义层
这里是一段 axios 请求调用
TypeScript
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
缺少 ts 类型提示, 我们通常喜欢创建一个 apis 文件夹做一层函数封装:
-
对函数的入参和返回值做类型定义
-
如果需要对 request 或者 response 处理, 也可以在这个函数里实现
如果一类接口有相同的配置, 如 baseURL , 我们通常会
TypeScript
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
对 request 或者 response 做统一处理, 可以用 instance 的 interceptors.
通常对 request 或者 response 的处理并不适用全部接口, 我们可能会把它抽象成自定义 config, 就可以在 interceptors 里做判断按不同场景处理.
怎样才算好的工程化设计
软件工程应用一种系统的、规范的、可量化的方法来开发、操作和维护软件,并研究这些方法。
基于高阶函数的接口定义
你是怎么想的, 代码就是什么样的
接口调用层
假如我们想要一个 getDetail(params, option): Promise<Detail> 接口在业务代码中调用.
在代码中使用就是:
TypeScript
const detail = await getDetail({ id: 'xxx' }, option);
接口定义层
我们来设计一个接口定义层:
TypeScript
export const getDetail =
createApi<Request, Response>('/api/getDetail', createOption);
createApi 就是创建一个接口, 只需要定义一下 path, 然后用 ts 的函数泛型来指定参数和返回值的类型, 接口调用时传的 option 如果在大多数情况下相同, 那么就可以放在 createOption 里;
如果定义的多个接口中, createOption 相同, 那么我们需要一个请求的 instance
TypeScript
export const { createApi, use } = createInstance(instanceOption);
如 baseUrl, 可以在 instanceOption 中使用.
我们可以通过 use 方法来集成中间件
TypeScript
/** 请求前后打日志 */
use(logger);
/**
* 静默登录&重登中间件
*/
use(
createLoginMiddleware({
checkLoginError,
})
);
/**
* 对后端错误码分类, 抛出各类错误
* 中间件必须放在最后一位
*/
use(unwrap);
基于后端接口定义我们可以通过一些工具生成前端 ts 类型.
简单的 @bolt/api 原理
基于 @bolt/compose, 参考 <静默重登>为例, 介绍<洋葱模型中间件>
TypeScript
import { compose } from "@bolt/compose"; // 见上述文档
export const createInstance = <T>(
instanceOption: ApiOption & Partial<T>
): ApiInstance<Partial<T>> => {
const middlewareArr: ApiMiddleware<unknown>[] = [];
let run: (
option: ApiOption & Partial<T>,
next: (option: ApiOption) => Promise<unknown>
) => Promise<any>;
return {
createApi(path, defineOption) {
if (!run) {
run = compose(middlewareArr);
}
return (data, _option) => {
const option = {
method: ApiMethod.POST,
path,
...initOption,
...instanceOption,
...defineOption,
..._option,
data,
};
if (option.method === ApiMethod.GET) {
option.query = {
...option.query,
...option.data,
};
}
option.middleware = option.middleware || autoMiddleware;
return option.middleware(option, () => run(option, option.requester));
};
},
use(middleware) {
middlewareArr.push(middleware);
},
};
};
@bolt/api 解决了什么问题
-
代码体积小
-
洋葱模型的中间件逻辑内聚
-
es module 导出函数, 支持 webpack tree-shaking
-
标准的工程化实践, 使用中间件和定义接口的代码整洁不冗长
更重要的是:
基于 ts 类型提示的中间件自定义配置可追溯
推荐项目开启 ts 类型的强制校验
@bolt/api 仅提供最基础的中间件, 推荐业务方根据自身情况实现自己的中间件.
中间件的能力范围取决于已存在的 Option, @bolt/api 通过规范 ts 类型来扩展自定义 Option.
这里是定义一个 instance 的 完整代码.
TypeScript
interface CustomOption extends LoginNeedOption, UnwrapOption {}
const { createApi, use } = createInstance<CustomOption>({
basePath: "/approval/web",
needLogin: true,
});
use(trace);
use(logger);
use<LoginNeedOption>(
createLoginMiddleware({
checkLoginError,
})
);
use(tracePureApi);
use<UnwrapOption>(unwrap);
创建 instance 时通过泛型约束中间件可以用的所有自定义 Option | 强制约束中间件声明所用到的 Option 字段 | 创建 api 时提示所有可选 Option |
---|---|---|
一个文件了解所需内容, 预防中间件配置项重名 | 否则, 判断 option.needLogin 时就会提示类型错误 | VSCode 可以通过 ts 快速定位到类型定义 |
声明: 本文说提到的 @bolt/api 为虚构, 并未发布到 npm 仓库