DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
Building Scalable Real-Time Apps with AstraDB and Vaadin
Register Now

Trending

  • Authorization: Get It Done Right, Get It Done Early
  • Redefining DevOps: The Transformative Power of Containerization
  • Introduction To Git
  • Auto-Scaling Kinesis Data Streams Applications on Kubernetes

Trending

  • Authorization: Get It Done Right, Get It Done Early
  • Redefining DevOps: The Transformative Power of Containerization
  • Introduction To Git
  • Auto-Scaling Kinesis Data Streams Applications on Kubernetes
  1. DZone
  2. Coding
  3. JavaScript
  4. Scale Your Frontend Application

Scale Your Frontend Application

Bogdan Nechyporenko user avatar by
Bogdan Nechyporenko
CORE ·
Mar. 06, 20 · Tutorial
Like (4)
Save
Tweet
Share
9.86K Views

Join the DZone community and get the full member experience.

Join For Free

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. 

Module architecture

Module architecture

   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) Home page
    Home page
  • View users (User)User page
    User page
  • Add new users (Admin) Admin page
    Admin page
  • 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
      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

JSON
 




x


 
1
{
2
  "name": "admin",
3
  "entry": "/modules/admin/index.js",
4
  "options": {
5
    "tab": {
6
      "title": "Admin"
7
    }
8
  }
9
}



And modules/setting/metadata.json with snippet:

JSON
 




x


 
1
{
2
  "name": "settings",
3
  "entry": "/modules/settings/index.js",
4
  "options": {
5
    "tab": {
6
      "title": "Settings"
7
    }
8
  }
9
}



Application workflow

Application workflow


In Express, to make this folder easily accessible from our frontend, we need to make this folder serve a static folder.

JavaScript
 




x


 
1
gulp.task('server:start', (cb) => {
2
    app.use(express.json());
3
    app.get('/', (req, res) => {
4
        res.sendFile(`${paths.distDir}/index.html`);
5
    });
6
    registerDatabaseApi();
7
    app.use(express.static(`${paths.projectDir}/dist`));
8
    app.use('/modules', express.static(`${paths.projectDir}/modules`)); // that happens here
9
    app.listen(serverPort, () => {
10
        log.info(`DSFA is started on port ${serverPort}!`);
11
        cb();
12
    });
13
});



On the frontend, we need to be aware that this file has to be there and request it in api-resource.js.

JavaScript
 




xxxxxxxxxx
1


 
1
export const getModulesMetadata = () => httpRequest('GET', 'modules/modules-metadata.json');



That call is done from redux-saga, so once data is received, it’s going to be saved in the redux store.

JavaScript
 




xxxxxxxxxx
1


 
1
export function* loadCustomModulesSaga() {
2
    try {
3
        const {data: {modules = []}} = yield call(getModulesMetadata);
4
        yield putResolve(customModulesActions.set(modules));
5
    } catch (exception) {
6
        yield put(toastrActions.show('Error', exception, TOASTR_TYPE.error));
7
    }
8
}



The main application component listens to changes, and when it finds changes in modules, it reacts to them (app.js).

JavaScript
 




xxxxxxxxxx
1


 
1
const mapStateToProps = (state) => ({
2
    bootstrappedModules: state.bootstrap.bootstrappedModules,
3
    permissions: state.permissions,
4
    modules: state.customModules, // here
5
    users: state.users
6
});
7

          
8
@connect(mapStateToProps)
9
export class App extends Component {


 
There are two places that are adapted to module changes:

HTML
 




xxxxxxxxxx
1


 
1
<CustomLinks modules={modules} restrictedModuleNames={restrictedModuleNames}></div>



This part adds more navigational components, on which user can click and go there.

JavaScript
 




xxxxxxxxxx
1


 
1
<CustomRoutes
2
    bootstrappedModules={bootstrappedModules}
3
    modules={modules}
4
    restrictedModuleNames={restrictedModuleNames}
5
></div>



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

JavaScript
 




xxxxxxxxxx
1
19


 
1
useEffect(() => {
2
    if (R.not(R.includes(moduleName, bootstrappedModules))) {
3
        const script = document.createElement('script’); 
4
        script.src = scriptUrl; // that’s a backend URL by which the JS bundle of custom module is accessible. 
5
        script.async = true; // no need to do it synchronously. 
6
        script.onload = () => {
7
            setLoadedModuleName(moduleName); // once module is loaded we can proceed with connecting it to the core module
8
        };
9
        document.body.appendChild(script); 
10
    } else {
11
        setLoadedModuleName(moduleName);
12
    }
13

          
14
    return () => {
15
        if (context.saga) { // once component is unmounted, we also cancel all running sagas.
16
            context.saga.cancel();
17
        }
18
    };
19
}, []);



Then, we get code out of a mounted bundle and incorporating it into a core module 

JavaScript
 




xxxxxxxxxx
1
18


 
1
if (R.complement(R.isNil)(loadedModuleName)) {
2
    const {
3
        component: Component,
4
        reducers,
5
        saga
6
    } = window[loadedModuleName].default;
7

          
8
    if (R.not(R.includes(loadedModuleName, bootstrappedModules))) {
9
        if (saga) { // it makes not compulsory for module to expose sagas, if it doesn’t have it 
10
            context.saga = window.dsfaSaga.run(saga); // window.dsfaSaga - is a reference to a joint middleware created in core module, so then we can run sagas brought by this custom module.
11
        }
12
        if (reducers) { // // it makes not compulsory for module/plugin to expose reducers, if it doesn’t have it
13
            window.dsfaReducerRegistry.register(reducers); // window.dsfaReducerRegistry - is a joint register, created by a core module, and is used to register/unregister reducers from the entire system. Here we add new reducers brought by this custom module.
14
        }
15
        dispatch(applicationActions.moduleBootstrapped(loadedModuleName)); // marks that current module is loaded and prevents of loading it once again.
16
    }
17
    return <Component></div>;
18
}


    

Dynamically loading components workflow

Dynamically loading components workflow

To understand where window.dsfaReducerRegistry  and  window.dsfaSaga come from, have a look at  configure-store.js  file:

JavaScript
 




xxxxxxxxxx
1


 
1
const sagaMiddleware = createSagaMiddleware();
2
window.dsfaSaga = sagaMiddleware;
3

          
4
export const reducerRegistry = new ReducerRegistry(allReducers);
5
window.dsfaReducerRegistry = reducerRegistry;



For making it possible to register reducers from different parts of an application a bit of wrapper is created, as you noticed that ReducerRegister:

JavaScript
 




xxxxxxxxxx
1
24


 
1
export class ReducerRegistry {
2
    constructor(initialReducers = {}) {
3
        this.reducers = {...initialReducers};
4
        this.emitChange = null;
5
    }
6

          
7
    register(newReducers) {
8
        this.reducers = {...this.reducers, ...newReducers};
9
        if (this.emitChange !== null) {
10
            this.emitChange(this.getReducers());
11
        }
12
    }
13

          
14
    getReducers() {
15
        return {...this.reducers};
16
    }
17

          
18
    setChangeListener(listener) {
19
        if (this.emitChange !== null) {
20
            throw new Error('Can only set the listener for a ReducerRegistry once.');
21
        }
22
        this.emitChange = listener;
23
    }
24
}



And when the store is created, you need to create a reducer:

JavaScript
 




xxxxxxxxxx
1
10


 
1
export const configureStore = (history) => {
2
    const mainReducer = configureReducers(reducerRegistry.getReducers());
3
    const store = createStoreWithMiddleware(history)(mainReducer);
4

          
5
    reducerRegistry.setChangeListener((reducers) => {
6
        store.replaceReducer(configureReducers(reducers));
7
    });
8
    window.dsfaStore = store;
9
    return store;
10
};



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 webpack.config.plugin.common.js: 

JavaScript
 




xxxxxxxxxx
1


 
1
output: {
2
    filename: 'index.js',
3
    globalObject: 'window’, // That’s the way how to make some points of the system shared between bundles.  
4
    library: pluginName,
5
    libraryTarget: 'umd’, // ! globalObject will work only if target is udm
6
    path: `${paths.modulesDistDir}/${moduleName}`,
7
    publicPath: '/'
8
},
9

          



And also externals, to not duplicate third-party libraries:

JavaScript
 




xxxxxxxxxx
1
13


 
1
const createItem = (key, globalName) => ({
2
    [key]: {
3
        amd: key,
4
        commonjs: key,
5
        commonjs2: key,
6
        root: globalName
7
    }
8
});
9

          
10
export const externals = {
11
    ...createItem('react', 'React'),
12
    ...createItem('react-dom', 'ReactDOM')
13
};



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 globals.js. 

JavaScript
 




xxxxxxxxxx
1


 
1
import React from 'react';
2
import ReactDOM from 'react-dom';
3

          
4
window.React = React;
5
window.ReactDOM = ReactDOM;



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.

JavaScript
 




xxxxxxxxxx
1
19


 
1
export default class CatchError extends PureComponent {
2
    static propTypes = {children: PropTypes.element};
3

          
4
    constructor(props) {
5
        super(props);
6
        this.state = {hasError: false};
7
    }
8

          
9
    componentDidCatch() { // This part does the logic to keep the application alive.
10
        this.setState({hasError: true});
11
    }
12

          
13
    render() {
14
        if (this.state.hasError) {
15
            return <h1>Plugin has not been loaded successfully due to found errors</h1>;
16
        }
17
        return this.props.children;
18
    }
19
}



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). 

Easy level

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?

Medium level

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!


Further Reading

  • Redux: A Practical Tutorial.
  • Application Scalability — How To Do Efficient Scaling.
  • Build Micro Front-Ends Using Angular Elements: The Beginner's Guide.
application JavaScript

Opinions expressed by DZone contributors are their own.

Trending

  • Authorization: Get It Done Right, Get It Done Early
  • Redefining DevOps: The Transformative Power of Containerization
  • Introduction To Git
  • Auto-Scaling Kinesis Data Streams Applications on Kubernetes

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com

Let's be friends: