primers
To add abort to the HTTP request method in the library, I refer to the axios source code and take a look at the axios interceptor implementation, plus a little knowledge of the middle of Redux, Express, and koa-compose. So with this active thinking we first think about the middleware we have used, implement some simple middleware, and then compare some middleware implementations in the community, and further think about it.
synchronous
const fns = [fn1, fn2, fn3];
for(var fn of fns){
fn()
}
Copy the code
asynchronous
Assume the following code: asynchronously multiply by 2
const double = (val) = > {
return new Promise((resolve, reject) = > {
setTimeout(() = > {
resolve(val * 2);
console.log(val * 2);
}, 1000);
});
};
const fn1 = double;
const fn2 = double;
const fn3 = double;
const fns = [fn1, fn2, fn3];
Copy the code
Then () to ensure sequential execution, the object code is as follows:
fn1(2)
.then((result) = > {
return fn2(result);
})
.then((result) = > {
return fn3(result);
})
.then((result) = > {
console.log(result);
return result;
});
Copy the code
For ease of use, we need to write a generator that generates the code above. Is also very simple
function compose(fns) {
return (val) = > {
let p = Promise.resolve(val);
// loop. Then ()
for (let fn of fns) {
// Make sure fn() returns promise
p = p.then((res) = > Promise.resolve(fn(res)));
}
return p;
};
}
const c = compose(fns);
Copy the code
Refer to the community
Axios
// Use middleware
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected,
synchronous: options ? options.synchronous : false.runWhen: options ? options.runWhen : null
});
return this.handlers.length - 1;
};
Copy the code
Adding Middleware
- This is a big pity
- Rejected: Indicates the failed callback
- Synchronous: Whether the Request middleware is executed synchronously
- RunWhen: The runtime determines whether the middleware will be added
module.exports = function dispatchRequest(config) {
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
return response;
}, function onAdapterRejection(reason) {
return Promise.reject(reason);
});
};
Copy the code
- DispatchRequest: Actual request function
- Adapter: An adapter that ADAPTS requests from node and browser.
Axios.prototype.request = function request(config) {
// filter out skipped interceptors
var requestInterceptorChain = [];
var synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
// If not synchronous interceptors, both request and response interceptors are called with promise.then
if(! synchronousRequestInterceptors) {var chain = [dispatchRequest, undefined];
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
// If it is a synchronous interceptor, request is called synchronously first, then promise.then calls the request and response interceptors
var newConfig = config;
// Build the key code for the PRMISE chain
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected(error);
break; }}try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
// Key code to build the Promise chain
while (responseInterceptorChain.length) {
promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
}
return promise;
};
Copy the code
With our active thinking in the asynchronous method is a way of thinking, simple implementation, easy to understand:
- Define request interceptors and Response interceptors, manually inserted in
dispatchRequest
Before and after. - Based on whether the Request interceptor is synchronized
- If both request interceptors are asynchronous, build one through a while loop
promise.then(fulfilled, rejected).then(fulfilled, rejected)
The chain call to. - If one of the request interceptors is synchronous, the request interceptor is executed through the while loop, the new configuration is retrieved, and a new one is constructed through the while loop
promise.then(fulfilled, rejected).then(fulfilled, rejected)
The chain call to.
Redux
Source code for compose
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))); }Copy the code
An 🌰 :
// Middleware 1
function add(next) {
return (num) = > {
next(num + 1);
};
}
// Middleware 2
function multiple(next) {
return (num) = > {
setTimeout(() = > {
next(num * 2);
}, 2000);
};
}
// Target function
function printf(num){
console.log(num)
}
const compute = compose(add, multiple)(printf)
compute(1)
Copy the code
Break it down:
const compute = (. args) = > {
return ((next) = > {
return (num) = > {
next(num + 1);
};
})(
((next) = > {
return (num) = > {
setTimeout(() = > {
next(num * 2);
}, 2000); }; }) (... args) ); };Copy the code
Let’s simplify the structure as follows
(... args) => (function m1(next) {
// ...
})(
(function m2(next) {
// ...
})(
(function printf(next) {
// ...}) (... args) ) );Copy the code
Using the array.prototype. reduce method, pass the first middleware as an argument to the previous middleware, i.e
- Combination:
compose([add, multiple])(printf)
Converted toadd(mulitple(printf))
, the steps are as follows: - will
printf
As amutiple
thenext
Parameter, run the command firstmutiple(next)
.(num)=>{setTimeout(()=>{console.log(num*2)})}
- Returns the result as
add
thenext
Parameters,(num) => setTimeOut(()=>console.log((num+1)*2))
add(next)
Finally return a functioncompute
- Execution: Final call
compute()
When theadd
.multiple
.printf
perform
In simple terms, reverse load, forward execution. And it has the characteristics
- Flexible organization: you can either add a layer to wrap the function’s common parameters, or you can pass parameters next
- Static generation: The compose process is the process of generating a function
It’s actually called pipe: see JavaScript pipe for details
Why does redux middleware have to return a function when koa-compose, Expres, does not?
Express
Handle -> router.handle -> (layer.handle_request -> (route.dispatch -> layer.handle_request)… .
Layer is an object that stores the relationship between path and Handle (one or more middleware)
- A: refers to
app.use
In this case, Handle defines its own middleware function - More than one:
router.get()
, handle isroute.dispatch
“, and recursively call their own defined Middleware functions
The core is a two-level recursion, which simplifies the code as follows :(not the same)
function express() {
var funcs = []; // Array of functions to be executed
var app = function (req, res) {
var i = 0;
function next() {
var task = funcs[i++]; // Retrieve the next function in the array of functions
if(! task) {// Return if the function does not exist
return;
}
task(req, res, next); // Otherwise, execute the next function, noting that there is no return
}
next();
};
The /** * use method adds a function to an array of functions *@param task* /
app.use = function (task) {
funcs.push(task);
};
return app; // Return the instance
}
Copy the code
- Array storage: Use arrays to store all middleware functions.
- Functions are middleware: the
app.get
.app.use
.router.get
Functions in the middleware. - Executor: Executes the next middleware by calling next.
- Next:
()=>task[0]()
- Next:
()=>task[1]()
- Next:
()=>task[2]()
- Next:
Executing next for the first time executes task[0], and next for task[0] executes task[1]… , up to the last task
What’s the difference between Express and Redux?
Koa-compose
Koa-compose is an example:
import compose from 'koa-compose';
async m1(ctx, next){
console.log('first before');
await next();
console.log('first after');
}
async m1(ctx , next){
console.log('second before');
await next();
console.log('second after');
}
const c = compose([m1, m2])
c(null.() = >{
console.log("done")})// Run the result
/**
* first before
* second before
* done
* second after
* first after
* /
Copy the code
The source code is as follows:
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! ')}return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
// When the middleware is finished executing, assign the value to the object function passed in externally, namely the onion heart (the last function normally executed).
if (i === middleware.length) fn = next
// Terminate the condition, return promise
if(! fn)return Promise.resolve()
try {
// Make sure fn() returns a promise and changes the next argument to the middleware
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
Copy the code
Parsing source code:
- Termination returns: If! Fn, return
Promise.resolve()
- Recursive return:
Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
; - The onion the heart:
i === middleware.length
whenfn=next
- Next =m1,next=m2, and finally next=next make forward calls by continuously assigning the middleware’s next function (external target function next in non-closure), and the code after await is equivalent to calling promise.then.
To disassemble the 🌰 above, the koa-compose conversion executes the following code:
Promise.resolve(m1(context, () = > {
return Promise.resolve(m2(ctx, () = > {
return Promise.resolve(next(ctx, () = > {
return Promise.resolve(); })); })); })); A bit abstract, expand m1, m2, next to simplify as follows (omit passing parameters) :Promise.resolve(
(async() = > {console.log('first before');
await Promise.resolve(
(async() = > {console.log('second before');
await Promise.resolve(
(() = > {
console.log('done');
})()
)
console.log('second after')}) ())console.log('first after')}) ())Copy the code
- Koa and Express are implemented in the same way. Why is Express not the Onion model?
- Why must promise.resolve() be returned?
Answer questions
- Why does redux middleware have to return a function when koa-compose, Expres, does not?
A: Because redux has a combination process, when redux is combined, it needs to take the execution result of the next middleware as the next parameter of the previous middleware. That is to say, the execution result of the middleware is not a function, and an error will be reported if it is passed to the previous middleware: Next is not a function
- What’s the difference between Express and Redux?
Redux is generated statically in compose(… Middlewares)(doSth) already determines what next is and thus the order of execution, while Express dynamically determines what next is
- What is the difference between KOA and Express? Why is Express not an Onion model?
Answer: Because expres calls task(res, req, next) without return, the final call is not await promise, but await undefind, so the onion model is not formed.
- Why must promise.resolve() be returned?
A: The result of compose must return a promise case to prevent only the request middleware from reporting an error for the synchronization function. Resolve () : for koa-compose, koa: for koa-compose, koa:
- Test case for KOA-compose
Error: Next function should return Promise
- Test cases for KOA
it('should merge properties'.() = > {
app1.use((ctx, next) = > {
assert.equal(ctx.msg, 'hello');
ctx.status = 204;
});
return request(app1.listen())
.get('/')
.expect(204);
});
Copy the code
Error: handleRequest error: handleRequest
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);
}
Copy the code
You can also see here that the final step in the KOA Onion model is to process the response after the middleware executes. Express handles the response in the last middleware.
conclusion
The differences between the four middleware
- Axios is the simplest, direct promise chain calls
- Redux syntax needs to return functions, which is not very convenient to write, but it has a special idea. It uses reduce, generates statically, and takes the next intermediate execution result as the next parameter of the previous middleware.
- For koA-compose, express and KoA-compose have the same idea. They both use recursions (actuators) to execute dynamically, from the first middleware to the last middleware. The difference is that KoA-compose implements the Onion model by returning a promise.
Middleware implementations, in essence, use these two things about the order of execution:
- Function call stack: Next intervenes in the execution order
- Promise.then: Asynchronous sequential invocation or onion model is implemented through promise.then.