學習 axios 源碼整體架構,打造屬于自己的請求庫

前言

這是學習源碼整體架構系列第六篇。整體架構這詞語好像有點大,姑且就算是源碼整體結構吧,主要就是學習是代碼整體結構,不深究其他不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。

學習源碼整體架構系列文章如下:

1.學習 jQuery 源碼整體架構,打造屬于自己的 js 類庫

2.學習underscore源碼整體架構,打造屬于自己的函數式編程類庫

3.學習 lodash 源碼整體架構,打造屬于自己的函數式編程類庫

4.學習 sentry 源碼整體架構,打造屬于自己的前端異常監控SDK

5.學習 vuex 源碼整體架構,打造屬于自己的狀態管理庫

感興趣的讀者可以點擊閱讀。下一篇可能是vue-router源碼。

本文比較長,手機上閱讀,可以滑到有圖的地方直接看文中的幾張圖即可。建議點贊或收藏后在電腦上閱讀,按照文中調試方式自己調試或許更容易吸收消化。

導讀
文章詳細介紹了 axios 調試方法。詳細介紹了 axios 構造函數,攔截器,取消等功能的實現。最后還對比了其他請求庫。

本文學習的版本是v0.19.0。克隆的官方倉庫的master分支。截至目前(2019 年 12 月 14 日),最新一次commit2019-12-09 15:52 ZhaoXC dc4bc49673943e352fix: fix ignore set withCredentials false (#2582)

本文倉庫在這里若川的 axios-analysis github 倉庫。求個star呀。

如果你是求職者,項目寫了運用了axios,面試官可能會問你:

1.為什么 axios 既可以當函數調用,也可以當對象使用,比如axios({})axios.get
2.簡述 axios 調用流程。
3.有用過攔截器嗎?原理是怎樣的?
4.有使用axios的取消功能嗎?是怎么實現的?
5.為什么支持瀏覽器中發送請求也支持node發送請求?
諸如這類問題。

chrome 和 vscode 調試 axios 源碼方法

前不久,筆者在知乎回答了一個問題一年內的前端看不懂前端框架源碼怎么辦?推薦了一些資料,閱讀量還不錯,大家有興趣可以看看。主要有四點:

1.借助調試
2.搜索查閱相關高贊文章
3.把不懂的地方記錄下來,查閱相關文檔
4.總結

看源碼,調試很重要,所以筆者詳細寫下 axios 源碼調試方法,幫助一些可能不知道如何調試的讀者。

chrome 調試瀏覽器環境的 axios

調試方法

axios打包后有sourcemap文件。

# 可以克隆筆者的這個倉庫代碼
git clone https://github.com/lxchuan12/axios-analysis.git
cd axios-analaysis/axios
npm install
npm start
# open [http://localhost:3000](http://localhost:3000)
# chrome F12 source 控制面板  webpack//   .  lib 目錄下,根據情況自行斷點調試

本文就是通過上述的例子axios/sandbox/client.html來調試的。

順便簡單提下調試example的例子,雖然文章最開始時寫了這部分,后來又刪了,最后想想還是寫下。

找到文件axios/examples/server.js,修改代碼如下:

server = http.createServer(function (req, res) {var url = req.url;// 調試 examplesconsole.log(url);// Process axios itselfif (/axios\.min\.js$/.test(url)) {// 原來的代碼 是 axios.min.js// pipeFileToResponse(res, '../dist/axios.min.js', 'text/javascript');pipeFileToResponse(res, '../dist/axios.js', 'text/javascript');return;}// 原來的代碼 是 axios.min.map// if (/axios\.min.map$/.test(url)) {if (/axios\.map$/.test(url)) {// 原來的代碼 是 axios.min.map// pipeFileToResponse(res, '../dist/axios.min.map', 'text/javascript');pipeFileToResponse(res, '../dist/axios.map', 'text/javascript');return;}
}
# 上述安裝好依賴后
# npm run examples 不能同時開啟,默認都是3000端口
# 可以指定端口 5000
# npm run examples ===  node ./examples/server.js
node ./examples/server.js -p 5000

打開http://localhost:5000,然后就可以開心的在Chrome瀏覽器中調試examples里的例子了。

axios 是支持 node 環境發送請求的。接下來看如何用 vscode 調試 node 環境下的axios

vscode 調試 node 環境的 axios

在根目錄下 axios-analysis/創建.vscode/launch.json文件如下:

{// 使用 IntelliSense 了解相關屬性。// 懸停以查看現有屬性的描述。// 欲了解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387"version": "0.2.0","configurations": [{"type": "node","request": "launch","name": "Launch Program","program": "${workspaceFolder}/axios/sandbox/client.js","skipFiles": ["<node_internals>/**"]},]
}

F5開始調試即可,按照自己的情況,單步跳過(F10)、單步調試(F11)斷點調試。

其實開源項目一般都有貢獻指南axios/CONTRIBUTING.md,筆者只是把這個指南的基礎上修改為引用sourcemap的文件可調試。

先看 axios 結構是怎樣的

git clone https://github.com/lxchuan12/axios-analysis.git
cd axios-analaysis/axios
npm install
npm start

按照上文說的調試方法, npm start 后,直接在 chrome 瀏覽器中調試。打開 http://localhost:3000,在控制臺打印出axios,估計很多人都沒打印出來看過。

console.log({axios: axios});

層層點開來看,axios 的結構是怎樣的,先有一個大概印象。

筆者畫了一張比較詳細的圖表示。

看完結構圖,如果看過jQueryunderscorelodash源碼,會發現其實跟axios源碼設計類似。

jQuery 別名 $underscore loadsh 別名 _ 也既是函數,也是對象。比如jQuery使用方式。$('#id'), $.ajax

接下來看具體源碼的實現。可以跟著斷點調試一下。

斷點調試要領:
賦值語句可以一步跳過,看返回值即可,后續詳細再看。
函數執行需要斷點跟著看,也可以結合注釋和上下文倒推這個函數做了什么。

axios 源碼 初始化

看源碼第一步,先看package.json。一般都會申明 main 主入口文件。

// package.json
{"name": "axios","version": "0.19.0","description": "Promise based HTTP client for the browser and node.js","main": "index.js",// ...
}

主入口文件

// index.js
module.exports = require('./lib/axios');

lib/axios.js主文件

axios.js文件 代碼相對比較多。分為三部分展開敘述。

  1. 第一部分:引入一些工具函數utilsAxios構造函數、默認配置defaults等。

  2. 第二部分:是生成實例對象 axiosaxios.Axiosaxios.create等。

  3. 第三部分取消相關 API 實現,還有allspread、導出等實現。

第一部分

引入一些工具函數utilsAxios構造函數、默認配置defaults等。

// 第一部分:
// lib/axios
// 嚴格模式
'use strict';
// 引入 utils 對象,有很多工具方法。
var utils = require('./utils');
// 引入 bind 方法
var bind = require('./helpers/bind');
// 核心構造函數 Axios
var Axios = require('./core/Axios');
// 合并配置方法
var mergeConfig = require('./core/mergeConfig');
// 引入默認配置
var defaults = require('./defaults');

第二部分

是生成實例對象 axiosaxios.Axiosaxios.create等。

/*** Create an instance of Axios** @param {Object} defaultConfig The default config for the instance* @return {Axios} A new instance of Axios*/
function createInstance(defaultConfig) {// new 一個 Axios 生成實例對象var context = new Axios(defaultConfig);// bind 返回一個新的 wrap 函數,// 也就是為什么調用 axios 是調用 Axios.prototype.request 函數的原因var instance = bind(Axios.prototype.request, context);// Copy axios.prototype to instance// 復制 Axios.prototype 到實例上。// 也就是為什么 有 axios.get 等別名方法,// 且調用的是 Axios.prototype.get 等別名方法。utils.extend(instance, Axios.prototype, context);// Copy context to instance// 復制 context 到 intance 實例// 也就是為什么默認配置 axios.defaults 和攔截器  axios.interceptors 可以使用的原因// 其實是new Axios().defaults 和 new Axios().interceptorsutils.extend(instance, context);// 最后返回實例對象,以上代碼,在上文的圖中都有體現。這時可以仔細看下上圖。return instance;
}// Create the default instance to be exported
// 導出 創建默認實例
var axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
// 暴露 Axios class 允許 class 繼承 也就是可以 new axios.Axios()
// 但  axios 文檔中 并沒有提到這個,我們平時也用得少。
axios.Axios = Axios;// Factory for creating new instances
// 工廠模式 創建新的實例 用戶可以自定義一些參數
axios.create = function create(instanceConfig) {return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

這里簡述下工廠模式。axios.create,也就是用戶不需要知道內部是怎么實現的。
舉個生活的例子,我們買手機,不需要知道手機是怎么做的,就是工廠模式。
看完第二部分,里面涉及幾個工具函數,如bindextend。接下來講述這幾個工具方法。

工具方法之 bind

axios/lib/helpers/bind.js

'use strict';
// 返回一個新的函數 wrap
module.exports = function bind(fn, thisArg) {return function wrap() {var args = new Array(arguments.length);for (var i = 0; i < args.length; i++) {args[i] = arguments[i];}// 把 argument 對象放在數組 args 里return fn.apply(thisArg, args);};
};

傳遞兩個參數函數和thisArg指向。
把參數arguments生成數組,最后調用返回參數結構。
其實現在 apply 支持 arguments這樣的類數組對象了,不需要手動轉數組。
那么為啥作者要轉數組,為了性能?當時不支持?抑或是作者不知道?這就不得而知了。有讀者知道歡迎評論區告訴筆者呀。

關于applycallbind等不是很熟悉的讀者,可以看筆者的另一個面試官問系列
面試官問:能否模擬實現 JS 的 bind 方法

舉個例子

function fn(){console.log.apply(console, arguments);
}
fn(1,2,3,4,5,6, '若川');
// 1 2 3 4 5 6 '若川'

工具方法之 utils.extend

axios/lib/utils.js

function extend(a, b, thisArg) {forEach(b, function assignValue(val, key) {if (thisArg && typeof val === 'function') {a[key] = bind(val, thisArg);} else {a[key] = val;}});return a;
}

其實就是遍歷參數 b 對象,復制到 a 對象上,如果是函數就是則用 bind 調用。

工具方法之 utils.forEach

axios/lib/utils.js

遍歷數組和對象。設計模式稱之為迭代器模式。很多源碼都有類似這樣的遍歷函數。比如大家熟知的jQuery $.each

/*** @param {Object|Array} obj The object to iterate* @param {Function} fn The callback to invoke for each item*/
function forEach(obj, fn) {// Don't bother if no value provided// 判斷 null 和 undefined 直接返回if (obj === null || typeof obj === 'undefined') {return;}// Force an array if not already something iterable// 如果不是對象,放在數組里。if (typeof obj !== 'object') {/*eslint no-param-reassign:0*/obj = [obj];}// 是數組 則用for 循環,調用 fn 函數。參數類似 Array.prototype.forEach 的前三個參數。if (isArray(obj)) {// Iterate over array valuesfor (var i = 0, l = obj.length; i < l; i++) {fn.call(null, obj[i], i, obj);}} else {// Iterate over object keys// 用 for in 遍歷對象,但 for in 會遍歷原型鏈上可遍歷的屬性。// 所以用 hasOwnProperty 來過濾自身屬性了。// 其實也可以用Object.keys來遍歷,它不遍歷原型鏈上可遍歷的屬性。for (var key in obj) {if (Object.prototype.hasOwnProperty.call(obj, key)) {fn.call(null, obj[key], key, obj);}}}
}

如果對Object相關的API不熟悉,可以查看筆者之前寫過的一篇文章。JavaScript 對象所有 API 解析

第三部分

取消相關 API 實現,還有allspread、導出等實現。

// Expose Cancel & CancelToken
// 導出 Cancel 和 CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');// Expose all/spread
// 導出 all 和 spread API
axios.all = function all(promises) {return Promise.all(promises);
};
axios.spread = require('./helpers/spread');module.exports = axios;// Allow use of default import syntax in TypeScript
// 也就是可以以下方式引入
// import axios from 'axios';
module.exports.default = axios;

這里介紹下 spread,取消的API暫時不做分析,后文再詳細分析。

假設你有這樣的需求。

function f(x, y, z) {}
var args = [1, 2, 3];
f.apply(null, args);

那么可以用spread方法。用法:

axios.spread(function(x, y, z) {})([1, 2, 3]);

實現也比較簡單。源碼實現:

/*** @param {Function} callback* @returns {Function}*/
module.exports = function spread(callback) {return function wrap(arr) {return callback.apply(null, arr);};
};

上文var context = new Axios(defaultConfig);,接下來介紹核心構造函數Axios

核心構造函數 Axios

axios/lib/core/Axios.js

構造函數Axios

function Axios(instanceConfig) {// 默認參數this.defaults = instanceConfig;// 攔截器 請求和響應攔截器this.interceptors = {request: new InterceptorManager(),response: new InterceptorManager()};
}
Axios.prototype.request = function(config){// 省略,這個是核心方法,后文結合例子詳細描述// code ...var promise = Promise.resolve(config);// code ...return promise;
}
// 這是獲取 Uri 的函數,這里省略
Axios.prototype.getUri = function(){}
// 提供一些請求方法的別名
// Provide aliases for supported request methods
// 遍歷執行
// 也就是為啥我們可以 axios.get 等別名的方式調用,而且調用的是 Axios.prototype.request 方法
// 這個也在上面的 axios 結構圖上有所體現。
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {/*eslint func-names:0*/Axios.prototype[method] = function(url, config) {return this.request(utils.merge(config || {}, {method: method,url: url}));};
});utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {/*eslint func-names:0*/Axios.prototype[method] = function(url, data, config) {return this.request(utils.merge(config || {}, {method: method,url: url,data: data}));};
});module.exports = Axios;

接下來看攔截器部分。

攔截器管理構造函數 InterceptorManager

請求前攔截,和請求后攔截。
Axios.prototype.request函數里使用,具體怎么實現的攔截的,后文配合例子詳細講述。

axios github 倉庫 攔截器文檔

如何使用:

// Add a request interceptor
// 添加請求前攔截器
axios.interceptors.request.use(function (config) {// Do something before request is sentreturn config;
}, function (error) {// Do something with request errorreturn Promise.reject(error);
});// Add a response interceptor
// 添加請求后攔截器
axios.interceptors.response.use(function (response) {// Any status code that lie within the range of 2xx cause this function to trigger// Do something with response datareturn response;
}, function (error) {// Any status codes that falls outside the range of 2xx cause this function to trigger// Do something with response errorreturn Promise.reject(error);
});

如果想把攔截器,可以用eject方法。

const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);

攔截器也可以添加自定義的實例上。

const instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});

源碼實現:

構造函數,handles 用于存儲攔截器函數。

function InterceptorManager() {this.handlers = [];
}

接下來聲明了三個方法:使用、移除、遍歷。

InterceptorManager.prototype.use 使用

傳遞兩個函數作為參數,數組中的一項存儲的是{fulfilled: function(){}, rejected: function(){}}。返回數字 ID,用于移除攔截器。

/*** @param {Function} fulfilled The function to handle `then` for a `Promise`* @param {Function} rejected The function to handle `reject` for a `Promise`** @return {Number} 返回ID 是為了用 eject 移除*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {this.handlers.push({fulfilled: fulfilled,rejected: rejected});return this.handlers.length - 1;
};

InterceptorManager.prototype.eject 移除

根據 use 返回的 ID 移除 攔截器。

/*** @param {Number} id The ID that was returned by `use`*/
InterceptorManager.prototype.eject = function eject(id) {if (this.handlers[id]) {this.handlers[id] = null;}
};

有點類似定時器setTimeoutsetInterval,返回值是id。用clearTimeoutclearInterval來清除定時器。

// 提一下 定時器回調函數是可以傳參的,返回值 timer 是數字
var timer = setInterval((name) => {console.log(name);
}, 1000, '若川');
console.log(timer); // 數字 ID
// 在控制臺等會再輸入執行這句,定時器就被清除了
clearInterval(timer);

InterceptorManager.prototype.forEach 遍歷

遍歷執行所有攔截器,傳遞一個回調函數(每一個攔截器函數作為參數)調用,被移除的一項是null,所以不會執行,也就達到了移除的效果。

/*** @param {Function} fn The function to call for each interceptor*/
InterceptorManager.prototype.forEach = function forEach(fn) {utils.forEach(this.handlers, function forEachHandler(h) {if (h !== null) {fn(h);}});
};

實例結合

上文敘述的調試時運行npm start 是用axios/sandbox/client.html路徑的文件作為示例的,讀者可以自行調試。

以下是一段這個文件中的代碼。

axios(options)
.then(function (res) {response.innerHTML = JSON.stringify(res.data, null, 2);
})
.catch(function (res) {response.innerHTML = JSON.stringify(res.data, null, 2);
});

先看調用棧流程

如果不想一步步調試,有個偷巧的方法。
知道 axios 使用了XMLHttpRequest
可以在項目中搜索:new XMLHttpRequest
定位到文件 axios/lib/adapters/xhr.js
在這條語句 var request = new XMLHttpRequest();
chrome 瀏覽器中 打個斷點調試下,再根據調用棧來細看具體函數等實現。

Call Stack

dispatchXhrRequest (xhr.js:19)
xhrAdapter (xhr.js:12)
dispatchRequest (dispatchRequest.js:60)
Promise.then (async)
request (Axios.js:54)
wrap (bind.js:10)
submit.onclick ((index):138)

簡述下流程:

  1. Send Request 按鈕點擊 submit.onclick

  2. 調用 axios 函數實際上是調用 Axios.prototype.request 函數,而這個函數使用 bind 返回的一個名為wrap的函數。

  3. 調用 Axios.prototype.request

  4. (有請求攔截器的情況下執行請求攔截器),中間會執行 dispatchRequest方法

  5. dispatchRequest 之后調用 adapter (xhrAdapter)

  6. 最后調用 Promise 中的函數dispatchXhrRequest,(有響應攔截器的情況下最后會再調用響應攔截器)

如果仔細看了文章開始的axios 結構關系圖,其實對這個流程也有大概的了解。

接下來看 Axios.prototype.request 具體實現。

Axios.prototype.request 請求核心方法

這個函數是核心函數。主要做了這幾件事:

1.判斷第一個參數是字符串,則設置 url,也就是支持axios('example/url', [, config]),也支持axios({})
2.合并默認參數和用戶傳遞的參數
3.設置請求的方法,默認是是get方法
4.將用戶設置的請求和響應攔截器、發送請求的dispatchRequest組成Promise鏈,最后返回還是Promise實例。

也就是保證了請求前攔截器先執行,然后發送請求,再響應攔截器執行這樣的順序。<br>
也就是為啥最后還是可以`then`,`catch`方法的緣故。<br>
Axios.prototype.request = function request(config) {/*eslint no-param-reassign:0*/// Allow for axios('example/url'[, config]) a la fetch API// 這一段代碼 其實就是 使 axios('example/url', [, config])// config 參數可以省略if (typeof config === 'string') {config = arguments[1] || {};config.url = arguments[0];} else {config = config || {};}// 合并默認參數和用戶傳遞的參數config = mergeConfig(this.defaults, config);// Set config.method// 設置 請求方法,默認 get 。if (config.method) {config.method = config.method.toLowerCase();} else if (this.defaults.method) {config.method = this.defaults.method.toLowerCase();} else {config.method = 'get';}// Hook up interceptors middleware// 組成`Promise`鏈 這段拆開到后文再講述
};

組成Promise鏈,返回Promise實例

這部分:用戶設置的請求和響應攔截器、發送請求的dispatchRequest組成Promise鏈。也就是保證了請求前攔截器先執行,然后發送請求,再響應攔截器執行這樣的順序

也就是保證了請求前攔截器先執行,然后發送請求,再響應攔截器執行這樣的順序<br>
也就是為啥最后還是可以`then`,`catch`方法的緣故。<br>

如果讀者對Promise不熟悉,建議讀阮老師的書籍《ES6 標準入門》。阮一峰老師 的 ES6 Promise-resolve 和 JavaScript Promise 迷你書(中文版)

  // 組成`Promise`鏈// Hook up interceptors middleware// 把 xhr 請求 的 dispatchRequest 和 undefined 放在一個數組里var chain = [dispatchRequest, undefined];// 創建 Promise 實例var promise = Promise.resolve(config);// 遍歷用戶設置的請求攔截器 放到數組的 chain 前面this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {chain.unshift(interceptor.fulfilled, interceptor.rejected);});// 遍歷用戶設置的響應攔截器 放到數組的 chain 后面this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {chain.push(interceptor.fulfilled, interceptor.rejected);});// 遍歷 chain 數組,直到遍歷 chain.length 為 0while (chain.length) {// 兩兩對應移出來 放到 then 的兩個參數里。promise = promise.then(chain.shift(), chain.shift());}return promise;
var promise = Promise.resolve(config);

解釋下這句。作用是生成Promise實例。

var promise = Promise.resolve({name: '若川'})
// 等價于
// new Promise(resolve => resolve({name: '若川'}))
promise.then(function (config){console.log(config)
});
// {name: "若川"}

同樣解釋下后文會出現的Promise.reject(error);

Promise.reject(error);
var promise = Promise.reject({name: '若川'})
// 等價于
// new Promise(reject => reject({name: '若川'}))// promise.then(null, function (config){
//   console.log(config)
// });
// 等價于
promise.catch(function (config){console.log(config)
});
// {name: "若川"}

接下來結合例子,來理解這段代碼。
很遺憾,在example文件夾沒有攔截器的例子。筆者在example中在example/get的基礎上添加了一個攔截器的示例。axios/examples/interceptors,便于讀者調試。

node ./examples/server.js -p 5000

promise = promise.then(chain.shift(), chain.shift());這段代碼打個斷點。

會得到這樣的這張圖。

特別關注下,右側,local中的chain數組。也就是這樣的結構。

var chain = ['請求成功攔截2', '請求失敗攔截2','請求成功攔截1', '請求失敗攔截1',dispatch,  undefined,'響應成功攔截1', '響應失敗攔截1','響應成功攔截2', '響應失敗攔截2',
]

這段代碼相對比較繞。也就是會生成如下類似的代碼,中間會調用dispatchRequest方法。

// config 是 用戶配置和默認配置合并的
var promise = Promise.resolve(config);
promise.then('請求成功攔截2', '請求失敗攔截2')
.then('請求成功攔截1', '請求失敗攔截1')
.then(dispatchRequest, undefined)
.then('響應成功攔截1', '響應失敗攔截1')
.then('響應成功攔截2', '響應失敗攔截2').then('用戶寫的業務處理函數')
.catch('用戶寫的報錯業務處理函數');

這里提下promise thencatch知識:
Promise.prototype.then方法的第一個參數是resolved狀態的回調函數,第二個參數(可選)是rejected狀態的回調函數。所以是成對出現的。
Promise.prototype.catch方法是.then(null, rejection).then(undefined, rejection)的別名,用于指定發生錯誤時的回調函數。
then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。因此可以采用鏈式寫法,即then方法后面再調用另一個then方法。

結合上述的例子更詳細一點,代碼則是這樣的。

var promise = Promise.resolve(config);
// promise.then('請求成功攔截2', '請求失敗攔截2')
promise.then(function requestSuccess2(config) {console.log('------request------success------2');return config;
}, function requestError2(error) {console.log('------response------error------2');return Promise.reject(error);
})// .then('請求成功攔截1', '請求失敗攔截1')
.then(function requestSuccess1(config) {console.log('------request------success------1');return config;
}, function requestError1(error) {console.log('------response------error------1');return Promise.reject(error);
})// .then(dispatchRequest, undefined)
.then( function dispatchRequest(config) {/*** 適配器返回的也是Promise 實例adapter = function xhrAdapter(config) {return new Promise(function dispatchXhrRequest(resolve, reject) {})}**/return adapter(config).then(function onAdapterResolution(response) {// 省略代碼 ...return response;}, function onAdapterRejection(reason) {// 省略代碼 ...return Promise.reject(reason);});
}, undefined)// .then('響應成功攔截1', '響應失敗攔截1')
.then(function responseSuccess1(response) {console.log('------response------success------1');return response;
}, function responseError1(error) {console.log('------response------error------1');return Promise.reject(error);
})// .then('響應成功攔截2', '響應失敗攔截2')
.then(function responseSuccess2(response) {console.log('------response------success------2');return response;
}, function responseError2(error) {console.log('------response------error------2');return Promise.reject(error);
})// .then('用戶寫的業務處理函數')
// .catch('用戶寫的報錯業務處理函數');
.then(function (response) {console.log('哈哈哈,終于獲取到數據了', response);
})
.catch(function (err) {console.log('哎呀,怎么報錯了', err);
});

仔細看這段Promise鏈式調用,代碼都類似。then方法最后返回的參數,就是下一個then方法第一個參數。
catch錯誤捕獲,都返回Promise.reject(error),這是為了便于用戶catch時能捕獲到錯誤。

舉個例子:

var p1 = new Promise((resolve, reject) => {reject(new Error({name: '若川'}));
});p1.catch(err => {console.log(res, 'err');return Promise.reject(err)
})
.catch(err => {console.log(err, 'err1');
})
.catch(err => {console.log(err, 'err2');
});

err2不會捕獲到,也就是不會執行,但如果都返回了return Promise.reject(err),則可以捕獲到。

最后畫個圖總結下 Promise 鏈式調用。

axios promise 鏈式調用

小結:1. 請求和響應的攔截器可以寫Promise

  1. 如果設置了多個請求響應器,后設置的先執行。

  2. 如果設置了多個響應攔截器,先設置的先執行。

dispatchRequest(config) 這里的config是請求成功攔截器返回的。接下來看dispatchRequest函數。

dispatchRequest 最終派發請求

這個函數主要做了如下幾件事情:

1.如果已經取消,則 throw 原因報錯,使Promise走向rejected
2.確保 config.header 存在。
3.利用用戶設置的和默認的請求轉換器轉換數據。
4.拍平 config.header
5.刪除一些 config.header
6.返回適配器adapterPromise實例)執行后 then執行后的 Promise實例。返回結果傳遞給響應攔截器處理。

'use strict';
// utils 工具函數
var utils = require('./../utils');
// 轉換數據
var transformData = require('./transformData');
// 取消狀態
var isCancel = require('../cancel/isCancel');
// 默認參數
var defaults = require('../defaults');/*** 拋出 錯誤原因,使`Promise`走向`rejected`*/
function throwIfCancellationRequested(config) {if (config.cancelToken) {config.cancelToken.throwIfRequested();}
}/*** Dispatch a request to the server using the configured adapter.** @param {object} config The config that is to be used for the request* @returns {Promise} The Promise to be fulfilled*/
module.exports = function dispatchRequest(config) {// 取消相關throwIfCancellationRequested(config);// Ensure headers exist// 確保 headers 存在config.headers = config.headers || {};// Transform request data// 轉換請求的數據config.data = transformData(config.data,config.headers,config.transformRequest);// Flatten headers// 拍平 headersconfig.headers = utils.merge(config.headers.common || {},config.headers[config.method] || {},config.headers || {});// 以下這些方法 刪除 headersutils.forEach(['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],function cleanHeaderConfig(method) {delete config.headers[method];});// adapter 適配器部分 拆開 放在下文講
};

dispatchRequest 之 transformData 轉換數據

上文的代碼里有個函數 transformData ,這里解釋下。其實就是遍歷傳遞的函數數組 對數據操作,最后返回數據。

axios.defaults.transformResponse 數組中默認就有一個函數,所以使用concat鏈接自定義的函數。

使用:

文件路徑axios/examples/transform-response/index.html

這段代碼其實就是對時間格式的字符串轉換成時間對象,可以直接調用getMonth等方法。

var ISO_8601 = /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z/;
function formatDate(d) {return (d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear();
}axios.get('https://api.github.com/users/mzabriskie', {transformResponse: axios.defaults.transformResponse.concat(function (data, headers) {Object.keys(data).forEach(function (k) {if (ISO_8601.test(data[k])) {data[k] = new Date(Date.parse(data[k]));}});return data;})
})
.then(function (res) {document.getElementById('created').innerHTML = formatDate(res.data.created_at);
});

源碼:

就是遍歷數組,調用數組里的傳遞 dataheaders 參數調用函數。

module.exports = function transformData(data, headers, fns) {/*eslint no-param-reassign:0*/utils.forEach(fns, function transform(fn) {data = fn(data, headers);});return data;
};

dispatchRequest 之 adapter 適配器執行部分

適配器,在設計模式中稱之為適配器模式。講個生活中簡單的例子,大家就容易理解。

我們常用以前手機耳機孔都是圓孔,而現在基本是耳機孔和充電接口合二為一。統一為typec

這時我們需要需要一個typec轉圓孔的轉接口,這就是適配器。

  // adapter 適配器部分var adapter = config.adapter || defaults.adapter;return adapter(config).then(function onAdapterResolution(response) {throwIfCancellationRequested(config);// Transform response data// 轉換響應的數據response.data = transformData(response.data,response.headers,config.transformResponse);return response;}, function onAdapterRejection(reason) {if (!isCancel(reason)) {// 取消相關throwIfCancellationRequested(config);// Transform response data// 轉換響應的數據if (reason && reason.response) {reason.response.data = transformData(reason.response.data,reason.response.headers,config.transformResponse);}}return Promise.reject(reason);});

接下來看具體的 adapter

adapter 適配器 真正發送請求

var adapter = config.adapter || defaults.adapter;

看了上文的 adapter,可以知道支持用戶自定義。比如可以通過微信小程序 wx.request 按照要求也寫一個 adapter
接著來看下 defaults.ddapter
文件路徑:axios/lib/defaults.js

根據當前環境引入,如果是瀏覽器環境引入xhr,是node環境則引入http
類似判斷node環境,也在sentry-javascript源碼中有看到。

function getDefaultAdapter() {var adapter;// 根據 XMLHttpRequest 判斷if (typeof XMLHttpRequest !== 'undefined') {// For browsers use XHR adapteradapter = require('./adapters/xhr');// 根據 process 判斷} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {// For node use HTTP adapteradapter = require('./adapters/http');}return adapter;
}
var defaults = {adapter: getDefaultAdapter(),// ...
};

xhr

接下來就是我們熟悉的 XMLHttpRequest 對象。

可能讀者不了解可以參考XMLHttpRequest MDN 文檔。

主要提醒下:onabort是請求取消事件,withCredentials是一個布爾值,用來指定跨域 Access-Control 請求是否應帶有授權信息,如 cookie 或授權 header 頭。

這塊代碼有刪減,具體可以看若川的axios-analysis倉庫,也可以克隆筆者的axios-analysis倉庫調試時再具體分析。

module.exports = function xhrAdapter(config) {return new Promise(function dispatchXhrRequest(resolve, reject) {// 這塊代碼有刪減var request = new XMLHttpRequest();request.open()request.timeout = config.timeout;// 監聽 state 改變request.onreadystatechange = function handleLoad() {if (!request || request.readyState !== 4) {return;}// ...}// 取消request.onabort = function(){};// 錯誤request.onerror = function(){};// 超時request.ontimeout = function(){};// cookies 跨域攜帶 cookies 面試官常喜歡考這個// 一個布爾值,用來指定跨域 Access-Control 請求是否應帶有授權信息,如 cookie 或授權 header 頭。// Add withCredentials to request if neededif (!utils.isUndefined(config.withCredentials)) {request.withCredentials = !!config.withCredentials;}// 上傳下載進度相關// Handle progress if neededif (typeof config.onDownloadProgress === 'function') {request.addEventListener('progress', config.onDownloadProgress);}// Not all browsers support upload eventsif (typeof config.onUploadProgress === 'function' && request.upload) {request.upload.addEventListener('progress', config.onUploadProgress);}// Send the request// 發送請求request.send(requestData);});
}

而實際上現在 fetch 支持的很好了,阿里開源的 umi-request 請求庫,就是用fetch封裝的,而不是用XMLHttpRequest。文章末尾,大概講述下 umi-requestaxios 的區別。

http

http這里就不詳細敘述了,感興趣的讀者可以自行查看,若川的axios-analysis倉庫。

module.exports = function httpAdapter(config) {return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {});
};

上文 dispatchRequest 有取消模塊,我覺得是重點,所以放在最后來細講:

dispatchRequest 之 取消模塊

可以使用cancel token取消請求。

axios cancel token API 是基于撤銷的 promise 取消提議。

The axios cancel token API is based on the withdrawn cancelable promises proposal.

axios 文檔 cancellation

文檔上詳細描述了兩種使用方式。

很遺憾,在example文件夾也沒有取消的例子。筆者在example中在example/get的基礎上添加了一個取消的示例。axios/examples/cancel,便于讀者調試。

node ./examples/server.js -p 5000

request中的攔截器和dispatch中的取消這兩個模塊相對復雜,可以多調試調試,吸收消化。

const CancelToken = axios.CancelToken;
const source = CancelToken.source();axios.get('/get/server', {cancelToken: source.token
}).catch(function (err) {if (axios.isCancel(err)) {console.log('Request canceled', err.message);} else {// handle error}
});// cancel the request (the message parameter is optional)
// 取消函數。
source.cancel('哎呀,我被若川取消了');

取消請求模塊代碼示例

結合源碼取消流程大概是這樣的。這段放在代碼在axios/examples/cancel-token/index.html

參數的 config.cancelToken 是觸發了source.cancel('哎呀,我被若川取消了');才生成的。

// source.cancel('哎呀,我被若川取消了');
// 點擊取消時才會 生成 cancelToken 實例對象。
// 點擊取消后,會生成原因,看懂了這段在看之后的源碼,可能就好理解了。
var config = {name: '若川',// 這里簡化了cancelToken: {promise: new Promise(function(resolve){resolve({ message: '哎呀,我被若川取消了'})}),reason: { message: '哎呀,我被若川取消了' }},
};
// 取消 拋出異常方法
function throwIfCancellationRequested(config){// 取消的情況下執行這句if(config.cancelToken){//   這里源代碼 便于執行,我改成具體代碼// config.cancelToken.throwIfRequested();// if (this.reason) {//     throw this.reason;//   }if(config.cancelToken.reason){throw config.cancelToken.reason;}}
}function dispatchRequest(config){// 有可能是執行到這里就取消了,所以拋出錯誤會被err2 捕獲到throwIfCancellationRequested(config);//  adapter xhr適配器return new Promise((resovle, reject) => {var request = new XMLHttpRequest();console.log('request', request);if (config.cancelToken) {// Handle cancellationconfig.cancelToken.promise.then(function onCanceled(cancel) {if (!request) {return;}request.abort();reject(cancel);// Clean up requestrequest = null;});}}).then(function(res){// 有可能是執行到這里就才取消 取消的情況下執行這句throwIfCancellationRequested(config);console.log('res', res);return res;}).catch(function(reason){// 有可能是執行到這里就才取消 取消的情況下執行這句throwIfCancellationRequested(config);console.log('reason', reason);return Promise.reject(reason);});
}var promise = Promise.resolve(config);// 沒設置攔截器的情況下是這樣的
promise
.then(dispatchRequest, undefined)
// 用戶定義的then 和 catch
.then(function(res){console.log('res1', res);return res;
})
.catch(function(err){console.log('err2', err);return Promise.reject(err);
});
// err2 {message: "哎呀,我被若川取消了"}

接下來看取消模塊的源碼

看如何通過生成config.cancelToken

文件路徑:

axios/lib/cancel/CancelToken.js

const CancelToken = axios.CancelToken;
const source = CancelToken.source();
source.cancel('哎呀,我被若川取消了');

由示例看 CancelToken.source的實現,

CancelToken.source = function source() {var cancel;var token = new CancelToken(function executor(c) {cancel = c;});// tokenreturn {token: token,cancel: cancel};
};

執行后source的大概結構是這樣的。

{token: {promise: new Promise(function(resolve){resolve({ message: '哎呀,我被若川取消了'})}),reason: { message: '哎呀,我被若川取消了' }},cancel: function cancel(message) {if (token.reason) {// Cancellation has already been requested// 已經取消return;}token.reason = {message: '哎呀,我被若川取消了'};}
}

接著看 new CancelToken

// CancelToken
// 通過 CancelToken 來取消請求操作
function CancelToken(executor) {if (typeof executor !== 'function') {throw new TypeError('executor must be a function.');}var resolvePromise;this.promise = new Promise(function promiseExecutor(resolve) {resolvePromise = resolve;});var token = this;executor(function cancel(message) {if (token.reason) {// Cancellation has already been requested// 已經取消return;}token.reason = new Cancel(message);resolvePromise(token.reason);});
}module.exports = CancelToken;

發送請求的適配器里是這樣使用的。

// xhr
if (config.cancelToken) {// Handle cancellationconfig.cancelToken.promise.then(function onCanceled(cancel) {if (!request) {return;}request.abort();reject(cancel);// Clean up requestrequest = null;});
}

dispatchRequest 中的throwIfCancellationRequested具體實現:throw 拋出異常。

// 拋出異常函數
function throwIfCancellationRequested(config) {if (config.cancelToken) {config.cancelToken.throwIfRequested();}
}
// 拋出異常 用戶 { message: '哎呀,我被若川取消了' }
CancelToken.prototype.throwIfRequested = function throwIfRequested() {if (this.reason) {throw this.reason;}
};

取消流程調用棧

1.source.cancel()
2.resolvePromise(token.reason);
3.config.cancelToken.promise.then(function onCanceled(cancel) {})

最后進入request.abort();``reject(cancel);

到這里取消的流程就介紹完畢了。主要就是通過傳遞配置參數cancelToken,取消時才會生成cancelToken,判斷有,則拋出錯誤,使Promise 走向rejected,讓用戶捕獲到消息{message: '用戶設置的取消信息'}。

文章寫到這里就基本到接近尾聲了。

能讀到最后,說明你已經超過很多人啦^_^

axios是非常優秀的請求庫,但肯定也不能滿足所有開發者的需求,接下來對比下其他庫,看看其他開發者有什么具體需求。

對比其他請求庫

KoAjax

FCC 成都社區負責人水歌開源的KoAJAX。

如何用開源軟件辦一場技術大會?以下這篇文章中摘抄的一段。

前端請求庫 —— KoAJAX 國內前端同學最常用的 HTTP 請求庫應該是 axios 了吧?雖然它的 Interceptor(攔截器)API 是 .use(),但和 Node.js 的 Express、Koa 等框架的中間件模式完全不同,相比 jQuery .ajaxPrefilter()、dataFilter() 并沒什么實質改進;上傳、下載進度比 jQuery.Deferred() 還簡陋,只是兩個專門的回調選項。所以,它還是要對特定的需求記憶特定的 API,不夠簡潔。

幸運的是,水歌在研究如何用 ES 2018 異步迭代器實現一個類 Koa 中間件引擎的過程中,做出了一個更有實際價值的上層應用 —— KoAJAX。它的整個執行過程基于 Koa 式的中間件,而且它自己就是一個中間件調用棧。除了 RESTful API 常用的 .get()、.post()、.put()、.delete() 等快捷方法外,開發者就只需記住 .use() 和 next(),其它都是 ES 標準語法和 TS 類型推導。

umi-request 阿里開源的請求庫

umi-request github 倉庫

umi-requestfetch, axios 異同。

umi-requestfetch, axios 異同

不得不說,umi-request 確實強大,有興趣的讀者可以閱讀下其源碼。

看懂axios的基礎上,看懂umi-request源碼應該不難。

比如 umi-request 取消模塊代碼幾乎與axios一模一樣。

總結

文章詳細介紹了 axios 調試方法。詳細介紹了 axios 構造函數,攔截器,取消等功能的實現。最后還對比了其他請求庫。

最后畫個圖總結一下 axios 的總體大致流程。

axios的總體大致流程

解答下文章開頭提的問題:

如果你是求職者,項目寫了運用了axios,面試官可能會問你:

1.為什么 axios 既可以當函數調用,也可以當對象使用,比如axios({})axios.get
答:axios本質是函數,賦值了一些別名方法,比如getpost方法,可被調用,最終調用的還是Axios.prototype.request函數。
2.簡述 axios 調用流程。
答:實際是調用的Axios.prototype.request方法,最終返回的是promise鏈式調用,實際請求是在dispatchRequest中派發的。
3.有用過攔截器嗎?原理是怎樣的?
答:用過,用axios.interceptors.request.use添加請求成功和失敗攔截器函數,用axios.interceptors.response.use添加響應成功和失敗攔截器函數。在Axios.prototype.request函數組成promise鏈式調用時,Interceptors.protype.forEach遍歷請求和響應攔截器添加到真正發送請求dispatchRequest的兩端,從而做到請求前攔截和響應后攔截。攔截器也支持用Interceptors.protype.eject方法移除。
4.有使用axios的取消功能嗎?是怎么實現的?
答:用過,通過傳遞config配置cancelToken的形式,來取消的。判斷有傳cancelToken,在promise鏈式調用的dispatchRequest拋出錯誤,在adapterrequest.abort()取消請求,使promise走向rejected,被用戶捕獲取消信息。
5.為什么支持瀏覽器中發送請求也支持node發送請求?
答:axios.defaults.adapter默認配置中根據環境判斷是瀏覽器還是node環境,使用對應的適配器。適配器支持自定義。

回答面試官的問題,讀者也可以根據自己的理解,組織語言,筆者的回答只是做一個參考。

axios 源碼相對不多,打包后一千多行,比較容易看完,非常值得學習。

建議 clone 若川的 axios-analysis github 倉庫,按照文中方法自己調試,印象更深刻。

基于Promise,構成Promise鏈,巧妙的設置請求攔截,發送請求,再試試響應攔截器。

request中的攔截器和dispatch中的取消這兩個模塊相對復雜,可以多調試調試,吸收消化。

axios 既是函數,是函數時調用的是Axios.prototype.request函數,又是對象,其上面有getpost等請求方法,最終也是調用Axios.prototype.request函數。

axios 源碼中使用了挺多設計模式。比如工廠模式、迭代器模式、適配器模式等。如果想系統學習設計模式,一般比較推薦豆瓣評分 9.1 的JavaScript 設計模式與開發實踐

如果讀者發現有不妥或可改善之處,再或者哪里沒寫明白的地方,歡迎加我微信?lxchuan12?交流。另外覺得寫得不錯,對您有些許幫助,可以點贊、評論、轉發分享,也是對筆者的一種支持,非常感謝呀。

原創精選文章

工作一年后,我有些感悟(寫于2017年)

高考七年后、工作三年后的感悟

面試官問:JS的繼承

前端使用puppeteer 爬蟲生成《React.js 小書》PDF并合并

微信公眾號

作者:常以若川為名混跡于江湖。前端路上 | PPT 愛好者 | 所知甚少,唯善學。
博客:https://lxchuan12.cn/posts/,閱讀體驗可能更好些。

若川視野

主要發布前端 | PPT | 生活 | 效率相關的文章,長按掃碼關注。歡迎加我微信lxchuan12(注明來源,基本來者不拒),拉您進【前端視野交流群】,長期交流學習~

由于公眾號限制外鏈,點擊讀原文,或許閱讀體驗更佳,覺得文章不錯,可以點個在看呀^_^

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/274408.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/274408.shtml
英文地址,請注明出處:http://en.pswp.cn/news/274408.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

404 錯誤頁面_如何設計404錯誤頁面,以使用戶留在您的網站上

404 錯誤頁面重點 (Top highlight)網站設計 (Website Design) There is a thin line between engaging and enraging when it comes to a site’s 404 error page. They are the most neglected of any website page. The main reason being, visitors are not supposed to end…

宏定義學習

【1】宏定義怎么理解&#xff1f; 關于宏定義&#xff0c;把握住本質&#xff1a;僅僅是一種字符替換&#xff0c;而且是在預處理之前就進行。 【2】宏定義可以包括分號嗎&#xff1f; 可以&#xff0c;示例代碼如下&#xff1a; 1 #include<iostream>2 using namespace…

學習 koa 源碼的整體架構,淺析koa洋蔥模型原理和co原理

前言這是學習源碼整體架構系列第七篇。整體架構這詞語好像有點大&#xff0c;姑且就算是源碼整體結構吧&#xff0c;主要就是學習是代碼整體結構&#xff0c;不深究其他不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。學習源碼整體架構系列文章如下&#xff1a;1.…

公網對講機修改對講機程序_更少的對講機,對講機-更多專心,專心

公網對講機修改對講機程序重點 (Top highlight)I often like to put a stick into the bike wheel of the UX industry as it’s strolling along feeling proud of itself. I believe — strongly — that as designers we should primarily be doers not talkers.我經常喜歡在…

spring配置文件-------通配符

<!-- 這里一定要注意是使用spring的mappingLocations屬性進行通配的 --> <property name"mappingLocations"> <list> <value>classpath:/com/model/domain/*.hbm.xml</value> </list> </proper…

若川知乎問答:2年前端經驗,做的項目沒什么技術含量,怎么辦?

知乎問答&#xff1a;做了兩年前端開發&#xff0c;平時就是拿 Vue 寫寫頁面和組件&#xff0c;簡歷的項目經歷應該怎么寫得好看&#xff1f;以下是我的回答&#xff0c;閱讀量5000&#xff0c;所以發布到公眾號申明原創。題主說的2年經驗做的東西沒什么技術含量&#xff0c;應…

ui設計基礎_我不知道的UI設計的9個重要基礎

ui設計基礎重點 (Top highlight)After listening to Craig Federighi’s talk on how to be a better software engineer I was sold on the idea that it is super important for a software engineer to learn the basic principles of software design.聽了克雷格費德里希(C…

Ubuntu下修改file descriptor

要修改Ubuntu下的file descriptor的話&#xff0c;請參照一下步驟。&#xff08;1&#xff09;修改limits.conf  $sudo vi /etc/security/limits.conf  增加一行  *  -  nofile  10000&#xff08;2&#xff09;修改 common-session  $ sudo vi/etc/pam.d/common…

C# 多線程控制 通訊 和切換

一.多線程的概念   Windows是一個多任務的系統&#xff0c;如果你使用的是windows 2000及其以上版本&#xff0c;你可以通過任務管理器查看當前系統運行的程序和進程。什么是進程呢&#xff1f;當一個程序開始運行時&#xff0c;它就是一個進程&#xff0c;進程所指包括運行中…

vue路由匹配實現包容性_包容性設計:面向老年用戶的數字平等

vue路由匹配實現包容性In Covid world, a lot of older users are getting online for the first time or using technology more than they previously had. For some, help may be needed.在Covid世界中&#xff0c;許多年長用戶首次上網或使用的技術比以前更多。 對于某些人…

IPhone開發 用子類搞定不同的設備(iphone和ipad)

用子類搞定不同的設備 因為要判斷我們的程序正運行在哪個設備上&#xff0c;所以&#xff0c;我們的代碼有些混亂了&#xff0c;IF來ELSE去的&#xff0c;記住&#xff0c;將來你花在維護代碼上的時間要比花在寫代碼上的時間多&#xff0c;如果你的項目比較大&#xff0c;且IF語…

見證開戶_見證中的發現

見證開戶Each time we pick up a new video game, we’re faced with the same dilemma: “How do I play this game?” Most games now feature tutorials, which can range from the innocuous — gently introducing each mechanic at a time through natural gameplay — …

使用JXL組件操作Excel和導出文件

使用JXL組件操作Excel和導出文件 原文鏈接&#xff1a;http://tianweili.github.io/blog/2015/01/29/use-jxl-produce-excel/ 前言&#xff1a;這段時間參與的項目要求做幾張Excel報表&#xff0c;由于項目框架使用了jxl組件&#xff0c;所以把jxl組件的詳細用法歸納總結一下。…

facebook有哪些信息_關于Facebook表情表情符號的所有信息

facebook有哪些信息Ever since worldwide lockdown and restriction on travel have been imposed, platforms like #Facebook, #Instagram, #Zoom, #GoogleDuo, & #Whatsapp have become more important than ever to connect with your loved ones (apart from the sourc…

M2總結報告

團隊成員 李嘉良 http://home.cnblogs.com/u/daisuke/ 王熹 http://home.cnblogs.com/u/vvnx/ 王冬 http://home.cnblogs.com/u/darewin/ 王泓洋 http://home.cnblogs.com/u/fiverice/ 劉明 http://home.cnblogs.com/u/liumingbuaa/ 由之望 http://www.cnbl…

react動畫庫_React 2020動畫庫

react動畫庫Animations are important in instances like page transitions, scroll events, entering and exiting components, and events that the user should be alerted to.動畫在諸如頁面過渡&#xff0c;滾動事件&#xff0c;進入和退出組件以及應提醒用戶的事件之類的…

Weather

public class WeatherModel { #region 定義成員變量 private string _temperature ""; private string _weather ""; private string _wind ""; private string _city ""; private …

線框模型_進行計劃之前:線框和模型

線框模型Before we start developing something, we need a plan about what we’re doing and what is the expected result from the project. Same as developing a website, we need to create a mockup before we start developing (coding) because it will cost so much…

撰寫論文時word使用技巧(轉)

------------------------------------- 1. Word2007 的表格自定義格式額度功能是很實用的&#xff0c;比如論文中需要經常插入表格的話&#xff0c; 可以在“表格設計”那里“修改表格樣式”一次性把默認的表格樣式設置為三線表&#xff0c;這樣&#xff0c; 你以后每次插入的…

工作經驗教訓_在設計工作五年后獲得的經驗教訓

工作經驗教訓This June it has been five years since I graduated from college. Since then I’ve been working as a UX designer for a lot of different companies, including a start-up, an application developer, and two consultancy firms.我從大學畢業已經五年了&a…