1. Introduction
Before looking closely at redux related things, found that redux also has middleware said. Express and KOA also have the concept of middleware, and Axios also has the similar mechanism of interceptor, so it is good to sort out the principles of these concepts. Before reading this article, readers should have some knowledge of Axios, KOA, Express, and Redux. Each part of the principle of parsing will be combined with the source code, in order to facilitate understanding, some code order has been adjusted and simplified appropriately.
2. Axios interceptors
2.1 registered
const axios = require('axios')
axios.interceptors.request.use((config) = > {
console.log('Request interceptor')
// Process the configuration before sending the request and return the processed configuration
return config
}, (error) = > {
// When an error occurs
return Promise.reject(error)
})
axios.interceptors.response.use((response) = > {
console.log('Response interceptor')
// Process the response data and return the processed data
return response
}, (error) = > {
// When an error occurs
return Promise.reject(error)
})
Copy the code
2.2 the principle
Axios. Interceptors. Request/response. Use method takes two function type parameters, dealing with normal and error conditions respectively. Axios takes these two functions as arguments to promise.then, and at runtime combines all the interceptors into a promise call chain to execute the overall process of axios sending the request:
- The config configuration is processed by the request interceptor
- Send the request
- Get response
- Response is handled by the response interceptor
- The results are returned to the user
The following uses version 0.21.1 as an example
// axios/lib/core/Axios.js
var InterceptorManager = require('./InterceptorManager');
/ /...
function Axios(instanceConfig) {
this.defaults = instanceConfig;
// This is the request and response interceptor
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Copy the code
// axios/lib/core/InterceptorManager.js
function InterceptorManager() {
// Place an array of interceptors
this.handlers = [];
}
// ...
// This is the use method used to register middleware
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
// ...
// Define the forEach method on the interceptor prototype, which will be used later
Handlers; // Handlers are the handlers
InterceptorManager.prototype.forEach = function forEach(fn) {
// ...
};
Copy the code
// axios/lib/core/Axios.js
// ...
// dispatchRequest is the method by which Axios actually executes the sending request
var dispatchRequest = require('./dispatchRequest');
// ...
// Define the method by which Axios sends requests
Axios.prototype.request = function request(config) {
// ...
// Chain starts with two methods, corresponding to promise.then
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
// Unshift the request interceptor into the array header
// So the request interceptors actually execute in reverse order of registration
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// Insert the response interceptor at the end of the array
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
// Chain always handles inserts in pairs of normal process and error process functions, so shift is used continuously
// Construct the call chain of promise.then through the while loop
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
Copy the code
As you can see from the above code, all interceptors are finally executed in a long promise.then call chain, and the configuration/data processed by the previous interceptor is passed to the next promise.then. This mechanism also supports the use of async/await in interceptors
3. The koa middleware
3.1 registered
const Koa = require('koa')
const app = new Koa()
async function fn_1(ctx, next) {
console.log('fn_1 start')
await next()
console.log('fn_1 end')
}
app.use(fn_1)
app.listen(3001)
Copy the code
3.2 the principle
Understanding koA’s middleware has a very classic onion ring model
Koa’s middleware eventually forms nested higher-order functions, similar to
middlewareA(ctx, () = > middlewareB(ctx, () = >middlewareC(ctx, ...) ))Copy the code
Looking at the source code, the VERSION of KOA used here is 2.13.1
// koa/lib/application.js
const Emitter = require('events');
const compose = require('koa-compose');
// ...
module.exports = class Application extends Emitter {
constructor(options) {
// ...
// An array to hold the middleware
this.middleware = [];
}
// ...
// Pass the middleware function to the use method
use(fn) {
// ...
this.middleware.push(fn);
return this;
}
callback() {
// Processing middleware
const fn = compose(this.middleware);
// ...
const handleRequest = (req, res) = > {
// Create a CTX object
const ctx = this.createContext(req, res);
// Pass the return of the compose function to handleRequest
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
// fnMiddleware is the result of the compose function
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err= > ctx.onerror(err);
const handleResponse = () = > respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
listen(. args) {
debug('listen');
// Call the callback function when the HTTP service has been successfully created
const server = http.createServer(this.callback());
return server.listen(...args);
}
}
Copy the code
Koa is handled internally by KOA-compose, and what does KOA-Compose do
// koa-compose/index.js version 4.1.0
module.exports = compose
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ')
for (const fn of middleware) {
if (typeoffn ! = ='function') throw new TypeError('Middleware must be composed of functions! ')}/ * * *@param {Object} context
* @return {Promise}
* @api public* /
// When the returned function is called in koA handleRequest, next is null
return function (context, next) {
// Record the subscript of the current middleware
let index = -1
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// Fetch the middleware
let fn = middleware[i]
// The following two if statements guarantee that the last middleware call to next will not report an error
if (i === middleware.length) fn = next
if(! fn)return Promise.resolve()
try {
return Promise.resolve(
fn(
context,
// Return a function via the bind method, which is actually the next middleware
// This is the second argument written to the middleware, usually named next
// Dispatch recurses whenever the last element in the Middleware array is not retrieved
dispatch.bind(null, i + 1))); }catch (err) {
return Promise.reject(err)
}
}
// Start implementing the first middleware
return dispatch(0)}}Copy the code
It is worth noting that koA registered middleware is eventually wrapped in promise.resolve and converted to promSIE objects. You can then wait for the next middleware to complete by writing await next(). When writing middleware, the second argument next is actually the next middleware, and if it is the last middleware, the next execution returns promise.resolve (), which is still called normally.
4. Express middleware
4.1 registered
const express = require('express')
const app = express()
async function fn_1(req, res, next) {
console.log('fn_1 start')
next()
console.log('fn_1 end')
}
app.use(fn_1)
app.listen(3002)
Copy the code
4.2 the principle
Express-registered middleware is eventually processed as layers of callback functions. The source code for Express feels a little more complicated than axios and KOA. First, in Express, there is a Layer object that wraps the middleware
/ / express/lib/router/layer 4.17.1 js version
module.exports = Layer;
function Layer(path, options, fn) {
// ...
// The handle method of the layer is the registered middleware
this.handle = fn;
// ...
}
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
// ...
fn(req, res, next);
};
Copy the code
Let’s first look at the implementation of the use method in express routing related code
// express/lib/router/index.js
var Layer = require('./layer');
// ...
var proto = module.exports = function(options) {
// ...
function router(req, res, next) {
// ...
}
// ...
// Store an array of layer objects
router.stack = [];
return router;
};
// Register the middleware
proto.use = function use(fn) {
// ...
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false.end: false
}, fn);
// ...
this.stack.push(layer);
}
// The actual method to execute when a request matches the route change
proto.handle = function handle(req, res, out) {
var self = this;
var idx = 0;
var stack = self.stack;
// ...
function next(err) {
while (idx < stack.length) {
// Fetch the middleware and add 1 to the index of the middleware array
var layer = stack[idx++];
// Execute the middleware
layer.handle_request(req, res, next)
}
}
next()
}
Copy the code
The express middleware can be registered globally through app.use(fn), or locally through app.use(path, fn) or app.method(path, fn), but it all ends up in the use method of the router object. As you can see from the above code, the third parameter next when writing Express middleware is actually the next middleware through the wrapper. Since the next middleware is directly called in the wrapper function, there is no processing logic for asynchron, and the wrapper function itself is a common synchronous function, it is naturally unable to support async/await and other methods to handle asynchron. This is the root cause of the Express middleware does not support asynchron.
5. Redux middleware
5.1 registered
import {
createStore,
applyMiddleware,
} from 'redux'
function reducer(state, action) {
letnewState = { ... state }// ...
return newState
}
export function logger({ getState, dispatch }) {
// Next represents the dispatch method after the next middleware wrapper, and action represents the action currently received
return (next) = > async (action) => {
console.log('logger before change', action)
// Call the dispatch of the next middleware wrapper
let val = await next(action)
console.log('logger after change', getState(), val)
return val
}
}
export function debug({ getState, dispatch}) {
return (next) = > async (action) => {
console.log('debug before change', action)
let val = await next(action)
console.log('debug after change', getState(), val)
return val
}
}
const initialState = {
// ...
}
export default createStore(
reducer,
initialState,
applyMiddleware(logger, debug),
)
Copy the code
5.2 the principle
Redux of the middleware processing logic is similar to the onion rings model of koa, which contains a variety of higher-order functions and a variety of curry, a bit not understand, we can try to understand such a function, it is a kind of high order aggregation function, takes a function array as a parameter, will join the array function after the execution result passed as a parameter to join the array function first
function compose(. funcs) {
return funcs.reduce((a, b) = > (. args) = >a(b(... args))) }// Simple understanding
// compose(fn1, fn2, fn3)(... args) = > fn1(fn2(fn3(... args)))
/ / sample
const a = []
a.push(function fn1 (val) { return val })
a.push(function fn2 (val) { return val * 2 })
a.push(function fn3 (val) { return val * 3 })
varx = compose(... a) x(2) // => a 12
Copy the code
Now, if you understand the compose function, let’s go ahead and say, compose(… A) Generate function x, i.e. (… a) = > fn1(fn2(fn3(… A))), when x is executed, fn3, fn2, and fn1 are executed in sequence, which is contrary to the order in which the three functions are added to the array. If the applyMiddleware method fills the array according to the parameter order, the middleware that is later in the execution will be executed first, which is inconsistent with the actual situation. Also, the compose function cannot handle asynchronous middleware. At this point, you need to pay attention to the way the Redux middleware is written
function logger({ getState, dispatch }) {
return (next) = > async (action) => {
console.log('logger before change', action)
// Call the dispatch of the next middleware wrapper
let val = await next(action)
console.log('logger after change', getState(), val)
return val
}
}
Copy the code
The first execution of the middleware function returns a; Function A executes, returning A function B; The body of function B is the real middleware code. Dude, what’s the difference between this and nesting dolls…… Let’s look at the logic of applyMiddleware
// redux/ SRC /compose. Js version 4.0.5
export default function compose(. funcs) {
if (funcs.length === 0) {
return arg= > arg
}
if (funcs.length === 1) {
return funcs[0]}return funcs.reduce((a, b) = > (. args) = >a(b(... args))) }/ / story/SRC/applyMiddleware 4.0.5 js version
export default function applyMiddleware(. middlewares) {
return createStore= > (. args) = > {
conststore = createStore(... args)let dispatch = () = > {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.')}const middlewareAPI = {
getState: store.getState,
dispatch: (. args) = >dispatch(... args) }(next) => async (action) => {... } part
const chain = middlewares.map(middleware= > middleware(middlewareAPI))
// Todo =... args) => a(b(c(... Args)))
// b(c(... Args)) is the next argument to A, c(... Args) is the next argument to B,... Args is the next argument to C
const consttodo = compose(... chain)/ / (... args) => a(b(c(... The function c/b/a is executed backwards
Async (action) => {... } part
// Store.diapatch will be the next parameter for the last middleware
const dispatch = todo(store.dispatch)
// If you print dispatch here, you'll see that it's the first piece of middleware passed to applyMiddleware
// Its next parameter is the next middleware
return {
...store,
dispatch
}
}
}
Copy the code
5.3 Understanding
If the above parsing doesn’t make sense, try running the following example to help
function a() {
return (next) = > {
return (action) = > {
console.log('a before', action)
const res = next(action)
console.log('a after', res)
}
}
}
function b() {
return (next) = > {
return (action) = > {
console.log('b before', action)
const res = next(action)
console.log('b after', res)
return res
}
}
}
function c() {
return (next) = > {
return (action) = > {
console.log('c before', action)
const res = next(action)
console.log('c after', res)
return res
}
}
}
function compose(. funcs) {
return funcs.reduce((a, b) = > (. args) = >a(b(... args))) }function applyMiddleware(. funcs) {
const middlewares = new Array(funcs.length)
funcs.forEach((func, index) = > {
middlewares[index] = func()
})
return middlewares
}
const chain = applyMiddleware(a, b, c)
console.log(chain)
consttodo = compose(... chain)console.log(todo)
const dispatch = todo((action) = > action)
console.log(dispatch)
dispatch({
type: 'SET_LOG',})// Output the result
// a before {type: "SET_LOG"}
// b before {type: "SET_LOG"}
// c before {type: "SET_LOG"}
// c after {type: "SET_LOG"}
// b after {type: "SET_LOG"}
// a after {type: "SET_LOG"}
Copy the code
This blog post is also helpful in understanding the redux middleware mechanism