preface
Axios is a very small HTTP library, but it contains a lot of programming ideas worth learning. Let’s learn it today.
This article will be divided into the following sections to learn the AXIOS library step by step:
axios
Introduction to theaxios
Engineering structures,axios
The source code to writeaxios
Packaging releases
If learning this article has helped you, please give a thumbs-up.
Axios profile
Axios is a Promise-based HTTP library that can be used in browsers and Node.js.
features
- Created from the browser
XMLHttpRequests
, fromnode.js
createhttp
Requests; - support
Promise API
; - Intercepting requests and responses;
- Transform request data and response data;
- Cancel the request;
- Automatic conversion
JSON
Data; - The client supports defense
XSRF
。
Axios engineering construction
Now I start to build my own project.
ES6+ syntax and Webpack packaging.
Initialization project:
1Create the lion-axios project on git2Git clone HTTPS://github.com/shiyou00/lion-axios.git
3Go to CD lion-axios/4Initialize project NPM init-yCopy the code
Webpack configuration:
Development environment configuration file: webpack.dev.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
mode: 'development'.// Development environment
devtool: 'cheap-module-eval-source-map'.// sourceMap is used for error debugging
devServer: {
contentBase: './example'.// The server boot root is set to example
open: true.// Automatically open the browser
port: 8088./ / the port number
hot: true / / open hot update, at the same time to configure the corresponding plug-in HotModuleReplacementPlugin
},
plugins: [
new HtmlWebpackPlugin({
template: 'public/index.html'.// Use a template file to view the effect
inject:'head' // Insert into the head tag
}),
new webpack.HotModuleReplacementPlugin() // Hot update plugin]};Copy the code
Production environment configuration file: webpack.pro.js
module.exports = {
mode: 'production' // Production environment
};
Copy the code
Common configuration file: webpack.common.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { merge } = require('webpack-merge');
const prodConfig = require("./webpack.pro"); // Import the production configuration
const devConfig = require("./webpack.dev"); // Import the development environment configuration
// Public configuration
const commonConfig = {
entry: './src/axios.js'.// Package the entry file
output: {
filename: 'axios.js'.// The output file name
path: path.resolve(__dirname, 'dist'), // The absolute path to the output
library: 'axios'.// The class library namespace, if introduced via a web page, can be accessed via window.axios
globalObject: 'this'.// Define global variables, compatible with Node and browser running, avoid the "window is not defined" situation
libraryTarget: "umd".Universal Module Definition is supported in CommonJS, AMD, and global variables
libraryExport: 'default' // If you expose the default attribute, you can call the default attribute directly
},
module: {rules: [// Configure the parsing of Babel, and have the Babel configuration file in the project directory
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"}},plugins: [
new CleanWebpackPlugin(), // Clean the export (dist) folder every time you pack]};module.exports = (env) = >{
// Determine the development environment or production environment according to the command to enable different configuration files
if(env && env.production){
return merge(commonConfig,prodConfig);
}else{
returnmerge(commonConfig,devConfig); }}Copy the code
Babelrc configuration:
{
"plugins": [["@babel/plugin-proposal-class-properties",
{
"loose": true}], ["@babel/plugin-transform-runtime",
{
"absoluteRuntime": false."corejs": 2."helpers": true."regenerator": true."useESModules": false}]],"presets": [["@babel/preset-env"]]}Copy the code
Package. json add script command:
"scripts": {
"dev": "webpack-dev-server --env.development --config webpack.common.js"."build": "webpack --env.production --config webpack.common.js"
},
Copy the code
- Development-time execution
npm run dev
Command; - Execution at package time
npm run build
Command.
At this point, the basic configuration of the AXIos library project is ready.
The code quality
The above configuration ensures that we can properly package the project as a class library, but for enterprise projects, code quality control is also very important.
This project will use the following tools to control code quality:
editorConfig
Helps maintain across multiple editors andIDE
A consistent coding style for multiple developers working on the same project.ESLint
, it can be assembledJavaScript
和JSX
Check tools.husky
,git
The commandhook
Dedicated configuration, which can be configured for executiongit commit
Etc command to execute the hook.lint-staged
, can be in a specificgit
Phases execute specific commands.prettier
, code unified formatting.commitlint
,git commit message
Specification.
editorConfig
Editorconfig configuration:
root = true // Root configuration
[*] // Apply to all files
end_of_line = lf / / define a newline [lf | cr | CRLF]
insert_final_newline = true // Whether the file ends with a blank line
[*.{js,html,css}] // Function file
charset = utf-8 // Encoding format
[*.js] // Function file
indent_style = space // Indent type
indent_size = 2 // Indent size
Copy the code
ESLint
# installation NPM install eslint - D | | yarn add eslint - D # initialization eslint. / node_modules/bin/eslint - init// You can do it step by step
Copy the code
. Eslintrc. Js configuration:
module.exports = {
env: { The env keyword specifies the environment you want to enable
browser: true.es2021: true.node: true,},parser: "babel-eslint"./ / the parser
extends: "eslint:recommended".// Inherited configuration rule set
parserOptions: {
ecmaVersion: 12.// Specify the version of ECMAScript you want to use
sourceType: "module"./ / enable ESModule
},
rules: { // Rule "off" = off" WARN "= warning "error" = error
strict: "off".// Strict mode, rule closed
"no-console": "off".// Disable the console object method. The rule is closed
"global-require": "off".// Require require() to appear in the top-level module scope, rule closed
"require-yield": "off".// Require that generator functions contain yield and the rule is closed}};Copy the code
husky + lint-staged + prettier
Installation:
NPM install husky lint-staged prettier --save-dev Or YARN Add husky Lint-staged prettier --devCopy the code
Huskyrc configuration:
{
"hooks": {
"pre-commit": "lint-staged"}}Copy the code
This configuration means lint-staged execution before committing.
Lintstagedrc configuration:
{
"*.{js,ts,jsx,tsx}": [
"eslint --fix --quiet".// fix = automatically fixes, quiet = ESLint reports an error
"prettier --write" // Format using prettier]."*.css": "prettier --write"."*.html": "prettier --write"."*.md": "prettier --write"."*.json": "prettier --write"
}
Copy the code
This configuration represents a lint-staged command set.
commitlint
Installation:
NPM install @commitlint/config-conventional @commitlint/cli --save-dev or YARN add @commitlint/config-conventional @commitlint/cli --devCopy the code
Commitlint. Config. Js configuration:
module.exports = {
extends: ['@commitlint/config-conventional']};Copy the code
.huskyrc add configuration:
{
"hooks": {+"commit-msg": "commitlint -E HUSKY_GIT_PARAMS".// CommitLint checks commit Message
"pre-commit": "lint-staged"}}Copy the code
With everything configured, we can test this by adding the following code to axios.js:
const a = 12;
console.log(a);
Copy the code
Add. && git commit -m “test”
As you can see, the project does ESLint and CommitLint checks. The commit message is not written properly, so we can commit to the Git repository as usual.
Project directory structure
├ ─ ─ the README, md// Documentation├ ─ ─ node_modules// Dependency package folder├ ─ ─ package. Json ├ ─ ─. Babelrc// Babel configuration file├ ─ ─ commitlint. Config. Js// Commitlint configuration file├ ─ ─ editorconfig// editorConfig configuration file├ ─ ─ eslintrc. Js// ESLint configuration files├ ─ ─ huskyrc// Husky configuration file├ ─ ─ lintstagedrc// Lint-staged configuration files├ ─ ─ webpack.com mon. Js// Webpack common configuration├ ─ ─ webpack. Dev. Js// WebPack development environment configuration├ ─ ─ webpack. Pro. Js// WebPack production environment configuration├ ─ ─ gitignore// Git upload ignores configuration└ ─ ─ the SRC └ ─ ─ axios. Js// Library entry file
Copy the code
Click here to see the complete code for this summary
Axios source code
Many of axios’s design ideas are worth referencing, which is one of the main purposes of our handwritten Axios library.
Axios infrastructure
The first step in writing Axios is to set up a simple shelf. And then refine it step by step.
The axios library is called:
Get (POST, PUT, DELETE, etc.);'/user? ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error); }); # call the axios request method to send the request axios.request({url:"/user? ID=12345"}) # You can create the request axios({method: 'post'.url: '/user/12345'.data: {
firstName: 'Fred'.lastName: 'Flintstone'}}); You can create a new axiOS instance using a custom configurationconst instance = axios.create({
baseURL: 'https://some-domain.com/api/'.timeout: 1000.headers: {'X-Custom-Header': 'foobar'}});Copy the code
- call
axios
的get
Method to send the request, it’s not hard to imagine that it hasPOST
.PUT
.DELETE
等HTTP
Protocol supported methods. - call
axios
的request
Method to send a request. - Can be accessed through
axios
Pass the configuration to create the request. - You can create a new one using custom configuration
axios
Instance.
With these uses, we first build a Lion-Axios infrastructure that supports this use.
Create: SRC/axios. Js
Create the Axios class
class Axios {
// Used to store configuration information.
config = {};
constructor(initConfig) {
// A configuration message is received at instantiation time and saved to the config property.
this.config = initConfig;
}
// This class has a request method, which can be used to send requests
request(config){}}Copy the code
2, createInstance method
Use to instantiate the Axios class.
function createInstance(initConfig) {
// Create an Axios instance
const context = new Axios(initConfig);
// The instance variable holds the request method on the Axios class and replaces this with the object instantiated in the previous step.
const instance = Axios.prototype.request.bind(context);
// Return the request method
return instance;
}
Copy the code
3. Provide axiOS instances externally
Git is the default parameter object if the user does not pass method
const defaults = {
method: 'get',}const axios = createInstance(defaults);
export default axios;
Copy the code
Use Axios
Axios:
axios = createInstance(defaults);
createInstance = Axios.prototype.request = request(config){}
So it supports axios({… }); That’s called.
Support the axios.create method
axios.create = function (config) {
// merge the default configuration with the configuration passed in by the user as createInstance.
// Config merge is currently not implemented. Therefore, pass default directly to create the instance.
// const initConfig = mergeConfig(config,defaults);
return createInstance(defaults);
}
Copy the code
The create method supports passing in a configuration object to create a new Axios instance.
Calling axios is now supported like this:
constinstance = axios.create({... });Copy the code
6, support axios. Request and axios. Get | axios. Post | axios. Put the form such as call
New Axios class first get | post | delete etc
class Axios{
config = {};
constructor(initConfig) {
this.config = initConfig;
}
request(config){}
get(){}
delete(){}
head(){}
options(){}
post(){}
put(){}
patch(){}}Copy the code
Instance inherits the methods and attributes of the Axios class
Extend extends methods and attributes from Axios instances:
function extend(to, from, ctx) {
// Inheritance method
Object.getOwnPropertyNames( from ).forEach((key) = >{
to[key] = from[key].bind(ctx);
});
// CTX inherits its own properties (not the properties on the prototype chain, so hasOwnProperty is required to determine)
for(let val in ctx){
if(ctx.hasOwnProperty(val)){ to[val] = ctx[val]; }}return to;
}
Copy the code
There is a pit for class instance method inheritance, see ES6 Iterate over class methods for details.
Call extend in createInstance:
function createInstance(initConfig) {
const context = new Axios(initConfig);
const instance = Axios.prototype.request.bind(context);
// if you want to call axios.request and axios.get
// Instance can inherit request, get, post, etc from context.
extend(instance, context);
return instance;
}
Copy the code
Thus instance inherits the properties and methods defined on the Axios class. Now you can call it like this:
axios.get('/user? ID=12345')
axios.request({url:"/user? ID=12345"})
Copy the code
So far all four invocation methods have been supported. And the basic framework for the AXIos library is in place.
Click here to see the complete code for this summary
Send a real request
We can already call Axios in various ways, but that’s just calling it, not really sending a request to the background.
When we use axios({… }) and axios.request({}) are actually calling the request method on the axios class. Then we ask it to do the tasks in the background.
XHR
To send a request to the background on the browser side, you can use fetch or XMLHttpRequest. Since FETCH is not yet fully fledged, it does not even support canceling requests. So we select XMLHttpRequest on the browser side.
XMLHttpRequest
The XMLHttpRequest (XHR) object is used to interact with the server. With XMLHttpRequest, you can request a specific URL to retrieve data without refreshing the page. This allows the web page to update parts of the page without affecting the user’s actions. XMLHttpRequest is widely used in AJAX programming.
const xhr = (config) = >{
Data is null if it is not passed. Method is get if it is not passed. Url is mandatory
const {data = null,url,method = 'get'} = config;
// Instantiate XMLHttpRequest
const request = new XMLHttpRequest();
// Initialize a request
request.open(method.toUpperCase(),url,true);
// Send the request
request.send(data);
}
class Axios {
config = {};
constructor(initConfig) {
this.config = initConfig;
}
request(config) {
xhr(config)
}
}
Copy the code
By simply implementing the XHR method and calling it in the Request method, we can send the actual request back to the background.
axios({
method: "post".url: "https://reqres.in/api/users".data: {
"name": "frankshi"."job": "FE"}});Copy the code
Reqres. in/ This site provides many public interfaces for us to use, so this article will use it to simulate interface requests.
Open the browser console and you can see that the request has been sent successfully.
Parse the param parameter
Demand analysis:
axios({
method: 'get'.url: '/base/get'.params: {
foo: ['bar'.'baz']}}) takes an array, and the final requested URL is => /base? foo[]=bar&foo[]=bazparams: {
foo: {
bar: 'baz'}} arguments are objects, and the final requested URL is => /base? foo=%7B%22:%22baz%22%7D
params: {
date:new DateThe ()} argument is a date, and the final requested URL is => /base? date=2020-12-24T08:00:00.000z
params: {
foo: '@ : $,} arguments are special characters and the final requested URL is => /base? Foo =@:$+ (Spaces converted to +)params: {
foo: 'bar'.baz: null} arguments with a null value will be ignored and the final requested URL is => /base? foo=bar axios({method: 'get'.url: '/base/get#hash'.params: {
foo: 'bar'}}) urls containing hash values will be discarded when requested. The final requested URL is => /base? foo=bar axios({method: 'get'.url: '/base/get? foo=bar'.params: {
bar: 'baz'=> /base? => /base? foo=bar&bar=bazCopy the code
Code implementation:
1. Add a method to request to handle configuration uniformly
request(config) {
// Process the incoming configuration
processConfig(config);
// Send the request
xhr(config)
}
Copy the code
2. Convert URL parameters in processConfig
const buildURL = (url,params) = >{}
const transformURL = (config) = >{
const { url, params } = config;
return buildURL(url,params);
}
const processConfig = (config) = >{
config.url = transformURL(config);
}
Copy the code
Params body function
// Determine whether the object is a Date object
function isDate(val) {
return toString.call(val) === '[object Date]'
}
// Check whether the Object is an Object
function isPlainObject(val){
return toString.call(val) === '[object Object]'
}
// Determine if URLSearchParams object is an instance
function isURLSearchParams(val) {
return typeofval ! = ='undefined' && val instanceof URLSearchParams
}
const buildURL = (url,params) = >{
// If the params parameter is empty, the original URL is returned
if(! params) {return url
}
// Define a variable to hold the final concatenated parameters
let serializedParams
// Check whether params is the URLSearchParams object type
if (isURLSearchParams(params)) {
// If it is (for example: new URLSearchParams(topic= API&foo =bar)), params is serialized directly
serializedParams = params.toString()
} else {
// If not, enter the main body to run
// Define an array
const parts = [];
Keys can get an array of all the keys of an Object, traversed by forEach
Object.keys(params).forEach(key= > {
// Get the val of each key object
const val = params[key]
// If val is null, or undefined terminates the loop and enters the next loop, where null is ignored
if (val === null || typeof val === 'undefined') {
return
}
// Define an array
let values = []
// Check if val is an array type
if (Array.isArray(val)) {
// If yes, the values array is assigned to val, and the key concatenation is [].
values = val
key += '[]'
} else {
// If val is not an array, make it an array
values = [val]
}
// Since the previous differences are smoothed out, this can be treated as an array
values.forEach(val= > {
If val is a date object,
if (isDate(val)) {
ToISOString Returns the Date object's standard date-time string format as a string
val = val.toISOString()
If val is an object type, serialize it directly
} else if (isPlainObject(val)) {
val = JSON.stringify(val)
}
// The result is pushed into the array
parts.push(`${encode(key)}=${encode(val)}`)})})// Finally concatenate the array
serializedParams = parts.join('&')}if (serializedParams) {
// Handle the hash case
const markIndex = url.indexOf(The '#')
if(markIndex ! = = -1) {
url = url.slice(0, markIndex)
}
If the pass already has an argument, concatenate it, otherwise add one manually.
url += (url.indexOf('? ') = = = -1 ? '? ' : '&') + serializedParams
}
// Print the full URL
return url
}
Copy the code
Parsing body data
Request. Send (data); . And data is the data we pass in:
axios({
method: "post".url: baseURL,
data: {
"name": "frankshi"."job": "FE"}});Copy the code
The data that is passed in cannot be directly used as an input parameter to the send function. It needs to be converted to a JSON string.
For details, see MDN XMLHttpRequest Send
Implementation:
// This function serializes data
const transformRequest = (data) = >{
if (isPlainObject(data)) {
return JSON.stringify(data)
}
return data
}
// Define a function whose responsibility is to process the requested data
const transformRequestData = (config) = >{
return transformRequest(config.data);
}
// processConfig is defined earlier to handle incoming configuration. It already handled the URL, adding processing data
const processConfig = (config) = >{
config.url = transformURL(config);
config.data = transformRequestData(config);
}
Copy the code
Parsing the headers
In an HTTP request, the header plays an important role as a bridge between the client and server to understand each other. So our library must also ensure that headers are passed and parsed correctly.
Content-type is added by default
axios({
method: "post".url: baseURL,
data: {
"name": "frankshi"."job": "FE"}});Copy the code
When the front end sends a request like this, we need to tell the server the data type of the data we are sending so that the server can parse it correctly.
Therefore, this configuration is required:
axios({
method: "post".url: baseURL,
headers: {'content-type':"application/json; charset=utf-8"
},
data: {
"name": "frankshi"."job": "FE"}});Copy the code
If the user does not add the Content-Type, we need to automatically add this configuration to it.
Let’s implement the logic for headers:
const normalizeHeaderName = (headers, normalizedName) = > {
if(! headers) {return
}
// Traverse all headers
Object.keys(headers).forEach(name= > {
// If name is content-type and normalizedName is content-type, use content-type
// Delete content-type.
if(name ! == normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) { headers[normalizedName] = headers[name]delete headers[name]
}
})
}
const processHeaders = (headers, data) = > {
normalizeHeaderName(headers, 'Content-Type')
// Set 'content-type' if data is an object
if (isPlainObject(data)) {
if(headers && ! headers['Content-Type']) {
headers['Content-Type'] = 'application/json; charset=utf-8'}}return headers
}
const transformHeaders = (config) = >{
const { headers = {}, data } = config;
return processHeaders(headers,data);
}
const processConfig = (config) = >{
config.url = transformURL(config);
config.headers = transformHeaders(config);
config.data = transformRequestData(config);
}
const xhr = (config) = >{
let {data = null,url,method = 'get',headers={}} = config;
const request = new XMLHttpRequest();
request.open(method.toUpperCase(),url,true);
// Iterate through all headers after processing
Object.keys(headers).forEach(name= > {
// Delete content-type if data is empty
if (data === null && name.toLowerCase() === 'content-type') {
delete headers[name]
} else {
// Set the header for the request
request.setRequestHeader(name, headers[name])
}
})
request.send(data);
}
Copy the code
Respond to the request
The above Request parses the param, body, and header of the Request. However, the client does not get the data of the background response. Let’s implement the Promise based data response.
Modify the XHR function
const xhr = (config) = >{
// All implementation isomorphic new Promise wrapped around
return new Promise((resolve, reject) = >{
let {data = null,url,method = 'get',headers={}, responseType} = config;
const request = new XMLHttpRequest();
request.open(method.toUpperCase(),url,true);
// Determine whether the user has set the return data type
if (responseType) {
request.responseType = responseType
}
// Listen to the onreadyStatechange function and receive the data returned by the background
request.onreadystatechange = () = > {
if(request.readyState ! = =4) {
return
}
if (request.status === 0) {
return
}
// The header returned is a string type that is parseHeaders parsed into an object type
const responseHeaders = parseHeaders(request.getAllResponseHeaders());
constresponseData = responseType && responseType ! = ='text' ? request.response : request.responseText
const response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
}
// Return data through resolve
resolve(response);
}
// Iterate through all headers after processing. Request. Send (data); })}Copy the code
Once we’ve done that, we can write the Promise syntax when we provide it to the user.
Click here to see the complete code for this summary
Directory structure adjustment
One of the biggest problems is that all the code is concentrated in the axios.js file, and proper file directory division is very important for a professional library.
Optimized directory structure map:
└ ─ ─ the SRC └ ─ ─ axios. Js// Import file└ ─ ─ the core// Core folder└ ─ ─ Axios. Js// Store the Axios class└ ─ ─ dispatchRequest. Js// Trigger the request└ ─ ─ adapters// Adaptor folder, axios can adapt HTTP in Node, XHR in browser└ ─ ─ XHR. Js// Browser XHR request└ ─ ─ HTTP. Js// Node HTTP request, currently not implemented└ ─ ─ helpers// Store utility functions└ ─ ─ data. Js// Convert data related functions└ ─ ─ headers. Js// Handle header-related functions└ ─ ─ url. Js// Handle urL-related functions└ ─ ─ util. Js// Common utility functions
Copy the code
Click here to see the complete code for this summary
The adapter
To support different environments, Axios introduced adapters. The dispatchRequest.js file has been added to the directory in the previous section. Its main responsibility is to send requests, and this is the best place to write the adapter.
dispatchRequest.js
// The default adapter, to determine whether there is XMLHttpRequest, to determine whether the browser environment, to determine whether to choose XHR or HTTP
const getDefaultAdapter = () = > {
let adapter;
if (typeofXMLHttpRequest ! = ='undefined') {
/ / the browser
adapter = require(".. /adapters/xhr");
} else if (typeofprocess ! = ='undefined' && Object.prototype.toString.call(process) === '[object process]') {
// node.js
adapter = require(".. /adapters/http");
}
return adapter;
}
const dispatchRequest = (config) = > {
// If the user passes in an adapter, use the user's adapter, otherwise use the default adapter (the default configuration file will be extracted later)
const adapter = config.adapter || getDefaultAdapter();
// Process the incoming configuration
processConfig(config);
// Send the request
return adapter(config).then((res) = > transformResponseData(res));
};
Copy the code
The principle of the adapter is not complicated, axios uses this function to smooth out the differences between the browser and node.js environment, so that the user does not notice. Adapter = require(“.. /adapters/*”), then use CommonJS require.
XHR/HTTP exports/module. Exports/module. Exports/module.
module.exports = function httpAdapter (config) {
console.log("httpAdapter",config);
}
Copy the code
Why use require instead of import?
This is because CommonJS loads an object (the module.exports property) that is only generated when the script is finished running. An ES6 module is not an object, and its external interface is a static definition that is generated during the code static parsing phase.
Click here to see the complete code for this summary
Improve Axios class methods
There were a lot of methods defined in the Axios class, but none of them were implemented. Now that the Request method is implemented, other methods are relatively easy to implement.
Axios.js
import dispatchRequest from "./dispatchRequest";
class Axios {
config = {};
constructor(initConfig) {
this.config = initConfig;
}
request(config) {
return dispatchRequest(config);
}
get(url,config) {
return this._requestMethodWithoutData('get',url,config)
}
delete(url,config) {
return this._requestMethodWithoutData('delete', url, config)
}
head(url,config) {
return this._requestMethodWithoutData('head', url, config)
}
options(url,config) {
return this._requestMethodWithoutData('head', url, config)
}
post(url,data,config) {
return this._requestMethodWithData('post', url, data, config)
}
put(url,data,config) {
return this._requestMethodWithData('put', url, data, config)
}
patch(url,data,config) {
return this._requestMethodWithData('patch', url, data, config)
}
// Call the require method without Data
_requestMethodWithoutData(method, url, config) {
return this.request(
Object.assign(config || {}, {
method,
url
})
)
}
// Generically call methods with Data
_requestMethodWithData(method, url, data, config) {
// Merge parameters
return this.request(
Object.assign(config || {}, {
method,
url,
data
})
)
}
}
export default Axios;
Copy the code
Once this is done, we can call it axios.get(), axios.post(), etc.
Click here to see the complete code for this summary
The interceptor
Introduction to interceptors
Intercepts requests or responses before they are processed by THEN or catch.
// Add request interceptor
axios.interceptors.request.use(function (config) {
// What to do before sending the request
return config;
}, function (error) {
// What to do about the request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// What to do with the response data
return response;
}, function (error) {
// Do something about the response error
return Promise.reject(error);
});
Copy the code
- Request interceptor: This is used to uniformly perform certain operations, such as adding to the request header, before the request is sent
token
Field. - Response interceptor: It is used to perform certain actions upon receiving a server response, such as finding that the response status code is
401
, the login page is automatically displayed.
We can also remove an interceptor:
const myInterceptor = axios.interceptors.request.use(function () {/ *... * /});
axios.interceptors.request.eject(myInterceptor);
Copy the code
At this point, you can see that the interceptor is very much like the middleware of Redux and KOA2. It will execute the Request interceptor in the order you add it, then the real request, and then the Response interceptor.
Interceptor implementation
To create an interceptor management class: core/InterceptorManager. Js
export default class InterceptorManager{
// Define an array to store interceptors
interceptors = [];
use(resolved, rejected) {
// Push the interceptor object into the array
this.interceptors.push({
resolved,
rejected
})
// Returns the interceptor index in the array
return this.interceptors.length - 1
}
// go through the number group
forEach(fn) {
this.interceptors.forEach(interceptor= > {
if(interceptor ! = =null) {
fn(interceptor)
}
})
}
// Delete interceptors based on the index
eject(id) {
if (this.interceptors[id]) {
this.interceptors[id] = null}}}Copy the code
The interceptor class is just an array and implements the use, forEach, and eject methods to manipulate it.
The next step is to implement the chain call to the interceptor: core/ axios.js
class Axios {
config = {};
// Define an interceptor object
interceptors = {};
constructor(initConfig) {
this.config = initConfig;
// Interceptor objects contain request interceptors and Response interceptors
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
}
request(config) {
// Define an array of objects that will send real requests. Think of it as an interceptor
const chain = [
{
resolved: dispatchRequest,
rejected: undefined}]/ / when the user use axios. Interceptors. Request. Use (...). When multiple request interceptors are pushed
/ / this. Interceptors. Request what with multiple interceptors, by iterating through interceptors, insert the chain in the front of the array
this.interceptors.request.forEach(interceptor= > {
chain.unshift(interceptor)
})
/ / when the user use axios. Interceptors. Response. Use (...). When multiple response interceptors are pushed
/ / this. Interceptors. The response in the multiple interceptors, by iterating through interceptors, inserted into the chain behind array
this.interceptors.response.forEach(interceptor= > {
chain.push(interceptor)
})
// The chain should look like this
/ * [{resolved: (config) = > {...}, / / the user custom request interceptor rejected (config) = > {...}}, {... resolved: dispatchRequest, rejected: Undefined}, {... resolved: (res) = > {...}, / / the user custom response interceptor rejected (res) = > {...}},] * /
let promise = Promise.resolve(config)
// If there are values in the chain array, the loop is iterated
while (chain.length) {
// Remove the first element from the array at a time
const { resolved, rejected } = chain.shift();
// Promise is copied to the next promise.then, implementing interceptor chain delivery
promise = promise.then(resolved, rejected)
}
// Return the final execution result
return promise
}
}
Copy the code
The idea of the interceptor is a good one to learn from. Its implementation is not too complicated, but cleverly uses promise to deliver, giving Axios a “middleware” -like function.
Click here to see the complete code for this summary
The default configuration
This section describes how to use the default configuration
The global AXIOS default value
axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
Copy the code
Custom instance defaults
// Set config defaults when creating the instance
const instance = axios.create({
baseURL: 'https://api.example.com'
});
// Alter defaults after instance has been created
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;
Copy the code
Default configuration implementation
Create defaults configuration files
src/defaults.js
const defaults = {
method: 'get'.// If method is not passed by default, a GET method is given
timeout: 0.// No timeout is set by default
headers: {
common: {
Accept: 'application/json, text/plain, */*' // An Accept header is given by default}}}// Headers is null by default for 'delete', 'get', 'head', and 'options'
const methodsNoData = ['delete'.'get'.'head'.'options']
methodsNoData.forEach(method= > {
defaults.headers[method] = {}
})
// Set a default content-type for 'post', 'put', and 'patch' requests
const methodsWithData = ['post'.'put'.'patch']
methodsWithData.forEach(method= > {
defaults.headers[method] = {
'Content-Type': 'application/x-www-form-urlencoded'}})export default defaults
Copy the code
Configuration merge in AXIOS
Axios.js
.import mergeConfig from "./mergeConfig";
class Axios {
defaults = {};
constructor(initConfig) {
this.defaults = initConfig; . }request(url, config) {
if (typeof url === "string") {
if(! config) { config = {}; } config.url = url; }else {
config = url;
}
// Merge the default configuration with the user-passed configuration
config = mergeConfig(this.defaults, config) ... }}Copy the code
Assume that the following two configuration files need to be merged:
const defaults = {
method: 'get'.timeout: 0.headers: {
common: {
Accept: 'application/json, text/plain, */*';
}
post: {
'Content-Type': 'application/x-www-form-urlencoded'}}}const customConfig = {
url: "/post".method: "post".data: {a:1},
headers: {test: "123"}}Copy the code
Use the default configuration only if the configuration in customConfig takes precedence over the user’s own configuration.
Concrete implementation of the merged configuration: SRC /core/ mergeconfig.js
import { deepMerge, isPlainObject } from '.. /helpers/util'
// Create a policy object
const strats = Object.create(null)
// Default policy: val2. If the parameter is not empty, use the parameter. Otherwise, use the default parameter val1
function defaultStrat(val1, val2) {
return typeofval2 ! = ='undefined' ? val2 : val1
}
// 'url', 'params', 'data'
function fromVal2Strat(val1, val2) {
if (typeofval2 ! = ='undefined') {
return val2
}
}
// Deep merge configuration applies when the configuration itself is an object
// The deepMerge method is a deep copy of an object, which is not described here
function deepMergeStrat(val1, val2) {
if (isPlainObject(val2)) {
return deepMerge(val1, val2)
} else if (typeofval2 ! = ='undefined') {
return val2
} else if (isPlainObject(val1)) {
return deepMerge(val1)
} else {
return val1
}
}
// Use three attributes configured by the user
const stratKeysFromVal2 = ['url'.'params'.'data']
stratKeysFromVal2.forEach(key= > {
strats[key] = fromVal2Strat
})
// Two attributes that require a deep merge
const stratKeysDeepMerge = ['headers'.'auth']
stratKeysDeepMerge.forEach(key= > {
strats[key] = deepMergeStrat
})
export default function mergeConfig(config1, config2) {
if(! config2) { config2 = {} }// Create an empty object to store the final merged configuration file
const config = Object.create(null)
// The mergeField method is to select different policies according to the attributes and merge the configurations
for (let key in config2) {
mergeField(key)
}
// Traverses the default configuration, and the configuration does not appear in the user configuration
for (let key in config1) {
if(! config2[key]) { mergeField(key) } }// This is the "strategy mode" of design mode, which effectively removes an infinite number of if and else cases from the code
function mergeField(key) {
const strat = strats[key] || defaultStrat
config[key] = strat(config1[key], config2[key])
}
// Export the final config
return config
}
Copy the code
The code here is more complex and handles more cases, so to fully understand this code you need to actually run the code, understand the core idea here, and use the policy pattern to eliminate the if in the code… Else is the most important.
Click here to see the complete code for this summary
Cancel the function
Cancel request requirements analysis
To cancel a request using cancel Token, you can create a Cancel token using the canceltoken. source factory method, like this:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// Processing error}}); axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// Cancel the request (the message argument is optional)
source.cancel('Operation canceled by the user.');
Copy the code
CancelToken can also be created by passing an executor function to the CancelToken constructor:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// The executor function takes a cancel function as an argumentcancel = c; })});// cancel the request
cancel();
Copy the code
Note: You can cancel multiple requests using the same Cancel token.
Cancel request implementation
Take a closer look at how it’s used:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
source.cancel('Operation canceled by the user.');
Copy the code
Perform source. Cancel (‘… ‘), you can cancel the request. The actual cancellation request can be implemented with the following code:
const request = new XMLHttpRequest();
request.abort();
Copy the code
In xhr.js, abort is invoked to cancel the request, but the Request object does not expose it. How do we do that?
We know by reading the source code, through the promise to achieve linkage. That is, when source.cancel(‘… ‘) to actually change the state of the promise.
In xhr.js, just listen for the state of the promise:
if (cancelToken) {
cancelToken.promise
.then(reason= > {
request.abort()
reject(reason)
})
.catch(() = >{})}Copy the code
To achieve its core classes now SRC/cancel/CancelToken js
class Cancel {
message;
constructor(message) {
this.message = message
}
}
// Determine whether the thrown error is an instance of Cancel
function isCancel(value) {
return value instanceof Cancel
}
export default class CancelToken {
promise;// Define the PROMISE variable to store the state
reason; // Define error cause variables
constructor(executor) {
// Define an empty variable to store the resolve method of a Promise instance
let resolvePromise;
this.promise = new Promise(resolve= > {
resolvePromise = resolve
})
const paramFn = message= > {
if (this.reason) {
return
}
this.reason = new Cancel(message)
resolvePromise(this.reason)
};
// Executes the method passed in when instantiating, using paramFn as an argument
executor(paramFn)
}
throwIfRequested() {
if (this.reason) {
throw this.reason
}
}
// Define the source static method, export the CancelToken instance, and cancel the method
static source() {
let cancel;
const token = new CancelToken(c= > {
cancel = c
})
return {
cancel,
token
}
}
}
Copy the code
Finally, add the corresponding method to axios.js:
axios.CancelToken = CancelToken
axios.Cancel = Cancel
axios.isCancel = isCancel
Copy the code
Finally, we analyze the downsizing process:
Execute source.cancel(‘… ‘); Equivalent to executing the paramFn method.
2. When executing the paramFn method, use resolvePromise(this.reason) to change this. Promise to resolve.
In xhr.js, canceltoken.promise.then (reason => {request.abort()}) is used to monitor the promise state and cancel the request when it changes.
Click here to see the complete code for this summary
withCredentials
In the same-domain case, the cookie in the current domain is sent by default, but in the cross-domain case, the cookie in the request domain is not carried by default. If you want to carry the cookie, you only need to set the withCredentials of the XHR object to true.
This is very simple to implement, just add this code to xhr.js:
module.exports = function xhrAdapter(config) {
return new Promise((resolve, reject) = > {
let{... withCredentials } = config;const request = newXMLHttpRequest(); .// Set the withCredentials attribute of the XHR object
if(withCredentials){ request.withCredentials = withCredentials; }... request.send(data); }); };Copy the code
Click here to see the complete code for this summary
CSRF defense
Cross-site Request Forgery, often shortened to CSRF or XSRF, is an attack method to trick a user into performing unintentional actions on a currently logged Web application.
A typical CSRF attack has the following flow:
- Victim login
a.com
And retained the login credentials (Cookie
); - The attacker lured the victim to visit
b.com
; b.com
向a.com
A request was sent:a.com/act=xx
. Browsers carry it by defaulta.com
的Cookie
;a.com
After receiving the request, it verifies the request and confirms that it is the victim’s credentials, mistaking it as a request sent by the victim himself;a.com
Executed on behalf of the victimact=xx
;- The attack is complete, the attacker impersonates the victim without the victim’s knowledge, and lets
a.com
The user-defined operation is performed.
If you are not familiar with CSRF defense, you can also read another article by the author: Front-end Security (same Origin Policy, XSS attack, CSRF attack).
How does Axios defend against CSRF
Token authentication is used to defend against CSRF attacks. The defense process is as follows:
- When the browser makes a request to the server, the server generates one
CSRF Token
. And through theset-cookie
To the client. - When a client sends a request from
cookie
Read from the corresponding fieldtoken
And then add to the requestheaders
In the. - The server resolves the request
headers
And verify that thetoken
.
Since this token is difficult to forge, it is possible to distinguish whether the request was properly initiated by the user.
So our axios library automatically does all of these things. Every time we send a request, we read the token value from the cookie and add it to the headers request. We allow the user to configure xsrfCookieName and xsrfHeaderName. XsrfCookieName indicates the cookie name of the token, and xsrfHeaderName indicates the header name of the token in the request headers.
axios({
url:"/".xsrfCookieName: 'XSRF-TOKEN'.// Default configuration
xsrfHeaderName: 'X-XSRF-TOKEN' // Default configuration
})
Copy the code
Axios implements CSRF defense
Its core implementation is in xhr.js:
module.exports = function xhrAdapter(config) {
return new Promise((resolve, reject) = > {
let{... withCredentials, xsrfCookieName, xsrfHeaderName } = config;const request = newXMLHttpRequest(); .// Set the withCredentials attribute of the XHR object
if(withCredentials){
request.withCredentials = withCredentials;
}
// Check if withCredentials are set to true, or if xsrfCookieName exists in a domain request
// Add XSRF related fields to request HEADERS.
if((withCredentials || isURLSameOrigin(url)) && xsrfCookieName ){
// Read the corresponding xsrfHeaderName value by cookie
const xsrfValue = cookie.read(xsrfCookieName);
if(xsrfValue && xsrfHeaderName){ headers[xsrfHeaderName] = xsrfValue; }}... request.send(data); }); };Copy the code
The implementation of CSRF defense is not complicated, but by implementing it, we can thoroughly understand the cause of CSRF and how to defend against it.
Click here to see the complete code for this summary
At this point, the core of writing Axios is complete and basically covers the important axios points. I believe you and the author can learn a lot, next we will pack it and send it to NPM online.
Axios shipped it as a package
1. Since we have already written the Webpack configuration, we just need to execute the package command NPM run build directly. 2. Package. json optimization
{
"name": "lion-axios"./ / package name
"version": "1.0.0"./ / version
"description": "lion-axios"./ / description
"scripts": {
"clean": "rimraf ./dist"."dev": "webpack-dev-server --env.development --config webpack.common.js"."build": "npm run clean && webpack --env.production --config webpack.common.js"."prepublishOnly": "npm run build" // NPM publish is automatically packaged before it is published
},
"repository": { // Code hosting information
"type": "git"."url": "git+https://github.com/shiyou00/lion-axios.git"
},
"files": [
"dist" // The files that the project uploads to the NPM server can be individual files, entire folders, or wildcard matched files.]."main": "dist/axios.js".// Import file
"keywords": ["lion-axios"]./ / keywords
"author": "Lion"./ / the author
"license": "ISC"./ / agreement
"bugs": {
"url": "https://github.com/shiyou00/lion-axios/issues" // Mention the bug address
},
"homepage": "https://github.com/shiyou00/lion-axios#readme".// Home address
}
Copy the code
3. Log in and register NPM
# register NPM adduserUsername:shiyou
Email([email protected]) # Login to NPM login and fill in the registration informationCopy the code
4. Execute NPM publish
View the Lion-Axios packet line address
5. Use Lion – Axios
# install yarn add lion-axios or NPM install lion-axios --save #import axios from 'lion-axios'; # useconst baseURL = "https://reqres.in/api/users";
axios({
method: "get".url: `${baseURL}? foo=bar`.params: {
bar: "baz1",
},
}).then((res) = > {
console.log(res);
});
Copy the code
conclusion
- through
axios
The introduction makes us onaxios
Have a preliminary understanding, here or suggest you to go over the official documents. - Through constructing
axios
Project engineering, you can learn how to build an enterprise – class library of engineering projects. - By doing it step by step
axios
Source code, you can learn a lot of subtle programming ideas, which is an important goal to achieve an open source library. - Finally, we packaged up the library and published it to
npm
Online, complete the full life cycle of an open source library.
If your company needs you to manually build an open source library, you already know what to do.
If learning this article has helped you, give it a thumbs up.