concept

What is single-SPA?

Single-spa is a javascrt-based micro-front-end framework that can be used to build coexisting micro-front-end applications, each of which can be written in its own framework, perfectly supporting Vue React Angular. Can realize the service registration event listening to the communication between the child and parent components and other functions.

Used by the parent project to integrate the child project

What is single-SPa-vue?

Single-spa-vue is an NPM package provided for use by subprojects using VUE. It can be quickly integrated with sigle-SPA parent projects and provides some portable apis.

For subproject use

What we’re going to achieve

  • Vue-cli is integrated with single-SPA
  • Remote loading service
  • Manifest automatically loads the required JS
  • Namespace style isolation
  • Compatibility problem solving

Handling of the parent project

Initialize the project

Both our parent and subprojects use vue-CLI for integration. In order to beautify the parent project, ant- Design – Vue is used as the front-end framework.

Create a new project called parent. We’ll leave vuex and ESLint out for convenience. Remember to enable history mode for the parent project vue-router.

Next we install Ant-design-Vue and single-SPA and start the project.

npm install ant-design-vue single-spa --save -d
Copy the code

The parent project registers the subproject route

We register a sub-service route, just register it, not fill in the Component field.

  {
    path: '/vue',
    name: 'vue',
  }
Copy the code

Build the basic framework

We simply write our base layout in the vUE component at the entrance of the parent project. On the left is the menu bar and on the right is the layout bar.

There is a vUE list item in the left menu bar, and there are two routes in vUE. Add a DOM element with the ID single-vue to the right of the tolerance bar. This is the target DOM element that our subproject will mount later.

<template> <a-layout id="components-layout-demo-custom-trigger"> <a-layout-sider :trigger="null" collapsible v-model="collapsed"> <div class="logo" /> <a-menu theme="dark" mode="inline"> <a-sub-menu key="1"> <span slot="title"> <a-icon type="user" /> <span>Vue</span> </span> <a-menu-item key="1-1"> <a href="/vue#"> Home </a> </a-menu-item> <a-menu-item key="1-2"> <a href="/vue#/about"> About </a> </a-menu-item> </a-sub-menu> </a-menu> </a-layout-sider> <a-layout> <a-layout-header style="background: #fff; padding: 0" /> <a-layout-content :style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '280px' }"> <div class="content"> <! - this is the right NaRongLan -- > < div id = "single - vue" class = "single - spa - vue" > < div id = "vue" > < / div > < / div > < / div > < / a - layout - content > </a-layout> </a-layout> </template> <script> export default { data() { return { collapsed: false, }; }}; </script> <style> #components-layout-demo-custom-trigger .trigger { font-size: 18px; line-height: 64px; padding: 0 24px; cursor: pointer; The transition: color 0.3 s; } #components-layout-demo-custom-trigger .trigger:hover { color: #1890ff; } #components-layout-demo-custom-trigger .logo { height: 32px; Background: rgba(255, 255, 255, 0.2); margin: 16px; } </style>Copy the code

Register a subproject

Here is our highlight: how to use the single-SPA registration subprogram. Before we register, let’s take a look at two apis:

SingleSpa registerApplication: this is registered component method. The parameters are as follows:

  • AppName: Subproject name
  • ApplicationOrLoadingFn: Subproject registration function that the user needs to return a single-SPA lifecycle object. The lifecycle mechanism of single-SPA is described later
  • ActivityFn: Callback function input parameterlocationObject that can write custom matching route loading rules.

Singlespa. start: This is the start function.

Let’s create a new single-spa-config.js and introduce it in main.js.

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Ant from 'ant-design-vue';
import './single-spa-config.js'
import 'ant-design-vue/dist/antd.css';
Vue.config.productionTip = false;

Vue.use(Ant);

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
Copy the code

single-spa-config.js:

// single-spa-config.js import * as singleSpa from 'single-spa'; // Import single-spa /* * runScript: a promise synchronization method. Instead of creating a script tag, Const runScript = async (URL) => {return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = url; script.onload = resolve; script.onerror = reject; const firstScript = document.getElementsByTagName('script')[0]; firstScript.parentNode.insertBefore(script, firstScript); }); }; SingleSpa. RegisterApplication (/ / registered micro front-end services' singleDemo ', Async () = > {await the runScript (' http://127.0.0.1:3000/js/chunk-vendors.js'); Await the runScript (' http://127.0.0.1:3000/js/app.js'); return window.singleVue; }, location = > location. The pathname. StartsWith ('/vue ') / / configure micro front-end module prefix); singleSpa.start(); / / startCopy the code

Unlike the official document, we used remote loading here. The principle of remote loading, which we’ll write separately later.

Now that the parent project is processed, let’s move on to the subproject.

Processing of subprojects

Initialize the project

Subprojects are a little more complicated than parent projects.

Let’s go ahead and create a new project, called vue-child, using vue create vue-child. The creation process of the subproject is arbitrary, and we will ignore the process here.

In addition, we need to install an NPM package called single-spa-Vue.

npm install single-spa-vue --save -d
Copy the code

single-spa-vue

If you want to register as a subproject, you also need a package for single-SPA-Vue.

Import single-spa-vue into main.js, passing in the vue object and the vue.js mount parameter, and you can register. It returns an object with the lifecycle functions required by the single-SPA. Use export to export

import singleSpaVue from "single-spa-vue";
import Vue from 'vue'

const vueOptions = {
    el: "#vue",
    router,
    store,
    render: h= > h(App)
};

// singleSpaVue wraps a VUE micro front-end service object
const vueLifecycles = singleSpaVue({
    Vue,
    appOptions: vueOptions
});

// Export the lifecycle object
export const bootstrap = vueLifecycles.bootstrap; / / start
export const mount = vueLifecycles.mount; / / mount
export const unmount = vueLifecycles.unmount; / / unloading

export default vueLifecycles;

Copy the code

The processing of webpack

It’s just exported, and you need to mount it to window.

Create vue.config.js in the project directory and modify our Webpack configuration. We modify the Library and libraryTarget fields in the Webpack output.

  • Output. library: the name of the exported object
  • Output. LibraryTarget: Where to mount after export

Also, since we are calling remotely, we need to set the publicPath field to your real service address. Otherwise, when the subchunk is loaded, it will be found in the root path of the current browser domain name, causing a 404 problem. Since our local service starts at localhost:3000, we will set //localhost:3000.

Module. exports = {publicPath: "//localhost:3000/", // CSS is not packaged as a file in all environments. CSS: {extract: false}, configureWebpack: {devtool: 'none', // unpack sourcemap output: {library: "SingleVue ", // export name libraryTarget: "window", // mount target}}, devServer: {contentBase: './', compress: true,}};Copy the code

Vue-cli-service serve –port 3000

On the left, you can switch routes in subprojects. The network is loaded on the right.

And so we have our first edition. Next, we do further optimization and sharing

Style isolation

For the style isolation section, we use a postCSS plug-in: Postcss-selector – Namespace. It prefixes all CSS in your project with a class name. This enables namespace isolation.

First, let’s install the plugin: NPM install postcss-selector-namespace –save -d

Create postcss.config.js in the project directory and use the plug-in:

// postcss.config.js module.exports = { plugins: { // postcss-selector-namespace: Add a uniform prefix to all CSS, and then add the namespace' postcss-selector-namespace' to the parent project: {namespace(CSS) {// Element-ui styles do not need to add a namespace if (css.includes(' element-elimination.scss ')) return ""; Return '.single-spa-vue' // Return the name of the class to be added}},}}Copy the code

In the block that the parent project wants to mount, add our namespace. The end of the

Independent operation

As you may have noticed, our sub-services are now unable to run on their own, but now we can run in both independent and integrated mode.

Single-spa has a property called window.Singlespanavigate. If true, it represents single-SPA mode. If false, it can render independently.

We remodel the main.js of the All at once project:

// main.js const vueOptions = { el: "#vue", router, render: h => h(App) }; /**** add here ****/ if (! Window.singlespanavigate) {// Delete vueoptions.el if not single-spa mode; new Vue(vueOptions).$mount('#vue'); } /**** end ****/ // singleSpaVue const vueLifecycles = singleSpaVue({vue, appOptions: vueOptions});Copy the code

This gives us independent access to the index. HTML of the child service. Don’t forget to add a namespace to public/index.html, otherwise the style will be lost.

<div class="single-spa-vue">
    <div id="app"></div>
</div>
Copy the code

Things to know

The remote loading

In this case, our remote load uses async await to build a synchronously executed task.

Create a script tag, wait for the script to load, and return the object on which the script was loaded.

/* * runScript: a promise synchronization method. Instead of creating a script tag, Const runScript = async (URL) => {return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = url; script.onload = resolve; script.onerror = reject; const firstScript = document.getElementsByTagName('script')[0]; firstScript.parentNode.insertBefore(script, firstScript); }); };Copy the code

Vue vs. React/Angular mounts

Vue 2.x is mounted by overwriting the DOM. For example, if a component is mounted on #app, it overwrites the #app element with the component.

React/Angular, however, mounts by adding elements inside the target mount element, rather than overwriting them directly. For example, if a component is mounted on #app, it will mount the component inside #app, and #app will still exist.

This creates a problem where I can’t find the DOM element to mount from the vue subproject => React project => vue subproject and throw an error.

The solution to this problem is to make the root element class /ID name of the vUE project component the same as the element to be mounted.

For example, if we want to mount to the #app dom, then the app.vue inside our subproject should also have the top DOM element id named #app.

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>
Copy the code

Manifest automatically loads bundles and chunk.vendor

We can see in the above code that the parent project loads the child project. To register a child service, we need to load two JS files at once. Can we write dead filenames and lists if we need to load more JS, or even if the production bundle has a unique hash?

SingleSpa. RegisterApplication (' singleVue, async () = > {await the runScript (' http://127.0.0.1:3000/js/chunk-vendors.js'); / / write dead file list await the runScript (' http://127.0.0.1:3000/js/app.js'); return window.singleVue; }, location => location.pathname.startsWith('/vue') );Copy the code

The idea is to have the subproject use the stats-webpack-plugin and output a manifest.json file containing only important information after each packaging. The parent project first ajax requests the JSON file, reads from it the JS directory that needs to be loaded, and then loads it synchronously.

stats-webpack-plugin

This is where the WebPack Plugin comes in. It generates a manifest.json file that contains the public_path bundle list chunk list file size dependencies and so on.

{
  "errors": []."warnings": []."version": "4.41.4"."hash": "d0601ce74a7b9821751e"."publicPath": "//localhost:3000/"."outputPath": "/Users/janlay/juejin-single/vue-chlid/dist"."entrypoints": { // Use this field only
    "app": {
      "chunks": [
        "chunk-vendors"."app"]."assets": [
        "js/chunk-vendors.75fba470.js"."js/app.3249afbe.js"]."children": {},
      "childAssets": {}}... . }Copy the code

Let’s switch to the subproject directory and install the WebPack plug-in:

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

Use in vue.config.js:

{ configureWebpack: { devtool: 'none', output: { library: "singleVue", libraryTarget: [new StatsPlugin('manifest.json', {chunkModules: false, entrypoints: true, source: false, chunks: false, modules: false, assets: false, children: false, exclude: [/node_modules/]}),] /**** add end ****/}}Copy the code

You can access the WebPack Chinese document – configuration – STATS to view the specific configuration items

Parent project transformation

Of course, the single runScript in the parent project is no longer supported, so write a getManifest method and handle it.

/* * getManifest: load the manifest.json file, parse the js * URL that needs to be loaded: Manifest. Json link * bundle: Entry name * */ const getManifest = bundle) => new Promise(async (resolve) => { const { data } = await axios.get(url); const { entrypoints, publicPath } = data; const assets = entrypoints[bundle].assets; for (let i = 0; i < assets.length; i++) { await runScript(publicPath + assets[i]).then(() => { if (i === assets.length - 1) { resolve() } }) } });Copy the code

Json file, deconstruct the entryPoints publicPath field, iterate through the real JS path, and then load it in order.

async () => { let singleVue = null; Await the getManifest (' http://127.0.0.1:3000/manifest.json ', 'app'). Then (() = > {singleVue = window. SingleVue; }); return singleVue; },Copy the code

In fact, if you want to do a micro front-end management platform, but also rely on this implementation.

Single – SPA life cycle and implementation principles

Here recommend a dalao principle to share: link

Looking forward to

communication

This can be implemented using a publish-subscribe pattern or a vuex like state management

Js sandbox to achieve state management

This can be monitored using proxy, toggle save, enter restore state

Code warehouse & video & last

Code repository for this article: Code Cloud

Video: Baidu Cloud password: NMne

Welcome to join the micro front end discussion group and study together.