Image: zhuanlan.zhihu.com/p/78362028

Author: Shi Zhipeng

preface

LOOK live operation background project is a “Boulder application” that has been iterated for 2+ years, with more than 10+ developers participating in business development and 250+ pages. The huge amount of code brings low efficiency in construction and deployment. In addition, relying on an internal Regularjs technology stack, the project has completed its historical mission, and the corresponding UI component library and engineering scaffolding have also been recommended to stop using, leading to the stage of little or no maintenance. Therefore, the new project and project splitting of LOOK live operation background based on React have been put on the work schedule. The new page will be developed in a new project based on React. The React project can be deployed independently, while the access address output by the LOOK live operation background is expected to remain unchanged.

This paper is based on the practice of micro-front-end implementation of LOOK live operation background. This paper mainly introduces the implementation process of micro front end of CMS application by using micro-front end qiankun under the coexisting scene of “Boulder application”, Regularjs and React technology stack.

For the introduction of Qiankun, please go to the official website. This article will not focus on the concept of micro front end.

A background.

1.1 the status quo

  1. As mentioned above, there is a CMS application as shown below. The project of this application is called LiveAdmin and its access address is:https://example.com/liveadminAccess as shown in the figure below.

  1. We want to stop adding new business pages to the liveAdmin project, so we created a new project called Increase based on an internal React scaffolding. We recommend using this project to develop new business pages. This application can be deployed independently and accessed independently.https://example.com/lookadmin, access as shown below:

1.2 the target

The way we want to use the micro front end, all of the integration of these two application menu, allow the user to perceive this change, is still in accordance with the original access https://example.com/liveadmin, you can access to liveadmin and happens engineering all pages. To achieve this goal, we need to address the following two core issues:

  1. Display the menus of the two systems;
  2. Access the pages of both applications using the original access addresses.

As for the second question, we believe that students who know more about Qiankun can reach a consensus with us. As for the first question, we solved it through some internal solutions in practice. The implementation process is described below. Here we first give the effect diagram of the whole project landing:

As you can see, the increase project’s first level menu is appended to the liveAdmin project’s first level menu. The original address can access all the menus of the two projects.

1.3 Rights Management

Speaking of CMS, also need to say about the realization of the authority management system, hereinafter referred to as PMS.

  1. Permissions: There are currently two types of permissions defined in our PMS: page permissions, which determine whether a user can see a page, and functional permissions, which determine whether a user can access a feature’s API. The front end is responsible for the realization of page permissions, while the function permissions are controlled and controlled by the server.
  2. Permission management: This article only describes the management of page permissions. First, each front-end application is associated with a PMS permission application. For example, liveadmin is associated with appCode = live_backend. After the front-end application project is successfully deployed, the front-end project page and the permission code data associated with the page are pushed to the PMS through the back door. The risk control operation finds the right application in the PMS system and assigns the right to the page based on the role granularity. The user with the role can access the page assigned to the role.
  3. Permission control: when the front-end application is accessed, the outermost module is responsible for requesting the page permission code list of the current user, filtering out the accessible valid menu according to the permission code list, registering the route of the valid menu, and finally generating a legitimate menu application under the current user’s permission.

2. Implement

2.1 LookCMS main application

  1. First, create a CMS infrastructure project and define it as the main application lookCMS with basic request permissions and menu data and menu rendering capabilities.

The entry file performs the following functions of requesting permissions and menu data and rendering menus.

// Use Redux Store to process data
const store = createAppStore(); 
// Check the login status
store.dispatch(checkLogin());
// Listen for asynchronous login status data
const unlistener = store.subscribe(() = > {
    unlistener();
    const { auth: { account: { login, name: userName } } } = store.getState();
    if (login) { // If you have logged in, request the permission and menu data of the current user based on the current user information
        store.dispatch(getAllMenusAndPrivileges({ userName }));
        subScribeMenusAndPrivileges();
    } else {
        injectView(); // Render the login page if not logged in}});// Listen for asynchronous permissions and menu data
const subScribeMenusAndPrivileges = () = > {
    const unlistener = store.subscribe(() = > {
        unlistener();
        const { auth: { privileges, menus, allMenus, account } } = store.getState();
        store.dispatch(setMenus(menus)); // Set the main app's menu and render the main app's lookcms menu accordingly
        injectView(); // Mount the logon view
        // Start qiankun, and transfer menu, permissions, user information, etc., for subsequent transmission to sub-applications and interception of sub-applications' requests
        startQiankun(allMenus, privileges, account, store); 
    });
};
// Render the page based on login status
const injectView = () = > {
    const { auth: { account: { login } } } = store.getState();
    if (login) {
        new App().$inject('#j-main');
    } else {
        new Auth().$inject('#j-main');
        window.history.pushState({}, ' '.`${$config.rootPath}/auth? redirect=The ${window.location.pathname}`); }};Copy the code
  1. Introduce qiankun and register liveAdmin and Increase sub-applications.

The sub-application was defined, and the fields name, Entry, Container and activeRule were determined according to the official document of Qiankun. The entry configuration paid attention to distinguish the environment, and received the data of the previous menus, PRIVILEGES and other data. The basic codes were as follows:

// Define a set of subapplications
const subApps = [{ // Liveadmin old project
    name: 'music-live-admin'.// Take the name field of package.json for the child application
    entrys: { // Entry distinguishes the environment
        dev: '//localhost:3001'.// LiveAdmin rootPath is defined as LiveAdminLegacy, so that the original LiveAdmin can be released to the main application to access the page using the original access address.
        test: ` / /The ${window.location.host}/liveadminlegacy/`.online: ` / /The ${window.location.host}/liveadminlegacy/`,},pmsAppCode: 'live_legacy_backend'.// Permission processing is related
    pmsCodePrefix: 'module_livelegacyadmin'.// Permission processing is related
    defaultMenus: ['welcome'.'activity'] {},// increase new project
    name: 'music-live-admin-react'.entrys: {
        dev: '//localhost:4444'.test: ` / /The ${window.location.host}/lookadmin/`.online: ` / /The ${window.location.host}/lookadmin/`,},pmsAppCode: 'look_backend'.pmsCodePrefix: 'module_lookadmin'.defaultMenus: []}];// Register child applications
registerMicroApps(subApps.map(app= > ({
    name: app.name,
    entry: app.entrys[$config.env], // Access to the child application
    container: '#j-subapp'.// The mount point of the child application in the master application
    activeRule: ({ pathname }) = > { // Define the route matching policy for loading the current subapplication. This is determined by comparing the pathName with the menu key of the current subapplication
        const curAppMenus = allMenus.find(m= > m.appCode === app.pmsAppCode).subMenus.map(({ name }) = > name);
        constisInCurApp = !! app.defaultMenus.concat(curAppMenus).find(headKey= > pathname.indexOf(`${$config.rootPath}/${headKey}`) > -1);
        return isInCurApp;
    },
    // Data passed to sub-applications: menus, permissions, accounts, can make sub-applications do not request related data, of course, sub-applications need to make a good judgment
    props: { menus: allMenus.find(m= > m.appCode === app.pmsAppCode).subMenus, privileges, account }
})));
// ...
start({ prefetch: false });
Copy the code
  1. Main application menu logic

We based on the existing menus menu data, using the internal UI components to complete the menu rendering, bound to each menu click event, click through pushState, change the path of the window. Click a – b menu, for example, the corresponding routing is http://example.com/liveadmin/a/b, qiankun will respond to the change of the routing, according to the definition of activeRule matched to the corresponding application, then the application to take over the routing, Load the page resources corresponding to the sub-application. The basic idea is to clean the

2.2 LiveAdmin subapplication

  1. According to the official qiankun document, the relevant lifecycle hook functions were exported in the entry file of the sub-application.
if (window.__POWERED_BY_QIANKUN__) { // Inject Webpack publicPath so that the parent app loads the child app's resources correctly
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

if (!window.__POWERED_BY_QIANKUN__) { // Independently access the startup logic
    bootstrapApp({});
}

export const bootstrap = async() = > {// Start the front hook
    await Promise.resolve(1);
};

export const mount = async (props) => { // Integrate access startup logic to take over data passed by the master application
    bootstrapApp(props);
};

export const unmount = async (props) => {  // Unhooks the child application
    props.container.querySelector('#j-look').remove();
};
Copy the code
  1. Modify the Webpack packaging configuration.
output: {
    path: DIST_PATH,
    publicPath: ROOTPATH,
    filename: '[name].js'.chunkFilename: '[name].js'.library: `${packageName}-[name]`.libraryTarget: 'umd'.// Specify the packaged Javascript UMD format
    jsonpFunction: `webpackJsonp_${packageName}`,},Copy the code
  1. Hide the header and sidebar elements of the child application when handling integration access.
const App = Regular.extend({
    template: window.__POWERED_BY_QIANKUN__
    ? ` 
      
`
: `
`
.name: 'App'.// ... }) Copy the code
  1. When processing integrated access, it shields requests for permission data and login information and receives permission and menu data transmitted by the main application instead, avoiding redundant HTTP requests and data Settings.
if (props.container) { // Set permissions and menus directly for integrated access
    store.dispatch(setMenus(props.menus))
    store.dispatch({
        type: 'GET_PRIVILEGES_SUCCESS'.payload: {
            privileges: props.privileges,
            menus: props.menus
        }
    });
} else { // The menu reads the local configuration directly
    MixInMenus(props.container);
    store.dispatch(getPrivileges({ userName: name }));
}
if (props.container) {  // Set a user login account for integrated access
    store.dispatch({
        type: 'LOGIN_STATUS_SUCCESS'.payload: {
            user: props.account,
            loginType: 'OPENID'}}); }else { // Request and set user login information for independent access
    store.dispatch(loginStatus());
}

Copy the code
  1. Routing base changes while processing integration access

Because the rootPath must be set to LiveAdmin during integration access, the routes registered during integration access must be changed to the rootPath of the main application and the new mount point.

const start = (container) = > {
    router.start({
        root: config.base,
        html5: true.view: container ? container.querySelector('#j-look') : Regular.dom.find('#j-look')}); };Copy the code

2.3 Increase subapp

This is similar to what the LiveAdmin child does.

  1. Export the corresponding lifecycle hooks.
if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

const CONTAINER = document.getElementById('container');

if (!window.__POWERED_BY_QIANKUN__) {
    const history = createBrowserHistory({ basename: Config.base });
    ReactDOM.render(
        <Provider store={store()}>
            <Symbol />
            <Router path="/" history={history}>
                {routeChildren()}
            </Router>
        </Provider>,
        CONTAINER
    );
}

export const bootstrap = async() = > {await Promise.resolve(1);
};

export const mount = async (props) => {
    const history = createBrowserHistory({ basename: Config.qiankun.base });
    ReactDOM.render(
        <Provider store={store()}>
            <Symbol />
            <Router path='/' history={history}>
                {routeChildren(props)}
            </Router>
        </Provider>,
        props.container.querySelector('#container') || CONTAINER
    );
};
export const unmount = async (props) => {
    ReactDOM.unmountComponentAtNode(props.container.querySelector('#container') || CONTAINER);
};
Copy the code
  1. Webpack packages the configuration.
output: {
    path: DIST_PATH,
    publicPath: ROOTPATH,
    filename: '[name].js'.chunkFilename: '[name].js'.library: `${packageName}-[name]`.libraryTarget: 'umd'.jsonpFunction: `webpackJsonp_${packageName}`,},Copy the code
  1. When integrating access, remove the header and sidebar.
if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-line
    return (
        <BaseLayout location={location} history={history} pms={pms}>
            <Fragment>
                {
                    curMenuItem && curMenuItem.block
                        ? blockPage
                        : children
                }
            </Fragment>
        </BaseLayout>
    );
}

Copy the code
  1. When integrating access, it shields permission and login requests and receives permission and menu data transmitted by the main application.
useEffect(() = > {
    if (login.status === 1) {
        history.push(redirectUrl);
    } else if (pms.account) { // Integrate access, set data directly
        dispatch('Login/success', pms.account);
        dispatch('Login/setPrivileges', pms.privileges);
    } else { // Independent access, request data
        loginAction.getLoginStatus().subscribe({
            next: () = > {
                history.push(redirectUrl);
            },
            error: (res) = > {
                if (res.code === 301) {
                    history.push('/login', { redirectUrl, host }); }}}); }});Copy the code
  1. Change the React-router Base when integrating access.
export const mount = async (props) => {
    const history = createBrowserHistory({ basename: Config.qiankun.base });
    ReactDOM.render(
        <Provider store={store()}>
            <Symbol />
            <Router path='/' history={history}>
                {routeChildren(props)}
            </Router>
        </Provider>,
        props.container.querySelector('#container') || CONTAINER
    );
};
Copy the code

2.4 Rights Integration (Optional)

  1. As mentioned above, a front-end application is associated with a PMS permission application. If each front-end application is combined in the way of micro-front-end, and each front-end application still corresponds to its own PMS permission application, then from the perspective of permission managers, multiple PMS permission applications need to be paid attention to. It is difficult to assign permissions and manage roles, such as differentiating the pages of two sub-applications and managing roles of two sub-applications with the same permission. Therefore, it is necessary to consider unifying the PMS permission applications corresponding to sub-applications. Here we only describe our processing method for reference only.
  2. To maintain the original permission management mode as far as possible (the permission manager pushes the page permission code to PMS through the back door of the front-end application, and then allocates the page permission to PMS), in the micro-front-end scenario, the permission integration needs to be described as follows:
    1. Each sub-application first pushes the menu and permission code data of the project to its own PMS permission application.
    2. The active application loads the menu and permission code data of each sub-application, modifies the data of each menu and permission code to the PMS permission data corresponding to the active application, and then pushes the data to the PMS permission application corresponding to the active application. The rights management personnel can centrally allocate and manage permissions within the PMS permission application corresponding to the active application.
  3. In practice, in order that permission managers are not aware of the changes caused by splitting applications, the appCode = Live_BACKEND PMS permission application corresponding to the original LiveAdmin application is used for permission assignment. We need to change the PMS permission application for LiveAdmin to the PMS permission application for lookCMS master and create a new PMS permission application for LiveAdmin child with appCode = Live_legacy_Backend. The new increase child continues to correspond to the PMS permission application appCode = look_BACKEND. The menu and permission code data of the preceding two subapplications are reported to the corresponding PMS permission application according to point 2 in the previous step. Finally, the primary lookcms application obtains the application menu and permission code data of the front terminal of PMS permission applications appCode = Live_LEGacy_backend and appCode = look_BACKEND. Application data with PMS permission changed to appCode = live_Backend is pushed to the PMS. The following figure shows the overall process. The original system design is on the left and the modified system design is on the right.

2.5 the deployment

  1. Liveadmin and Increase are independently deployed using cloud music’s front-end static deployment system, as are lookcms, the main application.
  2. Handle the cross-domain problem of primary application accessing sub-application resources. In our practice, resource packaging follows the same-domain rule because we are all deployed in the same domain.

2.6 summary

Since then, we have completed the implementation of the micro front end based on the qiankun LOOK live broadcast operation background. The main project was newly built, the responsibilities of the main application were divided, and the sub-project was modified so that the sub-application could be integrated into the main application and accessed, and the original independent access function could be maintained. The overall process can be described as follows:

3. Dependency sharing

Qiankun official did not recommend specific dependency sharing solutions. We also made some explorations and the conclusions can be summarized as follows: The dependence of Regularjs, React and other Javascript public libraries can be solved by externals and Qiankun loading sub-application life cycle functions of Webpack and import-HTML-entry plug-in. The Module Federation Plugin in Webapck 5 can be used for scenarios that require code sharing, such as components. The specific plan is as follows:

3.1. We sorted out two types of public dependencies

3.1.1. One type is the basic library, such as Regularjs, regular-state, MUI, React, React Router and other resources that are expected not to be loaded repeatedly during the entire access cycle.

3.1.2. The other type is common components. For example, the React component needs to be shared among sub-applications without code copy between projects.

3.2. For the above two types of dependencies, we have done some local practices because there is no pressing business need and Webpack 5 is currently in stable release mode (as of this publication, Webpack 5 has been released, This part of feature has not been verified in the production environment, but the processing methods and results can be shared here.

3.2.1. For the first type of common dependencies, what we expect to achieve sharing is that in the case of integrated access, the main application can dynamically load the libraries with strong dependencies of the sub-applications, and the sub-applications themselves will not be loaded any more. In the case of independent access, the sub-applications themselves can load the dependencies they need independently. There are two problems to deal with: a. How does the master application collect and dynamically load child application dependencies b. How sub-applications perform differently when integrating and accessing resources independently.

3.2.1.1. First, we need to maintain a definition of public dependencies, that is, define the public resources that each sub-application depends on in the master application, by inserting

// Define the child application's public dependencies
const dependencies = {
    live_backend: ['regular'.'restate'].look_backend: ['react'.'react-dom']};// Returns the dependency name
const getDependencies = appName= > dependencies[appName];
// Build the script tag
const loadScript = (url) = > {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = url;
    script.setAttribute('ignore'.'true'); // Avoid reloading
    script.onerror = () = > {
        Message.error('Load failed${url}Please refresh and retry ');
    };
    document.head.appendChild(script);
};
// Load the resources required by the current subapplication before loading a subapplication
beforeLoad: [
    (app) = > {
        console.log('[LifeCycle] before load %c%s'.'color: green; ', app.name);
        getDependencies(app.name).forEach((dependency) = > {
            loadScript(`The ${window.location.origin}/${$config.rootPath}${dependency}.js`); }); }].Copy the code

We use the copy-webpack-plugin to convert release resources under node_modules into packages that can be accessed through a separate URL.

/ / development
plugins: [
    new webpack.DefinePlugin({
        'process.env': {
            NODE_ENV: JSON.stringify('development')}}),new webpack.NoEmitOnErrorsPlugin(),
    new CopyWebpackPlugin({
        patterns: [{from: path.join(__dirname, '.. /node_modules/regularjs/dist/regular.js'), to: '.. /s/regular.js' },
            { from: path.join(__dirname, '.. /node_modules/regular-state/restate.pack.js'), to: '.. /s/restate.js' },
            { from: path.join(__dirname, '.. /node_modules/react/umd/react.development.js'), to: '.. /s/react.js' },
            { from: path.join(__dirname, '.. /node_modules/react-dom/umd/react-dom.development.js'), to: '.. /s/react-dom.js'}]})],/ / production
new CopyWebpackPlugin({
    patterns: [{from: path.join(__dirname, '.. /node_modules/regularjs/dist/regular.min.js'), to: '.. /s/regular.js' },
        { from: path.join(__dirname, '.. /node_modules/regular-state/restate.pack.js'), to: '.. /s/restate.js' },
        { from: path.join(__dirname, '.. /node_modules/react/umd/react.production.js'), to: '.. /s/react.js' },
        { from: path.join(__dirname, '.. /node_modules/react-dom/umd/react-dom.production.js'), to: '.. /s/react-dom.js'}]})Copy the code

3.2.1.2. Regarding the secondary loading of common dependencies in sub-application integration and independent access, the approach we adopt is, First of all, the child application will separate the public dependencies defined by the main application through copy-webpack-plugin and html-webpack-externals-plugin using the external mode, instead of packaging into the Webpack bundle. At the same time, the

plugins: [
    new CopyWebpackPlugin({
        patterns: [{from: path.join(__dirname, '.. /node_modules/regularjs/dist/regular.js'), to: '.. /s/regular.js' },
            { from: path.join(__dirname, '.. /node_modules/regular-state/restate.pack.js'), to: '.. /s/restate.js'}},]),new HtmlWebpackExternalsPlugin({
        externals: [{
            module: 'remoteEntry'.entry: 'http://localhost:3000/remoteEntry.js'
        }, {
            module: 'regularjs'.entry: {
                path: 'http://localhost:3001/regular.js'.attributes: { ignore: 'true'}},global: 'Regular'
        }, {
            module: 'regular-state'.entry: {
                path: 'http://localhost:3001/restate.js'.attributes: { ignore: 'true'}},global: 'restate'}],})],Copy the code

3.2.2. For the second type of code sharing scenario, we investigated the Module Federation Plugin of Webpack 5, which asynchronously loads the common code by referencing the common resource information imported and exported by each other between applications, so as to realize code sharing.

3.2.2.1. First, the scenario defined in our practice is that the lookCMS main application provides both regularJs-based RButton components and React based TButton components to be shared with the LiveAdmin and Increase subapplications respectively.

3.2.2.2. For the lookCMS main application, we define the Webpack5 Module federation plugin as follows:

plugins: [
        // new BundleAnalyzerPlugin(),
        new ModuleFederationPlugin({
            name: 'lookcms'.library: { type: 'var'.name: 'lookcms' },
            filename: 'remoteEntry.js'.exposes: {
                TButton: path.join(__dirname, '.. /client/exports/rgbtn.js'),
                RButton: path.join(__dirname, '.. /client/exports/rcbtn.js'),},shared: ['react'.'regularjs']})],Copy the code

The shared code components defined are shown below:

3.2.2.3. For the LiveAdmin sub-application, we define the Webpack5 Module federation plugin as follows:

plugins: [
    new BundleAnalyzerPlugin(),
    new ModuleFederationPlugin({
        name: 'liveadmin_remote'.library: { type: 'var'.name: 'liveadmin_remote' },
        remotes: {
            lookcms: 'lookcms',},shared: ['regularjs']})],Copy the code

Way of use, the child application must first insert in the HTML source for the main application of http://localhost:3000/remoteEntry.js access to the Shared resources can be through the HTML – webpack – externals – plugin inserts, See external handling for common dependencies of child applications above.

For the loading of external shared resources, sub-applications are asynchronously loaded by the import method of Webpack, and then inserted into the virtual DOM. We expect to implement Regularjs by referring to the React scheme given by Webapck. It’s a pity that Regularjs doesn’t have the basic functionality to help us implement Lazy and Suspense.

After some research, we chose to conditionally render asynchronously loaded components based on Regularjs’ R-Component API.

The basic idea is to define a Regularjs component that gets the name of the asynchronous component to load from props during initialization and loads the name of the lookCMS shared component through the Webpack import method during build. At the same time, update the presentation logic of RSuspense component R-Component to show the component bound by name.

As Regularjs syntax is limited, it is not easy to abstract the above RSuspense component logic out, so we adopt Babel transformation. Developers define a component loading mode statement and use Babel AST to transform it into RSuspense component. Finally, use the RSuspense component in Regularjs templates.

// Support defining a fallback
const Loading = Regular.extend({
    template: '
      
Loading... {content}
'
.name: 'Loading' }); // Write a lazy-loaded schema statement const TButton = Regular.lazy(() = > import('lookcms/TButton'), Loading); // Use Babel AST to transform RSuspense components in templates `<RSuspense origin='lookcms/TButton' fallback='Loading' />` Copy the code

Syntax conversions with Babel AST are shown below:

The actual operation effect is shown in the figure below:

3.2.2.4. For the increase sub-app, we define the Webpack 5 Module federation plugin as follows:

plugins: [
    new ModuleFederationPlugin({
        name: 'lookadmin_remote'.library: { type: 'var'.name: 'lookadmin_remote' },
        remotes: {
            lookcms: 'lookcms',},shared: ['react']})],Copy the code

For usage, please refer to the official Webpack 5 document, and the code is as follows:

const RemoteButton = React.lazy(() => import('lookcms/RButton')); Const Home = () => (<div className="m-home"> </React.Suspense> </div> );Copy the code

The actual operation effect is shown in the figure below:

  1. conclusion

Four. Precautions

  1. If you are using other methods to load cross-domain resources within your application, please note that Qiankun loads all sub-application resources through FETCH, so cross-domain resources need to be accessed through CORS.
  2. The HTML tag of one of your sub-apps may have some attributes or functions attached to it. Note that the HTML tag of the sub-app is removed in the actual processing. Therefore, if rem is required, please use other methods to adapt.

5. In the future

  1. Access to the automation sub-application is through platform access, which of course requires the sub-application to follow the specification of the journey.
  2. Dependency Shared Webpack 5 has been officially released, so the Use of the Module Federation Plugin can be put on the agenda.

6. Summary

Based on the actual business scenarios, LOOK live broadcast operation background used Qiankun to conduct the engineering split of micro front-end mode. Currently, it has been running smoothly in the production environment for nearly 4 months. In the process of practice, there are indeed some difficulties in demand establishment, implementation of access to Qiankun, and deployment and application. For example, at the beginning of the requirements establishment, we considered the main function to be implemented. We often encountered errors in the process of access to Qiankun, and encountered choices and obstacles of internal deployment system in the process of deployment. Fortunately, thanks to the efforts of our colleagues, the project was successfully launched and operated.

The resources

  • qiankun
  • Regularjs
  • Module Federation Plugin

This article is published from netease Cloud Music big front end team, the article is prohibited to be reproduced in any form without authorization. Grp.music – Fe (at) Corp.Netease.com We recruit front-end, iOS and Android all year long. If you are ready to change your job and you like cloud music, join us!