WordPress has plug-ins, jQuery has plug-ins, so does Gatsby, Eleventy, and Vue.
Plug-ins are a common feature of libraries and frameworks, and for good reason: they allow developers to add functionality in a safe, extensible way. This makes the core project more valuable and builds a community — all without adding an additional maintenance burden. That’s great!
So how do you build a plug-in system? Let’s answer this question by building our own plug-in in JavaScript.
Let’s build a plug-in system
Let’s start with an example project called BetaCalc. BetaCalc aims to be a minimalist JavaScript calculator that other developers can add “buttons” to. Here’s some basic code to 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); }};// Use a calculator
betaCalc.setValue(3); / / = > 3
betaCalc.plus(3); / / = > 6
betaCalc.minus(2); / / = > 4
Copy the code
To keep things simple, we defined the calculator as an objective thing, and the calculator works by printing results through console.log.
It’s really limited at the moment. We have a setValue method that takes a number and displays it on the screen. We also have the plus and minus methods, which perform an operation on the currently displayed value.
Now it’s time to add more features. Start by creating a plug-in system.
The world’s smallest plug-in system
We’ll start by creating a register method that other developers can use to register plug-ins with BetaCalc. The job of this method is simple: get the external plug-in, get its exec function, and attach it to our calculator as a new method:
/ / the calculator
const betaCalc = {
/ /... The other calculator code is here
register(plugin) {
const { name, exec } = plugin;
this[name] = exec; }};Copy the code
Here is an example plug-in that provides a “squared” button for our calculator:
// Define the plug-in
const squaredPlugin = {
name: 'squared'.exec: function() {
this.setValue(this.currentValue * this.currentValue)
}
};
// Register the plug-in
betaCalc.register(squaredPlugin);
Copy the code
In many plug-in systems, plug-ins are typically divided into two parts:
- The code to execute
- Metadata (such as name, description, version number, dependency, etc.)
In our plug-in, the exec function contains our code, and name is our metadata. After registering the plug-in, the exec function will be attached directly to our betaCalc object as a method to access betaCalc’s This.
BetaCalc now has a new “square” button that can be called directly:
betaCalc.setValue(3); / / = > 3
betaCalc.plus(2); / / = > 5
betaCalc.squared(); / / = > 25
betaCalc.squared(); / / = > 625
Copy the code
This system has many advantages. The plug-in is a simple object literal that can be passed to our function. This means that plug-ins can be downloaded via NPM and imported as ES6 modules. Easy distribution is super important!
But our system has some flaws.
By giving the plug-in the This permission to access BetaCalc, they can have read/write access to all BetaCalc code. While this is useful for getting and setting currentValue, it’s also dangerous. If the plug-in redefines internal functions (such as setValue), it may have unexpected results for BetaCalc and other plug-ins. This violates the open-closed principle, which states that a software entity should be open for extension, but closed for modification.
In addition, the “squared” function works by producing side effects. This is not uncommon in JavaScript, but it doesn’t feel good — especially when other plug-ins might be in the same internal state. A more practical approach would go a long way toward making our systems more secure and predictable.
Better plug-in architecture
Let’s look at a better plug-in architecture. The next example changes both 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; }};// We need the plugin, the square plugin
const squaredPlugin = {
name: 'squared'.exec: function(currentValue) {
returncurrentValue * currentValue; }}; betaCalc.register(squaredPlugin);// Use a calculator
betaCalc.setValue(3); / / = > 3
betaCalc.press('plus'.2); / / = > 5
betaCalc.press('squared'); / / = > 25
betaCalc.press('squared'); / / = > 625
Copy the code
We’ve made some notable changes here.
First, we separate the plug-in from the “core” calculator methods, such as Plus and minus, by putting them into our own plug-in object. Storing our plug-ins in plugins objects makes our system more secure. Plugins that access this plugins will now not see the BetaCalc properties, but only the properties of betacalc.plugins.
Second, we implement a press method that looks up the button by name and then invokes it. Now, when we call the plug-in’s exec function, we pass the current calculator value to the function and expect it to return the new calculator value.
In essence, this new Press method converts all of our calculator buttons into pure functions. They take a value, perform an action, and return the result. This has many benefits:
- It simplifies the API.
- It makes testing easier (both for BetaCalc and the plug-in itself).
- It reduces the dependency of our system and makes it more loosely coupled together.
This new architecture is more limited than the first example, but it works well. We basically put guardrails on plugin authors, limiting them to the changes we wanted them to make.
In fact, it may be too strict! For now, our calculator plug-in can only operate on currentValue. If plug-in authors want to add advanced features (such as a “remember” button or a way to track history), they can’t.
Maybe that’s good. The power you give to plugin authors is a delicate balance. Giving them too much power can affect the stability of your project. But give them too little power and it’s hard for them to solve their own problems — in which case you might as well not have plug-ins at all.
What else can we do?
We have a lot of work to do to improve our system.
If the plug-in author forgets to define a name or return value, we can add error handling to notify the plug-in author. It’s good to think like QA developers and imagine how our systems might crash so that we can proactively deal with those situations.
We can extend the scope of the plug-in. Currently, a BetaCalc plugin can add a button. But what if it could also register callbacks for certain life cycle events, such as when the calculator is about to display values? Or what if there was a place for it to store a piece of state in multiple interactions? Will this open up some new use cases?
We can also extend plug-in registration. What if a plug-in could be registered with some initial Settings? Does this make the plug-in more flexible? What if a plugin author wants to register a whole set of buttons instead of a single button — say, “BetaCalc statistics pack”? What changes do you need to make to support it?
Your plugin system
BetaCalc and its plug-in system are very simple. If your project is larger, you may want 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, this might mean jQuery, Gatsby, D3, CKEditor or others. You may also want to familiarize yourself with the various JavaScript design patterns, each of which offers a different level of interface and coupling, which gives you a lot of good plug-in architecture options. Knowing these options will help you better balance the needs of everyone who uses your project.
In addition to the patterns themselves, there are many good software development principles you can use to make such decisions. I’ve already mentioned some approaches (such as the open close principle and loose coupling), but other relevant approaches include the Demeter law and dependency injection.
I know that sounds like a lot, but you have to do your research. There’s nothing more painful than having everyone rewrite their plug-in because you need to change the plug-in architecture. It’s a quick way to lose trust and make people lose confidence in their future contributions.
conclusion
Writing a good plug-in architecture from scratch is hard! You have to balance a lot of considerations to build a system that meets everyone’s needs. Is it simple enough? Is it powerful enough? Does it work long term?
But it’s worth it, having a good plugin system helps everyone and gives developers the freedom to solve their problems. The end user can choose from a large number of selectable features. You can build an ecosystem and community around your project. It’s a win-win-win situation.
Recommended reading
- Full Stack Development Essential Skills: 13 Best practices for Building RESTful apis
- How to create your own CSS framework?
- Build a command-line weather forecast program using Deno
- 10 beautiful VSCode Light themes
- Underrated CSS filter: drop-shadow
- I heard that you are proficient in using Vue. Have you mastered these 9 Vue technologies?
Original text: css-tricks.com/designing-a… By Bryan Braun