background

With the rise of the Internet cloud, the concept of a number of different services concentrated on a large platform unified and open to the outside world is gradually familiar to people, and more and more cloud related or unrelated in the background management system or enterprise information system has or began to use this “unified platform” form. At the same time, the front-end field has maintained rapid development. The EARLY MVC solution of jQuery+Backbone+Bootstrap has supported the business for quite a long time. Later, Angular, Ember, and other MVVM frameworks came to the fore, and the idea of front end separation and front end componentization had its heyday. But in China, Vue framework with its simple and easy to understand API and excellent surrounding ecological support is the only champion, more and more small and medium-sized enterprises and developers began to turn to Vue camp; At the same time, React, a unique pure View layer framework in design, began to rise. Its technology-sensitive Diff DOM idea attracted a large number of developers and became the hottest topic in major technology communities. Its surrounding ecology also developed rapidly and became the preferred framework for major companies to build technology stacks.

Back to the platform. In many cases, a large platform that integrates different businesses is divided into multiple subsystems for development, and finally the platform provides a unified entrance. In today’s rapidly changing front-end environment, such platforms need to consider the following challenges:

  1. How to concentrate different business subsystems into a large platform, unified open to the outside world?
  2. How do I grant permissions to different users to access specific service modules of the platform and deny access to unauthorized service modules?
  3. How to access the new subsystem quickly, manage the version of the subsystem, and ensure the function synchronization?
  4. For the old system, how to achieve smooth upgrade from Backbone technology stack to React technology stack or Vue technology stack?

Next, I’ll introduce our implementation solutions based on each of these questions.

The product model

Let’s start with the first question: how do you bring the different business subsystems together on one platform and open them to the outside world?

As shown in the figure below, suppose we have three business subsystems. If a user wants to use different functions in all three systems, he needs to log in to all three systems at the same time and switch back and forth.

In fact, the ideal state is: A, B, C three subsystems on the same large platform, through the menu to provide entry, users can freely access the page of any subsystem. As shown in the figure below:

Note that in the figure above, we label A, B and C as App (Application), and label the large platform as Product. In the following, for convenience, we call each subsystem as App, and call the platform integrating subsystems as Product.

In fact, for the real business scenarios, in addition to the improvement of the user experience, as shown in figure 2 system still has a lot of advantages, than if the enterprise wants to sell products according to the business module, the second way is clearly better, after the user module fee gives its module permissions can use a new module, presented to the user rather than a new system. In addition, avoiding the deployment of separate business systems also means saving resources on domain names, servers, and operation and maintenance, saving enterprise costs.

Architecture plan

After determining the Product model of Product including App, we should then consider in what form to enable each App access to achieve seamless switching under Product.

As shown in the figure below, when accessing the page, we attach an application prefix to the access path to identify which App is currently accessed. After the App path prefix is the currently accessed page path, which is a prerequisite convention.

From the perspective of Product, we hope that users will not feel that each App is switching between system modules when using the platform, so Product needs to control the view rendering time of all apps, that is, Product needs to manage view routes of all apps in a unified manner.

At the same time, in order to present different view pages to users with different permissions, we also pass the user permission data returned from the back end into Product, which will automatically filter out routes without permissions, as shown in the figure below:

Here, we adopted the form of single-page application (SPA) to realize the routing control of the Product, because the switching between apps is just like switching the pages of a system application for users.

The architecture of the whole scheme is shown in the figure below:

Under this architecture scheme, each sub-service module can be dynamically added to the platform as required, and the access path prefix can be masked when not needed. For the platform system, each sub-service module is like a function plug-in, plug and play, no need to plug and play. This kind of pluggable thinking has been around for a long time. We call it “pluggable application architecture”. The pluggable application architecture has the following advantages over the traditional front-end architecture:

  • Distributed development of business modules and easier management of code warehouse.
  • The business module (App) has strong portability and can be independently deployed or integrated into a large platform (Product).
  • Module code is highly cohesive and more business focused.
  • In accordance with the open/close principle, the new module does not need to modify the existing module and does not affect the functions of other modules.

Resource Rights Management

Before we get to the implementation of the architecture solution, we need to do some preparatory work. Let’s look at the second and third questions we raised at the beginning.

First, the second question: how do you give different users access to specific business modules of the platform while denying them access to non-privileged business modules?

As mentioned above, the backend passes access permission data into Product. Our specific approach is that each App passes its full routing path into Product. When launching the platform (Product), the Product will obtain the routing path with permission from the backend according to the currently logged in user. When accessing any route of the App, it will compare with the authorized route path for the first time, and the failed route path will automatically guide to the page view without permission.

As for route permission maintenance, you can create a visual route configuration management page and customize the level of permission based on your own service conditions.

Then there is the third problem: how to quickly access the new subsystem, and the subsystem version management, ensure that the function synchronization?

To answer this question, we need to know exactly how each App is accessed. As mentioned above, the access of each App depends on the current path prefix. Our specific approach is to maintain the addresses of all App bundles based on webpack, and pass the configuration mapping of these bundle addresses into Product. When accessing an App for the first time, The Product will first load the bundle related to the App, and its JS bundle will call the global Product to inject its own routing information, and then the following routing processing will be performed by the Product.

Of course, the above implementation involves some issues with rendering the App view, which we will cover in the next implementation.

Implementation scheme

So much for the theoretical stuff, let’s move on to the dry stuff: How do you implement a plug-and-plug application framework?

Based on some of the implementation ideas described above, we will first have a general outline of the functionality of the plug-and-pull framework we will implement:

  • Self-implement a Router. The Router needs to automatically resolve the App id according to the path during routing, and then dynamically load the resource package corresponding to the App based on the id.
  • It is executed immediately after the App loads its JS resource package, and the app-related routing information is automatically injected into the Product.
  • After the App loads the resource bundle (the script is executed immediately after loading), the Router tries to render the App view page according to the path.
  • If the route is switched to another sub-app, the original App should clear the relevant DOM and event logic based on its own life cycle.

To summarize, our plug-and-plug application framework should implement the following function points: dynamic routing, script loading and scheduling, subapplication view rendering, and application lifecycle management.

Next, we will introduce the realization idea of each function point one by one.

Dynamic routing

When it comes to routing, there are different implementations for different technology stacks. For example, Vue has vue-router, React has React-router, and so on. In order to adapt to the development of different sub-apps using different technical systems, we need to standardize and unify the routing configuration. Therefore, we needed to redesign a Router that could dynamically inject routes and simultaneously support rendering of components of different technology architectures.

Here, we use a flexible Universal router, whose path and action configuration enables us to easily handle the customized routing logic. Although it doesn’t support dynamic injection routing, the code is well organized, and with the well-known History library, I easily implemented a Router that met my needs.

As shown in the figure below:

Script loading and scheduling

After completing the basic functions of dynamic routing, we are ready to deal with the first step of routing logic: dynamically loading resource bundles such as the scripts currently accessing the App.

First of all, we analyze the processing process: At the beginning of routing, we need to determine which App the current path to be routed corresponds to according to the first path name of the request path (for example, the first segment of /a/b is a). If the corresponding App has not injected route information, we need to dynamically load the App resource package. After executing the JS script resource package, Then continue with the subsequent rendering logic.

App resource packs can be packaged in various forms, such as AMD, Commonjs, UMD, etc. In order to be compatible with apps that can be deployed separately and integrated into the platform, and to maintain the simplest dependencies, we still use a WebPack-based UMD package that lets JS execute immediately after loading, eliminating dependencies such as AMD package loaders such as Requirejs.

Our resource pack loader works well with the browser’s own script-loading mechanism: dynamically insert the resource pack address under the head and body tags using the link and script tags, respectively.

Of course, there is also the issue of sequential load dependencies in resource bundles. In general, WebPack will handle dependencies on its own. If there is a need for multiple resource pack plug-ins to be executed in sequence (such as jQuery plug-in dependencies), special serialization processing can be done at load time.

The App script loading process is shown in the figure below:

Apply view rendering

After handling the dynamic loading of App resource packs, we are ready to implement the core function of the routing module: rendering the application view.

First of all, when introducing the scheme above, we mentioned that each sub-app should not only support independent deployment, but also be able to access the Product and run on the platform. Therefore, we should realize that the rendering of each App view should be done by each subapp, rather than by the framework.

If that sounds too jarring to you, consider these two questions:

  1. If the framework renders the routing results uniformly, how can it be compatible with the React Component, Backbone View, etc.?
  2. If the framework has to import the rendering interface to unify the rendering routing results, how do you ensure that the interface version of each subapp (e.g. ReactDOM version, etc.) is compatible?

Therefore, in order to reflect the plug-and-pull design concept of the framework that takes into account the different technology systems of the App, we must take the rendering of the application view out of the framework.

So, what else does frame routing need to do in view rendering logic?

We will soon be reminded of the problem with the removal of view rendering logic: each subapp will have to implement its own rendering, so where does the framework improve performance? How unified should the rendering interface be?

I mentioned the open and Closed principle earlier. The main design idea of the open and Closed principle is object-oriented design. Our solution:

  1. An Application base class is provided to regulate the rendering interface. Each subapp must inject an Application instance inherited from the Application base class when injecting an Application.
  2. By default, React Application and Backbone Application, which are widely used, are provided. They both inherit from the Application base class.

In the entry JS file of each subapp, you can directly instantiate ReactApplication or BackboneApplication according to your own technical system, or you can inherit the rendering interface from the Application base class. Of course, if your application class is used more, you can contribute it as a plug-in.

Sample code for the Application base class:

// application/index.js
class Application {
  static DEFAULTS = {
    // ...
  }

  constructor(options = {}) {
    this._options = Object.assign({}, DEFAULTS, options);
  }

  start() {
    // Start the application and enable the view path change listening event
  }

  stop() {
    // Stop the path change listening event
  }

  renderLayout() {
    // Render the layout interface
  }

  render() {
    // The interface to render the body content
  }

  // ...
}
Copy the code

ReactApplication class:

// application/react/index.js
import Application from '.. /index.js';

class ReactApplication extends Application {
  render(err, children, params = {}) {
    if (err) {
      // Render the error page
      throw err;
    }
    // React and ReactDOM are passed by the App itself during instantiation, making it easier for each App to control the React version
    const { React, ReactDOM } = this._options;
    ReactDOM.render(children, this._container); }}Copy the code

Example code for BackboneApplication class:

// application/backbone/index.js
import Application from '.. /index.js';

class BackboneApplication extends Application {
  render(err, viewAction, params = {}) {
    if (err) {
      // Render the error page
      throw err;
    }
    if (viewAction.prototype && isFunction(viewAction.prototype.render)) {
      this._currentView = new viewAction(params);
      return this._currentView.render();
    }
    if (typeof viewAction.render === 'function') {
      returnviewAction.render(params); }}}Copy the code

By delegating the rendering logic to each sub-app, we can avoid having to implement different rendering logic in the framework’s View class according to different technical systems. If the subapp changes to a rendering method other than Backbone and React, we don’t have to change the framework implementation and release a new version.

In addition, we need to construct a Product class that provides the entry point to inject the application instance in addition to the application reality. The example code is as follows:

class Product {
  static registerApplication = (app) = > {
    // Cache the app instance and inject the app route}}Copy the code

Inside each subapp’s entry JS file, call the Product class to inject the current App instance (for example, React App) :

// src/app.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Product, ReactApplication } from 'plugin-pkg';

const app = new ReactApplication({
  React,
  ReactDOM,
  // ...
});

Product.registerApplication(app);
Copy the code

Application lifecycle management

Now that we have a detailed idea of how to do this from dynamic routing to view rendering, let’s consider a practical problem: when we switch between sub-apps, the DOM of the previous App is replaced, but the related events are not cleared correctly. In the React case, we replace the DOM content directly, but the React component’s UnMount event is not triggered correctly. The Backbone View’s destroy callback is the same.

So, we need to add the destroy interface to the Application class:

class Application {
  destroy() {
    // called when the current App instance switches out}}Copy the code

In addition to destroying events, which sometimes require some unified processing after the App switches in, we also need to add a ready interface:

class Application {
  ready() {
    // called when the current App instance switches in}}Copy the code

Life cycle processing, each App instance according to their own actual situation to implement the relevant logic.

The framework automatically calls the previous instance’s destruct interface when switching apps, and then automatically calls the current App’s prepare interface after rendering the App.

Build configuration

The above contents are all the functions that the plug-and-plug framework needs to achieve. In addition, each sub-app should be configured uniformly when it is packaged. For example, the dependencies of the framework should be set to external and not into the resource bundle when packaged. Since each of our App JS resource bundles is executed directly by the UMD package, we can use the global variables of the framework package introduced by Product unified at runtime.

The example code for the WebPack configuration is as follows:

// webpack.config.js
const path = require('path');

const resolveApp = relativePath= > path.join(process.cwd(), relativePath);

module.exports = {
  entry: {
    bundle: resolveApp('src/app.js');
  },
  module: {
    // ...
  },
  plugins: [
    // ...].externals: {
    'plugin-pkg': 'Plugin',}};Copy the code

In this way, not only can it be compatible with independent deployment and integration into the platform, but also can be unified with the platform plug-in framework package in the plug-in platform mode, which facilitates the unified upgrade of the platform.

conclusion

The above plug-and-plug application design takes into account the sub-service modules compatible with different technical systems. The implementation of routing is a bit complicated and the dynamic loading of scripts is relatively simple. In the actual business requirements, if the unified technology system has been determined, in most cases, it is not necessary to consider the problem of compatibility with different sub-business modules, and it is completely possible to select one technology system (such as Vue or React) to implement it, and perhaps only the permission to deal with this small piece.

Therefore, the above content is only for reference, according to the actual business is different, to design a plug and pull scheme for their own business, is the most useful scheme.

reference

  • single-spa

The article can be reproduced at will, but please keep the original link. If you are passionate enough to join ES2049 Studio, please send your resume to caijun.hcj(at)alibaba-inc.com.