导读
本文是个短篇,主要解释一下封装网络请求的设计思想,为接下来 gRPC 的封装做理论铺垫。会从一个简单场景出发,来介绍封装的设计思想。
场景
实现一个最简单的场景:
| url | method | params | response | remarks |
|---|---|---|---|---|
| /test | POST | { name: string } | { message: string } | 接口功能:入参为 {name: 'xxx'},则返回值为 {message: 'Hello, xxx'} |
代码实现
传统的 RESTful 用 express + fetch 实现方式如下:
服务端
// by express
const express = require('express');
const app = express();
app.use(express.json({type: 'application/json'}));
app.post("/test", function (req, res, next) {
/* 约定好 body.name 是个 string */
res.send({ message: "Hello," + req.body.name });
});
app.listen(3000);
客户端
代码实现分了封装、声明、调用三层,详细分析见后文,先看代码实现:
封装
request.ts - 统一处理初始化、格式化、异常等逻辑;
// request.ts
export const apiPost = <R>(
url: string,
data: Record<string, unknown>
): Promise<R> =>
fetch(url, {
method: "POST",
body: JSON.stringify(data),
headers: new Headers({
"Content-Type": "application/json",
}),
})
.then((res) => {
if (res.status === 200) {
return res.json();
}
throw new Error(res.statusText);
})
.catch((error) => {
console.log(error);
});
声明
services.ts - 使用“封装”后的基础函数,实现业务函数,并加入参数与返回值的 TS 声明;
// services.ts
import { apiPost } from "./request";
export const sayHello = (name: string) => {
return apiPost<{ message: string }>("/test", { name });
};
调用
index.ts - 使用“声明”的业务函数,此时 TS 已经起到了校验和自动提示的作用。
sayHello("world").then((res) => {
console.log(res.message); // Hello, world
});
解析
我们关注客户端即可,上文将代码分成了封装、声明、调用 3 个模块,为什么呢?我们从后往前说。
【调用】最重要的是使用起来要足够方便,要做到极简。可以从上文看到,这部分没有任何多余的代码,但是又能够享受到 TS 的便利。更极端点说,如果未来从 ts 切回 js,此处的代码都不需要做任何改动。
【声明】“调用”部分之所以能够做到极简,是因为“声明”部分将需要声明的 TS 全部都声明了。可以看到每个函数都需要声明入参和出参,所以这部分最重要的就是 TS 声明。当然,此处 TS 声明的方便与否,是由更底层的“封装”部分决定的。
【封装】最核心的目标就是底层解耦。即无论底层用 fetch、axios 甚至 gRPC 实现,对外暴露的都是 apiPost 函数,使用方法也不变,这样能够充分保证代码的鲁棒性与拓展性。除此之外,上面也提到了,此部分决定了“声明”层是否便利,所以还是很考验设计和抽象能力的,尤其是在处理各种 TS 的泛型上。
结语
总之,关注两头即可,一个是做到底层解耦,一个是做到使用极简。不管怎么拆分,拆几层,只要能做到“两头极致”就可以了。
在激烈竞争中,取胜的系统在最大化或者最小化一个或几个变量上会走到近乎荒谬的极端。——《穷查理宝典》