Content Management and Capture

 View Only

A viable approach of adopting modern frontend technologies and migrating legacy UI in an incremental manner

By Kai Zhang posted Fri June 10, 2022 10:27 AM

  
Web frontend technologies have been evolving rapidly like never before in the past decade. New technologies bring many benefits, including improved development experience, enhanced security, and better performance. However, it's a challenge for enterprise applications to catch up with. Many are still built with jQuery, Dojo, the legacy AnguarJS 1.x, etc. Enterprise applications UI tends to update at a slower pace. The huge effort of re-implementing the complex functionality and user interactions is always an obstacle of adopting the new technologies. In this blog, I'll discuss a viable approach to enable the migration in an incremental manner, which can co-exist with other development activities on the legacy UI and minimize the impact on the releases.
Disclaimer: all code snippets in this blog are pseudo code for illustration purposes only. You can't run them directly.

The challenge

Modern UI technologies such as React, Angular, and Vue are very different from their counterpart 10 years ago. They are (more or less) designed with declarative programming paradigm and are built heavily on Virtual Dom. They don't work directly with the legacy technologies based on imperative programming paradigm and direct Dom manipulations, such as Dojo, jQuery, or even the legacy AngularJS 1.x. In most of the cases, "migrating" the legacy UI means rewriting it with the new technologies; very little legacy code may be reused. For complex enterprise applications, a complete overhaul could be an effort of years and a halt of new feature development, which provides little immediate business value.

A viable solution

Technically, we can't dodge the complete rewrite of the code for the migration - this is what we have to do eventually. What we can do is to explore and establish the mechanism of running the legacy and the new code in parallel, so we can carry out the migration in an incremental and methodical manner and minimize the risk and the impact to the release cycles. In Business Automation Content Analyzer 1.5, we implemented such a mechanism, which allowed us to incrementally migrate the legacy UI based on AngularJS 1.7 to React and to adopt the new IBM Carbon Components.
The legacy UI of Business Automation Content Analyzer 1.5 was a Single Page Application (SPA) built on the legacy AngularJS 1.7. It talks to the backend with REST API. Like most of the web applications, it maintains the authentication session using cookies. Logically, the UI can be broken down into about 10 functional modules, such as Document Processing, Model Design, User Management, Audit Log, etc. They have their own sub pages which are tightly inter-connected. However, pages between functional modules are relatively independent and inter-connected with "artificial links" or navigation bar via Angular router. The plan was to rewrite those functional modules one by one using React. When a new React module/pages are ready, we want to make it a "hidden feature" first that requires a feature flag in the URL or configuration file to make it visible. It allows QA to test it end-to-end. It also allows us to collect feedback from customers' early adoption programs. When the new module is production-ready, we make it public by default, and we keep the legacy code there just in case if there's a show shopper, we can still switch back to the legacy UI easily. We would completely retire the legacy UI when we complete the migration of all pages.

How it works

We converted the project into a multi-page application (MPA) using Webpack's multiple entry points. The entire legacy AngularJS app becomes one of the entry points of the naturally. We could build the entire React app as another entry point, but we decided to further break it down and make it a native MPA for better page loading experience. (see the discussions on optimizations section below.) Therefore, we create an entry point for each React functional module, each of which is a small SPA technically and can have multiple sub pages. Webpack builds each entry point into an html file with referece to the associate JS, CSS and image resources. The Webpack config file looks like:

module.exports = {
    mode: 'production',
    target: 'web',
   entry: {
        'user-mgmt': ['./src/common/polyfills.js', './src/user-mgmt/index.js'],
        'audit-log': ['./src/common/polyfills.js', './src/audit-log/index.js'],
        'model-design': ['./src/common/polyfills.js', './src/model-design/index.js'],
        ...
   },
    output: {
       path: './react-app'
   },
    ...
};

On the server side, we need to adjust the HTTP server and let it load from multiple entry points. We use NodeJS express as the HTTP server. It's not ideal to serve static resources using NodeJS, but it's Ok for the Business Automation Content Analyzer's use case where the UI is mainly used by a small group of business analyst. It's possible to serve multi-page app with an HTTP server like nginx, but NodeJS has the unique advantage that we can load our own configuration easily and control what to send in the response programmatically. Basically, we implemented the logic to load the entry point based on the request's path and the configuration file. The configuration file looks like:
react-config.json:
{
    "user-mgmt": {
        "entryPoint": "/react-app/user-mgmt/index.html",
        "enabled": true
   },
    "audit-log": {
        "entryPoint": "/react-app/audit-log/index.html",
        "enabled": true
   },
    "model-design": {
        "entryPoint": "/react-app/model-design/index.html",
        "enabled": true
   },
    ...
}
In the NodeJS's server app.js, we add the following code for serving the react pages:
// Import the react-config.json file
const reactConfig = require('./react-config.json')

// Middleware to server the react pages
app.use((req, res, next) => {

    // Determine if the request is for a react page
    const reactPageConfig = reactConfig[req.path];

   
// Check if the method is GET and the react page config exists and is enabled.
    if (req.method.toUpperCase === 'GET' && reactPageConfig && reactPageConfig.enabled) {

       
// All good, send back the react page entry point.
        res.sendFile(reactPageConfig.entryPoint);
    } else {
       // Fall back to other middleware
        next();
   }
});

// Middleware to serve all other static files from both the React App and the legacy AngularJS App.
app.use(serveStatic('/react-app'));
app.use(serveStatic('/angularjs-app'));
Interacting between the new React pages and the legacy pages.
At this point, the browser can load the pages and maintain the login session between those pages. However, the legacy Angular app has no idea about React pages. We need to wire them up. We simply use the old-school <a> tag to link them together:
<a href="/user-mgmt">User management</a>
It's possible to pass over some data in the link via query parameters:
<a href="/model-design?model-id=12345">Design the model</a>
Obviously, it's not suitable to pass large mount of data or sensitive information via query parameters. This is the main limitation of this approach.

Summary

In this blog we discussed a viable approach of migrating legacy web apps to new technologies. The approach allows migrating the legacy web app in an incremental manner, and the migration can stretch multiple releases without significant business impacts. The approach requires you to identify loosely coupled "functional modules" of you application, and then you can migrate them one by one. Each "functional module" is bundled as a small and independent SPA. They can co-exist with the legacy application in the same browser session. They are wired up with the old school <a> tag, and they can exchange simple and non-sensitive parameters using query parameters.

This approach does have the limitation that the migration has to be at the page level. In some rare situations, we do need to run UI components with incompatible technologies in the same page. I'll discuss a supplemental approach for this requirement in the next blog.

Permalink