神族九帝's blog 神族九帝's blog
首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)

神族九帝,永不言弃

首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)
  • 工作笔记

  • 前端基础建设与架构 30 讲

    • 开篇词 像架构师一样思考,突破技术成长瓶颈
    • npm 安装机制及企业级部署私服原理
    • Yarn 的安装理念及如何破解依赖管理困境
    • CI 环境上的 npm 优化及更多工程化问题解析
    • 横向对比主流构建工具,了解构建工具的设计考量
    • Vite 实现:从源码分析出发,构建 bundlele 开发工程
    • core-j 及垫片理念:设计一个“最完美”的 Polyfill 方案
    • 梳理混乱的 Babel,不再被编译报错困扰
    • 探索前端工具链生态,制定一个统一标准化 babel-preet
    • 从实战出发,从 0 到 1 构建一个符合标准的公共库
    • 代码拆分和按需加载:缩减 bundle ize,把性能做到极致
    • Tree Shaking:移除 JavaScript 上下文中的未引用代码
    • 如何理解 AST 实现和编译原理?
    • 工程化思维处理方案:如何实现应用主题切换功能?
    • 解析 Webpack 源码,实现自己的构建工具
    • 从编译到运行,跨端解析小程序多端方案
    • 原生跨平台技术:移动端跨平台到 Flutter 的技术变革
    • 学习 axio:封装一个结构清晰的 Fetch 库
      • 对比 Koa 和 Redux:分析前端中的中间件理念
      • 如何理解软件开发灵活性和高定制性?
      • 如何理解前端中面向对象的思想?
      • 如何利用 JavaScript 实现经典数据结构?
      • 剖析前端中的数据结构应用场景
      • npm cript:打造一体化的构建和部署流程
      • 自动化代码检查:剖析 Lint 工具和工程化接入&优化方案
      • 如何设计一个前端 + 移动端离线包方案?
      • 如何设计一个“万能”项目脚手架?
      • 同构渲染架构:实现一个 SSR 应用
      • 设计性能守卫系统:完善 CICD 流程
      • 实践打造网关:改造企业 BFF 方案
      • 实现高可用:使用 Puppeteer 生成性能最优的海报系统
      • 结束语 再谈项目的基建和架构,个人的价值和方向
    • vue2源码学习

    • 剖析vuejs内部运行机制

    • TypeScript 入门实战笔记

    • vue3源码学习

    • 2周刷完100道前端优质面试真题

    • 思维导图

    • npm发包

    • 重学node

    • 前端性能优化方法与实战

    • webpack原理与实战

    • webGl

    • 前端优化

    • Web3

    • React

    • 更多

    • 笔记
    • 前端基础建设与架构 30 讲
    wu529778790
    2024-04-07

    学习 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 发送请求:

    function success() {
        var data = JSON.parse(this.responseText);
        console.log(data);
    }
    function error(err) {
        console.log('Error Occurred :', err);
    }
    var xhr = new XMLHttpRequest();
    xhr.onload = success;
    xhr.onerror = error;
    xhr.open('GET', 'https://xxx');
    xhr.send();
    

    简单来说,XMLHttpRequest 存在一些缺点,比如:

    • 配置和使用方式较为烦琐;

    • 基于事件的异步模型不够友好。

    而 Fetch 的推出,主要也是为了解决上述问题。

    使用 Fetch 发送一个请求:

    fetch('https://xxx')
        .then(function (response) {
            console.log(response);
        })
        .catch(function (err) {
            console.log("Something went wrong!", err);
        });
    

    我们可以看到,Fetch 基于 Promise,语法更加简洁,语义化更加突出,但兼容性不如 XMLHttpRequest。

    对于一个请求库来说,在浏览器端使用 XMLHttpRequest 还是 Fetch?这是一个问题。下面我们通过 axios 的实现具体展开讲解。

    功能设计与抽象粒度

    无论是基于 XMLHttpRequest 还是 Fetch,实现一层封装,屏蔽一些基础能力并暴露给业务方使用,即实现一个请求库,这并不困难。我认为,真正难的是请求库的功能设计和抽象粒度。如果功能设计分层不够清晰,抽象方式不够灵活,很容易产出“屎山代码”。

    比如,对于请求库来说,是否要处理以下看似通用,但又具有定制性的功能呢?你需要考虑以下功能点:

    • 自定义 headers 添加

    • 统一断网/弱网处理

    • 接口缓存处理

    • 接口统一错误提示

    • 接口统一数据处理

    • 统一数据层结合

    • 统一请求埋点

    这些设计问题如果初期不考虑清楚,那么在业务层面,一旦真正使用了设计不良的请求库,很容易遇到不满足业务需求的场景,而沦为手写 Fetch,势必导致代码库中请求方式多种多样,风格不一。

    这里我们稍微展开,以一个请求库的分层封装为例,其实任何一种通用能力的封装都可以参考下图:

    202125-101326.png

    请求库分层封装示例图

    如图所示,底层能力部分,对应请求库中宿主提供的 XMLHttpRequest 或 Fetch 能力,以及项目中已经内置的框架/类库能力。这一部分对于一个已有项目来说,往往是较难改变或重构的,也是不同项目中可以复用的;而业务层,比如依赖 axios 请求库的更上层封装,我们一般可以分为:

    • 项目层

    • 页面层

    • 组件层

    三个方面,它们依次递进,完成最终业务消费。底层能力部分,对许多项目来说都可以使用,而让不同项目之间的代码质量和开发效率产生差异的,恰好是容易被轻视的业务级别的封装设计。

    比如设计者在项目层的封装上,如果做了几乎所有事情,囊括了所有请求相关的规则,很容易使封装复杂,过度设计。不同层级的功能和职责是不同的,错位的使用和设计,是让项目变得更加混乱的诱因之一。

    合理的设计是,底层部分保留对全局封装的影响范围,而项目层保留对页面层的影响能力,页面层保留对组件层的影响能力。

    前端基建 金句.png

    比如,我们在项目层提供一个基础请求库封装,在这一层可以提供默认发送 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.request.use(function (config) {
        // 请求发送前做一些事情,比如添加 headers
        return config;
      }, function (error) {
        // 请求出现错误时,处理逻辑
        return Promise.reject(error);
      });
    

    在请求返回后,用axios.interceptors.response.use方法注入拦截逻辑,比如:

    axios.interceptors.response.use(function (response) {
        // 响应返回 2xx 时,做一些操作,比如响应状态码为 401 时,自动跳转到登录页
        return response;
      }, function (error) {
        // 响应返回 2xx 外响应码时,错误处理逻辑
        return Promise.reject(error);
      });
    

    任务注册部分的源码实现也不复杂:

    // lib/core/Axios.js
    function Axios(instanceConfig) {
      this.defaults = instanceConfig;
      this.interceptors = {
        request: new InterceptorManager(),
        response: new InterceptorManager()
      };
    }
    // lib/core/InterceptorManager.js
    function InterceptorManager() {
      this.handlers = [];
    }
    InterceptorManager.prototype.use = function use(fulfilled, rejected) {
      this.handlers.push({
        fulfilled: fulfilled,
        rejected: rejected
      });
      // 返回当前的索引,用于移除已注册的拦截器
      return this.handlers.length - 1;
    };
    

    如上代码,我们定义的请求/响应拦截器,会在每一个 axios 实例的 Interceptors 属性中维护,this.interceptors.request和this.interceptors.response也都是一个 InterceptorManager 实例,该实例的handlers属性以数组的形式存储了使用方定义的一个个拦截器逻辑。

    注册了任务,我们再来看看任务编排时是如何将拦截器串联起来,并在任务调度阶段执行各个拦截器的。如下源码:

    // lib/core/Axios.js
    Axios.prototype.request = function request(config) {
      config = mergeConfig(this.defaults, config);
      // ...
      var chain = [dispatchRequest, undefined];
      var promise = Promise.resolve(config);
      // 任务编排
      this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
        chain.unshift(interceptor.fulfilled, interceptor.rejected);
      });
      this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
        chain.push(interceptor.fulfilled, interceptor.rejected);
      });
      // 任务调度
      while (chain.length) {
        promise = promise.then(chain.shift(), chain.shift());
      }
      return promise;
    };
    

    我们通过chain数组来编排调度任务,dispatchRequest方法实际执行请求的发送,编排过程实现:在实际发送请求的方法dispatchRequest前插入请求拦截器,在dispatchRequest方法后,插入响应拦截器。

    任务调度其实就是通过一个 While 循环,通过一个 Promise 实例,遍历迭代chain数组方法,并基于 Promise 回调特性,将各个拦截器串联执行起来。

    我们通过下图,来加深理解:

    Drawing 1.png

    适配器思想

    前文提到了 axios 同时支持 Node.js 环境和浏览器环境发送请求,在浏览器中我们可以选用 XMLHttpRequest 或 Fetch 方法发送请求,但是在 Node.js 中,需要通过 HTTP 模块发送请求。对此,axiso 是如何设计实现的呢?

    为了支持适配不同环境,axios 实现了适配器:Adapter,具体实现在dispatchRequest方法中:

    // lib/core/dispatchRequest.js
    module.exports = function dispatchRequest(config) {
      // ...
      var adapter = config.adapter || defaults.adapter;
    
      return adapter(config).then(function onAdapterResolution(response) {
        // ...
        return response;
      }, function onAdapterRejection(reason) {
        // ...
        return Promise.reject(reason);
      });
    };
    

    如上代码,axios 支持使用方实现自己的 Adapter,自定义不同环境中的请求实现方式,也提供了默认的 Adapter。默认 Adapter 逻辑代码如下:

    function getDefaultAdapter() {
      var adapter;
      if (typeof XMLHttpRequest !== 'undefined') {
        // 浏览器端使用 XMLHttpRequest 方法
        adapter = require('./adapters/xhr');
      } else if (typeof process !== 'undefined' &&
        Object.prototype.toString.call(process) === '[object process]') {
        // Node.js 端,使用 HTTP 模块
        adapter = require('./adapters/http');
      }
      return adapter;
    }
    

    一个 Adapter 需要返回一个 Promise 实例(这是因为axios 内部通过 Promise 链式调用完成请求调度),我们分别看看在浏览器端和 Node.js 端具体 Adapter 实现逻辑:

    module.exports = function xhrAdapter(config) {
      return new Promise(function dispatchXhrRequest(resolve, reject) {
        var requestData = config.data;
        var requestHeaders = config.headers;
        var request = new XMLHttpRequest();
        var fullPath = buildFullPath(config.baseURL, config.url);
        request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    
        // Listen for ready state
        request.onreadystatechange = function handleLoad() {
         // ....
        };
        // Handle browser request cancellation (as opposed to a manual cancellation)
        request.onabort = function handleAbort() {
          // ...
        };
        // Handle low level network errors
        request.onerror = function handleError() {
          // ...
        };
        // Handle timeout
        request.ontimeout = function handleTimeout() {
          // ...
        };
        // ...
    
        request.send(requestData);
      });
    };
    

    如上代码,就是一个典型的使用 XMLHttpRequest 发送请求的实现内容。在 Node.js 端的实现,精简后代码如下:

    var http = require('http');
    /*eslint consistent-return:0*/
    module.exports = function httpAdapter(config) {
      return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
        var resolve = function resolve(value) {
          resolvePromise(value);
        };
        var reject = function reject(value) {
          rejectPromise(value);
        };
        var data = config.data;
        var headers = config.headers;
        var options = {
          // ...
        };
    
        var transport = http;
        var req = http.request(options, function handleResponse(res) {
          // ...
        });
        // Handle errors
        req.on('error', function handleRequestError(err) {
          // ...
        });
        // Send the request
        if (utils.isStream(data)) {
          data.on('error', function handleStreamError(err) {
            reject(enhanceError(err, config, null, req));
          }).pipe(req);
        } else {
          req.end(data);
        }
      });
    };
    

    上述代码主要是调用 Node.js HTTP 模块,进行请求的发送和处理,当然,真实源码实现还需要考虑 HTTPS 以及 Redirect 等问题,这里我们不再展开。

    讲到这里,可能你会问,什么场景下,才会需要自定义 Adapter 进行请求发送呢?比如在测试阶段或特殊环境中,我们可以 mock 请求:

    if (isEnv === 'ui-test') {
     adapter = require('axios-mock-adapter')
    }
    

    实现一个自定义的 Adapter 也并不困难,说到底它也只是一个 Node.js 模块,导出一个 Promise 实例即可:

    module.exports = function myAdapter(config) {
      // ...
      return new Promise(function(resolve, reject) {
        // ...
        sendRequest(resolve, reject, response);
        // ....
      });
    }
    

    相信你学会了这些内容,就对 axios-mock-adapter 这个库的实现原理了然于胸了。

    安全思想

    说到请求,自然关联着安全问题。在本小节最后部分,我们对 axios 中的一些安全机制进行解析,涉及相关攻击手段:CSRF。

    Cross—Site Request Forgery,攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说,这个请求是完全合法的,但是却完成了攻击者期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至购买商品、虚拟货币转账等。

    在 axios 中,主要依赖双重 cookie 的方式防御 CSRF。具体来说,对于攻击者,获取用户 cookie 是比较困难的,因此,我们可以在请求中携带一个 cookie 值,来保证请求的安全性。这里我们将相关流程梳理为:

    • 用户访问页面,后端向请求域中注入一个 cookie,一般该 cookie 值为加密随机字符串;

    • 在前端通过 Ajax 请求数据时,取出上述 cookie,添加到 URL 参数或者请求 header 中;

    • 后端接口验证请求中携带的 cookie 值是否合法,不合法(不一致),则拒绝请求。

    我们看 axios 源码:

    // lib/defaults.js
    var defaults = {
      adapter: getDefaultAdapter(),
      // ...
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
    };
    

    在这里,axios 默认配置了默认xsrfCookieName和xsrfHeaderName,实际开发中可以按具体情况传入配置。在具体请求时,以lib/adapters/xhr.js为例:

    // 添加 xsrf header
    if (utils.isStandardBrowserEnv()) {
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;
      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }
    

    由此可见,对一个成熟请求库的设计来说,安全防范这个话题永不过时。

    总结

    本讲我们在开篇分析了代码设计、代码分层的方方面面,一个好的设计一定是层次明晰,各司其职的,一个好的设计也会直接帮助业务开发提升效率。封装和设计,是编程领域亘古不变的经典话题,需要每名开发者下沉到业务开发中体会、思考。

    本小节的后半部分,我们从源码入手,分析了 axios 的优秀设计思想。即便你在业务中没有使用过 axios,但对于 axios 的学习始终是必要且重要的。

    主要内容总结如下:

    Drawing 2.png

    最后,给大家布置一个思考题:axios 支持请求取消能力,这是如何实现的呢?欢迎在留言区和我分享你的观点。下一讲,我们将继续学习代码设计这一话题,通过对比 Koa 和 Redux,聚焦中间件化和插件化理念。我们下一讲再见。


    # 精选评论

    # **用户4723

    通过传递config配置cancelToken的形式,来取消的。判断有传cancelToken,在promise链式调用的dispatchRequest抛出错误,在adapter中request.abort()取消请求,使promise走向rejected,被用户捕获取消信息。具体分析之前写过一篇《学习 axios 源码整体架构,打造属于自己的请求库》链接:https://lxchuan12.gitee.io/axios/ (opens new window)

    # *勇

    axios 通过在config中传入一个cancelToken, cancelToken.promise 实际为一个PENDING状态的promise实例,当用户调用cancel方法,会使该promise 实例由PENDING状态变为RESOLVE状态,触发监听函数onCanceled,调用request.abort(),取消xhr调用

    # **贤

    哈哈, 我搞的项目就是 options 驱动一切...

    # **明

    合理的设计是,底层部分保留对全局封装的影响范围,而项目层保留对页面层的影响能力,页面层保留对组件层的影响能力。这里的 XXX 保留对 xxx 的影响能力怎么理解?

    #     讲师回复

        就是底层可以「修改和干预」上层,而上层只能依赖底层,不可以修改底层

    编辑 (opens new window)
    上次更新: 2025/03/17, 12:21:00
    原生跨平台技术:移动端跨平台到 Flutter 的技术变革
    对比 Koa 和 Redux:分析前端中的中间件理念

    ← 原生跨平台技术:移动端跨平台到 Flutter 的技术变革 对比 Koa 和 Redux:分析前端中的中间件理念→

    最近更新
    01
    Code Review
    10-14
    02
    ComfyUI
    10-11
    03
    vscode插件开发
    08-24
    更多文章>
    Power by vuepress | Copyright © 2015-2025 神族九帝
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×