axios هي مكتبة طلبات http مبنية على Promise، والتي يمكن استخدامها في المتصفحات وnode.js، وهي تحتوي حاليًا على 42 ألف نجمة على github.
├── /dist/ # 项目输出目录
├── /lib/ # 项目源码目录
│ ├── /cancel/ # 定义取消功能
│ ├── /core/ # 一些核心功能
│ │ ├── Axios.js # axios的核心主类
│ │ ├── dispatchRequest.js # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js # 拦截器构造函数
│ │ └── settle.js # 根据http响应状态,改变Promise的状态
│ ├── /helpers/ # 一些辅助方法
│ ├── /adapters/ # 定义请求的适配器 xhr、http
│ │ ├── http.js # 实现http适配器
│ │ └── xhr.js # 实现xhr适配器
│ ├── axios.js # 对外暴露接口
│ ├── defaults.js # 默认配置
│ └── utils.js # 公用工具
├── package.json # 项目信息
├── index.d.ts # 配置TypeScript的声明文件
└── index.js # 入口文件
ملاحظة: نظرًا لأن الرموز التي نحتاج إلى الاطلاع عليها هي جميع الملفات الموجودة في الدليل /lib/
، فسوف نبحث ضمن /lib/
عن جميع مسارات الملفات التالية.
اعتراضات اعتراضية
(إذا كنت معتادًا على البرامج الوسيطة، فسيكون من السهل فهمها، لأنها تلعب دور البرامج الوسيطة القائمة على الوعد)
تنقسم المعترضات إلى اعتراضات طلب ومعترضات استجابة، كما يوحي الاسم: يمكن لمعترضات الطلب ( interceptors.request
) اعتراض كل طلب http أو محدد، ويمكن استخدام اعتراضات الاستجابة ( interceptors.response
) في كل مرة. بعد عدة طلبات http، قم باعتراض كل طلب http أو طلب محدد، ويمكن تعديل عناصر النتيجة التي تم إرجاعها.
فيما يلي شرح موجز أولاً، وبعد ذلك سنقدم مقدمة تفصيلية حول كيفية اعتراض استجابة الطلب وتعديل معلمات الطلب وبيانات الاستجابة.
محول البيانات (في الواقع يحول البيانات، مثل تحويل الكائنات إلى سلاسل JSON)
تنقسم محولات البيانات إلى محولات الطلب ومحولات الاستجابة، كما يوحي الاسم: يشير محول الطلب ( transformRequest
) إلى تحويل البيانات قبل الطلب، ويقوم محول الاستجابة ( transformResponse
) بشكل أساسي بتحويل البيانات على نص الاستجابة بعد استجابة الطلب.
محول طلب http (في الواقع طريقة)
في مشروع Axios، يشير محول طلب http بشكل أساسي إلى نوعين: XHR وhttp. جوهر XHR هو كائن XMLHttpRequest على جانب المتصفح، وجوهر http هو طريقة http[s].request للعقدة.
بالطبع، تترك Axios أيضًا للمستخدم مهمة تكوين واجهة المحول من خلال التكوين. ومع ذلك، في الظروف العادية، يمكن لهذين المحولين تلبية الطلب من المتصفح إلى الخادم أو من عميل http الخاص بالعقدة إلى الخادم.
تركز هذه المشاركة بشكل أساسي على XHR.
عنصر تكوين التكوين (في الواقع كائن)
التكوين الذي نتحدث عنه هنا لا يُسمى حقًا اسم المتغير config في المشروع. هذا الاسم هو الاسم الذي أعطيته بناءً على الغرض منه لتسهيل فهم الجميع.
في مشروع axios، عند إعداد إعدادات القراءة، تسميها بعض الأماكن defaults
( /lib/defaults.js
)، وهنا عنصر التكوين الافتراضي، وتطلق عليه بعض الأماكن اسم config
، مثل معلمات Axios.prototype.request
، مثال آخر هو معلمات طريقة محول طلب http xhrAdapter
.
يعد التكوين رابطًا مهمًا جدًا في مشروع أكسيوس، وهو الجسر الرئيسي "للتواصل" بين المستخدمين ومشروع أكسيوس.
(ملاحظة: يمكنك تخطي هذا القسم أولاً والعودة للتحقق منه لاحقًا إذا كنت في حاجة إليه)
هناك بعض الطرق المستخدمة في العديد من الأماكن في المشروع، فلنقدم هذه الطرق باختصار.
bind ( fn , context ) ;
تأثير التنفيذ هو نفس طريقة Function.prototype.bind
: fn.bind(context)
var utils = require ( './utils' ) ;
var forEach = utils . forEach ;
// 数组
utils . forEach ( [ ] , ( value , index , array ) => { } )
// 对象
utils . forEach ( { } , ( value , key , object ) => { } )
var utils = require ( './utils' ) ;
var merge = utils . merge ;
var obj1 = {
a : 1 ,
b : {
bb : 11 ,
bbb : 111 ,
}
} ;
var obj2 = {
a : 2 ,
b : {
bb : 22 ,
}
} ;
var mergedObj = merge ( obj1 , obj2 ) ;
الكائن mergedObj هو:
{
a : 2 ,
b : {
bb : 22 ,
bbb : 111
}
}
var utils = require ( './utils' ) ;
var extend = utils . extend ;
var context = {
a : 4 ,
} ;
var target = {
k : 'k1' ,
fn ( ) {
console . log ( this . a + 1 )
}
} ;
var source = {
k : 'k2' ,
fn ( ) {
console . log ( this . a - 1 )
}
} ;
let extendObj = extend ( target , source , context ) ;
كائن ExtendObj هو:
{
k : 'k2' ,
fn : source . fn . bind ( context ) ,
}
نفذ extendObj.fn();
اطبع 3
// 首先将axios包引进来
import axios from 'axios'
الطريقة الأولى للاستخدام: axios(option)
axios ( {
url ,
method ,
headers ,
} )
الطريقة الثانية للاستخدام: axios(url[, option])
axios ( url , {
method ,
headers ,
} )
طريقة الاستخدام الثالثة (لطرق get、delete
وما إلى ذلك): axios[method](url[, option])
axios . get ( url , {
headers ,
} )
طريقة الاستخدام الرابعة (طرق post、put
وما إلى ذلك): axios[method](url[, data[, option]])
axios . post ( url , data , {
headers ,
} )
الطريقة الخامسة للاستخدام: axios.request(option)
axios . request ( {
url ,
method ,
headers ,
} )
كملف إدخال لمشروع axios، دعونا أولاً نلقي نظرة على الكود المصدري لـ axios.js
النواة التي يمكنها تحقيق الطرق المختلفة لاستخدام axios هي طريقة createInstance
:
// /lib/axios.js
function createInstance ( defaultConfig ) {
// 创建一个Axios实例
var context = new Axios ( defaultConfig ) ;
// 以下代码也可以这样实现:var instance = Axios.prototype.request.bind(context);
// 这样instance就指向了request方法,且上下文指向context,所以可以直接以 instance(option) 方式调用
// Axios.prototype.request 内对第一个参数的数据类型判断,使我们能够以 instance(url, option) 方式调用
var instance = bind ( Axios . prototype . request , context ) ;
// 把Axios.prototype上的方法扩展到instance对象上,
// 这样 instance 就有了 get、post、put等方法
// 并指定上下文为context,这样执行Axios原型链上的方法时,this会指向context
utils . extend ( instance , Axios . prototype , context ) ;
// 把context对象上的自身属性和方法扩展到instance上
// 注:因为extend内部使用的forEach方法对对象做for in 遍历时,只遍历对象本身的属性,而不会遍历原型链上的属性
// 这样,instance 就有了 defaults、interceptors 属性。(这两个属性后面我们会介绍)
utils . extend ( instance , context ) ;
return instance ;
}
// 接收默认配置项作为参数(后面会介绍配置项),创建一个Axios实例,最终会被作为对象导出
var axios = createInstance ( defaults ) ;
يبدو الكود أعلاه معقدًا. في الواقع، يأمل createInstance
في النهاية في الحصول على دالة تشير إلى Axios.prototype.request
. ستحتوي هذه الدالة أيضًا على كل طريقة في Axios.prototype
كطريقة ثابتة، وسياق هذه الطرق هو أشر إلى نفس الكائن.
لذلك دعونا نلقي نظرة على الكود المصدري لـ Axios、Axios.prototype.request
؟
Axios
هو جوهر حزمة Axios
وهو تطبيق Axios، والطرق الأخرى هي امتدادات لمحتوى Axios
. الطريقة الأساسية لمنشئ Axios
هي request
request
.
// /lib/core/Axios.js
function Axios ( instanceConfig ) {
this . defaults = instanceConfig ;
this . interceptors = {
request : new InterceptorManager ( ) ,
response : new InterceptorManager ( )
} ;
}
Axios . prototype . request = function request ( config ) {
// ...省略代码
} ;
// 为支持的请求方法提供别名
utils . forEach ( [ 'delete' , 'get' , 'head' , 'options' ] , function forEachMethodNoData ( method ) {
Axios . prototype [ method ] = function ( url , config ) {
return this . request ( utils . merge ( config || { } , {
method : method ,
url : url
} ) ) ;
} ;
} ) ;
utils . forEach ( [ 'post' , 'put' , 'patch' ] , function forEachMethodWithData ( method ) {
Axios . prototype [ method ] = function ( url , data , config ) {
return this . request ( utils . merge ( config || { } , {
method : method ,
url : url ,
data : data
} ) ) ;
} ;
} ) ;
من خلال الكود أعلاه، يمكننا بدء طلبات http بعدة طرق: axios()、axios.get()、axios.post()
بشكل عام، يمكن للمشروع تلبية الطلب باستخدام مثيل axios الافتراضي. إذا لم يتم تلبية الطلب، فيجب إنشاء مثيل axios جديد أيضًا.
// /lib/axios.js - 31行
axios . Axios = Axios ;
axios . create = function create ( instanceConfig ) {
return createInstance ( utils . merge ( defaults , instanceConfig ) ) ;
} ;
بعد الحديث عن سبب وجود العديد من الطرق لاستخدام axios، قد يتبادر إلى ذهنك سؤال: عند استخدام axios، بغض النظر عن طريقة get
أو طريقة post
، يتم استدعاء طريقة Axios.prototype.request
في النهاية تعتمد هذه الطريقة على ماذا عن تكوين التكوين الذي يرسل الطلب؟
قبل أن نبدأ الحديث عن Axios.prototype.request
، دعونا نلقي نظرة أولاً على كيفية عمل التكوين الذي قام المستخدم بتكوينه في مشروع axios؟
يشير config
المذكور هنا إلى كائن عنصر التكوين في المشروع بأكمله، ومن خلال هذا الكائن يمكنك تعيين:
http请求适配器、请求地址、请求方法、请求头header、 请求数据、请求或响应数据的转换、请求进度、http状态码验证规则、超时、取消请求等
يمكن العثور على أن جميع وظائف axios
تقريبًا يتم تكوينها وتسليمها من خلال هذا الكائن، وهو ليس جسر الاتصال داخل مشروع axios
فحسب، بل هو أيضًا جسر الاتصال بين المستخدمين و axios
.
أولاً، دعونا نلقي نظرة على كيفية تعريف المستخدمين لعناصر التكوين:
import axios from 'axios'
// 第1种:直接修改Axios实例上defaults属性,主要用来设置通用配置
axios . defaults [ configName ] = value ;
// 第2种:发起请求时最终会调用Axios.prototype.request方法,然后传入配置项,主要用来设置“个例”配置
axios ( {
url ,
method ,
headers ,
} )
// 第3种:新建一个Axios实例,传入配置项,此处设置的是通用配置
let newAxiosInstance = axios . create ( {
[ configName ] : value ,
} )
ألقِ نظرة على سطر التعليمات البرمجية في طريقة Axios.prototype.request
: ( /lib/core/Axios.js
- السطر 35)
config = utils . merge ( defaults , { method : 'get' } , this . defaults , config ) ;
يمكن العثور على أنه defaults
كائنات التكوين الافتراضية ( /lib/defaults.js
)، وسمة مثيل Axios this.defaults
، config
معلمة request
هنا.
من هذا، فإن أولوية التكوينات المتعددة من الأقل إلى الأعلى هي: —> defaults
الافتراضية لكائنات التكوين ( /lib/defaults.js
)
—> { الطريقة: "الحصول على" }
—> خاصية مثيل Axios this.defaults
-> request
config
المعلمة
أتركك مع سؤال: متى تكون تكوينات defaults
و this.defaults
متماثلة ومتى تختلف؟
لقد حصلنا حتى الآن على كائن config
بعد merge
أماكن متعددة، فكيف يتم نقل هذا الكائن في المشروع؟
Axios . prototype . request = function request ( config ) {
// ...
config = utils . merge ( defaults , { method : 'get' } , this . defaults , config ) ;
var chain = [ dispatchRequest , undefined ] ;
// 将config对象当作参数传给Primise.resolve方法
var promise = Promise . resolve ( config ) ;
// ...省略代码
while ( chain . length ) {
// config会按序通过 请求拦截器 - dispatchRequest方法 - 响应拦截器
// 关于拦截器 和 dispatchRequest方法,下面会作为一个专门的小节来介绍。
promise = promise . then ( chain . shift ( ) , chain . shift ( ) ) ;
}
return promise ;
} ;
في هذه المرحلة، أكمل config
حياته الأسطورية -_-
سيتحدث القسم التالي عن أهم ما يميز: Axios.prototype.request
الكود هنا معقد نسبيًا، وتحتاج بعض الطرق إلى الرجوع إلى المصدر لمعرفة ذلك، لذلك ما عليك سوى أن يكون لديك فهم بسيط للمصفوفة المتسلسلة، وسيتم تقديم المعترضات المعنية و[ dispatchRequest
] بالتفصيل لاحقًا .
يتم استخدام مصفوفة chain
للاحتفاظ بطريقة المعترض وطريقة dispatchRequest
. يتم إخراج وظائف رد الاتصال من مصفوفة chain
بالتسلسل من خلال الوعود وتنفيذها واحدًا تلو الآخر، وأخيرًا، يتم إرجاع الوعد الجديد الذي تمت معالجته في طريقة Axios.prototype.request
. ويتم إرسال الرد أو الخطأ. وهذه هي مهمة Axios.prototype.request
.
عرض كود المصدر:
// /lib/core/Axios.js
Axios . prototype . request = function request ( 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 ;
} ;
في هذه المرحلة، يجب أن يكون لديك فضول كبير بشأن هذا المعترض، دعنا نكتشف ذلك في القسم التالي.
// 添加请求拦截器
const myRequestInterceptor = axios . interceptors . request . use ( config => {
// 在发送http请求之前做些什么
return config ; // 有且必须有一个config对象被返回
} , error => {
// 对请求错误做些什么
return Promise . reject ( error ) ;
} ) ;
// 添加响应拦截器
axios . interceptors . response . use ( response => {
// 对响应数据做点什么
return response ; // 有且必须有一个response对象被返回
} , error => {
// 对响应错误做点什么
return Promise . reject ( error ) ;
} ) ;
// 移除某次拦截器
axios . interceptors . request . eject ( myRequestInterceptor ) ;
axios . interceptors . request . use ( config => config , error => {
// 是否可以直接 return error ?
return Promise . reject ( error ) ;
} ) ;
new People ( 'whr' ) . sleep ( 3000 ) . eat ( 'apple' ) . sleep ( 5000 ) . eat ( 'durian' ) ;
// 打印结果
// (等待3s)--> 'whr eat apple' -(等待5s)--> 'whr eat durian'
فيما يتعلق بالصواريخ الاعتراضية، تم تقديم شرح موجز في قسم المسرد.
يحتوي كل مثيل من Axios على سمة مثيل interceptors
، وهناك سمتان request
response
على كائن interceptors
.
function Axios ( instanceConfig ) {
// ...
this . interceptors = {
request : new InterceptorManager ( ) ,
response : new InterceptorManager ( )
} ;
}
كلتا الخاصيتين عبارة عن مثيل InterceptorManager
، ويتم استخدام مُنشئ InterceptorManager
لإدارة المعترضات.
دعونا أولاً نلقي نظرة على مُنشئ InterceptorManager
:
يتم استخدام مُنشئ InterceptorManager
لتنفيذ المعترضات. هناك ثلاث طرق في النموذج الأولي لهذا المنشئ: الاستخدام والإخراج وforEach. فيما يتعلق بالكود المصدري، فهو في الواقع بسيط نسبيًا ويستخدم لتشغيل سمات مثيل المعالج الخاصة بالمنشئ.
// /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 ;
} ;
// 用来注销指定的拦截器
InterceptorManager . prototype . eject = function eject ( id ) {
if ( this . handlers [ id ] ) {
this . handlers [ id ] = null ;
}
} ;
// 遍历this.handlers,并将this.handlers里的每一项作为参数传给fn执行
InterceptorManager . prototype . forEach = function forEach ( fn ) {
utils . forEach ( this . handlers , function forEachHandler ( h ) {
if ( h !== null ) {
fn ( h ) ;
}
} ) ;
} ;
لذا، عندما نضيف أجهزة اعتراضية من خلال axios.interceptors.request.use
، كيف يمكن axios داخليًا تمكين هذه الأجهزة الاعتراضية من الحصول على البيانات التي نريدها قبل الطلب وبعده؟
دعونا نلقي نظرة على الكود أولاً:
// /lib/core/Axios.js
Axios . prototype . request = function request ( config ) {
// ...
var chain = [ dispatchRequest , undefined ] ;
// 初始化一个promise对象,状态为resolved,接收到的参数为config对象
var promise = Promise . resolve ( config ) ;
// 注意:interceptor.fulfilled 或 interceptor.rejected 是可能为undefined
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 ) ;
} ) ;
// 添加了拦截器后的chain数组大概会是这样的:
// [
// requestFulfilledFn, requestRejectedFn, ...,
// dispatchRequest, undefined,
// responseFulfilledFn, responseRejectedFn, ....,
// ]
// 只要chain数组长度不为0,就一直执行while循环
while ( chain . length ) {
// 数组的 shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
// 每次执行while循环,从chain数组里按序取出两项,并分别作为promise.then方法的第一个和第二个参数
// 按照我们使用InterceptorManager.prototype.use添加拦截器的规则,
// 正好每次添加的就是我们通过InterceptorManager.prototype.use方法添加的成功和失败回调
// 通过InterceptorManager.prototype.use往拦截器数组里添加拦截器时使用的数组的push方法,
// 对于请求拦截器:
// 从拦截器数组按序读到后是通过unshift方法往chain数组数里添加的,又通过shift方法从chain数组里取出的,
// 所以得出结论:对于请求拦截器,先添加的拦截器会后执行
// 对于响应拦截器:
// 从拦截器数组按序读到后是通过push方法往chain数组里添加的,又通过shift方法从chain数组里取出的,
// 所以得出结论:对于响应拦截器,添加的拦截器先执行
// 第一个请求拦截器的fulfilled函数会接收到promise对象初始化时传入的config对象,
// 而请求拦截器又规定用户写的fulfilled函数必须返回一个config对象,
// 所以通过promise实现链式调用时,每个请求拦截器的fulfilled函数都会接收到一个config对象
// 第一个响应拦截器的fulfilled函数会接受到dispatchRequest(也就是我们的请求方法)请求到的数据(也就是response对象),
// 而响应拦截器又规定用户写的fulfilled函数必须返回一个response对象,
// 所以通过promise实现链式调用时,每个响应拦截器的fulfilled函数都会接收到一个response对象
// 任何一个拦截器的抛出的错误,都会被下一个拦截器的rejected函数收到,
// 所以dispatchRequest抛出的错误才会被响应拦截器接收到。
// 因为axios是通过promise实现的链式调用,所以我们可以在拦截器里进行异步操作,
// 而拦截器的执行顺序还是会按照我们上面说的顺序执行,
// 也就是 dispatchRequest 方法一定会等待所有的请求拦截器执行完后再开始执行,
// 响应拦截器一定会等待 dispatchRequest 执行完后再开始执行。
promise = promise . then ( chain . shift ( ) , chain . shift ( ) ) ;
}
return promise ;
} ;
الآن، يجب أن يكون لديك فكرة واضحة عن ماهية المعترض وكيف يعمل في طريقة Axios.prototype.request
، فكيف يرسل dispatchRequest
في "وضع منتصف الطريق" طلب http؟
يقوم SubmitRequest بشكل أساسي بثلاثة أشياء: 1. احصل على كائن التكوين وأجري المعالجة النهائية للتكوين قبل تمريره إلى محول طلب http؛ 2. يبدأ محول طلب http طلبًا بناءً على تكوين التكوين اكتمل الطلب، إذا نجح، احصل على الاستجابة بعد تحويل البيانات بناءً على الرأس والبيانات وconfig.transformResponse (حول TransformResponse، سيشرحها محول البيانات أدناه) وقم بإعادتها.
// /lib/core/dispatchRequest.js
module . exports = function dispatchRequest ( config ) {
throwIfCancellationRequested ( config ) ;
// Support baseURL config
if ( config . baseURL && ! isAbsoluteURL ( config . url ) ) {
config . url = combineURLs ( config . baseURL , config . url ) ;
}
// Ensure headers exist
config . headers = config . headers || { } ;
// 对请求data进行转换
config . data = transformData (
config . data ,
config . headers ,
config . transformRequest
) ;
// 对header进行合并处理
config . headers = utils . merge (
config . headers . common || { } ,
config . headers [ config . method ] || { } ,
config . headers || { }
) ;
// 删除header属性里无用的属性
utils . forEach (
[ 'delete' , 'get' , 'head' , 'post' , 'put' , 'patch' , 'common' ] ,
function cleanHeaderConfig ( method ) {
delete config . headers [ method ] ;
}
) ;
// http请求适配器会优先使用config上自定义的适配器,没有配置时才会使用默认的XHR或http适配器,
// 不过大部分时候,axios提供的默认适配器是能够满足我们的
var adapter = config . adapter || defaults . adapter ;
return adapter ( config ) . then ( /**/ ) ;
} ;
حسنًا، بعد رؤية هذا، حان الوقت لنفكر: كيف يستخدم Axios الوعود لبناء جسر غير متزامن يعتمد على XHR؟
كيف تقوم أكسيوس بإجراء معالجة غير متزامنة من خلال Promise؟
import axios from 'axios'
axios . get ( /**/ )
. then ( data => {
// 此处可以拿到向服务端请求回的数据
} )
. catch ( error => {
// 此处可以拿到请求失败或取消或其他处理失败的错误对象
} )
لنأخذ أولاً رسمًا تخطيطيًا لنفهم بإيجاز التدفق التسلسلي للوصول إلى المستخدم بعد اكتمال طلب http في مشروع axios:
من خلال سبب إمكانية استخدام axios بعدة طرق، نعلم أنه بغض النظر عن كيفية استدعاء المستخدم لـ axios، فإنه سيستدعي في النهاية طريقة Axios.prototype.request
، والتي تُرجع في النهاية كائن Promise.
Axios . prototype . request = function request ( config ) {
// ...
var chain = [ dispatchRequest , undefined ] ;
// 将config对象当作参数传给Primise.resolve方法
var promise = Promise . resolve ( config ) ;
while ( chain . length ) {
promise = promise . then ( chain . shift ( ) , chain . shift ( ) ) ;
}
return promise ;
} ;
سوف يستدعي الأسلوب Axios.prototype.request
أسلوب dispatchRequest
، وسوف يستدعي أسلوب dispatchRequest
أسلوب xhrAdapter
، يقوم أسلوب xhrAdapter
بإرجاع كائن Promise.
// /lib/adapters/xhr.js
function xhrAdapter ( config ) {
return new Promise ( function dispatchXhrRequest ( resolve , reject ) {
// ... 省略代码
} ) ;
} ;
بعد أن يرسل XHR في xhrAdapter
الطلب بنجاح، فإنه سيتم تنفيذ طريقة resolve
لكائن Promise وتمرير البيانات المطلوبة، وإلا فإنه سيتم تنفيذ طريقة reject
وتمرير معلومات الخطأ كمعلمة.
// /lib/adapters/xhr.js
var request = new XMLHttpRequest ( ) ;
var loadEvent = 'onreadystatechange' ;
request [ loadEvent ] = function handleLoad ( ) {
// ...
// 往下走有settle的源码
settle ( resolve , reject , response ) ;
// ...
} ;
request . onerror = function handleError ( ) {
reject ( /**/ ) ;
request = null ;
} ;
request . ontimeout = function handleTimeout ( ) {
reject ( /**/ ) ;
request = null ;
} ;
تحقق مما إذا كانت نتيجة الإرجاع من الخادم قد اجتازت عملية التحقق:
// /lib/core/settle.js
function settle ( resolve , reject , response ) {
var validateStatus = response . config . validateStatus ;
if ( ! response . status || ! validateStatus || validateStatus ( response . status ) ) {
resolve ( response ) ;
} else {
reject ( /**/ ) ;
}
} ;
ارجع إلى طريقة dispatchRequest
، واحصل أولاً على كائن Promise الذي تم إرجاعه بواسطة طريقة xhrAdapter
، ثم قم بمعالجة نتيجة النجاح أو الفشل لكائن Promise الذي تم إرجاعه بواسطة xhrAdapter
من خلال طريقة .then
. إذا نجحت، فسيتم إرجاع response
المعالجة يفشل، ثم يُرجع كائن الوعد بحالة rejected
،
return adapter ( config ) . then ( function onAdapterResolution ( response ) {
// ...
return response ;
} , function onAdapterRejection ( reason ) {
// ...
return Promise . reject ( reason ) ;
} ) ;
} ;
لذا، في هذه المرحلة، عندما يستدعي المستخدم طريقة axios()
، يمكنه الاتصال مباشرة بـ Promise's .then
أو .catch
لمعالجة الأعمال.
إذا نظرنا إلى الوراء، تحدثنا عن تحويل البيانات عند تقديم dispatchRequest
، وقدمت Axios تحويل البيانات رسميًا كنقطة بارزة، فما هو التأثير الذي يمكن أن يلعبه تحويل البيانات في استخدام Axios؟
import axios from 'axios'
// 往现有的请求转换器里增加转换方法
axios . defaults . transformRequest . push ( ( data , headers ) => {
// ...处理data
return data ;
} ) ;
// 重写请求转换器
axios . defaults . transformRequest = [ ( data , headers ) => {
// ...处理data
return data ;
} ] ;
// 往现有的响应转换器里增加转换方法
axios . defaults . transformResponse . push ( ( data , headers ) => {
// ...处理data
return data ;
} ) ;
// 重写响应转换器
axios . defaults . transformResponse = [ ( data , headers ) => {
// ...处理data
return data ;
} ] ;
import axios from 'axios'
// 往已经存在的转换器里增加转换方法
axios . get ( url , {
// ...
transformRequest : [
... axios . defaults . transformRequest , // 去掉这行代码就等于重写请求转换器了
( data , headers ) => {
// ...处理data
return data ;
}
] ,
transformResponse : [
... axios . defaults . transformResponse , // 去掉这行代码就等于重写响应转换器了
( data , headers ) => {
// ...处理data
return data ;
}
] ,
} )
تم تخصيص محول الطلب ومحول الاستجابة في عنصر تكوين defaults
الافتراضية. ألق نظرة على الكود المصدري:
// /lib/defaults.js
var defaults = {
transformRequest : [ function transformRequest ( data , headers ) {
normalizeHeaderName ( headers , 'Content-Type' ) ;
// ...
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 ) ) {
setContentTypeIfUnset ( headers , 'application/json;charset=utf-8' ) ;
return JSON . stringify ( data ) ;
}
return data ;
} ] ,
transformResponse : [ function transformResponse ( data ) {
if ( typeof data === 'string' ) {
try {
data = JSON . parse ( data ) ;
} catch ( e ) { /* Ignore */ }
}
return data ;
} ] ,
} ;
إذن في مشروع أكسيوس، أين يتم استخدام المحول؟
يتم استخدام محول الطلب قبل طلب http، ويتم استخدام محول الطلب لمعالجة بيانات الطلب، ثم يتم تمريره إلى محول طلب http للاستخدام.
// /lib/core/dispatchRequest.js
function dispatchRequest ( config ) {
config . data = transformData (
config . data ,
config . headers ,
config . transformRequest
) ;
return adapter ( config ) . then ( /* ... */ ) ;
} ;
ألقِ نظرة على كود طريقة transformData
، فهو يجتاز بشكل أساسي مصفوفة المحول، وينفذ كل محول على حدة، ويعيد بيانات جديدة وفقًا لمعلمات البيانات والرؤوس.
// /lib/core/transformData.js
function transformData ( data , headers , fns ) {
utils . forEach ( fns , function transform ( fn ) {
data = fn ( data , headers ) ;
} ) ;
return data ;
} ;
يتم استخدام محول الاستجابة لإجراء معالجة تحويل البيانات بناءً على القيمة المرجعة لمحول طلب http بعد اكتمال طلب http:
// /lib/core/dispatchRequest.js
return adapter ( config ) . then ( function onAdapterResolution ( response ) {
// ...
response . data = transformData (
response . data ,
response . headers ,
config . transformResponse
) ;
return response ;
} , function onAdapterRejection ( reason ) {
if ( ! isCancel ( reason ) ) {
// ...
if ( reason && reason . response ) {
reason . response . data = transformData (
reason . response . data ,
reason . response . headers ,
config . transformResponse
) ;
}
}
return Promise . reject ( reason ) ;
} ) ;
يمكن للمعترض أيضًا أن يدرك الحاجة إلى تحويل بيانات الطلب والاستجابة، ولكن وفقًا لتصميم المؤلف والتعليمات البرمجية الشاملة، يمكن ملاحظة أنه أثناء الطلب، يكون المعترض مسؤولاً بشكل أساسي عن تعديل عناصر تكوين التكوين، ويكون محول البيانات هو مسؤول بشكل أساسي عن تحويل نص الطلب، مثل تحويل الكائنات، بعد طلب استجابة لسلسلة، يمكن للمعترض الحصول على response
. محول البيانات مسؤول بشكل أساسي عن معالجة نص الاستجابة، مثل تحويل السلسلة إلى كائن.
تقدم Axios رسميًا "التحويل التلقائي إلى بيانات JSON" كميزة مستقلة، فكيف يكمل محول البيانات هذه الوظيفة؟ انها في الواقع بسيطة جدا، دعونا نلقي نظرة.
افتراضيًا، ستقوم axios بإجراء تسلسل لكائن البيانات الوارد تلقائيًا إلى سلسلة JSON وتحويل سلسلة JSON في بيانات الاستجابة إلى كائن JavaScript.
// 请求时,将data数据转换为JSON 字符串
// /lib/defaults.js
transformRequest: [ function transformRequest ( data , headers ) {
// ...
if ( utils . isObject ( data ) ) {
setContentTypeIfUnset ( headers , 'application/json;charset=utf-8' ) ;
return JSON . stringify ( data ) ;
}
return data ;
} ]
// 得到响应后,将请求到的数据转换为JSON对象
// /lib/defaults.js
transformResponse: [ function transformResponse ( data ) {
if ( typeof data === 'string' ) {
try {
data = JSON . parse ( data ) ;
} catch ( e ) { /* Ignore */ }
}
return data ;
} ]
في هذه المرحلة، تم تقديم عملية تشغيل مشروع axios. هل قمت بالفعل بفتح قناتي Ren وDu؟ بعد ذلك، دعنا نلقي نظرة على ما جلبته لنا محاور المهارات المفيدة الأخرى.
import axios from 'axios'
// 设置通用header
axios . defaults . headers . common [ 'X-Requested-With' ] = 'XMLHttpRequest' ; // xhr标识
// 设置某种请求的header
axios . defaults . headers . post [ 'Content-Type' ] = 'application/x-www-form-urlencoded;charset=utf-8' ;
// 设置某次请求的header
axios . get ( url , {
headers : {
'Authorization' : 'whr1' ,
} ,
} )
// /lib/core/dispatchRequest.js - 44行
config . headers = utils . merge (
config . headers . common || { } ,
config . headers [ config . method ] || { } ,
config . headers || { }
) ;
import axios from 'axios'
// 第一种取消方法
axios . get ( url , {
cancelToken : new axios . CancelToken ( cancel => {
if ( /* 取消条件 */ ) {
cancel ( '取消日志' ) ;
}
} )
} ) ;
// 第二种取消方法
const CancelToken = axios . CancelToken ;
const source = CancelToken . source ( ) ;
axios . get ( url , {
cancelToken : source . token
} ) ;
source . cancel ( '取消日志' ) ;
// /cancel/CancelToken.js - 11行
function CancelToken ( executor ) {
var resolvePromise ;
this . promise = new Promise ( function promiseExecutor ( resolve ) {
resolvePromise = resolve ;
} ) ;
var token = this ;
executor ( function cancel ( message ) {
if ( token . reason ) {
return ;
}
token . reason = new Cancel ( message ) ;
resolvePromise ( token . reason ) ;
} ) ;
}
// /lib/adapters/xhr.js - 159行
if ( config . cancelToken ) {
config . cancelToken . promise . then ( function onCanceled ( cancel ) {
if ( ! request ) {
return ;
}
request . abort ( ) ;
reject ( cancel ) ;
request = null ;
} ) ;
}
جوهر وظيفة الإلغاء هو الحصول على promise
سمة المثيل من خلال this.promise = new Promise(resolve => resolvePromise = resolve)
في CancelToken في هذا الوقت، حالة promise
pending
من خلال هذه السمة، في /lib/adapters/xhr.js
استمر في إضافة طريقة .then
إلى مثيل promise
هذا في الملف (السطر 159 من ملف xhr.js
config.cancelToken.promise.then(message => request.abort())
);
خارج CancelToken
، يتم استخدام معلمة executor
للتحكم في طريقة cancel
، وبهذه الطريقة، عند تنفيذ طريقة cancel
، يمكن تغيير حالة سمة promise
للمثيل إلى rejected
، وبالتالي تنفيذ request.abort()
طريقة إلغاء الطلب.
يمكن اعتبار الطريقة الثانية للكتابة أعلاه بمثابة تحسين للطريقة الأولى للكتابة، لأنه في كثير من الأحيان يتم استخدام طريقة إلغاء الطلب لدينا خارج طريقة الطلب الحالية، على سبيل المثال، إذا أرسلنا طلبين A وB، عندما يتم إرسال الطلب B تم بنجاح، إلغاء الطلب أ.
// 第1种写法:
let source ;
axios . get ( Aurl , {
cancelToken : new axios . CancelToken ( cancel => {
source = cancel ;
} )
} ) ;
axios . get ( Burl )
. then ( ( ) => source ( 'B请求成功了' ) ) ;
// 第2种写法:
const CancelToken = axios . CancelToken ;
const source = CancelToken . source ( ) ;
axios . get ( Aurl , {
cancelToken : source . token
} ) ;
axios . get ( Burl )
. then ( ( ) => source . cancel ( 'B请求成功了' ) ) ;
نسبيًا، أفضل الطريقة الأولى للكتابة، لأن الطريقة الثانية للكتابة مخفية جدًا وليست بديهية وسهلة الفهم مثل الطريقة الأولى.
في الملف /lib/adapters/xhr.js، ألا يجب تسمية معلمة الأسلوب onCanceled برسالة؟ لماذا يطلق عليها إلغاء؟
في الملف /lib/adapters/xhr.js، في الأسلوب onCanceled، يجب أيضًا تمرير معلومات التكوين في حالة الرفض.
import axios from 'axios'
axios . defaults . withCredentials = true ;
لقد قدمنا بالفعل عملية نقل التكوين في مشروع axios في القسم الخاص بكيفية عمل التكوين الذي تم تكوينه بواسطة المستخدم، ومن هذا، يمكننا أن نستنتج أن التكوين الذي قمنا به من خلال axios.defaults.withCredentials = true
موجود في /lib/adapters/xhr.js
، ومن ثم تهيئته لعنصر كائن xhr من خلال الكود التالي.
var request = new XMLHttpRequest ( ) ;
// /lib/adapters/xhr.js
if ( config . withCredentials ) {
request . withCredentials = true ;
}
import axios from 'axios'
axios . defaults . timeout = 3000 ;
// /adapters/xhr.js
request . timeout = config . timeout ;
// /adapters/xhr.js
// 通过createError方法,将错误信息合为一个字符串
request . ontimeout = function handleTimeout ( ) {
reject ( createError ( 'timeout of ' + config . timeout + 'ms exceeded' ,
config , 'ECONNABORTED' , request ) ) ;
} ;
axios ( ) . catch ( error => {
const { message } = error ;
if ( message . indexOf ( 'timeout' ) > - 1 ) {
// 超时处理
}
} )
تخصيص نطاق النجاح والفشل لرمز حالة http
import axios from 'axios'
axios . defaults . validateStatus = status => status >= 200 && status < 300 ;
في التكوين الافتراضي، يتم تحديد قواعد التحقق من رمز حالة http الافتراضية، لذا فإن تخصيص validateStatus
هو في الواقع تجاوز للطريقة هنا.
// `/lib/defaults.js`
var defaults = {
// ...
validateStatus : function validateStatus ( status ) {
return status >= 200 && status < 300 ;
} ,
// ...
}
متى بدأت أكسيوس في التحقق من صحة رموز حالة http؟
// /lib/adapters/xhr.js
var request = new XMLHttpRequest ( ) ;
var loadEvent = 'onreadystatechange' ;
// /lib/adapters/xhr.js
// 每当 readyState 改变时,就会触发 onreadystatechange 事件
request [ loadEvent ] = function handleLoad ( ) {
if ( ! request || ( request . readyState !== 4 && ! xDomain ) ) {
return ;
}
// ...省略代码
var response = {
// ...
// IE sends 1223 instead of 204 (https://github.com/axios/axios/issues/201)
status : request . status === 1223 ? 204 : request . status ,
config : config ,
} ;
settle ( resolve , reject , response ) ;
// ...省略代码
}
// /lib/core/settle.js
function settle ( resolve , reject , response ) {
// 如果我们往上捣一捣就会发现,config对象的validateStatus就是我们自定义的validateStatus方法或默认的validateStatus方法
var validateStatus = response . config . validateStatus ;
// validateStatus验证通过,就会触发resolve方法
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
) ) ;
}
} ;
في مشروع Axios، هناك العديد من الطرق الذكية لاستخدام JS، مثل التشغيل التسلسلي للوعود (بالطبع يمكنك أيضًا القول أن هذا يعتمد على أساليب المعالجة للعديد من البرامج الوسيطة غير المتزامنة)، مما يسمح لنا بالتعامل بسهولة مع مختلف العمليات قبل وبعد الطلب. التحكم في تدفق كل طريقة معالجة؛ العديد من التحسينات العملية الصغيرة، مثل معالجة البيانات قبل وبعد الطلبات، مما يوفر على المبرمجين كتابة JSON.xxx مرارًا وتكرارًا لدعم كل من بيئات المتصفح والعقدة؛ باستخدام العقدة فهو بلا شك ممتاز.
باختصار، هذا النجم الذي يمكنه الحصول على 42 ألف+ على جيثب (اعتبارًا من 2018.05.27) ليس تقديرًا لقوته بأي حال من الأحوال ويستحق التحدث إليه!