Why use a micro front end

Now with the continuous development of the front-end, enterprise project volume is more and more big, more and more pages, the project becomes swollen and maintenance is also very difficult, sometimes we just change the project simple style, need the whole project set up to repackage, created a lot of trouble to the developer, is also a waste of time. In order to integrate the old project into the new project, the reconstruction needs to be carried out constantly, resulting in high human cost.

The micro front-end architecture has the following core values:

  • Stack independent The main framework does not restrict access to the application stack, sub-applications have full autonomy
  • Independent development, independent deployment of the sub-application repository independent, the front and back end can be independently developed, after the deployment of the main framework automatically complete synchronization update
  • Independent run time state is isolated between each child application and run time state is not shared

The implementation principle of single-SPA

Firstly, the micro-front-end route is registered, and single-SPA is used as the micro-front-end loader and the single entrance of the project to accept the access of all page urls. According to the matching relationship between the page URL and the micro-front-end, the corresponding micro-front-end module is selected to load, and then the micro-front-end module responds to the URL. That is, the micro-front-end module route to find the corresponding components, rendering page content.

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 be used with the externals attribute of webpack to configure these modules as external links, and then load them on demand. Of course, you can also import all these common JS with script tags. We just need to expose the app.js of the subproject to it.

What is a Lerna

As front-end projects get larger and larger, common code is often broken up and maintained as separate NPM packages. But then dependency management between packages becomes a headache. To solve this problem, we can manage different NPM package projects in the same project. Such a project development strategy is also known as monorePO. Lerna is such a tool for you to do this better. Lerna is a tool that uses Git and NPM to handle multi-package dependency management, which automatically helps us manage version dependencies between various module packages. There are many public libraries that use Lerna as their dependency management tools, such as Babel, create-react-app, react-Router, jest, etc.

  1. Resolve dependencies between packages.
  2. Git repository detects changes and synchronizes them automatically.
  3. Generated based on the associated Git commitCHANGELOG.

You will also need to install Lerna globally:

npm install -g lerna
Copy the code

Built based on vUE micro front end project

1. Initialize the project

mkdir lerna-project & cd lerna-project`

lerna init
Copy the code

After the command is executed successfully, this directory structure is generated under the directory.

├─ readme.md ├─ Lerna. json # ├─ Package. json ├─ Package ├─Copy the code

2.Set up yarn workspaces mode

The default is NPM, and each subpackage has its own node_modules, so that only the top layer has one node_modules

{
  "packages": [
    "packages/*"]."useWorkspaces": true."npmClient": "yarn"."version": "0.0.0"
}
Copy the code

Also set package.json to true to prevent the root directory from being published to NPM:

{
 "private": true."workspaces": [
    "packages/*"]}Copy the code

Configure lerna.json in the root directory to use the YARN client and use workspaces

yarn config set workspaces-experimental true
Copy the code

3. Register sub-applications

Step 1: Create a child application using vue-CLI

#Go to the Packages directory
cd packages

#Create an├─ public ├─ SRC │ ├─ main.js │ ├── assets │ ├── ├─ vue ├─ vue.config.js ├─ Package. json ├─ Readme.md.├ ─ lesson.txtCopy the code

Step 2: Use vuE-CLI-plugin-single-SPA to quickly generate spa projects

#It will automatically modify main.js to add singleSpaVue and generate set-public-path.js
vue add single-spa
Copy the code

The generated main.js file

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    // el: '#app', // no mount point is mounted under body by default
    render: (h) = > h(App),
    router,
    store: window.rootStore,
  },
});

export const bootstrap = [
  vueLifecycles.bootstrap
];
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;

Copy the code

Step 3: Set the environment variable.env

#The application name
VUE_APP_NAME=app1
#Apply the root path. The default value is:'/'This value must be specified if you want to publish to a subdirectory
VUE_APP_BASE_URL=/
#Port. It is better to set a fixed port for sub-project development to avoid frequent modification of configuration files and set a fixed special port to avoid port conflicts as far as possible.
port=8081
Copy the code

Step 4: Set vue.config.js to modify the WebPack configuration

const isProduction = process.env.NODE_ENV === 'production'
const appName = process.env.VUE_APP_NAME
const port = process.env.port
const baseUrl = process.env.VUE_APP_BASE_URL
module.exports = {
  // Prevent loading problems in development environments
  publicPath: isProduction ? `${baseUrl}${appName}/ ` : `http://localhost:${port}/ `.// CSS is not packaged as a separate file in any environment. This is to ensure minimal introduction (only JS)
    css: {
        extract: false
    },

  productionSourceMap: false.outputDir: path.resolve(dirname, `.. /.. /dist/${appName}`), // package to root dist
  chainWebpack: config= > {
    config.devServer.set('inline'.false)
    config.devServer.set('hot'.true)

    // Make sure that the package is a JS file for the main application to load
    config.output.library(appName).libraryTarget('umd')

    config.externals(['vue'.'vue-router'.'vuex'])  // Do not register

    if (process.env.NODE_ENV === 'production') {
      // Wrap the target file with a hash string to disable browser caching
      config.output.filename('js/index.[hash:8].js')}}}Copy the code

4. Create a main project

Step 1: Add the main project Package

#Go to the Packages directory
cd packages
#Create a packge directory and go to the root-html-file directory
mkdir root-html-file && cd root-html-file
#Initialize a package
npm init -y
Copy the code

Step 2: Create the main project index.html

The main application mainly plays the role of route distribution and resource loading

<! DOCTYPEhtml>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Vue-Microfrontends</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="importmap-type" content="systemjs-importmap">
    <! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
   <script type="systemjs-importmap" src="importmap.json"></script>
    <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" />
    <! -- SystemJS package -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
    <! Parsing subpackages -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
    <! -- Parse package default -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
    <! -- SystemJS package -->
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
Copy the code

Step 3: Edit the importMapjson file and configure the file for the child application

{
  "imports": {
    "navbar": "http://localhost:8888/js/app.js"."app1": "http://localhost:8081/js/app.js"."app2": "http://localhost:8082/js/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"."vuex": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vuex.min.js"}}Copy the code

Then systemJS can directly import, see systemJS

Step 4: Register the app

singleSpa.registerApplication(
  'navbar'.// systemjs-webpack-interop, to match the application name
  () = > System.import('navbar'), // Resource path
  location= > location.hash.startsWith('#/navbar') // The resource is active
)


// Register child applications
singleSpa.registerApplication(
  'app1'.// systemjs-webpack-interop, to match the name of the child application
  () = > System.import('app1'), // Resource path
  location= > location.hash.startsWith('#/app1') // The resource is active
)


singleSpa.registerApplication(
  'app2'.// systemjs-webpack-interop, to match the name of the child application
  () = > System.import('app2'), // Resource path
  location= > location.hash.startsWith('#/app2') // The resource is active
)
/ / start singleSpa
singleSpa.start();
Copy the code

Step 5: Project development

The basic directory structure of the project is as follows:

.├ ─ readme.md ├─ Lerna. json # ├─ Node_modules ├─ Package. json ├─ ├─ App1 # ├ ─ ─ app2 # 2 │ ├ ─ ─ # navbar main application │ └ ─ ─ root - HTML - # file entry └ ─ ─ yarn. The lockCopy the code

As shown in the figure above, all applications are stored in the Packages directory. Root-html-file is the entry project, navbar is the resident main application, and the corresponding services must be started in the development process. Others are sub-applications to be developed.

Project optimization

Extract subapplication resource configurations

Extract all child applications from the main application into a common app.config.json file configuration

{
  "apps": [{"name": "navbar".// Application name
      "main": "http://localhost:8888/js/app.js".// The entry to the application
      "path": "/".// Whether it is a resident application
      "base": true.// Whether to use history mode
      "hash": true // Whether to use hash mode
    },
    {
      "name": "app1"."main": "http://localhost:8081/js/app.js"."path": "/app1"."base": false."hash": true
    },
    {
      "name": "app2"."main": "http://localhost:8082/js/app.js"."path": "/app2"."base": false."hash": true}}]Copy the code

The child application is registered in the entry file of the main application

try {
    // Read the application configuration and register the application
    const config = await System.import(`/app.config.json`)
    const { apps } = config.default
    apps && apps.forEach((app) = > registerApp(singleSpa, app))
    singleSpa.start()
  } catch (e) {
    throw new Error('Failed to load application configuration')}/** * Register application ** /
function registerApp (spa, app) {
  const activityFunc = app.hash ? hashPrefix(app) : pathPrefix(app)
  spa.registerApplication(
    app.name,
    () = > System.import(app.main),
    app.base ? (() = > true) : activityFunc,
  )
}


/** * Hash matching mode *@param App configuration */
 function hashPrefix (app) {
  return function (location) {
    if(! app.path)return true

    if (Array.isArray(app.path)) {
      if (app.path.some(path= > location.hash.startsWith(` #${path}`))) {
        return true}}else if (location.hash.startsWith(` #${app.path}`)) {
      return true
    }

    return false}}/** * Common path matching mode *@param App configuration */
function pathPrefix (app) {
  return function (location) {
    if(! app.path)return true

    if (Array.isArray(app.path)) {
      if (app.path.some(path= > location.pathname.startsWith(path))) {
        return true}}else if (location.pathname.startsWith(app.path)) {
      return true
    }

    return false}}Copy the code

All subprojects share one using VUEX

The main project index. HTML registers the vuex plug-in, the Window object store, and the sub-project load starts with registerModule injection of the sub-application’s module and its own Vue instance

// in the js of the main application
Vue.use(Vuex)
window.rootStore = new Vuex.Store() // globally register a unique VUEX for sub-applications to share


// Main.js of the child application
export const bootstrap = [
  () = > {
    return new Promise(async (resolve, reject) => {
      // Register the current app store
      window.rootStore.registerModule(VUE_APP_NAME, store)
      resolve()
    })
  },
  vueLifecycles.bootstrap
];
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;


Copy the code

Style isolation

We use a plugin for postCSS: postCSs-selector -namespace. It will add a class name prefix to all CSS in your project. This enables namespace isolation. NPM install postcss-selector-namespace –save -d postcss.config.js

// postcss.config.js

module.exports = {
  plugins: {
    // postcss-selector-namespace: Adds a uniform prefix to all CSS, and then adds a namespace to the parent project
    'postcss-selector-namespace': {
      namespace(css) {
        // Element-UI styles do not require namespaces
        if (css.includes('element-variables.scss')) return ' ';
        return '.app1' // Returns the name of the class to be added}}}},Copy the code

The parent project then adds the namespace

// Add the corresponding subsystem's class namespace to the body when switching subsystems
window.addEventListener('single-spa:app-change'.() = > {
  const app = singleSpa.getMountedApps().pop();
  const isApp = /^app-\w+$/.test(app);
  if (app) document.body.className = app;
});
Copy the code

Generate app.config.json and importMapjson using the manifest

Stats-webpack-plugin generates a manifest.json file that contains the public_path bundle list chunk list file size dependencies for each package. You can use this information to generate the app.config.json path and importMapjson for the child application.

npm install stats-webpack-plugin --save -d
Copy the code

Used in vue.config.js:

{
    configureWebpack: {
        plugins: [
            new StatsPlugin('manifest.json', {
                chunkModules: false.entrypoints: true.source: false.chunks: false.modules: false.assets: false.children: false.exclude: [/node_modules/]}),]}}Copy the code

Finally, the script generate-app.js generates the json path and importMapjson for the corresponding child application

const path = require('path')
const fs = require('fs')
const root = process.cwd()
console.log(The current working directory is:${root}`);
const dir = readDir(root)
const jsons = readManifests(dir)
generateFile(jsons)

console.log('Configuration file generated successfully')


function readDir(root) {
  const manifests = []
  const files = fs.readdirSync(root)
  files.forEach(i= > {
    const filePath = path.resolve(root, '. ', i)
    const stat = fs.statSync(filePath);
    const is_direc = stat.isDirectory();

    if (is_direc) {
      manifests.push(filePath)
    }

  })
  return manifests
}


function readManifests(files) {
  const jsons = {}
  files.forEach(i= > {
    const manifest = path.resolve(i, './manifest.json')
    if (fs.existsSync(manifest)) {
      const { publicPath, entrypoints: { app: { assets } } } = require(manifest)
      const name = publicPath.slice(1, -1)
      jsons[name] = `${publicPath}${assets}`}})return jsons

}



function generateFile(jsons) {
  const { apps } = require('./app.config.json')
  const { imports } = require('./importmap.json')
  Object.keys(jsons).forEach(key= > {
    imports[key] = jsons[key]
  })
  apps.forEach(i= > {
    const { name } = i

    if (jsons[name]) {
      i.main = jsons[name]
    }
  })

  fs.writeFileSync('./importmap.json'.JSON.stringify(
    {
      imports
    }
  ))

  fs.writeFileSync('./app.config.json'.JSON.stringify(
    {
      apps
    }
  ))

}


Copy the code

The application package

The build command is executed in the root directory. All build commands in packages will be executed. This will generate dist in the root directory.

lerna run build
Copy the code

The resulting directory structure is as follows

. ├ ─ ─ dist │ ├ ─ ─ app1 / │ ├ ─ ─ app2 / ├ ─ ─ navbar / │ ├ ─ ─ app. Config. The json │ ├ ─ ─ importmap. Json │ ├ ─ ─ the main, js │ ├ ─ ─ The generate - app. Js │ └ ─ ─ index. The HTMLCopy the code

Finally, run the following command to generate generate-app.js and regenerate the importmap.json and app.config.json files with hash resource paths: importmap.json

cd dist && node generate-app.js
Copy the code

The full demo file address in the article, give a star if you find it useful

Reference documentation

  • Lerna manages best practices for front-end modules
  • Lerna and YARN implement monorepo
  • Implementing a Single-SPA front-end Microservice from 0 (middle)
  • Single-spa + Vue Cli Micro front-end Landing Guide + Video (project isolation remote loading, automatic introduction)
  • Single-spa Microfront-end landing (including NGINx deployment)
  • Probably the most complete microfront-end solution you’ve ever seen
  • coexisting-vue-microfrontends