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