{"id":318994,"date":"2020-08-25T07:49:48","date_gmt":"2020-08-25T14:49:48","guid":{"rendered":"https:\/\/css-tricks.com\/?p=318994"},"modified":"2020-08-25T07:49:50","modified_gmt":"2020-08-25T14:49:50","slug":"designing-a-javascript-plugin-system","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/designing-a-javascript-plugin-system\/","title":{"rendered":"Designing a JavaScript Plugin System"},"content":{"rendered":"\n

WordPress has plugins<\/a>. jQuery has plugins<\/a>. Gatsby<\/a>, Eleventy<\/a>, and Vue <\/a>do, too.<\/p>\n\n\n\n

Plugins are a common feature of libraries and frameworks, and for a good reason: they allow developers to add functionality, in a safe, scalable way. This makes the core project more valuable, and it builds a community \u2014 all without creating an additional maintenance burden. What a great deal!<\/p>\n\n\n\n

So how do you go about building a plugin system? Let\u2019s answer that question by building one of our own, in JavaScript.<\/p>\n\n\n\n\n\n\n\n

I\u2019m using the word \u201cplugin\u201d but these things are sometimes called other names, like \u201cextensions,\u201d \u201cadd-ons,\u201d or \u201cmodules.\u201d Whatever you call them, the concept (and benefit) is the same.<\/p>\n\n\n

Let\u2019s build a plugin system<\/h3>\n\n\n

Let\u2019s start with an example project called BetaCalc. The goal for BetaCalc is to be a minimalist JavaScript calculator that other developers can add \u201cbuttons\u201d to. Here\u2019s some basic code to get us started:<\/p>\n\n\n\n

\/\/ The Calculator\nconst betaCalc = {\n\u00a0 currentValue: 0,\n\u00a0\u00a0\n\u00a0 setValue(newValue) {\n\u00a0 \u00a0 this.currentValue = newValue;\n\u00a0 \u00a0 console.log(this.currentValue);\n\u00a0 },\n\u00a0\u00a0\n\u00a0 plus(addend) {\n\u00a0 \u00a0 this.setValue(this.currentValue + addend);\n\u00a0 },\n\u00a0\u00a0\n\u00a0 minus(subtrahend) {\n\u00a0 \u00a0 this.setValue(this.currentValue - subtrahend);\n\u00a0 }\n};\n\u2028\n\/\/ Using the calculator\nbetaCalc.setValue(3); \/\/ => 3\nbetaCalc.plus(3); \u00a0 \u00a0 \/\/ => 6\nbetaCalc.minus(2); \u00a0 \u00a0\/\/ => 4<\/code><\/pre>\n\n\n\n

We\u2019re defining our calculator as an object-literal to keep things simple. The calculator works by printing its result via console.log<\/code>.<\/p>\n\n\n\n

Functionality is really limited right now. We have a setValue<\/code> method, which takes a number and displays it on the \u201cscreen.\u201d We also have plus<\/code> and minus<\/code> methods, which will perform an operation on the currently displayed value.<\/p>\n\n\n\n

It\u2019s time to add more functionality. Let\u2019s start by creating a plugin system.<\/p>\n\n\n

The world\u2019s smallest plugin system<\/h3>\n\n\n

We\u2019ll start by creating a register<\/code> method that other developers can use to register a plugin with BetaCalc. The job of this method is simple: take the external plugin, grab its exec<\/code> function, and attach it to our calculator as a new method:<\/p>\n\n\n\n

\/\/ The Calculator\nconst betaCalc = {\n\u00a0 \/\/ ...other calculator code up here\n\u2028\n\u00a0 register(plugin) {\n\u00a0 \u00a0 const { name, exec } = plugin;\n\u00a0 \u00a0 this[name] = exec;\n\u00a0 }\n};<\/code><\/pre>\n\n\n\n

And here\u2019s an example plugin, which gives our calculator a \u201csquared\u201d button:<\/p>\n\n\n\n

\/\/ Define the plugin\nconst squaredPlugin = {\n\u00a0 name: 'squared',\n\u00a0 exec: function() {\n\u00a0 \u00a0 this.setValue(this.currentValue * this.currentValue)\n\u00a0 }\n};\n\u2028\n\/\/ Register the plugin\nbetaCalc.register(squaredPlugin);<\/code><\/pre>\n\n\n\n

In many plugin systems, it\u2019s common for plugins to have two parts:<\/p>\n\n\n\n

  1. Code to be executed<\/li>
  2. Metadata (like a name, description, version number, dependencies, etc.)<\/li><\/ol>\n\n\n\n

    In our plugin, the exec<\/code> function contains our code, and the name<\/code> is our metadata. When the plugin is registered, the exec function is attached directly to our betaCalc<\/code> object as a method, giving it access to BetaCalc\u2019s this<\/code>.<\/p>\n\n\n\n

    So now, BetaCalc has a new \u201csquared\u201d button, which can be called directly:<\/p>\n\n\n\n

    betaCalc.setValue(3); \/\/ => 3\nbetaCalc.plus(2); \u00a0 \u00a0 \/\/ => 5\nbetaCalc.squared(); \u00a0 \/\/ => 25\nbetaCalc.squared(); \u00a0 \/\/ => 625<\/code><\/pre>\n\n\n\n

    There\u2019s a lot to like about this system. The plugin is a simple object-literal that can be passed into our function. This means that plugins can be downloaded via npm and imported as ES6 modules. Easy distribution is super important!<\/p>\n\n\n\n

    But our system has a few flaws.<\/p>\n\n\n\n

    By giving plugins access to BetaCalc\u2019s this<\/code>, they get read\/write access to all of BetaCalc\u2019s code. While this is useful for getting and setting the currentValue<\/code>, it\u2019s also dangerous. If a plugin was to redefine an internal function (like setValue<\/code>), it could produce unexpected results for BetaCalc and other plugins. This violates the open-closed principle<\/a>, which states that a software entity should be open for extension but closed for modification.<\/p>\n\n\n\n

    Also, the \u201csquared\u201d function works by producing side effects<\/a>. That\u2019s not uncommon in JavaScript, but it doesn\u2019t feel great \u2014 especially when other plugins could be in there messing with the same internal state. A more functional<\/a> approach would go a long way toward making our system safer and more predictable.<\/p>\n\n\n

    A better plugin architecture<\/h3>\n\n\n

    Let\u2019s take another pass at a better plugin architecture. This next example changes both the calculator and its plugin API:<\/p>\n\n\n\n

    \/\/ The Calculator\nconst betaCalc = {\n\u00a0 currentValue: 0,\n\u00a0\u00a0\n\u00a0 setValue(value) {\n\u00a0 \u00a0 this.currentValue = value;\n\u00a0 \u00a0 console.log(this.currentValue);\n\u00a0 },\n\u00a0\n\u00a0 core: {\n\u00a0 \u00a0 'plus': (currentVal, addend) => currentVal + addend,\n\u00a0 \u00a0 'minus': (currentVal, subtrahend) => currentVal - subtrahend\n\u00a0 },\n\u2028\n\u00a0 plugins: {}, \u00a0 \u00a0\n\u2028\n\u00a0 press(buttonName, newVal) {\n\u00a0 \u00a0 const func = this.core[buttonName] || this.plugins[buttonName];\n\u00a0 \u00a0 this.setValue(func(this.currentValue, newVal));\n\u00a0 },\n\u2028\n\u00a0 register(plugin) {\n\u00a0 \u00a0 const { name, exec } = plugin;\n\u00a0 \u00a0 this.plugins[name] = exec;\n\u00a0 }\n};\n\u00a0\u00a0\n\/\/ Our Plugin\nconst squaredPlugin = {\u00a0\n\u00a0 name: 'squared',\n\u00a0 exec: function(currentValue) {\n\u00a0 \u00a0 return currentValue * currentValue;\n\u00a0 }\n};\n\u2028\nbetaCalc.register(squaredPlugin);\n\u2028\n\/\/ Using the calculator\nbetaCalc.setValue(3); \u00a0 \u00a0 \u00a0\/\/ => 3\nbetaCalc.press('plus', 2); \/\/ => 5\nbetaCalc.press('squared'); \/\/ => 25\nbetaCalc.press('squared'); \/\/ => 625<\/code><\/pre>\n\n\n\n

    We\u2019ve got a few notable changes here.<\/p>\n\n\n\n

    First, we\u2019ve separated the plugins from \u201ccore\u201d calculator methods (like plus<\/code> and minus<\/code>), by putting them in their own plugins object. Storing our plugins in a plugin<\/code> object makes our system safer. Now plugins accessing this can\u2019t see the BetaCalc properties \u2014 they can only see properties of betaCalc.plugins<\/code>.<\/p>\n\n\n\n

    Second, we\u2019ve implemented a press<\/code> method, which looks up the button\u2019s function by name and then calls it. Now when we call a plugin\u2019s exec<\/code> function, we pass it the current calculator value (currentValue<\/code>), and we expect it to return the new calculator value.<\/p>\n\n\n\n

    Essentially, this new press<\/code> method converts all of our calculator buttons into pure functions<\/a>. They take a value, perform an operation, and return the result. This has a lot of benefits:<\/p>\n\n\n\n