Preliminary knowledge

The principle of single-SPA is to insert link/script tags and

into the main project. The core of this operation is to dynamically load JS and CSS.

We use System.js. With this plug-in, we only need to expose the app.js of the subproject to it.

This article is based on a modified GitHub single-SPA demo, so you’d better have studied that demo, and is based on the latest vuE-CLI4 development.

Single-spa-vue implementation steps

The effect is that subprojects can be deployed independently and, incidentally, integrated by the main project.

Create a new navigation main project

  1. vue-cli4Direct use ofvue create navCommand to generate avueThe project.

Note that the navigation project route must be in history mode

  1. Modify theindex.htmlfile

      
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>home-nav</title>
   <! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
   <script type="systemjs-importmap" src="/config/importmap.json"></script>
   <! Single-spa,vue,vue-router file -->
   <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js" as="script" crossorigin="anonymous" />
   <link rel="preload" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js" as="script" crossorigin="anonymous" />
   <link rel="preload" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.min.js" as="script" crossorigin="anonymous" />
   <! System. Js file -->
   <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-exports.js"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
  </head>
  <body>
    <script>
      (function() {
        System.import('single-spa').then(singleSpa= > {
          singleSpa.registerApplication(
            'appVueHistory',
            () => System.import('appVueHistory'),
            location => location.pathname.startsWith('/app-vue-history/')
          )
          singleSpa.registerApplication(
            'appVueHash',
            () => System.import('appVueHash'),
            location => location.pathname.startsWith('/app-vue-hash/')
          )
          singleSpa.start();
        })
      })()
    </script>
    <div class="wrap">
      <div class="nav-wrap">
        <div id="app"></div>
      </div>
      <div class="single-spa-container">
        <div id="single-spa-application:appVueHash"></div>
        <div id="single-spa-application:appVueHistory"></div>
      </div>
    </div>
    <style>
    .wrap{
      display: flex;
    }
    .nav-wrap{
      flex: 0 0 200px;
    }
    .single-spa-container{
      width: 200px;
      flex-grow: 1;
    }
    </style>
  </body>
</html>
Copy the code
  1. Configuration files for subprojects and public file urlsconfig/importmap.json:
{
  "imports": {
    "appVue": "http://localhost:7778/app.js"."appVueHistory": "http://localhost:7779/app.js"."single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"."vue": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"."vue-router": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.min.js"}}Copy the code

Sub-project transformation

Vue item of hash mode routing

If it is a newly developed project, you can generate a VUE project with VUE-CLI4, routing using hash mode.

1. Install the plug-in (which will be explained later) :

For older projects, you need to install the following three plug-ins:

npm install systemjs-webpack-interop -S
Copy the code
npm install single-spa-vue -S
Copy the code
npm install vue-cli-plugin-single-spa -D
Copy the code

If it is a new project, you can use the following command:

vue add single-spa
Copy the code

Note: this command overwrites your main.js, and should not be used for older projects

This command does four things:

  • (1) Install single-SPA-vue plug-in

  • (2) Install systemjs-webpack-interop plugin and generate set-public-path.js

  • (3) to modify the main js

  • (4) Modify webpack configuration (allow cross-domain, close hot updates, remove splitChunks, etc.)

2. Add two environment variables

Since single-SPA mode also has development and production environments, there are four environments: normal development, Single-SPA development, normal packaging, and Single-SPA packaging. But we only need two environment variable files to distinguish them, respectively, in the root directory of the new environment variable file:

Env.devsinglespa file (distinguish between normal development and single-SPA mode development) :

NODE_ENV = development
VUE_APP__ENV = singleSpa
Copy the code

.env.singlespa files (distinguish between normal packaging and single-SPA mode packaging) :

NODE_ENV = production
VUE_APP__ENV = singleSpa
Copy the code

3. Modify the import file

The only thing that separates single-SPA from normal development is the entry file. The plugins (vuex, VUE-Router, Axios, element-UI, etc.) that need to be introduced in the entry file are exactly the same. The difference is that normal development is new Vue(options). Single-spa calls singleSpaVue(Vue,options) and exports the three life cycles.

So I wrote the parts common to both modes in main.js and exported the configuration objects required for both modes:

import store from "./store";
import Vue from 'vue';
import App from './App.vue';
import router from './router';

const appOptions = {
  render: (h) = > h(App),
  router,
  store,
}

Vue.config.productionTip = false;

export default appOptions;
Copy the code

Added index.js (normal mode entry file) :

import appOptions from './main';
import './main';
import Vue from 'vue';

new Vue(appOptions).$mount('#app');
Copy the code

New index.spa.js (single-SPA mode entry file) :

import './set-public-path'
import singleSpaVue from 'single-spa-vue';
import appOptions from './main';
import './main';
import Vue from 'vue';

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions
});

const { bootstrap, mount, unmount } = vueLifecycles;

export { bootstrap, mount, unmount };
Copy the code

Index.spa. js set-public-path.js:

import { setPublicPath } from 'systemjs-webpack-interop'
// The module name must be the same as the module name in the system.js configuration file (importmap.json)
setPublicPath('appVueHash')
Copy the code

4. Modify package configuration (vue.config.js)

Single-spa mode differs from normal mode only in the entry file, everything else is the same. In other words, after packaging, only the app.js file is different, so can the other files be reused, can be packaged in one time, can deploy both modes?

The answer is yes: I did the sing-spa package first, then the normal mode package, and finally copied the app.js file generated by the single-spa package to the root of the normal package. Just deploy with the dist directory in hand, and single-SPA updates synchronically without making any changes.

Note that the file cannot have a hash value. If the file does not have a hash value, the server needs to generate the hash value to set the cache.

const CopyPlugin = require('copy-webpack-plugin');

const env = process.env.VUE_APP__ENV; // Is it single-SPA
const modeEnv = process.env.NODE_ENV; // Development or production environment

const config = {
  productionSourceMap: false./ / remove sourceMap
  filenameHashing: false.// Remove the hash value of the file name
};

const enteyFile = env === 'singleSpa' ? './src/index.spa.js' : './src/index.js';
// Normally packaged app.js is in the js directory, while single-spa mode is in the root directory.
// App.js will be copied from the dist-spa/js directory to the root directory of the normal packaging, so don't worry about it, just decide the development mode of single-spa
const filename = modeEnv === 'development' ? '[name].js' : 'js/[name].js';

chainWebpack = config= > {
  config.entry('app')
    .add(enteyFile)
    .end()
    .output
      .filename(filename);
  if(env === 'singleSpa') {// Vue,vue-router does not package into app.js, uses external links
    config.externals(['vue'.'vue-router'])}}if(env === 'singleSpa') {Object.assign(config, {
    outputDir: 'dist-spa'.devServer: {
      hot: false.// Turn off hot updates
      port: 7778
    },
    chainWebpack,
  })
}else{
  Object.assign(config, {
    chainWebpack,
    configureWebpack: modeEnv === 'production' ? {
      plugins: [
        // Copy the app.js generated in single-spa mode to the home directory of the normal mode package
        new CopyPlugin([{ 
          from: 'dist-spa/js/app.js'.to: ' '}])],} : {},})}module.exports = config;
Copy the code

Packaged file effect:

Js /app.js is generated in normal mode, while app.js in the same directory as index.html is copied from dist-spa/js/app.js, which is the entry file of single-SPA mode, and other files are reused.

5. Modify package command (package.json)

Development/packaging in single-SPA mode requires changes to environment variables. Change the normal build command to: package twice in order to achieve the same package deployment process as before.

"scripts": {
    "spa-serve": "vue-cli-service serve --mode devSingleSpa"."serve": "vue-cli-service serve"."spa-build": "vue-cli-service build --mode singleSpa"."usual-build": "vue-cli-service build"."build": "npm run spa-build && npm run usual-build"."lint": "vue-cli-service lint"
},
Copy the code

Single-spa development using NPM Run SPA-serve, normal development unchanged.

Packaging is done using the NPM run build, and then deploying the files in the Dist directory to the subproject server.

Vue project for routing in history mode

Since we force a different prefix (/app-vue-history) on the subproject route, it works in hash mode because the route jump in hash mode only changes the HASH value of the URL, not the path value. In history mode, vue-router needs to be told that /app-vue-history/ is the route prefix of the project, and only the latter part needs to be modified to jump, otherwise the route jump will directly cover all paths. This configuration item is the base property:

const router = new VueRouter({
  mode: "history".base: '/'.// The default is base
  routes,
});
Copy the code

In single-SPA mode, the base property is /app-vue-history. In normal mode, the base property is unchanged.

However, since we packaged and reused files other than app.js, only the entry file can distinguish the environment. The solution is:

The router/index.js routing file does not export the instantiated routing object. Instead, it exports a function:

const router = base= > new VueRouter({
  mode: "history",
  base,
  routes,
});
Copy the code

And main.js no longer imports route files, but imports them separately in entry files.

Normal mode entry file index.js:

import router from './router';

const baseUrl = '/';
appOptions.router = router(baseUrl);
Copy the code

Single-spa mode entry file index.spa.js:

import router from './router';

const baseUrl = '/app-vue-history';
appOptions.router = router(baseUrl);
Copy the code

Analysis of some principles

The function and benefit of sysyem.js

The purpose of system.js is to dynamically load modules on demand. If all our sub-projects use VUE,vuex, vuE-Router, it would be wasteful to package each project once. System.js can use the externals attribute of webpack to configure these modules as external chains and then load them on demand:

Of course, you could import all of these common JS files directly with script tags, but this would be wasteful. For example, child A uses vue-Router and Axios but doesn’t use vuex, so child A would refresh and still request vuex, which would be wasteful, and System.js would be loaded on demand.

At the same time, subprojects are packaged into UMD format, and system.js can load subprojects on demand.

What does the systemjs-webpack-interop plugin do?Making the address)

As mentioned in the previous article, js/ CSS directly introduced into the sub-project can render the subsystem, but dynamically generated HTML files such as img/video/audio have relative paths, so they cannot be loaded. Solution 1: Set the publicPath of VUe-cli4 to the complete absolute path http://localhost:8080/.

This plugin exposes the publicPath of the subproject to system.js. System.js matches the project name to the configuration file (importmap.json), and then parses the configured URL to assign the prefix to publicPath.

So how does publicPath set dynamically? Webpack exposes a global variable named __webpack_public_path__. Change the value directly.

Systemjs-webpack-interop (public-path-system-resolve.js) :

So that’s why the entry file app.js for single-spa is the same as the directory index.html, because it intercepts the app.js path directly as publicPath.

What does single-SPA-Vue do? (Making the address)

The main function of this plugin is to help us write the three cycle events required for single-SPA: bootstrap, mount, and unmount.

What we do in the mount cycle is generate the

we need, of course, the name of the ID which is derived from the project name:

Then we instantiate vue inside this div:

So if we want the subproject content to be in our custom area (inserted into the body by default), one way to do this is to write div:

home-nav/public/index.html:

Another option is to modify this part of the code so that it inserts where we want it to, not the body.

The unmount cycle unmounts the instantiated Vue and clears the DOM. To implement the keep-alive effect we need to modify this part of the code (described later).

Vue-cli-plugin-single spaMaking the address)

This plugin is mainly used to overwrite your main.js and generate set-public-path.js while modifying your webpack configuration when the command vue add single-spa is executed. But when NPM install vue-cli-plugin-single-spa -d is executed, it will only overwrite your Webpack configuration.

Modify the webpack configuration source:

module.exports = (api, options) = > {
  options.css.extract = false
  api.chainWebpack(webpackConfig= > {
    webpackConfig
      .devServer
      .headers({
        'Access-Control-Allow-Origin': The '*',
      })
      .set('disableHostCheck'.true)
    
    webpackConfig.optimization.delete('splitChunks')
    webpackConfig.output.libraryTarget('umd')
    webpackConfig.set('devtool'.'sourcemap')})}Copy the code

Back to where we started, the most important thing we did with single-SPA was to dynamically import js/ CSS for the subproject, but you didn’t see it. All you saw was js and no CSS. What about CSS files? The answer is options.css.extract = false.

In vUE cli3, this value is false, which means that the CSS file is not generated separately, but is packed together with the JS file. This allows us to only care about the introduction of the JS file, but also opens the door to CSS contamination.

Another configuration is to allow cross-domains, as well as the system.js requirement that subprojects be packaged in UMD form mentioned at the beginning of this article.

There is one more key configuration: WebpackConfig. Optimization. The delete (‘ splitChunks), under normal circumstances, after we pack the file in addition to the entrance to the app, js, another file is the chunk – vendors. Js. This file contains some common third-party plug-ins, so the subproject will have two entry files (or will have to load both files at the same time), so the splitChunks will have to be removed.

Notes and other details

  1. The environment variable

During deployment, all routing files except the entry file (app.js) reuse normally packaged files, so environment variables need to be injected from the entry file for global use.

Index. Spa. Js file:

appOptions.store.commit('setSingleSpa'.true);
Copy the code
  1. It is best to have fixed ports for sub-project development

To avoid frequent configuration file changes, set a fixed special port to avoid port conflicts.

  1. Single-spa turns off hot updates

The development mode is still under normal development, but hot update needs to be turned off for single-SPA co-tuning, otherwise the local Websocket will always report failed.

During single-SPA development I found that hot updates worked well.

  1. The import URL of an external file in index.html should be written as an absolute path

Note that the configuration file is an absolute path. Otherwise, when accessing the sub-project, the route is redirected back to the main project index.

home-nav/public/index.html:

<script type="systemjs-importmap" src="/config/importmap.json"></script>
Copy the code
  1. How to implement “keep-Alive”

Looking at the single-SPa-Vue source code, you can see that during the unmount life cycle, it destroyed the Vue instance and emptied the DOM. To implement keep-alive, we simply remove destroy and leave the DOM empty, and use display: None to hide and display the DOM of the subproject ourselves.

function unmount(opts, mountedInstances) {
  return Promise
    .resolve()
    .then((a)= > {
      mountedInstances.instance.$destroy();
      mountedInstances.instance.$el.innerHTML = ' ';
      delete mountedInstances.instance;

      if (mountedInstances.domEl) {
        mountedInstances.domEl.innerHTML = ' '
        delete mountedInstances.domEl
      }
    })
}
Copy the code
  1. How do I avoid CSS contamination

Extract = true > extract = true > extract = true > extract = true > extract = true > extract = true

Solutions:

Option 1: Naming conventions + CSS-scope + remove global styles

Solution 2: Remove the style tag when uninstalling the application (to be investigated)

If you must write global variables, you can do something like “skin” : Give the body/ HTML a unique ID in the subproject (for normal deployment), and then precede the global style with this ID. Single-spa mode needs to modify single-SPa-vue to give the body/ HTML a unique ID during the mount cycle. This is removed during the unmount cycle so that the global CSS is only valid for this project.

  1. How to avoid JS conflicts

The first step is to normalize development: remove global properties/events during the component’s Destroy lifecycle, and a second option is to take a snapshot of the Window object before the subproject loads, and then restore the previous state on uninstall.

  1. How do subprojects communicate

You can communicate with custom events using LocalStorage. Localstorage is used to share user login information, and custom events are used to share real-time data, such as the number of messages.

Child component A creates the event and carries the data
const myCustom = new CustomEvent("custom", {detail: { data: 'test'}});//2. Child component B registers event listeners
window.addEventListener("custom".function(e){
  // Data is received
})
//3. Child component A triggers the event
window.dispatchEvent(myCustom);
Copy the code
  1. How do I control subsystem permissions

One way to do this is if the system does not have permission to directly hide the entry navigation, and then directly enter the URL, it will still load the subproject, but the subproject determines that it does not have permission to display a 403 page. You can see that the subsystem entry file is written in a JSON file, so not everyone can read the JSON, or users who want to implement different permissions of the JSON configuration is different.

We can dynamically generate script tags:

// Configure json before loading the module
function insertNewImportMap(newMapJSON) {
  const newScript = document.createElement('script')
  newScript.type = 'systemjs-importmap';
  newScript.innerText = JSON.stringify(newMapJSON);
  const test = document.querySelector('#test')
  test.insertAdjacentElement('beforebegin',newScript);
}
// The content is retrieved from the interface
const devDependencies = {
  imports: {
    "navbar": "http://localhost:8083/app.js"."app1": "http://localhost:8082/app.js"."app2": "http://localhost/app.js"."single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"."vue": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"."vue-router": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.min.js"
  }
}
insertNewImportMap(devDependencies);
Copy the code

conclusion

If you don’t want to build your own Node static file server, I recommend a software: XAMPP

The full demo file in this article is at github.com/gongshun/si…

  1. Current problems

    • Routing between subprojects cannot remove the HASH value of the URL. For example, when switching from ‘/app1/#/home’ to ‘/app2/’, the hash value will still be carried: ‘/app2/#/’. Currently, it does not affect the routing judgment of the subprojects, but it may affect the routing judgment of the subprojects.

    • Even if the sub-projects are in the same technology stack, the framework version cannot be unified. Although there is an operation to pull out the common framework, it may be difficult to control in practice.

    • During the overall development and debugging of the project, if project A is the development environment and project B is the packaging environment, an error will be reported for route switching back and forth. If both project A and Project B are the development environment, or both project B and project B are the production environment, an error will be reported. (Cause unknown)

  2. Next step

    • Ali’sqiankunThe framework
    • reactProject transformation andangularProject transformation, although the principle is similar, but the details will be different

Finally, thank you for reading and happy New Year to you all!

Any questions are welcome to point out that the next article has been updated: Implementing a single-SPA front-end microservice from 0 (below)