As a continuation to my first article, I prepared an implementation of a simple project to show a workable solution.
Before going into detail, I’d like to note that I decided to change the name to Scale instead of Pluggable, as it better highlights the potential of this architecture. The idea is not only to create plugins for an application but actually to drive the development of frontend itself from multiple sources. And the core feature of this itself has to be to split the application into pieces and not create monolith.
The only mechanism that must be in the heart of the system is the line of communication between components, the logic that loads necessary scripts dynamically, and the process of gluing it together with defined rules. There are of course many other aspects that you must take into account like:
- How to share the same event messages across different evolutions of the system (like versioning, etc.).
- How to upgrade libraries across all modules in a less intrusive way.
- How to set up the development environment of one module without a need to boot up a monster application.
- How to set up an infrastructure for E2E tests and keep these tests in the repository of the module.
- And many more.
Before diving into all of these challenges, in this article, I’ll introduce you to the first implementation of a simple application. Currently, the code for this article is in master. Once I polish it and start working on a more advanced part of it, I’ll move it to branch part-1.
To set up the project locally, you need to have the latest version of Node.js and yarn and gulp globally installed.
You may also like:
The Fundamentals of Redux.
Once that’s done, running
gulp is enough to set up the entire infrastructure of the project. To make it simple for setting it up and demonstrating from the single place, I placed three modules into one GitHub repository. Though in a real-life scenario, they would be broken up into three separate repositories.
The example I created is for a User Management System, where it is possible to:
- See a home page with some basic information about the site (Home)
- View users (User)
- Add new users (Admin)
- Grant/revoke permissions of allowing individuals to remove users or hiding/showing admin page (Settings)
- When the “allow to remove users” permission is enabled, a trash icon appears on the user page.
- When “allow to view admin page” is disabled, the navigation to “Admin” disappears.
Setting admin permissions
“Home” and “User” pages come together with the main application and two others are loaded during system boot-up.
First, we need to configure our backend to read metadata about our core modules and provide the API to load. In this project, we created folder
“modules” for that. Here, the compiled modules thta are ready for loading are distributed.
The only file created manually is
modules-metadata.json. Again, this is done for simplicity. In a real-world scenario, it will be inside of each module, like
modules/admin/metadata.json with snippet
modules/setting/metadata.json with snippet:
In Express, to make this folder easily accessible from our frontend, we need to make this folder serve a static folder.
On the frontend, we need to be aware that this file has to be there and request it in
That call is done from redux-saga, so once data is received, it’s going to be saved in the redux store.
The main application component listens to changes, and when it finds changes in modules, it reacts to them (
There are two places that are adapted to module changes:
This part adds more navigational components, on which user can click and go there.
This component actually loads the plugin through an API call, attaches it to the HTML page, pulls all sources from that bundle, and connects it to the appropriate parts of the core module.
CustomRoutes uses the
LoadModule component to perform these actions.
Let’s watch this process piece-by-piece:
First, we load the script and wait until it is mounted to the page
Then, we get code out of a mounted bundle and incorporating it into a core module
To understand where
window.dsfaSaga come from, have a look at
For making it possible to register reducers from different parts of an application a bit of wrapper is created, as you noticed that
And when the store is created, you need to create a reducer:
As new reducers come, they are replaced in the store. And the same thing happens here with sharing the Redux store via a window with custom modules. Why use a window? Because that’s a way how is it possible to make a shared objects between different modules compiled individually by webpack.
Let’s have a look at this configuration, which is required to be done in custom module
And also externals, to not duplicate third-party libraries:
That scrapes the bundle of React and react-dom from a custom plugin. React doesn’t complain about the two versions of itself in the application (and crashes with react-redux in that case) and makes plugin really slight in comparison with a fully-packed version provided in case of iframe usage. The list of these externals will grow with the number of libraries you will use.
On the side of core module, you need to register such modules globally
If in some cases plugin can’t be loaded successfully, to not crash the whole application, we have to isolate it, catch exception and show to the user information about it. For that, the
CatchError component is created.
This is, in essence, all of the major parts of the application described above. The rest of the code is to make the application more or less appealing with bare minimum functionality. I encourage you to download the working scenario; setup is super easy. Play around with it! Add some goal, to enhance it with some small feature and feel how is the development going for you. For example, take that you are a developer who is asked to add “history” module with own tab and show all performed actions (adding/removing users).
With the current given implementation what is the bare minimum you will modify in core module and what will you add to a history-module? How are you going to communicate in an efficient way?
How core plugin has to be rewritten so that new history or any other potential functionality won’t require changes in the core module? As this is an ultimate goal of this architecture.
I’m curious and excited to see your forks/PRs/smart and genius ideas regarding that. Looking forward for feedbacks and comments as well!