This week’s intensive reading is Plug-in Thinking. There are no references, but they come from Webpack, FIS, Egg, and my own development experience.
1 the introduction
Those of you who have used build tools know that Grunt, Webpack, and gulp all support plug-in development. Back-end frameworks such as Egg KOa support plug-in mechanism extension, and front-end pages have many extensibility requirements. Plugins are everywhere, and all frameworks want to be the most extensible and maintainable, and all choose plugins to achieve that goal.
I think the plug-in mentality is geeky, and a lot of scalable, collaborative applications are supported by plug-in mechanisms.
Without plug-ins, the code in the core library becomes redundant and increasingly coupled, leading to maintenance difficulties. Plug-in is to spread the expanding functions in plug-ins, internal centralized maintenance logic, which is a bit like the horizontal expansion of the database, the structure remains unchanged, split data.
2. Intensive reading
Ideally, we want a library, or a framework, to be extensible enough. This scalability is reflected in the following three aspects:
- It allows the community to contribute code without affecting the stability of the core code if there are problems with it.
- Secondary development is supported to meet specific requirements of different business scenarios.
- It is especially important to have code aggregated in terms of functionality, rather than a one-sided logical structure, in scenarios with large numbers of code.
We all know that plug-ins should solve the problem, but where to start? This is the experience I wish to share with you.
When it comes to technical design, it’s best to start from the user’s point of view, and then think about implementation when you have a comfortable invocation. So let’s start with the plug-in user perspective and see what we can offer developers to use plug-ins.
2.1 Plug-in classification
Many of the plug-ins evolved from design patterns, including command pattern, factory pattern, and abstract factory pattern. Based on my personal experience, I have summarized three types of plug-ins:
- Convention/injection plug-in.
- Event plug-in.
- Slot plug-in.
Finally, there is a non-plug-in implementation that is more elegant, let’s call it fractal plug-in. Here are some explanations.
2.1.1 Convention/Injection plug-in
Json /.ts file. As long as the returned object is written according to the convention name, it will be loaded and can get some context.
For example, a new command line is automatically registered whenever the commands attribute is present in the Apollo of the project package.json:
{
"apollo": {
"commands": [{ "name": "publish"."action": "doPublish"}}}]Copy the code
Json can be used to define functions in a ts file, but it is more common to write a ts file. For example, the./controllers of the project are stored in the TS file, which automatically acts as a controller in response to requests from the front end.
In this case, the requirement for ts file code structure depends on the function type. For example, the node controller layer, a file to respond to multiple requests, and the logic is simple, it is suitable to use class as a convention, such as:
export default class User {
async login(ctx: Context) {
ctx.json({ ok: true}); }}Copy the code
If the function is relatively cluttered and there is no clear plan of function entry, such as gulp plug-in, the use object will be more concise and prefer to use a single entry, because the main operation is the context, and only one entry is required, the internal logic type can not be controlled. So it might read something like this:
export default (context: Context) => {
// context.sourceFiles.xx
};
Copy the code
Examples are fis, gulp, webpack, and Egg.
2.1.2 Event plug-in
As the name implies, it provides plug-in development capabilities by way of events.
This way frameworks cross more boundaries, such as DOM events:
document.on("focus", callback);
Copy the code
It’s just plain business code, but this is essentially a plug-in mechanism:
- Expandable: N focus events can be repeatedly defined to be independent of each other.
- Events are independent: Each callback is not affected by the other.
It can also be interpreted as an event mechanism that releases hooks at certain stages, allowing user code to extend the overall framework lifecycle.
Service workers are more obvious. Business codes are almost entirely composed of a bunch of time listeners, such as install timing. You can add a listener at any time to delay the install timing without invading other codes.
Koa is known for its middleware Onion model, which can be thought of as event pluginization that controls execution timing. If you want to put the code after next() when all the events are finished, if you want to stop plug-in execution, You don’t have to call next().
Examples: KOA, Service worker, DOM Events.
2.1.3 Plug-in slots
This type of pluginization is typically used to extend UI elements. React’s built-in data streams conform to the physical structure of components, while Redux’s data streams conform to the logical structure defined by users. The same applies to HTML layouts: The default layout of HTML is a physical structure, so slot layouts are redux in HTML.
Normal UI organization logic looks like this:
<div>
<Layout>
<Header>
<Logo />
</Header>
<Footer>
<Help />
</Footer>
</Layout>
</div>
Copy the code
Slots are organized like this:
{
position: "root",
View: <Layout>{insertPosition("layout")}</Layout>
}
Copy the code
{
position: "layout",
View: [
<Header>{insertPosition("header")}</Header>,
<Footer>{insertPosition("footer")}</Footer>
]
}
Copy the code
{
position: "header",
View: <Logo />
}
Copy the code
{
position: "footer",
View: <Help />
}
Copy the code
This allows code in the plug-in to be inserted directly into any insertion point, regardless of the physical structure.
More importantly, UI decoupling is achieved so that the parent element does not need to know the concrete instance of the child element. In general, it is the parent element rather than the child element that determines the state of a component; for example, a button might behave as a composite state in
This means that the parent element does not need to know the instance of the child element, such as Tabs:
<Tabs>{insertPosition(`tabs-${this.state.selectedTab}`)}</Tabs>
Copy the code
Of course, some cases seem to be exceptions, such as Tree’s query function, which relies on the cooperation of the child element TreeNode. But it relies on a child element based on a convention, rather than an instance of a specific child element, and the parent only needs to interface with the child element convention. It is the child elements that really care about the physical structure. For example, TreeNode inserted into the Tree child element node must implement some method. If this function is not met, do not place the component under the Tree. The Tree implementation does not need to worry about the default child element conventions.
Example: Gaea-editor.
2.1.4 Plug-in classification
Represents egg, which is characterized by plug-in structure and project structure classification, that is, small plug-ins that constitute a large project, and their own structure is the same as the project structure.
As for the node Server plug-in, the functions to be implemented should be a subset of the project functions, and the egg itself is divided according to the directory structure, so the directory structure of the plug-in is consistent with the project, and looks good.
Example: egg.
Of course, not all plug-ins can be written as directory fractals, which explains the relationship between Egg and KOA: Koa is a Node framework, which has nothing to do with the project structure. Egg is based on the upper koA framework, which transforms the project structure into server function, while the plug-in needs to expand the server function, so it can be written in the way of the project structure.
2.2 How does the core code load the plug-in
A plugin-enabled framework whose core function is to integrate plug-ins and define the lifecycle. Function-related code can instead be implemented through plug-ins, as described in the next section.
2.2.1 Determine the plug-in loading form
As described in Section 2.1, we found an appropriate way to use the plug-in based on the functionality of the project, which will determine how we implement the plug-in.
2.2.2 Determining the plug-in registration method
There are many ways to register plug-ins. Here are a few examples:
Register with NPM: For example, as long as the NPM package conforms to a certain prefix, it is automatically registered as a plug-in.
By file name registration: if xx.plugin.ts exists in the project, the plugin reference will be automatically done. Of course, this is usually used as a secondary solution.
Register with code: This is very basic, requiring code such as babel-polyfill, but this requires the plugin execution logic to run in the browser, so the scenario is more limited.
Register by describing, for example, a property in package.json that indicates the plug-in to be loaded, such as.babelrc:
{
"presets": ["es2015"]}Copy the code
Automatic registration: more violent, through traversing the possible location, as long as meet the plug-in convention, will automatically register as a plug-in. This behavior is more like require behavior, which automatically recurses to node_modules, but don’t forget to provide paths like require for the user to manually configure the addressing start path.
2.2.3 Determine the life cycle
After deciding how to register a plug-in, the first thing to do is to load the plug-in, followed by a different lifecycle depending on the business logic of the framework. Plug-ins perform different functions in these lifecycle, and we need some way for the plug-in to influence these processes.
2.2.4 Plug-in interception of the life cycle
Plug-in lifecycle interception is generally supported through events and callback functions. The simplest examples are:
document.on("click", callback);
Copy the code
The plugin intercepts the click event, which is a tiny event compared to the DOM lifecycle, but it is still a tiny lifecycle. We can also affect the lifecycle logic by preventing bubblings through event.stopPropagation().
2.2.5 Dependency and communication between plug-ins
There are inevitably dependencies between plug-ins. At present, there are two ways to deal with them: dependencies are defined in business projects, and dependencies are defined in plug-ins.
To explain a little bit, dependencies are defined in business projects, such as webpack configuration, which we use in business projects:
{
"use": ["babel-loader"."ts-loader"]}Copy the code
In Webpack, the execution logic is TS-loader -> babel-loader. Of course, this rule is up to the framework, but there must be an order in which plug-ins are loaded, and it depends on how the configuration is written, and the configuration needs to be written in the project (at least not in the plug-in).
Another way to write plug-in dependencies in plug-ins, such as webpack-preload-plugin, is to rely on htMl-webpack-plugin.
The two scenarios are different. One is the business-related order, that is, the business logic that the plug-in does not control, and the order needs to be configured by the business project. One is the internal order of the plug-in, that is, the order that the business does not need to care about, which is defined by the plug-in itself. Note that the framework core will probably support both configurations, ultimately determining the order in which plug-ins are loaded.
Communication between plug-ins can also be supported by hook or context. Hook mainly transmits timing information, while context mainly transmits data information. However, whether it takes effect depends on the loading order of plug-ins mentioned above.
The react analogy is that context is usually scoped and strictly related to the order in which it is executed.
A hook is equal to an event mechanism within a plug-in, registered by a plug-in. The industry has a better implementation, called Tapable, here is a brief introduction.
Use tapable to register A new hook in A plugin:
const SyncWaterfallHook = require("tapable").SyncWaterfallHook;
compilation.hooks.htmlWebpackPluginAlterChunks = new SyncWaterfallHook([
"chunks"."objectWithPluginRef"
]);
Copy the code
Use this hook somewhere in the A plug-in to implement A specific business logic.
const chunks = compilation.hooks.htmlWebpackPluginAlterChunks.call(chunks, {
plugin: self
});
Copy the code
Plug-in B can extend this hook to change A’s behavior:
compilation.hooks.htmlWebpackPluginAlterChunks.tap(
"HtmlWebpackIncludeSiblingChunksPlugin".chunks= > {
constids = [] .concat(... chunks.map(chunk= > [...chunk.siblings, chunk.id]))
.filter(onlyUnique);
return ids.map(id= >allChunks[id]); });Copy the code
In this way, the chunks that A gets are changed by B.
2.3 Plug-in of core functions
As mentioned at the beginning of 2.2, the main functions of the core code of plug-in framework are the loading and life cycle sorting of plug-ins, and the implementation of hook to allow plug-ins to affect the life cycle. Finally, the loading sequence and communication of plug-ins are added, which is relatively complete.
At this point, the key to code quality is, can all the core business logic be done by plug-ins? Because only by implementing the core business logic with plug-ins can the capabilities of plug-ins be tested, and then it can be inferred whether third-party plug-ins have sufficient scalability.
If some of the code in the core logic is not written through the plug-in mechanism, it not only prevents third-party plug-ins from extending the logic, but also prevents the maintenance of the framework.
So this is basically the idea that developers should first figure out which features should be made into plug-ins, and which plug-ins should be solidify into built-in plug-ins.
The author thinks three points should be considered in advance:
2.3.1 Which Plug-ins need to be built in
This is a business related issue, but in general, open source, basic functions and those that reflect core competitiveness can be built in. It is easier to understand both open source and core competitiveness. Here are the basic functions:
The basic functionality is a business shelf. Because the code for the plug-in mechanism doesn’t solve any business problem, and a framework without built-in plug-ins certainly doesn’t mean anything, it’s important to choose the basics.
For example, building a build tool requires at least one basic configuration as a template, and other plug-ins can extend this configuration to modify the build. This basic configuration then determines how other plug-ins can modify it and also determines the configuration tone of the framework.
For example: create-react-app dev template configuration. Without this template, native development is impossible, so the plug-in must be built in, and you need to consider how other plug-ins can extend it, as explained in Section 2.3.2.
For example, Babel’s CommonJS analysis logic for JS modules does not need to be exposed. Because this standard has been established, it does not need to be expanded, and it is the basis of Babel’s operation, so it must be built in.
2.3.2 Are plug-ins dependent or completely orthogonal
A completely orthogonal plug-in is perfect because it doesn’t affect other plug-ins, doesn’t depend on any plug-ins, and doesn’t need to be extended by any plug-ins.
To worry about writing non-orthogonal plugins, let’s break it down into three points:
2.3.2.1 Plug-ins that depend on other plug-ins
For example, plug-in X needs to extend the command line to count current user information and click when NPM start is executed. The plugin needs to know who is currently logged in. This happens to be done by another “user login” plug-in, so plug-in X depends on the “user login” plug-in.
In this case, as learned in section 2.2.5 Plug-in Dependencies, it is necessary to determine whether the plug-in is plug-in level dependent or project level dependent.
Of course, this case is plugin-level dependencies, which we define in plugin X, such as package.json:
"plugin-dep": ["user-login"]
Copy the code
In other cases, for example, we write a babel-Loader plug-in that relies on TS-Loader in a TS project. That dependency can only be defined in the project. In this case, we need to add some documentation to explain the order in which the TS scenarios are used.
2.3.2.2 Plug-ins that depend on and extend other plug-ins
If plug-in X is based on the “user login” plug-in, it also needs to expand the user information obtained during login, for example, to obtain the user’s mobile phone number, and the “user login” plug-in does not obtain this information by default, but can be achieved by extension, what should plug-in X pay attention to?
First of all, plug-in X had better not reduce the functionality of the other plug-in (see section 2.2.5 for the extension method, which assumes that plug-ins are relatively extensible), otherwise plug-in X may break the collaboration between the “user login” plug-in and other plug-ins.
Reducing functionality is very common, so to give you a better understanding, here is an example: a plug-in extends the template content directly with pipeTemplate, but plug-in X returns the new content directly without concat content, which is reducing functionality.
However, not all cases are guaranteed without reduced functionality. For example, if a necessary configuration item is missing, an exception can be thrown to terminate the program prematurely.
Second, make sure that the added functionality has as few possible conflicts with other plug-ins as possible. Taking the extension webPack configuration as an example, we’re now going to extend the processing of node_modules JS files to pass through Babel.
The bad thing is to change the rules for js and add an include for node_modules and babel-loader. Because this will break the processing of the js files in the project by the original plug-in, maybe the js files in the project don’t need to be processed by Babel?
It’s a good idea to add a new rule that handles the node_modules js file alone without affecting other rules.
2.3.2.3 Plug-ins that may be extended by other plug-ins
This is the hardest part, the difficulty is how to design the granularity of the extension.
Since all scenarios are similar, let’s take extending the template as an example. Other scenarios can be compared: Plug-in X defines the basic content of the entry file, but also provides hooks for other plug-ins to modify the entry file.
Suppose the entry file looks like this:
import * as React from "react";
import * as ReactDOM from "react-dom";
import { App } from "./app";
ReactDOM.render(<App />, document.getELementById("root"));
Copy the code
The simplest template has the following potential expansion needs to be considered internally:
- Where you need to insert other code, how do you support that?
- How do I ensure the order in which I insert the code?
- Replace react with react-Lite.
- Dev mode is required
hot(App)
replaceApp
As an entrance, how to support? - The id of the template entry div might not be
root
How to support? - Template entry div is automatically generated, how to support?
- In reactNative, there is no document, how to support?
- Required for back-end rendering
ReactDOM.hydrate
Rather thanReactDOM.render
How to support? - The above 8 scenarios may be combined differently, and any combination needs to be ensured to work correctly. Therefore, the template cannot be replaced in full. What should I do?
The author gives a solution here for your reference. Also note that this solution is subject to change as the number of usage scenarios considered increases.
get(
"entry".`
${get("importBefore", "")}
${get("importReact", `import * as React from "react"`)}
${get("importReactDOM", `import * as ReactDOM from "react-dom"`)}
import { App } from "./app"
${get("importAfter", "")}
${get("renderMethod", `ReactDOM.render`)}(${get( "renderApp", "
" )}.${get("rootElement", `document.getELementById("root")`)})
${get("renderAfter", "")}
`
);
Copy the code
The reader can imagine the above eight cases without elaborating.
2.3.3 How do Built-in Plug-ins Interact with Third-party Plug-ins
The conflict between built-in plug-ins and third-party plug-ins is that if built-in plug-ins are poorly extensible, it is better not to build them, because built-in plug-ins will hinder the expansion of third-party plug-ins.
Therefore, refer to Section 2.3.2.3 and consider the maximum extension mechanism for the built-in plug-in to ensure that the functionality of the built-in plug-in does not become an extension bottleneck.
Every time you add a built-in plugin, you eliminate a portion of the extensibility, because the extensibility of the plugin-extended block should be gradually reduced. Here is a mouthful, can be compared to, a small stream, plug-in is layer upon layer of water treatment station, each new treatment station will change the downstream water potential change, and may even block the water, downstream also can not get a drop of water.
2.3.1 explains which plug-ins need to be built in, but it is important to be careful about increasing the number of built-in plug-ins, because the more you build, the less extensible your framework will be.
2.4 Which scenarios can be plug-in
Finally, the application scenarios of plug-in are sorted out. The author lists some scenarios based on limited experience.
2.4.1 Front and rear end frames
The React lifecycle, the KOA middleware, and even the request processing used by business code are all examples of plug-ins.
2.4.2 scaffolding
Plugin-enabled scaffolding is extensible, the community can easily provide plug-ins, and it is important that scaffolding be pluggable in order to accommodate multiple code.
2.4.3 tool library
Small libraries, such as the middleware mechanism provided by Redux, which manages data flows, allow the community to contribute plug-ins to improve their capabilities.
2.4.4 Complex business projects requiring multi-party collaboration
If the business project is complex and involves multiple people working together, it is best to divide the work by function. However, if the division of labor is only a simple way of file directory allocation, it will inevitably lead to uneven functions, that is, the modules developed by each person may not access all system capabilities, or when it comes to collaboration with other functions, the mutual reference of files will improve the coupling degree of the code, and ultimately lead to difficult maintenance.
The biggest advantage plug-ins bring to this kind of project is that everyone develops a plug-in as a fully functional individual, so you only need to worry about the distribution of functions, and you don’t have to worry about partial code function imbalance; The invocation framework layer between plug-ins has been removed, so collaboration does not occur coupling, just declaring dependencies.
A project development with a good plug-in mechanism is similar to the experience of git functional branch development. Git opens a branch for each function or requirement, while plug-ins can make each function act as a plug-in. There is no correlation between git functional branches, so only orthogonal requirements between functions can open multiple branches. Plug-in mechanisms can take dependencies into account for more complex functional synergy.
3 summary
I haven’t found any articles on systematization of plug-ins yet, so this one is a bit of a primer, and I’m sure you’ll have more framework development tips to share.
At the same time, I also want to use this article to raise people’s attention to the necessity of plug-ins. In many cases, plug-ins are not a storm in a teacup, because it can bring better division of labor, and the importance of division of labor is self-evident.
4 More Discussions
The discussion address is: close reading “Plug-in Thinking” · Issue #75 · dT-fe /weekly
If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released on weekends or Mondays.