github: plugin-anything
preface
When the front-end team implements the engineered Cli suite, Node Server and other systems, there are usually three ways to meet the openness of functions: configuration, plug-in, and the combination of configuration and plug-in.
All three have their advantages and disadvantages:
-
Configuration change
-
advantage
As the name implies, users can use the configuration file exposed by the tool to perform various quick configurations to affect the tool.
-
disadvantage
The core functionality of the tool is built into the tool module, and its logic cannot be interfered with outside.
-
-
pluggable
-
advantage
Within the tool, only a series of life cycles and task scheduling are maintained. All business functions are connected in the form of plug-ins, and users can customize their own functions with as much freedom as possible.
-
disadvantage
Getting started costs are not as out-of-the-box as configuration files, requiring users to understand plug-in development specifications.
-
-
Configuration and plug-in combination
This is now the mainstream way Webpack, Babel and other tools adopt this way, plug-in to greatly improve the ecosystem of such tools.
Webpack plug-in ideas
Webpack maintains a life cycle that does two things: 1. Core code runs; 2. Expose hooks in the lifecycle.
Document address.
How users import plug-ins:
webpackConfig: {
plugins: [
new PluginA(),
new PluginB(),
// ...]}Copy the code
How to develop plug-ins:
class PluginA {
constructor(){}apply(compiler) {
compiler.hooks.someHook.tap(...);
}
}
Copy the code
At plug-in development time, the developer intercepts the someHook hook to intervene in the Webpack life cycle at that point.
In this code, Compiler.hooks. SomeHook is used because Webpack attaches exposed lifecycle hooks (such as someHook) to internal hooks, as in the following example:
hooks = {
hookA: new WaterfallHook(),
hookB: new BailHook(),
}
Copy the code
Webpack’s event mechanism relies on Tappable, which has many types of event levels that won’t be described here
To sum up: While implementing the core logic inside Webpack, it exposes a variety of lifecycle hooks for plug-in intervention to achieve flexible functionality.
Plug-in ideas for Babel
Babel’s plug-in mechanism is a little different from Webpack’s.
Babel is essentially a converter of StringA -> AST -> AST change -> StringB, and its plug-ins serve this core function.
When Babel runs, it first converts the String to an AST, iterates through the AST, modifies the AST node in the traversal diagram, and finally converts the AST to a new String.
Babel’s plug-in mechanism mainly affects the process of “walking through the AST and modifying the AST nodes in the traversal diagram” described above.
Develop the Babel plug-in
The Babel plug-in is developed by providing Vistor objects, which are callbacks to each AST node, intercepting the traversal behavior of the node, and modifying the node.
const Visitor = {
ASTNode1() {
// change node
},
ASTNode2() {
// change node}}Copy the code
Use the Babel plug-in
It’s worth learning from this point that Babel accepts a wide variety of plug-in introductions, such as:
-
.babelrc.json / babel.config.json
{ "plugins": [ "transform-runtime"."class-properties"]}Copy the code
-
babel.config.js
module.exports = { plugins: [ 'transform-runtime'.'class-properties']};Copy the code
-
packge.json
{ "name": "my-package"."version": "1.0.0"."babel": { "presets": [...]. ."plugins": [...]. ,}}Copy the code
-
JS API
const babel = require('babel-core'); babel.transform(code, { plugins: [ ... ] }); Copy the code
Most importantly, it provides plugins that can be read to interfere with Babel’s state and meet a number of new functional requirements.
In summary, Babel provides a variety of plug-in access methods, including configuration files, command lines, JS apis, and so on.
How to implement a plug-in tool
So, if we want to implement a plug-in tool, such as the command line, build suite, how do we provide a plug-in mechanism to make the tool as open as possible?
-
The tools themselves
The tool exposes the necessary lifecycle hooks as it runs the core logic.
A set of event handling logic needs to be maintained within the tool to intercept the callback function provided when the plug-in declares the cycle
-
Plugin developer
- A plug-in developer can intercept a lifecycle hook and provide a callback function for that hook
- Plug-in developers can customize new hooks for use by subsequent plug-ins
-
Plug-in consumer
Plug-in consumers, that is, users, need to introduce plug-ins as easily as possible and be free to develop them.
We can learn from Babel’s plugin-import approach, accept plugins options, and provide a variety of configurations (configuration files/JS apis).
Feedback in the code, there are three main steps:
- Initial chemical has lifecycle hooks
- Execute the user-provided plug-in
- Execute core logic and execute a lifecycle hook at some point
Here is the pseudo-code for the implementation logic of the tool:
class PluginedTool {
constructor(options) {
this.options = {
plugins: options.plugins
};
// step1: init hooks
this.hooks = {
hookA: new WaterfallHook(),
hookB: new WaterfallHook(),
};
// step2: run plugins
for (let i = 0, len = this.options.plugins.length; i < len; i++) {
// find plugin function
const pluginFunction = this.findPluginFunction(this.options.plugins[i]);
// run plugin and supply context object
pluginFunction({ ... });
}
// step3: run core code and flush hooks
this.hooks.hookA.fire();
// do something
this.hooks.hookB.fire();
}
private options: {};
}
Copy the code
As a result, a simple plug-in tool can trigger as many lifecycle hooks as possible while executing the tool’s core logic in step3 for extreme flexibility.
This mechanism can be applied to the command line, build suite, Node Server, etc.
How to implement multiple plug-in tools
Admittedly, as mentioned above, the implementation logic for most plugins is similar:
- Initial chemical has lifecycle hooks
- Execute the user-provided plug-in
- Execute core logic and execute a lifecycle hook at some point
The tool needs to be maintained: event registration, consumption mechanisms, its own life cycle, and so on.
Soon, when we plug-in multiple tools using the same logic, we found that this general logic could be abstracted without having to re-implement each of the facilities.
To this end, a plugin factory was written: plugin-anything.
Usage:
const { runPluginAnything } = require('plugin-anything');
runPluginAnything(
{
// Array< string >
searchList: [
// string: absolute folder path].// Array< string | FunctionContructor | Array<string | FunctionContructor, object> >
plugins: [
// string: plugin name
// FunctionContructor: Plugin Constructor
// Array: [ string | FunctionContructor, options object ]],}, {// init something like: hooks, customs config
async init({ hooks, Events, customs }) {
hooks.done = new Events();
customs.myConfig = {};
},
// run lifecycle
async lifecycle({ hooks, Events, customs }) {
// flush hooks
await hooks.done.flush('waterfall');
// do something
// ...
// console.log(customs.myConfig);}});Copy the code
It’s nice to think that developers can quickly create a plug-in tool.
Thanks for reading.