TypeScript 重构 Axios 经验分享

核心提示拒绝做一个只会用 API 的文档工程师,本文将会让你从重复造轮子的过程中掌握 web 开发相关的基本知识,特别是 XMLHttpRequest。又是一篇关于 TypeScript 的分享,年底了,请允许我沉淀一下。上次用 TypeScrip
拒绝做一个只会用 API 的文档工程师,本文将会让你从重复造轮子的过程中掌握 web 开发相关的基本知识,特别是 XMLHttpRequest。

又是一篇关于 Typescript 的分享,年底了,请允许我沉淀一下。上次用 Typescript 重构 Vconsole 的项目 埋下了对 Axios 源码解析的梗。于是,这次分享的主题就是 如何从零用 Typescript 重构 Axios 以及为什么我要这么做

笔者在用 Typescript 重复造轮子的时候目的还是很明确的,不仅是为了用 Typescript 养成一种好的开发习惯,更重要的是了解工具库关联的基础知识。 只有更多地注重基础知识,才能早日摆脱文档工程师的困扰。

本次分享包括以下内容:

  • 工程简介 & 开发技巧
  • API 实现
  • XHR,XHR,XHR
  • HTTP,HTTP,HTTP
  • 单元测试

项目源码,分享可能会错过某些细节实现,需要的可以看源码,测试用例基本跑通了。想想,5w star 的库,就这样自己实现了一遍。

工程简介

Axios 是什么

Promise based HTTP client for the browser and node.js

axios 是基于 Promise 用于浏览器和 nodejs 的 HTTP 客户端,它本身具有以下特性 :

  • √ 从浏览器创建 XMLHttpRequest => XHR 实现
  • √ 支持 Promise API => XHR 实现
  • √ 拦截请求和响应 => 请求拦截
  • √ 转换请求和响应数据 => 对应项目目录 /src/core/dispatchRequest.ts
  • √ 取消请求 取消请求
  • √ 自动转换 JSON 数据 => 对应项目目录 /src/core/dispatchRequest.ts
  • √ 客户端支持防止 CSRF/XSRF => CSRF
  • × 从 node.js 发出 http 请求

这里主要讲解浏览器端的 XHR 实现,限于篇幅不会涉及 node 下的 http 。如果你愿意一层一层了解它,你会发现实现 axios 还是很简单的,来一起探索吧!

目录说明

首先来看下目录。



Typescript 配置

Typescript 整体配置和规范检测参考如下:

  • tsconfig.json
  • tslint

强烈建议开启 tslint ,安装 vscode tslint 插件 并在 .vscode 目录下的 .setting 配置如下格式:

{ "editor.tabSize": 2, "editor.rulers": [120], "files.trimTrailingWhitespace": true, "files.insertFinalnewline": true, "files.exclude": { "**/.git": true, "**/.DS_Store": true }, "eslint.enable": false, "tslint.autoFixOnSave": true, "typescript.format.enable": true, "typescript.tsdk": "node_modules/typescript/lib"}

如果有安装 Prettier需注意两者风格冲突,无论格式化代码的插件是什么,我们的目的只有一个,就是 保证代码格式化风格统一。( 最好遵循 lint 规范 )。

ps:.vscode 目录可随 git 跟踪进版本管理,这样可以让 clone 仓库的使用者更友好。

另外可以通过,vscode 的 控制面板中的问题 tab 迅速查看当前项目问题所在。



  • 如果需要导入其他库可参考quokka 配置
  • 希望引入浏览器环境,可在 quokkajs 项目目录全局安装jsdom-quokka-plugin插件

接着像这样

;const testDiv = document.getElementById;console.log;



总得下来就是五类 API,比葫芦娃还少。有信心了吧,我们来一个个"送人头"。

Axios 类

这些 API 可以统称为实例方法,有实例,就肯定有类。所以在讲 API 实现之前,先让我们来看一下 Axios 类。



对于这种情况,使用 Typescript 可以在开发阶段规避这些问题。但如果是动态赋值,需要给值判断下类型,必要时可抛出错误或转换为其他想要的值。

接着来看下 axios url 相关,主要提供了 baseURL 的支持,可以通过 axios.defaults.baseURLaxios

const isAbsoluteURL = : boolean => { // 1、判断是否为协议形式比如 http:// return /^ ///i.test;};const combineURLs = : string => { return relativeURL baseURL.replace + '/' + relativeURL.replace : baseURL;};const suportbaseURL = => { // 2、baseURL 处理 return baseURL && !isAbsoluteURL combineURLs : url;};

params 与 data

在 axios 中 发送请求时 params 和 data 的区别在于:

  • params 是添加到 url 的请求字符串中的,用于 get 请求。
  • data 是添加到请求体(body)中的, 用于 post 请求。
params

axios 对 params 的处理分为赋值和序列化



请求头可以被定义为:被用于 http 请求中并且和请求主体无关的那一类 HTTP header。某些请求头如 Accept, Accept-*, If-*``允许执行条件请求。某些请求头如:cookie, User-AgentReferer 描述了请求本身以确保服务端能返回正确的响应。

并非所有出现在请求中的 http 首部都属于请求头,例如在 POST 请求中经常出现的 Content-Length 实际上是一个代表请求主体大小的 entity header,虽然你也可以把它叫做请求头。

消息头列表

axios 根据请求方法 设置了不同的 Content-TypeAccpect 请求头。

设置请求头

XMLHttpRequest 对象提供的 XMLHttpRequest对象提供的.setRequestHeader 方法为开发者提供了一个操作这两种头部信息的方法,并允许开发者自定义请求头的头部信息。

XMLHttpRequest.setRequestHeader 是设置 HTTP 请求头部的方法。此方法必须在 open 方法和 send 之间调用。如果多次对同一个请求头赋值,只会生成一个合并了多个值的请求头。

如果没有设置 Accept 属性,则此发送出 send 的值为此属性的默认值/

安全起见,有些请求头的值只能由 user agent 设置:forbidden header names 和 forbidden response header names.

默认情况下,当发送 AJAX 请求时,会附带以下头部信息:

axios 设置代码如下:

// 在 adapters 目录下的 xhr.ts 文件中:if { // 通过 XHR 的 setRequestHeader 方法设置请求头信息 for { if ) { const val = requestHeaders[key]; if === 'content-type' ) { delete requestHeaders[key]; } else { request.setRequestHeader; } } }}

至于能不能修改 http header,我的建议是当然不能随便修改任何字段。

  • 有一些字段是绝对不能修改的,比如最重要的 host 字段,如果没有 host 值,http1.1 协议会认为这是一个不规范的请求从而直接丢弃。同样的如果随便修改这个值,那目的网站也返回不了正确的内容
  • user-agent 也不建议随便修改,有很多网站是根据这个字段做内容适配的,比如 PC 和手机肯定是不一样的内容。
  • 有一些字段能够修改,比如 connectioncache-control等。不会影响你的正常访问,但有可能会慢一点。
  • 还有一些字段可以删除,比如你不希望网站记录你的访问行为或者历史信息,你可以删除 cookie,referfer 等字段。
  • 当然你也可以自定义构造任意你想要的字段,一般没什么影响,除非 header 太长导致内容截断。通常自定义的字段都建议 X-开头。比如 X-test: lance。
HTTP 小结

只要是用户主动输入网址访问时发送的 http 请求,那这些头部字段都是浏览器自动生成的,比如 host,cookie,user-agent, Accept-Encoding 等。JS 能够控制浏览器发起请求,也能在这里增加一些 header,但是考虑到安全和性能的原因,对 JS 控制 header 的能力做了一些限制,比如 host 和 cookie, user-agent 等这些字段,JS 是无法干预的禁止修改的消息首部。关于 HTTP 的知识实在多,这里简单谈到相关联的知识。这里埋下伏笔,后续若有更适合讲 HTTP 的例子,再延伸。

接下来的 CSRF,就会修改 headers。

CSRF

与 CSRF 相关的配置属性有这三个:

export interface AxiosRequestConfig { xsrfcookieName : string xsrfHeaderName : string withCredentials : boolean;}// 默认配置为{ xsrfcookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', withCredentials: false}

那么,先来简单了解 CSRF

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
什么是 CSRF 攻击

你这可以这么理解 CSRF 攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF 能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账。造成的问题包括:个人隐私泄露以及财产安全。

CSRF 原理

在他们的钓鱼站点,攻击者可以通过创建一个 AJAX 按钮或者表单来针对你的网站创建一个请求:

<form action="https://my.site.com/me/something-destructive" method="POST"> <button type="submit">Click here for free money!button>form>

要完成一次 CSRF 攻击,受害者必须依次完成两个步骤:

1.登录受信任网站 A,并在本地生成 cookie。

2.在不登出 A 的情况下,访问危险网站 B。

如果减轻 CSRF 攻击?只使用 JSON api

使用 Javascript 发起 AJAX 请求是限制跨域的。 不能通过一个简单的

来发送 JSON, 所以,通过只接收 JSON,你可以降低发生上面那种情况的可能性。

禁用 CORS

第一种减轻 CSRF 攻击的方法是禁用 cross-origin requests。如果你希望允许跨域请求,那么请只允许 OPTIONS, HEAD, GET 方法,因为他们没有副作用。不幸的是,这不会阻止上面的请求由于它没有使用 Javascript。

检查 Referer 字段

HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 http 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。

CSRF Tokens

最终的解决办法是使用 CSRF tokens。 CSRF tokens 是如何工作的呢?

  1. 服务器发送给客户端一个 token。
  2. 客户端提交的表单中带着这个 token。
  3. 如果这个 token 不合法,那么服务器拒绝这个请求。

攻击者需要通过某种手段获取你站点的 CSRF token, 他们只能使用 Javascript 来做。 所以,如果你的站点不支持 CORS, 那么他们就没有办法来获取 CSRF token, 降低了威胁。

确保 CSRF token 不能通过 AJAX 访问到!

不要创建一个/CSRF路由来获取一个 token, 尤其不要在这个路由上支持 CORS!

token 需要是不容易被猜到的, 让它很难被攻击者尝试几次得到。 它不需要是密码安全的。 攻击来自从一个未知的用户的一次或者两次的点击, 而不是来自一台服务器的暴力攻击。

axios 中的 CSRF Tokens

这里有个 withCredentials ,先来了解下。

XMLHttpRequest.withCredentials 属性是一个 Boolean 类型,它指示了是否该使用类似 cookies,authorization headers或者 TLS 客户端证书这一类资格证书来创建一个跨站点访问控制(cross-site Access-Control)请求。在同一个站点下使用 withCredentials 属性是无效的。
如果在发送来自其他域的 XMLHttpRequest 请求之前,未设置 withCredentials 为 true,那么就不能为它自己的域设置 cookie 值。而通过设置 withCredentials 为 true 获得的第三方 cookies,将会依旧享受同源策略,因此不能被通过 document.cookie 或者从头部相应请求的脚本等访问。

// 在标准浏览器环境下 则添加 xsrf 头if ) { // 必须在 withCredentials 或 同源的情况,才设置 xsrfHeader 头 const xsrfValue = ) && xsrfcookieName cookies.read : undefined; if { requestHeaders[xsrfHeaderName] = xsrfValue; }}

CSRF 小结

对于 CSRF,需要让后端同学,敏感的请求不要使用类似 get 这种幂等的,但是由于 Form 表单发起的 POST 请求并不受 CORS 的限制,因此可以任意地使用其他域的 cookie 向其他域发送 POST 请求,形成 CSRF 攻击。

这时,如果有涉及敏感信息的请求,需要跟后端同学配合,进行 XSRF-Token 认证。此时,我们用 axios 请求的时候,就可以通过设置 XMLHttpRequest.withCredentials=true 以及设置 axios,不使用则会用默认的 XSRF-TOKENX-XSRF-TOKEN

所以,axios 特性中,客户端支持防止 CSRF/XSRF。只是方便设置 CORF-TOKEN ,关键还是要后端同学的接口支持。(PS:前后端相亲相爱多重要,所以作为前端的我们还是尽可能多了解这方面的知识

XHR 实现

axios 通过适配器模式,提供了支持 node.js 的 http 以及客户端的 XMLHttpRequest 的两张实现,本文主要讲解 XHR 实现。

大概的实现逻辑如下:

const xhrAdapter = : AxiosPromise => { return new Promise => { let request: XMLHttpRequest | null = new XMLHttpRequest; setHeaders; openXHR; setXHR; sendXHR; });};

如果逐行讲解,不如录个教程视频,建议大家直接看 adapters 目录下的 xhr.ts ,在关键地方都有注释!

  1. xhrAdapter 接受 config 参数
  2. 设置请求头,比如根据传入的参数 dataauth,xsrfHeaderName 设置对应的 headers
  3. setXHR 主要是在 request.readyState === 4 的时候对响应数据作处理以及错误处理
  4. 最后执行 XMLHttpRequest.send 方法

返回的是一个 Promise 对象,所以支持 Promise 的所有特性。

请求拦截

请求拦截在 axios 应该算是一个比较骚的操作,实现非常简单。有点像一系列按顺序执行的 Promise。

直接看代码实现:

// interceptors 分为 request 和 response。 interface interceptors { request: InterceptorManager; response: InterceptorManager; } request { const { method } = config const newConfig: AxiosRequestConfig = { ...this.defaults, ...config, method: method method.toLowerCase : 'get' } // 拦截器原理:[请求拦截器,发送请求,响应拦截器] 顺序执行 // 1、建立一个存放 [ resolve , reject ] 的数组, // 这里如果没有拦截器,则执行发送请求的操作。 // 由于之后都是 resolve 和 reject 的组合,所以这里默认 undefined。真是骚操作! const chain = [ dispatchRequest, undefined ] // 2、Promise 成功后会往下传递参数,于是这里先传入合并后的参数,供之后的拦截器使用 。 let promise: any = Promise.resolve // 3、又是一波骚操作,完美的运用了数组的方法。咋不用 reduce 实现 promise 顺序执行呢 // request 请求拦截器肯定需要 `dispatchRequest` 在前面,于是 [interceptor.fulfilled, interceptor.rejected, dispatchRequest, undefined] this.interceptors.request.forEach => { chain.unshift }) // response 响应拦截器肯定需要在 `dispatchRequest` 后面,于是 [dispatchRequest, undefined,interceptor.fulfilled, interceptor.rejected] this.interceptors.response.forEach => { chain.push }) // 4、依次执行 Promise while { promise = promise.then, chain.shift) } return promise }

又是对基础知识的完美运用,无论是 Promise 还是数组的变异方法都算巧妙运用。

当然,Promise 的顺序执行还可以这样:

function sequenceTasks { function recordValue { results.push; return results; } var pushValue = recordValue.bind; return tasks.reduce { return promise.then.then; }, Promise.resolve);}

取消请求

如果不知道 XMLHttpRequest 有 absort 方法,肯定会觉得取消请求这种秀操作的怎么可能呢!

const { cancelToken } = config;const request = new XMLHttpRequest;if { cancelToken.promise .then { return; } request.abort; reject; request = null; }) .catch; });}

至于 CancelToken 就不讲了,好奇怪的实现。没有感悟到原作者的设计真谛!

单元测试

最后到了单元测试的环节,先来看下相关依赖。



执行命令:

yarn test



本项目是基于 jasmine 来写测试用例,还是比较简单的。

karma 会跑 test 目录下的所有测试用例,感觉测试用例用 Typescript 来写,有点难受。因为测试本来就是要让参数多样化,然而 Typescript 事先规定了数据类型。虽然可以使用泛型来解决,但是总觉得有点变扭。

不过,整个测试用例跑下来,代码强壮了很多。对于这种库来说,还是很有必要的。如果需要二次重构,基于 Typescript 和 有覆盖大部分函数的单元测试支持,应该会容易很多。

总结

感谢能看到这里的朋友,想必也是 Typescript 或 Axios 的粉丝,不妨相互认识一下。

还是那句话,Typescript 确实好用。短时间内就能将 Axios 大致重构了一遍,感兴趣的可以跟着一起。老规矩,在分享中不会具体讲库怎么用 ,更多的是从广度拓展大家的知识点。如果对某个关键词比较陌生,这就是进步的时候了。比如笔者接下来要去深入涉略 HTTP 了。虽然,感觉目前 Typescript 的热度好像好不是很高。好东西,总是那些不容易变的。哈,别到时候打脸了。

我变强了吗 不扯了,听杨宗纬的 "我变了,我没变" 了。

切记,没有什么是看源码解决不了的 bug。

参考
  • 跨站请求伪造
  • 跨站脚本
  • HTTP 请求
  • HTTP 缓存头部 - 完全指南
  • XMLHttpRequest
 
友情链接
鄂ICP备19019357号-22