Plugging 23 January 2014
When working with node applications I've often needed a plugin architecture, whether it be with personal projects, the Adapt framework or more recently jsbin. Plugins can make an application much more flexible, especially if it's distributable or a framework. They enable both you and other developers to add to and build upon your application in a non-intrusive way, with no modification to core code.
When integrating the Stripe payment service into jsbin, we had to ensure that if it was running on a local machine or an environment that didn't support stripe, jsbin would continue to work. Our github auth module was built with the same idea. Here's a quick run through how that was done.
First we require it and pass some options
<span class="c1">//...</span></code></pre></figure>
Then we initialize it
<span class="k">if</span> <span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">github</span> <span class="o">&&</span> <span class="nx">options</span><span class="p">.</span><span class="nx">github</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">github</span><span class="p">.</span><span class="nx">initialize</span><span class="p">(</span><span class="nx">app</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">express</span><span class="p">.</span><span class="nx">urlencoded</span><span class="p">());</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">express</span><span class="p">.</span><span class="nx">json</span><span class="p">());</span>
<span class="c1">//...</span></code></pre></figure>
After using the same technique with the stripe module, I realised that our app.js is going to become bloated if plugins keep getting added, so I set to work on a plugin architecture that is scalable and allows plugins to be added and removed with no application code change.
This lead to Plugging, a small, easy to use module that will make all of the above easy and painless.
To install to your project just run:
Plugins can take two forms, either as an object with a predefined 'init' method or as a function. Here are two basic examples.
As a function
<span class="kd">var</span> <span class="nx">APIRouteHandler</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./lib/handlers/api'</span><span class="p">),</span>
<span class="nx">APIAuth</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./lib/handlers/auth'</span><span class="p">),</span>
<span class="nx">APIParamParser</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./lib/params/api'</span><span class="p">);</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">app</span><span class="p">,</span>
<span class="nx">APISecurityEnabled</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">API</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">APISecurityEnabled</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">param</span><span class="p">(</span><span class="s1">'apiKey'</span><span class="p">,</span> <span class="nx">APIParamParser</span><span class="p">);</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="sr">/api/</span><span class="err">:</span><span class="nx">apiKey</span><span class="p">,</span> <span class="nx">APIRouteHandler</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
As an object
<span class="kd">var</span> <span class="nx">APIRouteHandler</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./lib/handlers/api'</span><span class="p">),</span>
<span class="nx">APIAuth</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./lib/handlers/auth'</span><span class="p">),</span>
<span class="nx">APIParamParser</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">'./lib/params/api'</span><span class="p">),</span>
<span class="nx">APISecurityEnabled</span><span class="p">;</span>
<span class="nx">exports</span><span class="p">.</span><span class="nx">init</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">app</span><span class="p">;</span>
<span class="nx">APISecurityEnabled</span> <span class="o">=</span> <span class="nx">options</span><span class="p">.</span><span class="nx">API</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">APISecurityEnabled</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">param</span><span class="p">(</span><span class="s1">'apiKey'</span><span class="p">,</span> <span class="nx">APIParamParser</span><span class="p">);</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="sr">/api/</span><span class="err">:</span><span class="nx">apiKey</span><span class="p">,</span> <span class="nx">APIRouteHandler</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">exports</span><span class="p">.</span><span class="nx">checkAPIKey</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">APISecurityEnabled</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">APIParamParser</span><span class="p">(</span><span class="nx">key</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></figure>
Plugins as functions are my most common use case. Plugins that export an object for use as a public API aren't plugins in the sense that they can be removed from a project and it'll continue to work. However, I've added support for them as I can see them being used in applications.
Loading
Plugins are loaded via the initial call to the Plugging library, and can either be executed immediately or later in the app. If your plugins need to be started at different times, you can have multiple calls to Plugging and store both references to start them up at seperate times.
Deffered plugin loading
<span class="kd">var</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">configure</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
<span class="c1">//...</span>
<span class="p">})</span>
<span class="kd">var</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">createServer</span><span class="p">(</span><span class="nx">app</span><span class="p">);</span>
<span class="nx">server</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">'port'</span><span class="p">));</span>
<span class="nx">plugging</span><span class="p">.</span><span class="nx">start</span><span class="p">({</span>
<span class="na">app</span><span class="p">:</span> <span class="nx">app</span><span class="p">,</span>
<span class="na">API</span><span class="p">:</span> <span class="nx">options</span><span class="p">.</span><span class="nx">API</span>
<span class="p">})</span> <span class="c1">// this initializes the plugins passing all parameters to the plugin</span></code></pre></figure>
Immediate plugin loading
<span class="kd">var</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">configure</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
<span class="c1">//...</span>
<span class="p">})</span>
<span class="kd">var</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">createServer</span><span class="p">(</span><span class="nx">app</span><span class="p">);</span>
<span class="nx">server</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">'port'</span><span class="p">));</span>
<span class="kd">var</span> <span class="nx">pluginOptions</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">app</span><span class="p">:</span> <span class="nx">app</span><span class="p">,</span>
<span class="na">API</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">'port'</span><span class="p">)</span> <span class="o">===</span> <span class="nx">options</span><span class="p">.</span><span class="nx">APIPort</span> <span class="p">?</span> <span class="kc">true</span> <span class="p">:</span> <span class="kc">false</span>
<span class="p">}</span>
<span class="c1">// If pluggings is passed more than one argument it initialises </span>
<span class="c1">// plugins immediately passing all extra parameters to plugins</span>
<span class="nx">require</span><span class="p">(</span><span class="s1">'plugging'</span><span class="p">)(</span><span class="nx">__dirname</span> <span class="o">+</span> <span class="s1">'/plugins'</span><span class="p">,</span> <span class="nx">pluginOptions</span><span class="p">);</span></code></pre></figure>
Configuration
You can configure how Plugging works on a basic level. At the moment there are two config options but I'm open to adding more as and when requested.
These are the default configs, setting "node_modules": true
will tell Plugging to scan the top
level of your node_modules, look for anything with a "plugging: true"
in the package.json
and use that as a plugin. The init
key is the name of the method that gets called on object
style plugins when they're started.