Building an JavaScript plug in system
- 2021-08-31 07:20:11
- OfStack
This article is translated from https://css-tricks.com/designing-a-javascript-plugin-system/
Plugins are a common feature of libraries and frameworks, and have a good reason to use them: They allow developers to add functionality in a secure, extensible way. This makes the core project more valuable, and this open situation can help the project build a community without adding additional maintenance burden to us.
In this paper, we use JavaScript to build our own plug-in system.
I use the word "pluginn" here, but these things are sometimes called other names, such as "extensions", "add-ons" or "modules". No matter what you call it, its meaning (and benefits) are the same.
Let's build a plug-in system
Let's start from a file named
BetaCalc
Start the sample project of.
BetaCalc
The goal is to become a simple
JavaScript
Calculator, where other developers can add "buttons". Here are some basic codes to help us get started:
// The Calculator
const betaCalc = {
currentValue: 0,
setValue(newValue) {
this.currentValue = newValue;
console.log(this.currentValue);
},
plus(addend) {
this.setValue(this.currentValue + addend);
},
minus(subtrahend) {
this.setValue(this.currentValue - subtrahend);
}
};
// Using the calculator
betaCalc.setValue(3); // => 3
betaCalc.plus(3); // => 6
betaCalc.minus(2); // => 4
For simplicity, we define the calculator as
object-literal
. The calculator passes through
console.log
Print the results.
Now the function is really simple. We have one
setValue
Method, which accepts a number and displays it on the screen. We still have
plus
And
minus
Method that performs an action on the currently displayed value.
It's time to add more features. Let's start by creating a plug-in system.
The world's smallest plug-in system
We will start by creating 1
register
Method, other developers can use it in the
BetaCalc
Register the plug-in on. The principle of this method is simple: get the external plug-in and get its
BetaCalc
0
Function and attach it to our calculator as a new method:
// The Calculator
const betaCalc = {
// ...other calculator code up here
register(plugin) {
const { name, exec } = plugin;
this[name] = exec;
}
};
This is a sample plug-in, which provides 1 for our calculator
squared
Button:
// Define the plugin
const squaredPlugin = {
name: 'squared',
exec: function() {
this.setValue(this.currentValue * this.currentValue)
}
};
// Register the plugin
betaCalc.register(squaredPlugin);
In many plug-in systems, plug-ins are usually divided into two parts:
Code to execute Metadata (such as name, description, version number, dependencies, etc.)
In our plug-in,
exec
The function contains our code and the name is our metadata. When the plug-in is registered,
BetaCalc
0
Function is attached directly as a method to the
betaCalc
Object so that it can access the
BetaCalc
Adj.
this
.
Now,
BetaCalc
There is 1 new
squared
Button, you can call directly:
betaCalc.setValue(3); // => 3
betaCalc.plus(2); // => 5
betaCalc.squared(); // => 25
betaCalc.squared(); // => 625
This system has many advantages. This plug-in is a simple object literal, which can be passed to our function. This means that you can pass the
npm
Download the plug-in and use it as a
ES6
Module import.
But our system has one flaw.
By providing the plug-in with a
BetaCalc
Adj.
this
Access rights, plug-ins can access all
BetaCalc
Read/write access to the code of. Although this is important for getting and setting
currentValue
It's useful, but it's also dangerous. If a plug-in wants to redefine internal functions, such as
setValue
), it may set the
BetaCalc
And other plug-ins. This violates the open-close principle, which states that software entities should be open for extensions and closed for modifications.
Similarly,
squared
Function works by producing side effects. This is in
JavaScript
Is not uncommon, but it doesn't feel very good-especially when other plug-ins may be dealing with the same internal state of 1. A more practical method will greatly help to make our system safer and more predictable.
Better plug-in architecture
Let's look at a better plug-in architecture. The next example changes the calculator and its plug-in API:
// The Calculator
const betaCalc = {
currentValue: 0,
setValue(value) {
this.currentValue = value;
console.log(this.currentValue);
},
core: {
'plus': (currentVal, addend) => currentVal + addend,
'minus': (currentVal, subtrahend) => currentVal - subtrahend
},
plugins: {},
press(buttonName, newVal) {
const func = this.core[buttonName] || this.plugins[buttonName];
this.setValue(func(this.currentValue, newVal));
},
register(plugin) {
const { name, exec } = plugin;
this.plugins[name] = exec;
}
};
// Our Plugin
const squaredPlugin = {
name: 'squared',
exec: function(currentValue) {
return currentValue * currentValue;
}
};
betaCalc.register(squaredPlugin);
// Using the calculator
betaCalc.setValue(3); // => 3
betaCalc.press('plus', 2); // => 5
betaCalc.press('squared'); // => 25
betaCalc.press('squared'); // => 625
We have made one notable change here.
First, we combine plug-ins with "core" calculator methods such as
plus
And
minus
) by placing it in its own plug-in object. Store the plug-in in a
object-literal
1
Object can make our system more secure. Now, plug-ins can't access BetaCalc properties-they can only access betaCalc. plugins properties.
Second, we implemented an press method that looks up the functionality of the button by name and then calls it. Now, when we call the plug-in's exec function, we pass it the current calculator value (currentValue), and we expect it to return the new calculator value.
Essentially, this new press method converts all of our calculator buttons to pure functions. They get 1 value, perform 1 action, and return the result. There are many advantages to this:
Simplified API. Make testing easier (for BetaCalc and the plug-in itself). It reduces the dependence of our system and makes it more loosely coupled in 1.This new architecture has more limitations than the first example, but the way is good. We put a fence in place for plug-in authors to limit them to making only the changes we want them to make.
In fact, it may be too strict! Now, our calculator plug-in can only operate
object-literal
2
. If plug-in authors want to add advanced features, such as "memory" buttons or methods to track history, they cannot.
Maybe it doesn't matter. The power you give to plug-in authors is a delicate balance. Giving them too many permissions may affect the stability of the project. However, giving them little authority will make it difficult for them to solve their problems.
What else can we do?
We can still do a lot of work to improve our system.
If the plug-in author forgets to define the name or return value, we can add error handling to notify the plug-in author. Think like QA Developer 1 and imagine how our system crashes so that we can proactively handle these situations.
We can extend the functional scope of plug-ins. Now, one
BetaCalc
Plugin can add 1 button. But what if it can also register callbacks for certain lifecycle events, such as when the calculator is about to display values? Or, what if there is a dedicated location to store the state in multiple interactions?
We can also extend plug-in registration. What if you can register the plug-in with 1 initial settings? Can you make plug-ins more flexible? If the plug-in author wants to register the entire button suite instead of one button suite, such as
BetaCalc Statistics Pack
), and what changes are needed to support this 1 point?
Your plug-in system
BetaCalc and its plug-in systems are very simple. If your project is large, you need to explore other plug-in architectures.
A good starting point is to look at existing projects for examples of successful plug-in systems. For JavaScript, you can look at jQuery, Gatsby, D3, CKEditor, or others.
You may also want to familiarize yourself with various JavaScript design patterns. Each pattern provides a different level of interface and coupling, which gives you many good plug-in architecture options to choose from. Understanding these options can help you better balance the needs of everyone who uses the project.
In addition to the pattern itself, you can draw on many good software development principles to make such decisions. I have mentioned 1 method (such as open-close principle and loose coupling), but other 1 related methods include
Demeter
Law and dependency injection.
I know it sounds like a lot, but you have to do research. Nothing is more painful than having everyone rewrite their plug-in because you need to change the plug-in architecture. This is a quick way to lose trust and prevent people from contributing in the future.
Conclusion
Writing a good plug-in architecture from scratch is difficult! You have to weigh many considerations to build a system that meets everyone's needs. Is it simple enough? Is it strong enough? Can it work for a long time?
It's worth the effort. Having a good plug-in system can help everyone. Developers are free to solve problems. End users can obtain a large number of selection functions. In this way, you can develop ecosystems and communities around the project. This is a win-win situation.
The above is the construction of an JavaScript plug-in system details, more about the JavaScript plug-in information please pay attention to other related articles on this site!