17学习axio:封装一个结构清晰的Fetch库
文章目录
从这一讲开始,我们将进入核心框架原理与代码设计模式的学习。任何一个动态应用的实现,都离不开前后端的互动配合。前端发送请求获取数据是开发者必不可少的场景。正因为如此,每一个前端项目都有必要接入一个请求库。
那么请求库如何设计,才能保证使用者的顺畅?请求逻辑如何抽象成统一请求库,才能避免出现代码混乱堆积,难以维护的现象呢?下面我们就进入正题。
一个请求库需要考虑哪些问题
一个请求,纵向向前承载了数据的发送,向后链接了数据的接收和消费,横向还需要处理网络环境和宿主能力,以及业务的扩展需求。因此设计一个好的请求库,首先需要预见可能会发生的问题。下面我们将重点展开几个关键问题。
适配浏览器 or Node.js 环境
如今,前端开发不再局限于浏览器层面,Node.js 环境的出现,使得请求库的适配需求变得更加复杂。Node.js 基于 V8 JavaScript Engine,顶层对象是 global,不存在 Window 对象和浏览器宿主,因此使用传统的 XMLHttpRequest/Fetch 在 Node.js 上发送请求是行不通的。对于搭建了 Node.js 环境的前端来说,请求库的设计实现需要考虑是否同时支持在浏览器和 Node.js 两种环境发送请求。在同构的背景下,如何使不同环境的请求库使用体验趋于一致呢?下面我们将会对这部分内容进一步讲解。
XHR or Fetch
单就浏览器环境发送请求来说,一般存在两种技术方法:
XMLHttpRequest 规范
Fetch 规范
我们先简要对比两种技术的使用方式。
使用 XMLHttpRequest 发送请求:
|
|
简单来说,XMLHttpRequest 存在一些缺点,比如:
配置和使用方式较为烦琐;
基于事件的异步模型不够友好。
而 Fetch 的推出,主要也是为了解决上述问题。
使用 Fetch 发送一个请求:
|
|
我们可以看到,Fetch 基于 Promise,语法更加简洁,语义化更加突出,但兼容性不如 XMLHttpRequest。
对于一个请求库来说,在浏览器端使用 XMLHttpRequest 还是 Fetch?这是一个问题。下面我们通过 axios 的实现具体展开讲解。
功能设计与抽象粒度
无论是基于 XMLHttpRequest 还是 Fetch,实现一层封装,屏蔽一些基础能力并暴露给业务方使用,即实现一个请求库,这并不困难。我认为,真正难的是请求库的功能设计和抽象粒度。如果功能设计分层不够清晰,抽象方式不够灵活,很容易产出“屎山代码”。
比如,对于请求库来说,是否要处理以下看似通用,但又具有定制性的功能呢?你需要考虑以下功能点:
自定义 headers 添加
统一断网/弱网处理
接口缓存处理
接口统一错误提示
接口统一数据处理
统一数据层结合
统一请求埋点
这些设计问题如果初期不考虑清楚,那么在业务层面,一旦真正使用了设计不良的请求库,很容易遇到不满足业务需求的场景,而沦为手写 Fetch,势必导致代码库中请求方式多种多样,风格不一。
这里我们稍微展开,以一个请求库的分层封装为例,其实任何一种通用能力的封装都可以参考下图:
请求库分层封装示例图
如图所示,底层能力部分,对应请求库中宿主提供的 XMLHttpRequest 或 Fetch 能力,以及项目中已经内置的框架/类库能力。这一部分对于一个已有项目来说,往往是较难改变或重构的,也是不同项目中可以复用的;而业务层,比如依赖 axios 请求库的更上层封装,我们一般可以分为:
项目层
页面层
组件层
三个方面,它们依次递进,完成最终业务消费。底层能力部分,对许多项目来说都可以使用,而让不同项目之间的代码质量和开发效率产生差异的,恰好是容易被轻视的业务级别的封装设计。
比如设计者在项目层的封装上,如果做了几乎所有事情,囊括了所有请求相关的规则,很容易使封装复杂,过度设计。不同层级的功能和职责是不同的,错位的使用和设计,是让项目变得更加混乱的诱因之一。
合理的设计是,底层部分保留对全局封装的影响范围,而项目层保留对页面层的影响能力,页面层保留对组件层的影响能力。
比如,我们在项目层提供一个基础请求库封装,在这一层可以提供默认发送 cookie 等(一定需要存在)的行为,同时通过配置 options.fetch 保留覆盖 globalThis.fetch 的能力,这样可以在 Node 等环境中,通过注入一个 node-fetch npm 库的方式,支持 SSR 能力。
这里需要注意的是,我们一定要避免设计一个特别大的 Fetch 方法,通过拓展 options 把所有事情都做了,用 options 驱动一切行为,这比较容易让 Fetch 代码和逻辑变得复杂、难以理解,而且不利于 tree-shaking 和 code-spliting。
那么如何做到这种层次清晰的基础库呢?接下来,我们就从 axios 的设计分析寻找答案。
axios 设计之美
axios 是一个被前端广泛使用的请求库,对应上述分层结构中,属于框架/类库层,我们来总结一下它的功能特点:
在浏览器端,使用 XMLHttpRequest 发送请求;
支持 Node.js 端发送请求;
支持 Promise API,使用 Promise 风格语法;
支持请求和响应拦截;
支持自定义修改请求和返回内容;
支持请求取消;
默认支持 XSRF 防御。
下面,我们主要从拦截器思想、适配器思想、安全思想三方面展开,分析 axios 设计的可取之处。
拦截器思想
拦截器思想是 axios 带来的最具启发性的思想之一。它赋予了分层开发时借助拦截行为,注入自定义能力的功能。简单来说,axios 的拦截器主要由:任务注册 → 任务编排 → 任务调度(执行)三步组成。
我们先看任务注册,在请求发出前,可以使用 axios.interceptors.request.use 方法注入拦截逻辑,比如:
|
|
在请求返回后,用 axios.interceptors.response.use 方法注入拦截逻辑,比如:
|
|
任务注册部分的源码实现也不复杂:
|
|
如上代码,我们定义的请求/响应拦截器,会在每一个 axios 实例的 Interceptors 属性中维护, this.interceptors.request 和 this.interceptors.response 也都是一个 InterceptorManager 实例,该实例的 handlers 属性以数组的形式存储了使用方定义的一个个拦截器逻辑。
注册了任务,我们再来看看任务编排时是如何将拦截器串联起来,并在任务调度阶段执行各个拦截器的。如下源码:
|
|
我们通过 chain 数组来编排调度任务, dispatchRequest 方法实际执行请求的发送,编排过程实现:在实际发送请求的方法 dispatchRequest 前插入请求拦截器,在 dispatchRequest 方法后,插入响应拦截器。
任务调度其实就是通过一个 While 循环,通过一个 Promise 实例,遍历迭代 chain 数组方法,并基于 Promise 回调特性,将各个拦截器串联执行起来。
我们通过下图,来加深理解:
适配器思想
前文提到了 axios 同时支持 Node.js 环境和浏览器环境发送请求,在浏览器中我们可以选用 XMLHttpRequest 或 Fetch 方法发送请求,但是在 Node.js 中,需要通过 HTTP 模块发送请求。对此,axiso 是如何设计实现的呢?
为了支持适配不同环境,axios 实现了适配器:Adapter,具体实现在 dispatchRequest 方法中:
|
|
如上代码,axios 支持使用方实现自己的 Adapter,自定义不同环境中的请求实现方式,也提供了默认的 Adapter。默认 Adapter 逻辑代码如下:
|
|
一个 Adapter 需要返回一个 Promise 实例(这是因为axios 内部通过 Promise 链式调用完成请求调度),我们分别看看在浏览器端和 Node.js 端具体 Adapter 实现逻辑:
|
|
如上代码,就是一个典型的使用 XMLHttpRequest 发送请求的实现内容。在 Node.js 端的实现,精简后代码如下:
|
|
上述代码主要是调用 Node.js HTTP 模块,进行请求的发送和处理,当然,真实源码实现还需要考虑 HTTPS 以及 Redirect 等问题,这里我们不再展开。
讲到这里,可能你会问,什么场景下,才会需要自定义 Adapter 进行请求发送呢?比如在测试阶段或特殊环境中,我们可以 mock 请求:
|
|
实现一个自定义的 Adapter 也并不困难,说到底它也只是一个 Node.js 模块,导出一个 Promise 实例即可:
|
|
相信你学会了这些内容,就对 axios-mock-adapter 这个库的实现原理了然于胸了。
安全思想
说到请求,自然关联着安全问题。在本小节最后部分,我们对 axios 中的一些安全机制进行解析,涉及相关攻击手段:CSRF。
Cross—Site Request Forgery,攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说,这个请求是完全合法的,但是却完成了攻击者期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至购买商品、虚拟货币转账等。
在 axios 中,主要依赖双重 cookie 的方式防御 CSRF。具体来说,对于攻击者,获取用户 cookie 是比较困难的,因此,我们可以在请求中携带一个 cookie 值,来保证请求的安全性。这里我们将相关流程梳理为:
用户访问页面,后端向请求域中注入一个 cookie,一般该 cookie 值为加密随机字符串;
在前端通过 Ajax 请求数据时,取出上述 cookie,添加到 URL 参数或者请求 header 中;
后端接口验证请求中携带的 cookie 值是否合法,不合法(不一致),则拒绝请求。
我们看 axios 源码:
|
|
在这里,axios 默认配置了默认 xsrfCookieName 和 xsrfHeaderName ,实际开发中可以按具体情况传入配置。在具体请求时,以 lib/adapters/xhr.js 为例:
|
|
由此可见,对一个成熟请求库的设计来说,安全防范这个话题永不过时。
总结
本讲我们在开篇分析了代码设计、代码分层的方方面面,一个好的设计一定是层次明晰,各司其职的,一个好的设计也会直接帮助业务开发提升效率。封装和设计,是编程领域亘古不变的经典话题,需要每名开发者下沉到业务开发中体会、思考。
本小节的后半部分,我们从源码入手,分析了 axios 的优秀设计思想。即便你在业务中没有使用过 axios,但对于 axios 的学习始终是必要且重要的。
主要内容总结如下:
最后,给大家布置一个思考题:axios 支持请求取消能力,这是如何实现的呢?欢迎在留言区和我分享你的观点。下一讲,我们将继续学习代码设计这一话题,通过对比 Koa 和 Redux,聚焦中间件化和插件化理念。我们下一讲再见。
-– ### 精选评论 ##### **用户4723: > 通过传递config配置cancelToken的形式,来取消的。判断有传cancelToken,在promise链式调用的dispatchRequest抛出错误,在adapter中request.abort()取消请求,使promise走向rejected,被用户捕获取消信息。具体分析之前写过一篇《学习 axios 源码整体架构,打造属于自己的请求库》链接:https://lxchuan12.gitee.io/axios/ ##### *勇: > axios 通过在config中传入一个cancelToken, cancelToken.promise 实际为一个PENDING状态的promise实例,当用户调用cancel方法,会使该promise 实例由PENDING状态变为RESOLVE状态,触发监听函数onCanceled,调用request.abort(),取消xhr调用 ##### **贤: > 哈哈, 我搞的项目就是 options 驱动一切… ##### **明: > 合理的设计是,底层部分保留对全局封装的影响范围,而项目层保留对页面层的影响能力,页面层保留对组件层的影响能力。这里的 XXX 保留对 xxx 的影响能力怎么理解? ###### 讲师回复: > 就是底层可以「修改和干预」上层,而上层只能依赖底层,不可以修改底层
文章作者 anonymous
上次更新 2024-06-19