手寫Axios核心原理
Axios是一個(gè)基于promise的HTTP庫(kù),它能夠自動(dòng)判斷當(dāng)前環(huán)境,自由切換在瀏覽器和 node.js環(huán)境中。如果是瀏覽器,就會(huì)基于XMLHttpRequests實(shí)現(xiàn);如果是node環(huán)境,就會(huì)基于node內(nèi)置核心http模塊實(shí)現(xiàn)。同時(shí),它還用promise處理了響應(yīng)結(jié)果,避免陷入回調(diào)地獄中去。
不僅如此,Axios還可以攔截請(qǐng)求和響應(yīng)、轉(zhuǎn)化請(qǐng)求數(shù)據(jù)和響應(yīng)數(shù)據(jù)、中斷請(qǐng)求、自動(dòng)轉(zhuǎn)換JSON數(shù)據(jù)、客戶端支持防御XSRF等。如此眾多好用的功能,快來(lái)一起看看它是如何實(shí)現(xiàn)的吧!
1.基本使用
axios基本使用方式主要有:
- axios(config)
- axios.method(url,data,config)
- // 發(fā)送 POST 請(qǐng)求
- axios({
- method: 'post',
- url: '/user/12345',
- data: {
- username: 'Web前端嚴(yán)選',
- age: 2
- }
- });
- // GET請(qǐng)求ID參數(shù)
- axios.get('/user?ID=12345')
- .then(function (response) {
- console.log(response);
- })
- .catch(function (error) {
- console.log(error);
- });
2.實(shí)現(xiàn)axios
從axios(config)的使用上可以看出導(dǎo)出的axios是一個(gè)方法,從axios.get()的使用上可以看出導(dǎo)出的axios原型上會(huì)有g(shù)et,post,put,delete等方法。
由分析可知,axios實(shí)際上是Axios類中的一個(gè)方法。我們可以先寫一個(gè)request方法來(lái)實(shí)現(xiàn)主要的請(qǐng)求功能,這樣就能使用axios(config)形式來(lái)調(diào)用了。
- class Axios{
- constructor(){
- }
- request(config){
- return new Promise((resolve) => {
- const {url='',data={},method='get'} = config; //解構(gòu)傳參
- const xhr = new XMLHttpRequest; //創(chuàng)建請(qǐng)求對(duì)象
- xhr.open(method,url,true);
- xhr.onreadystatechange = () => {
- if(xhr.readyState == 4 && xhr.status == 200){
- resolve(xhr.responseText);
- //異步請(qǐng)求返回后將Promise轉(zhuǎn)為成功態(tài)并將結(jié)果導(dǎo)出
- }
- }
- xhr.send(JSON.stringfy(data));
- })
- }
- }
- function CreateAxiosFn(){
- let axios = new Axios;
- let req = axios.request.bind(axios);
- return req;
- }
- let axios = CreateAxiosFn();
然后搭建一個(gè)簡(jiǎn)易服務(wù)端代碼,以測(cè)試請(qǐng)求的效果:
- const express = require('express')
- let app = express();
- app.all('*', function (req, res, next) {
- res.header('Access-Control-Allow-Origin', '*');
- res.header('Access-Control-Allow-Headers', 'Content-Type');
- res.header('Access-Control-Allow-Methods', '*');
- res.header('Content-Type', 'application/json;charset=utf-8');
- next();
- });
- app.get('/getInfo', function(request, response){
- let data = {
- 'username':'前端嚴(yán)選',
- 'age':'2'
- };
- response.json(data);
- });
- app.listen(3000, function(){
- console.log("服務(wù)器啟動(dòng)");
- });
啟動(dòng)服務(wù)后,在頁(yè)面中測(cè)試請(qǐng)求是否成功:
- <button onclick="getMsg()">點(diǎn)擊</button>
- <script src="./axios.js"></script>
- <script>
- function getMsg(){
- axios({
- method: 'get',
- url: 'http://localhost:3000/getInfo'
- }).then(res => {
- console.log(res);
- })
- }
- </script>
點(diǎn)擊按鈕后,可以看到請(qǐng)求成功并獲取到數(shù)據(jù)。
3.原型上的方法
接下來(lái)實(shí)現(xiàn)以axios.method()形式的方法。
通過(guò)axios.get(),axios.post(),axios.put()等方法可以看出它們都是Axios.prototype上的方法,這些方法調(diào)用內(nèi)部的request方法即可:
- const methodsArr = ['get','post','put','delete','head','options','patch','head'];
- methodsArr.forEach(method => {
- Axios.prototype[method] = function(){
- return this.request({
- method: method,
- ...arguments[0]
- })
- }
- })
arguments的第一個(gè)參數(shù)包含url,data等信息,直接解構(gòu)它的第一個(gè)元素即可
還需要實(shí)現(xiàn)一個(gè)工具方法,用來(lái)將b方法屬性混入到a中去:
- const utils = {
- extend(a,b,context){
- for(let key in b){
- if(b.hasOwnProperty(key)){
- if(typeof b[key] == 'function'){
- a[key] = b[key].bind(context);
- }else{
- a[key] = b[key]
- }
- }
- }
- }
- }
最終導(dǎo)出axios的request方法,使之擁有g(shù)et,post等方法
- function CreateAxiosFn(){
- let axios = new Axios;
- let req = axios.request.bind(axios);
- //新增如下代碼
- utils.extend(req, Axios.prototype, axios)
- return req;
- }
再來(lái)測(cè)試一下post的請(qǐng)求:
- axios.post({
- url: 'http://localhost:3000/postTest',
- data: {
- a: 1,
- b: 2
- }
- }).then(res => {
- console.log(res);
- })
可以看到正確返回結(jié)果了。
4.攔截器
先來(lái)看看攔截器的使用:
- // 請(qǐng)求攔截
- axios.interceptors.request.use(function (config) {
- // 在發(fā)送請(qǐng)求之前
- return config;
- }, function (error) {
- // 請(qǐng)求錯(cuò)誤處理
- return Promise.reject(error);
- });
- // 響應(yīng)攔截
- axios.interceptors.response.use(function (response) {
- // 響應(yīng)數(shù)據(jù)處理
- return response;
- }, function (error) {
- // 響應(yīng)錯(cuò)誤處理
- return Promise.reject(error);
- });
攔截器,顧名思義就是在請(qǐng)求之前和響應(yīng)之前,對(duì)真正要執(zhí)行的操作數(shù)據(jù)攔截住進(jìn)行一些處理。
那么如何實(shí)現(xiàn)呢,首先攔截器也是一個(gè)類,用于管理響應(yīng)和請(qǐng)求。
- class InterceptorsManage{
- constructor(){
- this.handlers = [];
- }
- use(onFulField,onRejected){
- //將成功的回調(diào)和失敗的回調(diào)都存放到隊(duì)列中
- this.handlers.push({
- onFulField,
- onRejected
- })
- }
- }
axios.interceptors.response.use和axios.interceptors.request.use來(lái)定義請(qǐng)求和響應(yīng)的攔截方法。
這說(shuō)明axios上有響應(yīng)攔截器和請(qǐng)求攔截器,那么如何在axios上實(shí)現(xiàn)呢:
- class Axios{
- constructor(){
- this.interceptors = {
- request: new InterceptorsManage,
- response: new InterceptorsManage
- }
- }
- //....
- }
在Axios的構(gòu)造函數(shù)中新增interceptors屬性,然后定義request和response屬性用于處理請(qǐng)求和響應(yīng)。
執(zhí)行use方法時(shí),會(huì)把傳入的回調(diào)函數(shù)放到handlers數(shù)組中。
這時(shí)再回看使用方式,axios.interceptors.request.use方法是綁在axios實(shí)例上的,因此同樣需要把Axios上的屬性和方法轉(zhuǎn)移到request上,將interceptors對(duì)象掛載到request方法上。
- function CreateAxiosFn() {
- let axios = new Axios();
- let req = axios.request.bind(axios);
- utils.extend(req, Axios.prototype, axios)
- //新增如下代碼
- utils.extend(req, axios)
- return req;
- }
但是現(xiàn)在request不僅要執(zhí)行請(qǐng)求的發(fā)送,還要執(zhí)行攔截器中handler的回調(diào)函數(shù),因此還需要把request方法進(jìn)行一下改造:
- request(config){
- //攔截器和請(qǐng)求的隊(duì)列
- let chain = [this.sendAjax.bind(this),undefined];
- //請(qǐng)求的攔截
- this.interceptors.request.handlers.forEach(interceptor => {
- chain.unshift(interceptor.onFulField,interceptor.onRejected);
- })
- //響應(yīng)的攔截
- this.interceptors.response.handlers.forEach(interceptor => {
- chain.push(interceptor.onFulField,interceptor.onRejected)
- })
- let promise = Promise.resolve(config);
- while(chain.length > 0){
- //從頭部開始依次執(zhí)行請(qǐng)求的攔截、真正的請(qǐng)求、響應(yīng)的攔截
- promise = promise.then(chain.shift(),chain.shift());
- }
- return promise;
- }
- sendAjax(config){
- return new Promise((resolve) => {
- const {url='',method='get',data={}} = config;
- const xhr = new XMLHttpRequest();
- xhr.open(method,url,true);
- xhr.onreadystatechange = () => {
- if(xhr.readyState == 4 && xhr.status == 200){
- resolve(xhr.responseText)
- }
- }
- xhr.send(JSON.stringify(data));
- })
- }
最后執(zhí)行chain的時(shí)候是這個(gè)樣子的:
- chain = [
- //請(qǐng)求之前成功的回調(diào)和失敗的回調(diào)
- function (config) {
- return config;
- },
- function (error) {
- return Promise.reject(error);
- }
- //真正的請(qǐng)求執(zhí)行
- this.sendAjax.bind(this),
- undefined,
- //請(qǐng)求之后響應(yīng)的成功回調(diào)和失敗回調(diào)
- function (response) {
- return response;
- },
- function (error) {
- return Promise.reject(error);
- }
- ]
請(qǐng)求之前,promise執(zhí)行為:
- promise.then(
- function (config) {
- return config;
- },
- function (error) {
- return Promise.reject(error);
- }
- )
請(qǐng)求時(shí),執(zhí)行為:
- promise.then(
- this.sendAjax.bind(this),
- undefined,
- )
響應(yīng)后,執(zhí)行為:
- promise.then(
- function (response) {
- return response;
- },
- function (error) {
- return Promise.reject(error);
- }
- )
這時(shí)我們測(cè)試一下攔截器的使用:
- function getMsg(){
- axios.interceptors.request.use((config) => {
- console.log('請(qǐng)求攔截:',config);
- return config;
- },err => {
- return Promise.reject(err)
- })
- axios.interceptors.response.use(response => {
- response = {
- message: '響應(yīng)數(shù)據(jù)替換',
- data: response
- }
- return response
- },err => {
- console.log(err,'響應(yīng)錯(cuò)誤')
- return Promise.reject(err)
- })
- axios.get({
- url: 'http://localhost:3000/getTest',
- }).then(res => {
- console.log(res);
- })
- }
可以在控制臺(tái)中看到攔截處理的打印輸出,證明攔截成功!
5.總結(jié)
Axios天然支持Promise的性能讓其方便對(duì)異步進(jìn)行處理,同時(shí)又利用了Promise對(duì)請(qǐng)求進(jìn)行了攔截,使得用戶可以在請(qǐng)求過(guò)程中添加更多的功能,對(duì)請(qǐng)求的中斷能自如操作。它的思想既清新樸實(shí)又不落入俗套,具有很好的借鑒意義。
看完這篇文章,你了解了Axios的核心原理了嗎?
本文轉(zhuǎn)載自微信公眾號(hào)「Web前端嚴(yán)選」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Web前端嚴(yán)選公眾號(hào)。