Axiso源码目录及核心功能的梳理
前言:此文是个人对axios源码的一个整理,没有细入到每个api功能,主要是把文件目录结构及对应的功能,和几个核心功能的原理解析了一遍。目的是领入源码的入门,以及了解核心流程
看之前需要?
需要充分了解promise相关用法,比如.then()接收2个参数都是函数,分别作用是什么?这2个函数的返回值对后面的链式调用有什么影响?如果函数返回值也是个promise 后面会怎么执行?
需要充分了解axios的基本使用和配置: https://www.kancloud.cn/yunye/axios/234845
比如:
- axios既可以像函数一样执行,又类似对象一样,可以访问属性
// axios作为函数执行axios({method: 'post',url: '/user/12345',data: {firstName: 'Fred',lastName: 'Flintstone'}});// axios类似对象一样访问属性 去执行axios.get('/user?ID=12345').then(function (response) {console.log(response);}).catch(function (error) {console.log(error);});
- 可以使用自定义配置新建一个 axios 实例
var instance = axios.create({baseURL: 'https://some-domain.com/api/',timeout: 1000,headers: {'X-Custom-Header': 'foobar'}});// 了解配置项{// `url` 是用于请求的服务器 URLurl: '/user',// `method` 是创建请求时使用的方法method: 'get', // 默认是 get// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URLbaseURL: 'https://some-domain.com/api/',// `transformRequest` 允许在向服务器发送前,修改请求数据// 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法// 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 StreamtransformRequest: [function (data) {// 对 data 进行任意转换处理return data;}],...............}
- 拦截器
// 添加请求拦截器axios.interceptors.request.use(function (config) {// 在发送请求之前做些什么return config;}, function (error) {// 对请求错误做些什么return Promise.reject(error);});// 添加响应拦截器axios.interceptors.response.use(function (response) {// 对响应数据做点什么return response;}, function (error) {// 对响应错误做点什么return Promise.reject(error);});
开始:先解析目录结构
- 打开package.json找到main字段,就是源代码dev模式的入口(打包后的文件另算)
"main": "index.js",
- 我们主要关注lib目录,axios核心功能代码都在这里面(源码的目录结构和功能拆分的非常清晰,非常有美感,很值得学习)
./lib├── adapters 适配器(当前环境是 浏览器则用xhr发请求,node环境则用http模块发请求)│ ├── README.md│ ├── http.js│ └── xhr.js├── axios.js 主入口(聚合所有的功能)├── cancel 取消请求 相关的│ ├── Cancel.js│ ├── CancelToken.js│ └── isCancel.js├── core│ ├── Axios.js 初始化axios实例(主要是请求相关的功能,单独拆出来的,利于维护。属于axios.js的子集)│ ├── InterceptorManager.js 拦截器管理│ ├── README.md│ ├── buildFullPath.js 合并成完整的path,比如baseURL: 'https://some-domain.com/api/', 请求path是 someModule/getList。 结果合成https://some-domain.com/api/someModule/getList│ ├── createError.js 封装报错的功能│ ├── dispatchRequest.js 管理适配器adapters,类似小老板 代理管 适配器工人(适配器工人有3种,用户自定义,浏览器的xhr,node的http模块)│ ├── enhanceError.js 增强错误提示,更具体│ ├── mergeConfig.js 合并配置,axios有默认配置,用户可以axios.create自定义实例,用户自定义的配置,和 axios的默认配置的合并│ ├── settle.js promise处理的一个代理。根据响应状态解析或拒绝 Promise。│ └── transformData.js 负责执行转换数据的函数├── defaults.js 默认配置├── env 当前axios版本│ ├── README.md│ └── data.js 当前axios版本├── helpers 工具函数,几乎见名知意│ ├── README.md│ ├── bind.js 和现在的bind一样,此处是兼容老浏览器│ ├── buildURL.js 往url后面添加参数,比如 xxx?id=1&page=2│ ├── combineURLs.js 通过组合指定的 URL 创建一个新的 URL│ ├── cookies.js 操作cookies│ ├── deprecatedMethod.js 废弃的写法报错提示│ ├── isAbsoluteURL.js 判断url是否是绝对路径│ ├── isAxiosError.js 判断是否是axios内部错误│ ├── isURLSameOrigin.js 判断url是否同源│ ├── normalizeHeaderName.js 标准化请求头字段名(兼容用户的不规范写法)│ ├── parseHeaders.js 解析http头 成js对象│ ├── spread.js 类似apply的功能(兼容老浏览器)│ └── validator.js 校验(如一些配置项等等)└── utils.js
拆解一下主要几个流程


主要讲解:
- 创建实例的流程?
- 执行请求的流程?
- 适配器是什么?
- 多个拦截器的执行顺序是什么?
- 如何转换请求数据?
- 如何取消请求?
- 最终返回响应结果
1. 创建实例的流程?
function Axios(instanceConfig) {this.defaults = instanceConfig;this.interceptors = {request: new InterceptorManager(),response: new InterceptorManager()};}Axios.prototype.request = function request(config) {...}// Axios.prototype.xx 省略很多方法function createInstance(defaultConfig) {var context = new Axios(defaultConfig);var instance = bind(Axios.prototype.request, context); // 注意实例是一个函数// 此处的.extend类似 Object.assign(), 把Axios.prototype和context的属性 方法,copy的实例instance身上去。 此时实例拥有类似对象的属性和方法(之前只是一个函数)utils.extend(instance, Axios.prototype, context);// 让实例instance的this指向contextutils.extend(instance, context);// 用户可以自定义实例,配置项通过mergeConfig(defaultConfig, instanceConfig) 合并instance.create = function create(instanceConfig) {return createInstance(mergeConfig(defaultConfig, instanceConfig));};return instance;}var axios = createInstance(defaults);
总结:
- 实例instance最开始是一个函数,后面通过类似 Object.assign(),把Axios.prototype和context的属性 方法,copy的实例instance身上去。 此时实例拥有类似对象的属性和方法。可以完成
axios({method: 'get'}) 也可以 axios.get('')2种灵活调用 - 可以用默认axios配置,也可以自定义,自定义用axios.create,可以自定义一些配置。配置项通过mergeConfig(defaultConfig, instanceConfig) 合并
var instance = axios.create({baseURL: 'https://some-domain.com/api/',timeout: 1000,headers: {'X-Custom-Header': 'foobar'}});
2. 执行请求的流程?
流程: Axios.prototype.request -> 请求拦截器 -> dispatchRequest(处理请求参数,调用适配器,小老板 代理) -> adapter适配器 发起请求 -> 报错/取消请求 -> 响应拦截器 -> 返回结果
从axios官网上copy下来 特性展示:
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
- 首先理解适配器,针对特性1,2
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
axios默认会根据当前环境,是否存在 XMLHttpRequest,确定是浏览器环境还是node环境,浏览器环境用XHR对象发请求,node环境用http模块发请求
function getDefaultAdapter() {var adapter;if (typeof XMLHttpRequest !== 'undefined') {// For browsers use XHR adapteradapter = require('./adapters/xhr');} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {// For node use HTTP adapteradapter = require('./adapters/http');}return adapter;}
也可以支持用户自定义请求处理,在自定义实例的配置项里设置adapter(此功能可以写mock,axios-mock-adapter库就是基于这个公共)
var adapter = config.adapter || defaults.adapter;
- 多个拦截器的执行顺序?
多个拦截器的话,是先 外到里,在里到外。
比如按顺序写的,请求拦截器1请求拦截器2响应拦截器1响应拦截器2执行顺序是 请求拦截器2 - 请求拦截器1 - 发起请求/等待结果 - 响应拦截器1 - 响应拦截器2// 因为请求拦截器是unshift插入数组的,响应拦截器是按顺序push进数组的if (!synchronousRequestInterceptors) {var chain = [dispatchRequest, undefined];Array.prototype.unshift.apply(chain, requestInterceptorChain); // 请求拦截器是unshift插入数组的chain = chain.concat(responseInterceptorChain); // 响应拦截器是按顺序push进数组的promise = Promise.resolve(config);while (chain.length) {promise = promise.then(chain.shift(), chain.shift());}return promise;}
- 如何转换请求数据?
在 dispatchRequest 阶段执行的
- dispatchRequest阶段:处理请求参数,调用适配器,类似小老板 代理
以下就是有一些默认配置,比如
- 数据是 ArrayBuffer,Blob,File,stream,Formdata等就直接return
- 数据是json格式则JSON.stringify转成字符串
- 如果content-type是application/x-www-form-urlencoded,则return data.toString() 等等 可以自行看代码
var defaults = {...transformRequest: [function transformRequest(data, headers) {normalizeHeaderName(headers, 'Accept');normalizeHeaderName(headers, 'Content-Type');if (utils.isFormData(data) ||utils.isArrayBuffer(data) ||utils.isBuffer(data) ||utils.isStream(data) ||utils.isFile(data) ||utils.isBlob(data)) {return data;}if (utils.isArrayBufferView(data)) {return data.buffer;}if (utils.isURLSearchParams(data)) {setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');return data.toString();}if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {setContentTypeIfUnset(headers, 'application/json');return stringifySafely(data);}return data;}],transformResponse: [function transformResponse(data) {var transitional = this.transitional || defaults.transitional;var silentJSONParsing = transitional && transitional.silentJSONParsing;var forcedJSONParsing = transitional && transitional.forcedJSONParsing;var strictJSONParsing = !silentJSONParsing && this.responseType === 'json';if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {try {return JSON.parse(data);} catch (e) {if (strictJSONParsing) {if (e.name === 'SyntaxError') {throw enhanceError(e, this, 'E_JSON_PARSE');}throw e;}}}return data;}],...};
3. 取消请求
首先看一个使用案例:
- 配置 cancelToken 对象
- 缓存用于取消请求的 cancel 函数,在后面特定时机调用 cancel 函数取消请求
- 在错误回调中判断如果 error 是 cancel, 做相应处理
- 实现功能 点击按钮, 取消某个正在请求中的请求(主动触发的)
<script>//获取按钮const btns = document.querySelectorAll('button');//2.声明全局变量let cancel = null;//发送请求btns[0].onclick = function () {//检测上一次的请求是否已经完成if (cancel !== null) {//取消上一次的请求cancel();}axios({method: 'GET',url: 'http://localhost:3000/posts',//1. 添加配置对象的属性cancelToken: new axios.CancelToken(function (c) {//3. 将 c 的值赋值给 cancelcancel = c;})}).then(response => {console.log(response);//将 cancel 的值初始化cancel = null;})}//绑定第二个事件取消请求btns[1].onclick = function () {cancel(); }</script>
源码细节在cancel文件内 和 xhr.js内
- 主要的方法是:XMLHttpRequest对象的 abort 方法,才能取消请求 (node内的http模块也复用了abort这个名字)
if (config.cancelToken || config.signal) {// Handle cancellation// eslint-disable-next-line func-namesonCanceled = function(cancel) {if (!request) {return;}reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);request.abort();request = null;};config.cancelToken && config.cancelToken.subscribe(onCanceled);if (config.signal) {config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);}}
最终返回响应结果
监听xhr的onreadystatechange方法,当readyState === 4 时,才正常得到响应结果
最终会由settle,对结果进行一下校验,在resolve或reject对外返回响应结果
request.onreadystatechange = function handleLoad() {if (!request || request.readyState !== 4) {return;}// 请求出错,没有得到响应,会由 onerror 处理,通过promise的 reject 抛出去// 有一个例外:请求使用 file: 协议,大多数浏览器 即使请求成功,也会返回状态为 0if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {return;}// readystate handler is calling before onerror or ontimeout handlers,// so we should call onloadend on the next 'tick'setTimeout(onloadend);}function onloadend() {if (!request) {return;}// Prepare the responsevar responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;var responseData = !responseType || responseType === 'text' || responseType === 'json' ?request.responseText : request.response;var response = {data: responseData,status: request.status,statusText: request.statusText,headers: responseHeaders,config: config,request: request};settle(function _resolve(value) {resolve(value);done();}, function _reject(err) {reject(err);done();}, response);// Clean up requestrequest = null;}// 最终会由settle,对结果进行一下校验,在resolve或reject,对外返回function settle(resolve, reject, response) {var validateStatus = response.config.validateStatus;if (!response.status || !validateStatus || validateStatus(response.status)) {resolve(response);} else {reject(createError('Request failed with status code ' + response.status,response.config,null,response.request,response));}};
感受: 源码的目录结构和功能拆分的非常清晰,非常有美感,很值得学习!
码字不易,点赞鼓励!
