基于洋葱模型的工程化网络请求库: @bolt/api

不要轻视简单,简单意味着坚固。

-- 『三体』

为什么需要接口定义层

这里是一段 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 里做判断按不同场景处理.

怎样才算好的工程化设计

软件工程应用一种系统的、规范的、可量化的方法来开发、操作和维护软件,并研究这些方法。

-- segmentfault.com/a/119000002...

基于高阶函数的接口定义

你是怎么想的, 代码就是什么样的

接口调用层

假如我们想要一个 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 仓库

相关推荐
_柳青杨18 分钟前
一文吃透 Node.js 事件循环:从原理到 Node 20+ 重大变更
javascript·后端
JieE21211 小时前
LeetCode 101. 对称二叉树|JS 递归 + 迭代双解法,彻底搞懂镜像判断
javascript·算法
冬奇Lab13 小时前
AI Workflow 定义的四次演进:从 Markdown 到 JS 脚本,再到分布式多 Agent
javascript·人工智能·agent
一颗烂土豆19 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
kyriewen21 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
weedsfly1 天前
迭代器、生成器与异步迭代——让数据“按需流动”的艺术
前端·javascript
假如让我当三天老蒯1 天前
前端跨域解决方案(学习用)
前端·javascript·面试
铁皮饭盒1 天前
Bun 哪比 Node.js 快?
javascript·后端
JieE2121 天前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试
candyTong2 天前
RTK 技术原理:一次典型会话里,80% 上下文是怎么省下来的
javascript·后端·架构