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 BetaCalc0 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, BetaCalc0 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!


Related articles: