preface
In this paper, I hope to analyze the combination principle of middleware, so as to help you understand the Onion model.
Without further ado, the text is as follows.
This code comes from the compose function exported in redux. I made some changes. The main reason for adding names to anonymous functions, such as Reducer and nextWrapper, is that anonymous is not easy to debug. So Uncle Kyle Simpson, author of You-Dont-Know-JS, has reservations about using arrow functions, but gets off topic.
First post code as follows.
function compose(. funcs) {
if (funcs.length === 0) {
return arg= > arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(function reducer(a, b) {
return function nextWrapper(. args) {
returna(b(... args)); }; }); }Copy the code
The following full text will be based on this function analysis.
A few simple Redux middleware will be provided next. Again, I have avoided the use of arrow functions for the same reason as above. The code is as follows:
function next(action) {
console.log("[next]", action);
}
function fooMiddleware(next) {
console.log("[fooMiddleware] trigger");
return function next_from_foo(action) {
console.log("[fooMiddleware] before next");
next(action);
console.log("[fooMiddleware] after next");
};
}
function barMiddleware(next) {
console.log("[barMiddleware] trigger");
return function next_from_bar(action) {
console.log("[barMiddleware] before next");
next(action);
console.log("[barMiddleware] after next");
};
}
function bazMiddleware(next) {
console.log("[bazMiddleware] trigger");
return function next_from_baz(action) {
console.log("[bazMiddleware] before next");
next(action);
console.log("[bazMiddleware] after next");
};
}
Copy the code
At this point, if the above three middleware of Foo, Bar and Baz are combined to run as follows:
const chain = compose(fooMiddleware, barMiddleware, bazMiddleware);
const nextChain = chain(next);
nextChain("{data}");
Copy the code
What will this output be on the console?
You can think about it.
.
Those of you familiar with the running order of middleware may quickly come to the answer:
[bazMiddleware] trigger
[barMiddleware] trigger
[fooMiddleware] trigger
[fooMiddleware] before next
[barMiddleware] before next
[bazMiddleware] before next
[next] {data}
[bazMiddleware] after next
[barMiddleware] after next
[fooMiddleware] after next
Copy the code
Students who cannot write the correct answer should not be discouraged. The purpose of this article is to help you understand this mechanism better.
This onion model, also known as the power of middleware, is now playing a large role in the Web community Redux, Express, Koa, developers use the Onion model to build numerous powerful and interesting Web applications and Node applications. Not to mention Dva, Egg, etc. derived from these three. So what you need to understand is the implementation mechanism. It would be too boring to just remember the middleware execution order. Now let’s deconstruct the code layer by layer to explore the Onion model.
Here, the text begins!
The soul of the code above is that the Array. The prototype, the reduce (), do not understand the function of students strongly recommended to MDN walking a circle MDN | Array. The prototype, the reduce ().
The reduce function is an important concept in functional programming and can be used for composing functions.
Combined middleware mechanism
const chain = compose(fooMiddleware, barMiddleware, bazMiddleware);
Copy the code
For compose, three middleware components, fooMiddleware, barMiddleware, and bazMiddleware, are added to compose. The internal execution steps can be divided into the following two steps.
- Step 1 Input parameters: A -> fooMiddleware, b -> barMiddleware
Function nextWrapper#1(… args) { return fooMiddleware(barMiddleware(… args)) }
- Function nextWrapper#1(a -> function nextWrapper#1) args) { return fooMiddleware(barMiddleware(… Args))}, b -> bazMiddleware
Function nextWrapper#2(… args) { return nextWrapper#1(bazMiddleware(… The args)}.
So chain is equal to the nextWrapper that’s finally returned.
(#1 and #2 are used to refer to different combinations of Nextwrappers, but there is no such syntax.)
Application Middleware mechanisms
Note, however, that none of the middleware is executing at this point, and so far has finally returned through the higher-order function nextWrapper.
Because it’s not until the following sentence, passing in the next function as an argument, that nextWrapper actually starts iterating through all the combined middleware.
const nextChain = chain(next);
Copy the code
We learned above that chain is eventually shaped like (… args) => fooMiddleware(barMiddleware(bazMiddleware(… Args))). So when the next function is passed in, the internal execution steps can be divided into the following steps:
-
The first step is to execute chain (nextWrapper#2), which is composed from the inside out of the compose function. The next argument is first passed to the bazMiddleware function. After printing the log, next_from_baz is returned.
-
Next, next_from_baz immediately passes nextWrapper#1, returning fooMiddleware(barMiddleware(… The args)). Therefore, the expected next argument received by barMiddleware is not the original next function, but the next_from_baz returned by bazMiddleware. BarMiddleware receives the next argument, prints the log, and returns the next_from_bar function.
-
Third, again, the expected next argument received by fooMiddleware is next_from_bar returned by barMiddleware. FooMiddleware receives the next argument and starts executing, prints the log and returns next_from_foo.
So at this point we know that after we run chain, nextChain is actually next_from_foo.
Detailed description with schematic diagram is as follows:
After the above steps, the console outputs the following log:
[bazMiddleware] trigger
[barMiddleware] trigger
[fooMiddleware] trigger
Copy the code
Next_from_baz, next_from_bar, and next_from_foo are just layers of arguments that are passed in as next. Officially, it’s called Monkeypatching.
It is clear that next_from_foo wraps next_from_bar, next_from_bar wraps next_from_baz, and next_from_baz wraps next.
If I could just write Monkeypatching as follows
const prevNext = next;
next = (. args) = > {
// @todoprevNext(... args);// @todo
};
Copy the code
However, if patch needs many functions, we need to repeat the above code many times. It’s not really DRY.
Monkeypatching is essentially a hack. “Replace any method with whatever you want.”
About Monkeypatching and redux Middleware is introduced, highly recommend reading website document redux Docs | Middleware.
Here I came up with an exam question, as follows:
function add5(x) {
return x + 5;
}
function div2(x) {
return x / 2;
}
function sub3(x) {
return x - 3;
}
const chain = [add5, div2, sub3].reduce((a, b) = >(... args) => a(b(... args)));Copy the code
Chain (1) output value?
Execute sub3 -> div2 -> ADD5. 1 minus 3 over 2 plus 5 is 4. The answer is 4.
So again:
const chain = [add5, div2, sub3].reduceRight((a, b) = >(... args) => b(a(... args)));Copy the code
Chain (1) outputs the value, right? Or 4.
Take a look at the following code:
const chain = [add5, div2, sub3].reverse().reduce((a, b) = >(... args) => b(a(... args)));Copy the code
Chain (1) outputs the value, right? It’s still 4.
If you can clearly calculate the answer to all of the above examples, you should be ok with chain(Next) above, so read on.
Onion model mechanism
nextChain("{data}");
Copy the code
Finally, the nextChain function is hard to come by, but there is no doubt that it is very powerful. (See, in Redux, the nextChain function is actually redux’s dispatch function.)
So far in this article, we have learned that nextChain is the next_from_foo function.
So the following sequence of execution will be illustrated by a function stack diagram.
Each time the next function is executed, a new next function is pushed and the cycle continues until next_from_baz. The process of pushing the function is equivalent to completing the process of entering the onion model from the outside to the inside.
Console output log:
[fooMiddleware] before next
[barMiddleware] before next
[bazMiddleware] before next
Copy the code
The function is pushed up to the final next function, which, as we know, doesn’t have any function left, so it reaches the end.
The next step is to stack. The schematic diagram is as follows
Console output log:
[next] {data}
[bazMiddleware] after next
[barMiddleware] after next
[fooMiddleware] after next
Copy the code
The process of going out of the stack is the same as the process of going out of the onion model.
This is the order in which the function stack is executed. The following diagram is a linear order of execution that I’ve compiled to help you understand. Whenever next(Action) is executed, the function is pushed, the original next function is temporarily stopped, and the new next function is executed, as shown in the curved arrow below.
Above, the code runs from top to bottom, which is essentially a program control flow on the call stack. So theoretically, no matter how many functions are nested, it can be understood equally.
Let’s modify the original Onion model as follows:
summary
Redux’s middleware has one more level of functionality than the middleware in the example above to fetch stores within the framework.
const reduxMiddleware = store= > next => action= > {
// ...
next(action);
// ...
};
Copy the code
Koa’s middleware has CTX context parameters and supports asynchrony.
app.use(async (ctx, next) => {
// ...
await next();
// ...
});
Copy the code
Do you have any idea how to do that? Doesn’t it feel like the sun is coming out of the clouds?
If there is, this article has played its role and value, the author will not be very honored. If not, the author’s ability to express needs to be strengthened.