做爲一名前端er,對於數據請求的第三方工具axios
,必定不會陌生,若是仍是有沒有用過,或者不瞭解的小夥伴,這裏給大家準備了貼心的中文文檔 ,聰明的大家一看就會~html
唔,爲了更好的瞭解和學習 axios
封裝思想和實現原理,咱們一塊兒來動手來實現一個簡版的 axios
~前端
工欲善其事,必先利其器,咱們在開始咱們的項目以前,必定要作好其相關的準備工做,咱們須要準備的也很簡單,一個 客戶端(client) 方便咱們調試,一個 服務端(server) 作接口測試~node
服務端webpack
服務端我這裏爲了方便調試,直接使用基於 nodejs
實現的 koa
框架,經過 koa-router
來實現接口,參考代碼以下:ios
const Koa = require('koa');
const KoaRouter = require('koa-router')
//app 實例
const app = new Koa();
//router 實例
const router = new KoaRouter();
//請求中間件,解決跨域
app.use(async (ctx,next)=>{
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'content-type,token,accept');
ctx.set('Access-Control-Allow-Methods', 'POST,GET,OPTIONS');
ctx.set("Content-Type", "application/json")
ctx.set('Access-Control-Max-Age', 10)
//處理 options
if (ctx.request.method.toLowerCase() === 'options'){
ctx.response.status = 200;
ctx.body = '';
} else await next();
})
//接口測試地址
router.get('/',async ctx=>{
ctx.body = {
data : 'Hello World'
}
})
router.get('/user/info',async ctx =>{
ctx.body = {
name : 'Chris' ,
msg : 'Hello World'
}
})
app.use(router.routes());
//啓動服務
app.listen(3000,function () {
console.log('app is running ~')
})
複製代碼
這裏咱們經過 node app.js
就能夠啓動咱們的服務,若是你在服務端控制檯看到 app is running ~
說明你的服務已經啓動成功,此時你打開瀏覽器訪問 http://localhost:3000/
,不出意外你能看到 Hello World
的返回信息,說明服務端這一塊就 配置 ok 了,是否是 so easy~git
客戶端es6
客戶端這塊的話,emm,咱們須要準備一個 html
文件,和 一個 js
文件夾,主要存放咱們要實現的核心代碼~github
html
文件很是簡單,以下web
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>axios-demo</title>
</head>
<body>
<div class="">
<h1>axios 的簡版實現</h1>
</div>
<script src="./js/main.js"></script>
</body>
</html>
複製代碼
其中 main.js
是咱們的要使用的js
文件~npm
要注意的是,因爲咱們的代碼是基於 es6
模塊化開發的,若是直接丟到瀏覽器裏,是沒法識別的,會報錯,不過也不要緊,咱們能夠藉助第三方的打包工具幫咱們搞定這些事~
打包不是咱們主要關注的問題,這裏我就不採用webpack
這種工具,給你們推薦一個零配置的打包工具 Parcel ,使用方式也很簡單,在你的客戶端目錄下經過 npm init -y
初始化,經過 npm install parcel-bundler --save-dev
安裝 Parcel
,而後在你的 package.json
文件中添加以下腳本:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "parcel ./*.html",
"build": "parcel build ./*.html"
},
複製代碼
這樣,咱們能夠經過 npm run dev
腳本打開咱們的 html
文件,若是大家跟咱們配置同樣,那麼你在瀏覽器的 http://localhost:1234/
地址會看到 axios 的簡版實現 這幾個字,而且控制檯不會報錯,就證實一切準備 ok 了!!
雛形
咱們首先在客戶端的 js
文件夾下建立一個 axios
的文件夾,裏面存放咱們本身實現的 axios
相關代碼。
在 axios
文件夾下新建 index.js
入口文件 和 axios.js
核心js
文件~
axios
的本質是一個類,這裏咱們經過 class
實現,即:
axios.js
class Axios {
constructor(){
}
}
export default Axios;
複製代碼
經過 index.js
進行 new
初始化,導出 axios
實例,這也是咱們在使用axios
中 不須要 new
的緣由~
index.js
import Axios from './Axios'
const axios = new Axios();
export default axios;
複製代碼
此時,咱們只須要在 main.js
經過 import
導入便可
main.js
import axios from './axios'
console.log(axios)
複製代碼
此時整個 axios
雛形就已經完成了~
一個簡單的get請求
咱們先實現一個簡單 axios.get
方法,即經過 axios.get
獲取咱們服務端的響應~
咱們回憶一下咱們平時使用 axios.get
的時候,一般是 axios.get().then
的方式,那麼咱們首先就肯定了咱們的 axios.get
方法返回的是一個 Promise
對象,咱們在 axios.js
中添加這個方法~
get(url){
return new Promise((resolve => {
let xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve({
data: JSON.parse(xhr.responseText),
status: xhr.status,
statusText: xhr.statusText
});
}
xhr.open( 'get', url , true );
xhr.send();
}))
}
複製代碼
此時咱們在 main.js
調用 get
方法 ,
axios.get('http://127.0.0.1:3000/user/info').then(res=>{
console.log(res);
})
複製代碼
控制檯輸出以下:
對比官方的 axios
,咱們少了好比 header
之類的信息,由於官方對請求返回作了二次包裝,這裏咱們只是簡單的json
處理,具體的要根據返回的數據類型作不一樣的處理~
默認配置
咱們在使用官方 axios
的,會有不少配置項,包括全局配置,實例配置和請求配置,所以咱們就來看看配置信息這一塊。
咱們在 axios
文件夾下新建一個 config.js
,用於 axios
的默認配置,爲了方便,咱們的默認配置以下:
config.js
export default {
baseURL : '' ,
method : 'get' ,
headers : {
'content-type' : 'application/json'
}
}
複製代碼
咱們將默認的配置傳入到咱們的構造函數中,以下:
index.js
import Axios from './Axios'
import config from './config'
const axios = new Axios(config);
export default axios;
複製代碼
因此,咱們須要在構造函數中接收一個 config
參數進行處理,即將默認配置寫入到實例中,即:
axios.js
constructor(config){
//配置
this.defaults = config;
}
複製代碼
這樣咱們的 get
方法裏請求的 url
就能夠改寫成 :
this.defaults.baseURL += url
......
xhr.open( 'get', this.defaults.baseURL , true );
//添加header頭
for(let key in configs.headers){
xhr.setRequestHeader(key,configs.headers[key])
}
......
複製代碼
若是你此時在config.js
中配置 baseURL
那麼,你在axios.get
中就能夠省略前面的 baseURL
, 由於在請求以前已經幫你拼接完成了~
固然,你也能夠經過 axios.defaults.baseURL = xxx
這種方式修改默認配置,都是沒問題的~
實例配置
在使用官方 axios
的時候,咱們能夠經過一個create
方法建立一個axios
實例,並傳入配置信息便可,咱們只須要在 index.js
中建立的 axios
添加一個 create
方法便可 。
index.js
axios.create = function (config) {
return new Axios(config);
}
複製代碼
這樣咱們也能夠經過 create
方法構建一個 axios
實例,它也擁有相應的方法~
可是這麼作存在一個問題,若是咱們建立多個實例,傳入不一樣的 config
,因爲咱們直接在構建的時候 經過 this.defaults = config;
這種方式複製,並無切斷對象的引用關係,所以會致使配置對象會被相互引用,出問題~
所以,咱們須要對其進行 深拷貝 賦值,即 this.defaults = deepClone(config)
, 其中 deepClone
時深拷貝函數,這裏再也不贅述~
請求配置
咱們發現官方的 axios
的get
、post
等請求會有第二個可選參數,也是 config
,即單獨本次請求的配置,若是存在,咱們須要進行配置合併,對於簡單的 baseURL
、method
等這種簡單的配置直接覆蓋,對於headers
這種複雜的對象配置,進行對象合併,有點相似 Object.assign
方法~
因此,咱們更改咱們的 get
方法以下:
get(url,config){
let configs = mergeConfig(this.defaults,config);
return new Promise((resolve => {
let xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve({
data: JSON.parse(xhr.responseText),
status: xhr.status,
statusText: xhr.statusText
});
}
xhr.open( 'get', configs.baseURL + url , true );
//添加header頭
for(let key in configs.headers){
xhr.setRequestHeader(key,configs.headers[key])
}
xhr.send();
}))
}
複製代碼
其中 mergeConfig
是合併兩配置對象的方法,具體實現參考以下:
function mergeConfig (obj1, obj2) {
let target = deepClone(obj1),
source = deepClone(obj2);
return Object.keys(source).reduce((t,k)=>{
if(['url','baseURL','method'].includes(k)){
t[k] = source[k]
}
if(['headers'].includes(k)){
t[k] = Object.assign({},source[k],t[k])
}
return t;
},target)
}
複製代碼
ok~ 如今咱們就能夠經過以下方式進行請求了:
axios.get('/user/info',{
baseURL : 'http://127.0.0.1:3000' ,
headers : {
token : 'x-token-123456'
}
}).then(res=>{
console.log(res);
})
複製代碼
能夠看到控制檯輸出跟以前的是同樣的~
細心的小夥伴能夠看到 header
頭已經添加了 token
信息~
攔截器
攔截器主要用於在請求以前或者請求以後可自定義對配置或者響應結果作一系列的處理,axios
官方給咱們提供了 use
方法,能夠添加多個攔截器,使用方式以下:
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});
複製代碼
那麼,接下來咱們本身來實現這麼一個 use
方法~
首先咱們須要在咱們的 axios
實例上添加一個 interceptors
對象,該對象有 request
和 response
兩個屬性,他們都擁有 use
方法,咱們發現 use
方法的結構都相同,入參爲兩個函數,其實他們是同一個 Interceptor
類的不一樣實例而已。
咱們先來構建 Interceptor
這個類,首先在 axios
文件夾下新建 Interceptor.js
文件,並定義以下:
Interceptor.js
export default class Interceptor {
constructor() {
this.handlers = [];
}
use( resolvedHandler, rejectedHandler ) {
this.handlers.push({
resolvedHandler,
rejectedHandler
});
}
}
複製代碼
這裏,咱們 new
出來的的實例都會擁有 use
方法,而且咱們經過一個 handlers
數組來保存,這樣能夠保證咱們能夠多調用 use
方法,添加多個攔截器~
咱們只需在 Axios.js
中的 constructor
構造函數中初始化便可。
Axios.js
constructor(config){
//默認配置
this.defaults = deepClone(config);
//攔截器
this.interceptors = {
request : new Interceptor() ,
response : new Interceptor()
}
}
複製代碼
這樣儘管咱們已經能夠在咱們的 main.js
中使用 use
方法添加攔截器了,可是仍是沒法正確使用,由於請求這一塊還未進行處理,接下來,咱們須要對咱們以前的 Axios.js
進行改造~
首先,咱們統一封裝一個 request
函數,日後全部的請求都會調用這個方法,入參須要一個 config
,返回一個 Promise
對象,咱們在這裏對攔截器進行操做,定義以下:
//request請求
request (config) {
//配置合併
let configs = mergeConfig(this.defaults, config);
//將配置轉成 Promise 對象,鏈式調用和返回 Promise 對象
let promise = Promise.resolve(configs);
//請求攔截器,遍歷 interceptors.request 裏的處理函數
let requestHandlers = this.interceptors.request.handlers;
requestHandlers.forEach(handler => {
promise = promise.then(handler.resolvedHandler, handler.rejectedHandler)
});
//數據請求
promise = promise.then(this.send)
//相應攔截器,遍歷 interceptors.response 裏的處理函數
let responseHandlers = this.interceptors.response.handlers;
responseHandlers.forEach(handler => {
promise = promise.then(handler.resolvedHandler, handler.rejectedHandler)
})
//返回響應信息
return promise;
}
複製代碼
上面,爲了代碼簡潔,我又將 send
方法提出來,定義跟以前基本一致:
//發送請求
send (configs) {
return new Promise((resolve => {
let xhr = new XMLHttpRequest();
xhr.onload = function () {
resolve({
data: JSON.parse(xhr.responseText),
status: xhr.status,
statusText: xhr.statusText
});
}
xhr.open(configs.method, configs.baseURL + configs.url, true);
//添加header頭
for ( let key in configs.headers ) {
xhr.setRequestHeader(key, configs.headers[key])
}
xhr.send();
}))
}
複製代碼
哦對啦,咱們以前的 get
方法也有一點點的不一樣,主要是加入了請求攔截器~
// 發送get請求
get (url, config) {
config.method = 'get';
config.url = url;
return this.request(config);
}
複製代碼
趁熱打鐵,咱們來試試~
這裏我在 main.js
中分別添加了 2 個響應攔截器和請求攔截器:
//請求攔截器
axios.interceptors.request.use(config=>{
console.log('請求配置信息:',config);
return config
})
axios.interceptors.request.use(config=>{
config.headers.token = 'x-token-654321';
return config
})
//響應攔截器
axios.interceptors.response.use(res=>{
console.log('請求響應信息',res)
return res;
})
axios.interceptors.response.use(res=>{
res.msg = 'request is ok ~';
return res;
})
複製代碼
請求攔截器分別打印了請求的配置並將請求的 token
值經行了修改,響應攔截器分別打印了響應信息並將響應添加了 msg
的屬性~
不出意外,你在控制檯能夠看到以下信息,在請求 header
裏看到 token
已經被更改~
大功告成!
總算是有點樣子啦~
至此,咱們本身封裝了一個很是簡單的 axios
的請求庫,因爲篇幅有限,這裏我只是用了最簡單的 get
請求示例,axios
源碼中遠不止這些,像一些異常處理、取消請求等的一系列的東西都尚未實現,這裏主要是借鑑其一些思想和實現的思路,我這裏只是牽個頭,剩下的靠大家本身不斷的去完善,動動手老是好的~
文末,附上 git
地址 感興趣的小夥伴能夠參考參考~