Very few business information systems these days use anything other than the web platform as the user interface. It is also regarded as wise to only include presentation logic in the frontend; pushing as much business logic into the backend as possible. However, there are always exceptions and in some specialist applications such "rules" are occasionally bent for sound architectural reasons.
An increasingly popular architecture these days is the JAMStack where the frontend is hosted in the same way as a static website (via a CDN for example) but with more dynamic behaviour. In this architecture there tends to be some specific business logic in the frontend with the "heavy lifting", such as storage and authentication, provided in a generic manner through third-party APIs.
This novel application design offers a great deal of resilience and flexibility but not necessarily the extensibility that can be achieved using a plug-in architecture. When an application requires greater extensibility or the ability to call on functionality not necessarily developed as part of the original application, or even by the same developers, a plug-in architecture is a candidate solution. As described below, there are several possible approaches but using a web worker can provide significant benefits.
In order to discuss a variety of approaches around a common context we will start by describing an example use case (application) used to exercise and demonstrate.
Test scenario
In this very simple example (honestly I do not think I could envisage a simpler) we will perform the four basic mathematical operations (addition, subtraction, multiplication, division) on two numbers and return the answer. Plugins will provide the actual arithmetic operations.
The 'application' will provide the inputs and present the output. In addition the program will;
- load the list of available plugins (manifest)
- present buttons for each plugin
- when the button is pressed, the plugin will be loaded, passed the inputs and execute with the output returned for presentation.
All four plugins will expose a single 'calculate' function that accepts two numeric inputs and returns the result.
Example Plugin: addition.js
function calculate(num1, num2) {
return num1 + num2;
}
List of plugins (manifest.json)
[
{
"pluginName": "Addition",
"pluginFile": "addition.js"
}
]
A working example can be found in my web-worker GitHub repository.
Exploring candidate solutions
How can a plug-in architecture be achieved when desktop applications employ web technologies and, more specifically, operate in the web browser. The answer, or at least part of it, is the Web Worker API. Granted, I am discussing a rather exotic requirement in the wide world of web applications but as the capability of the web browser increases such potential solutions become possible.
In a plugin architecture it is usual for the application to load a specific file at runtime, which we will call the manifest, that details what plugins are available and how to find/load them. This file could be sourced from outside the application easily using a AJAX/XHR request and having the data in JSON format.
Before making the plug-in model available for 'foreign' developers the application developers need to establish a Software Development Kit (SDK). As a minimum the SDK should be a document outlining the API ('api'), which outlines the interface the application expects all plugins to support. In addition, the SDK can provide test code developers can use to exercise and validate their plugin before using it with the application.
Some less effective alternatives
Before we dive deeper into the technicalities of the Web Worker mechanism, we will explore a couple of ways in which similar functionality can be achieved and their limitation.
At the crux of the following examples is need for a mechanism to load scripts on demand and with the name of the file/module supplied at runtime. The name of the "plugins" are not know at build time, but supplied through a file loaded at runtime, and certainly not included in the application bundle.
In the example code we have to demonstrate each of the approaches described below there is a single application (index.html) with a script element for each approach. For each script there is a loosely-couped plugin (JS) file.
index.html
<body>
<script src="dom-injection.js"></script>
<script type="module" src="dynamic-import.js"></script>
</body>
DOM inject
dom-injection.js
console.log('dom-injection');
function loadScript(scriptPath, functionName) {
window[functionName] = () =>
console.log(`${functionName} is not yet loaded`);
const scriptElement = document.createElement('script');
scriptElement.type = 'text/javascript';
scriptElement.async = true;
scriptElement.src = scriptPath;
scriptElement.onload = () => {
console.log(`${functionName}() is now ready`);
};
document.body.appendChild(scriptElement);
}
loadScript('./hello-world.js', 'helloWorld');
hello-world.js
function helloWorld() {
alert('Hello, World! (injection)');
}
1) injection of script tags into the DOM
2) Pollution of the global namespace
Dynamic imports
dynamic-import.js
console.log('dynamic-import');
async function loadScript(path, functionName) {
window[functionName] = () =>
console.log(`${functionName} is not yet loaded`);
const module = await import(path);
window[functionName] = module.default;
console.log(`${functionName}() is now ready`);
}
loadScript('./hello-world.module.js', 'modHelloWorld');
hello-world.module.js
function helloWorld() {
alert('Hello, World! (import)');
}
export default helloWorld;
1) Web bundlers
2) Short lived / not cacheable
Both: execution in the primary thread
Output
Viewing the index.html in the web browser will produce the following output in the developer console.
This shows the two scripts loading followed by the two loadScript functions running to load the stipulated plugin script and execute the named function it contains. In both scenarios the function is initialised with a temporary version that is never executed but prevents an error being reported should the function be called before it is loaded.
A worked example (Web Worker)
Let's start by listing the 'moving' parts:
- The application that will utilise the plugins
- The manifest file that details what plugins are available at runtime
- The web-worker code that will load, execute (and potentially cache) plugins
- Finally, the plugins themselves.
The Scenario
We will use simple mathematical operations, each their own plug-in, to perform Celsius and Fahrenheit temperature conversion. To recap, here are the calculations.
Fahrenheit = (Celsius * 9) / 5 + 32
Celsius = (Fahrenheit - 32) * 5 / 9
These are the test cases we will use to exercise the process.
-40°C => -40°F
0°C => 32°F
32°F => 0°C
50°C => 122°F
122°F => 50°C
100°C => 212°F
212°F => 100°C
The main application file will load the test cases as a JSON file and initialise the primary Web Worker. Each test case will be sent one-by-one to the primary worker for calculation and the response output by the application code.
The initial call to the primary worker will include a reference to the manifest that will be loaded in preparation for subsequent calls.
For each test case called three plug-ins will be used, with the first use also involving loading of the plug-in itself.
Exercising each test case will involve splitting the input string into its measurement and scale components. The measurement will be converted into a number whilst the scale will identify which formula is to be adopted and therefore which plugins will be called.
NB: In the following code example I have deliberately excluded any defensive code to keep it on topic. The source code can be found in this GitHub repo.
Word of Caution
This kind of architecture should only be considered for use in a controlled environment and only where the authors of the plugins can be trusted. Failing to take adequate precautions would have the potential of exposing the user to considerable risk.





Top comments (0)