This article is licensed under a “CC BY 4.0” license. You are welcome to reprint or modify this article, but the source must be noted.
Author: Baiying front-end team @0o Howie O0 launched at juejin.cn/post/695534…
directory
- preface
- Use single-SPA for the first time
- Base application modification
- Sub-application modification
- Single-spa advanced use: sing sing
- Component transformation
- Application of modified
- Common API Usage
- Points of concern when using single-SPA
- Application mode How to switch sub-applications
- How does single SPA work
- How is the lifecycle method of the child application/component obtained
- How do the sub-applications/components communicate
- Why does the child application/component lifecycle method return a Promise object
- Single-spa life cycle hooks
- NPM package for use with single-SPA
- Single – the shortage of the spa
- conclusion
preface
Through the last micro front-end learning series (1): micro front-end introduction learning, I believe you have a preliminary understanding of the micro front-end and current popular technology solutions. On the basis of the last one, this paper will comb the single-SPA scheme in detail from the aspects of usage, common API, implementation principle and so on, hoping to give you some help.
Use single-SPA for the first time
Before we get started, let’s do a quick review.
Single-spa provides a micro-front-end scheme for routing-based basetization, which divides applications into two categories: base-based applications and sub-applications. Where the child application corresponds to the application we talked about earlier that needs to be aggregated, the base application is a separate application for the aggregator application. In a base application, we maintain a routing registry – one child for each route. After the base application is started, when we switch routes, if it is a new subapplication, we will dynamically obtain the js script of the subapplication, and then execute the script and render the corresponding page. If it’s an already accessed child, it gets the cached child from the base’s cache, activates the child, and renders the corresponding page.
Next, we will see how to use single-SPA through a simple demo-micro-frontend /single-spa/application.
The project structure of Demo is as follows:
|-- single-spa
|--application
|--main
|--package.json
|--public
|--src
|--...
|--app1
|--package.json
|--public
|--src
|--...
|--app2
|--package.json
|--public
|--src
|--...
|--app3
|--package.json
|--public
|--src
|--...
Copy the code
Among them, App1, APP2 and App3 are three sub-applications. App1 uses vuE2 technology stack, App2 uses VUE3 technology stack, and App3 uses React technology stack. Main is the base application, using the technology stack vuE2.
After all applications are started, the effect is as follows:
If single-SPA is to work properly, we need to modify the base application and sub-application respectively.
-
Base application modification
In single-SPA, the base application is mainly used to manage sub-applications, including switching sub-applications according to routing and communication between sub-applications.
We simply create a routing registry for the subapplications. We then register the subapplications using the registerApplication method provided by single-SPA based on the routing registry. Finally, after the base application is mounted, Execute the start method provided by single-SPA.
The specific code is as follows:
import Vue from 'vue' import App from './App.vue' import VueRouter from 'vue-router'; const { registerApplication, start } = require('single-spa'); Use (VueRouter) vue.config.productionTip = false window.__SINGLE_SPA__ = true const router = new VueRouter({ mode: 'history', routes: [] }); Function createScript(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, Function loadApp(url, globalVar, entrypoints) {return async () => {for(let I = 0; i < entrypoints.length; I ++) {await createScript(URL + entryPoints [I])} return window[globalVar]}} // Child application route registry const apps = [{// Child application name Name: 'app1', // subapp load function app: LoadApp ('http://localhost:8081', 'app1', ["/js/ chunk-pds.js ", "/js/app.js"]), // if the route satisfies the criteria (returns true), Activation (mount) application activeWhen: location = > the location, the pathname. StartsWith ('/app1), / / son passed to the application object customProps: {}}, {name: 'app2', app: loadApp('http://localhost:8082', 'app2', [ "/js/chunk-vendors.js", "/js/app.js" ]), activeWhen: Location = > location. The pathname. StartsWith ('/app2), customProps: {}}, {/ / child application name name: 'app3'/app/child application load function: LoadApp ('http://localhost:3000', 'app3', ["/main.js"]), // activate (mount) the child application activeWhen: Location = > location. The pathname. StartsWith ('/app3), / / is passed to the child object customProps application: For (let I = apps.length-1; let I = apps.length-1; i >= 0; i--) { registerApplication(apps[i]) } new Vue({ router, render: H = > h (App), mounted () {/ / start start ()}}) $mount (' # App)Copy the code
In the above code, the key is to create the routing registry for the child application. In the registry, you need to specify name, APP, activeWhen, and customProps for each sub-application.
-
name
The unique identifier of the sub-application is a string that cannot be repeated.
-
activeWhen
The condition for activation of a subapplication is a function. When the page URL changes, the activeWhen method of the child application that executes the registration is traversed. If activeWhen returns true, the corresponding child application is activated.
-
app
The bootstrap, mount, unmount function is used to get the lifecycle methods provided by the child application to the base application: bootstrap, mount, unmount, etc.
We know that in a single page vue, React based application, each page exists as a component. When switching pages based on routes, the unmount operation of the components on the previous page is performed first, and then the mount operation of the components on the next page is performed.
The base application performs the same operation when switching between children, that is, unmount the previous child and mount the next child. Therefore, the child application needs to provide life cycle methods such as mount and unmount for the base application to call.
Just like the lazy loading of a single-page application, when a base application activates a child application, if the child is activated for the first time, it executes the app method to dynamically load the entry JS file of the child application and then executes it to get the life cycle method of the child application.
-
customProps
When a child application is activated, a custom attribute that can be passed to the child application is an object.
-
-
Sub-application modification
The transformation of sub-application involves two aspects:
- Add life cycle methods to entry file index.js – mount, unmount, update, etc.
- Packaging construction and transformation;
Take APP1 as an example. The code is as follows:
// index.js import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false const appOptions = { render: (h) => h(App) }; let vueInstance; // The sub-application is not connected to single-spa if (! Window.__single_spa__) {new Vue(appOptions).$mount('#app')} export function bootstrap () { console.log('app1 bootstrap') return Promise.resolve().then(() => { }); } export function mount (props) {console.log('app1 mount', props); props) return Promise.resolve().then(() => { vueInstance = new Vue(appOptions) vueInstance.$mount('#microApp') }) } // Export function unmount () {console.log('app1 unmount') return promise.resolve ().then(() => {if (! $el.id) {vueinstance.$el.id = 'microApp'} vueinstance.$destroy() vueinstance.$el.innerhtml = ''}) Export function update () {console.log('app1 update'); }Copy the code
The most critical of the lifecycle methods mentioned above are mount and unmount. Mount is called when the child application is activated, and the child application is mounted using the API provided by the framework (vue/react/Angular). Unmount is called when the child application is frozen, also using the API provided by the framework to uninstall the child application.
If mount or unmount is missing, an exception is thrown.
In addition, we need to change the build script of the project as follows:
// vue.config.js module.exports = { configureWebpack: { ... publicPath: 'http://localhost:8081' output: { library: 'app1', libraryTarget: 'var' }, ... }}Copy the code
Often, the JS scripts generated by webPack build tools are presented in the form of IIFF, or immediate execution of function expressions. In this case, the js scripts corresponding to each sub-application are executed in isolation from each other. If this is the case, then the base application will not be able to obtain the lifecycle methods of the child application when it is activated and will not be able to mount the child application.
In order to break this isolation, we need to modify the output configuration item, add libaray and libraryTarget configuration items, and expose the return value of the child application entry file, namely the lifecycle method, to the window. This allows the base application to retrieve the lifecycle methods of the child application from the window.
In addition, we need to configure publicPath. The publicPath configuration item specifies the public URL corresponding to the output directory (path). It is mainly used to complete the paths of imported resources in the page. The default value is ‘/’. App1, for example, if you do not configure publicPath, applied in a lazy loading js file, use the url of the default host for the current application and file location, such as http://localhost:8081/js/0.chunk.js. If app1 access single – spa, app1 in lazy loading js file using the url for the base application host and file locations, such as http://localhost:8080/js/0.chunk.js, failed to load file. Therefore, we need to add the publicPath configuration item in App1, specifying the host of app1 lazy load file as http://localhost:8081.
The above example is the basic use of single-SPA, also known as the Application pattern. In application mode, the switching (mount and unmount) of the sub-application is triggered by the modified route. The whole switching process is controlled by the single-SPA framework, and the sub-application only needs to provide the correct lifecycle method.
Single-spa advanced use: sing sing
In the process of project development, we may encounter such A situation: the child application A is developed using React. In the process of project iteration, A common component Component1 about A specific business is extracted and uploaded as an NPM package. Sub-application B is developed using VUE. In the process of project iteration, a specific business mentioned just now needs to be used. If another version of VUE is developed, it will be a bit repetitive.
Can we use the React component directly in the vUE application? The answer is yes. We can use ReactDOM’s Render and unmountComponentAtNode methods to directly load/update/uninstall react components in vUE applications.
Again, we use a simple demo-micro-frontend /single-spa/parcel to see how the React component is added to the VUE application.
Project structure for Demo:
|-- single-spa
|-- application
|-- ...
|-- parcel
|-- app
|-- package.json
|-- public
|-- src
|-- main.js
|-- App.vue
|-- components
|-- vue-component.vue
|-- react-component.js
Copy the code
In the demo above, app is a VUE2 based application. The react-Component is a react component that emulates an external NPM package.
import React from 'react';
import ReactDOM from 'react-dom';
const ReactComponent = (props) => {
return React.createElement("div", null, `react component: ${props.val}`);
}
export default ReactComponent;
Copy the code
The vue component vue-component.vue code is as follows:
// vue-component.vue <template> <div> <div>vue component: <input v-model="val" /></div> <div id="parcel"></div> </div> </template> <script> import ReactComponent from './react-component' import ReactDOM from 'react-dom' export default { name: 'vue', data() { return { val: }}, mounted() {// React reactdom.render (ReactComponent({val: This.val}), document.getelementById ('parcel'))}, updated() {// after vue component is updated, Reactdom.render (ReactComponent({val:}); This.val}), document.getelementById ('parcel'))}, beforeDestory() {// Before vue components are uninstalled, Manually uninstall the react component ReactDOM. UnmountComponentAtNode (document. GetElementById (' parcel ')); } } </script>Copy the code
With this code, we can mount/update/uninstall the React component in the VUE application. The problem with this approach is that it is not elegant enough. We need to introduce react-DOM into the VUE application. If multiple react components are introduced that have different versions of the React-DOM, this can be a problem.
So is there a better way?
There is. Single-spa offers a model called Parcel to help us achieve this more elegantly and conveniently.
To use the single-SPA Parcel mode, we need to make changes to the React component and vUE application.
-
Component transformation
The first is component modification. We need to modify the component like the sub-application in the Application pattern. We need to add lifecycle methods to the component: bootstrap, mount, unmount, and Update.
The details are as follows:
import React from 'react'; import ReactDOM from 'react-dom'; const ReactComponent = (props) => { return React.createElement("div", null, `react component: ${props.val}`); } export default ReactComponent; export function bootstrap() { return Promise.resolve().then(() => { console.log('bootstrap'); }) } export function mount(props) { return Promise.resolve().then(() => { console.log('mount'); ReactDOM.render(ReactComponent(props), props.domElement); }); } export function unmount(props) { return Promise.resolve().then(() => { console.log('unmount'); ReactDOM.unmountComponentAtNode(props.domElement); }) } export function update(props) { return Promise.resolve().then(() => { console.log('update'); ReactDOM.render(ReactComponent(props), props.domElement); })}Copy the code
-
Application of modified
In Parcel mode, we need to manually mount/update/uninstall components using the mountRootParcel method provided by single-SPA. The code is as follows:
<template> <div> <div> Vue component: <input v-model="val" /></div> <div id="parcel"></div> </div> </template> <script> import { bootstrap, mount, unmount, update} from './react-component' import { mountRootParcel } from 'single-spa' export default { name: 'vue', data() { return { val: '123' } }, mounted() { this.parcel = mountRootParcel({bootstrap, mount, unmount, update}, { val: this.val, domElement: document.getElementById('parcel') }) }, updated() { if (this.parcel && this.parcel.update) { this.parcel.update({ val: this.val, domElement: document.getElementById('parcel') }) } }, beforeDestory() { if (this.parcel && this.parcel.unmount) { this.parcel.unmount({ domElement: document.getElementById('parcel1') }) } } } </script>Copy the code
With these changes, we can use the React component in a vUE application with elegance and convenience, and it doesn’t matter that the react and react-DOM versions of each react component are not consistent.
In addition, in Parcel mode, components can be mounted through another APi-mountParcel, in addition to the mountRootParcel method provided by single-SPA. The use of mountRootParcel is exactly the same as that of mountParcel, except that the mountParcel method cannot be obtained directly from the single-SPA. Instead, it needs to be obtained from the props passed when the mount lifecycle method of the child application/component executes, as follows:
. export function mount(props) { ... props.mountParcel(...) . }...Copy the code
The different methods of obtaining mountRootParcel and mountParcel result in different application scenarios.
MountParcel can only be obtained from the props passed in when the mount lifecycle method of the child application/component is executed. This makes mountParcel available only when other components are mounted in the child application/component. MountParcel binds the component to be mounted to the child application (parent), and when the child application (parent) is to be unmounted, the unmount method of the component is automatically triggered.
MountRootParcel has no mountParcel use restrictions, we can mount components anywhere using the mountRootParcel method. Note, however, that components mounted via mountRootParcel must be manually unmounted yourself.
For example, app1, a vue-based sub-application, is connected to single-SPA, and in App1 we use comp2, a component developed based on React. App1 and Comp2 both provide complete lifecycle methods – bootstrap, mount, and unmount. If we use mountParcel to mount Comp2, then we do not need to uninstall Comp2 as shown in componentWillUnmount of Comp2’s parent component. When the app1 application uninstalls comp2, it automatically uninstalls comp2. If we use mountRootParcel to mount comp2, we must uninstall comp2 as shown in componentWillUnmount of comp2’s parent component, otherwise app1 will not uninstall comp2 when unmounting comp2.
This concludes the use of the Parcel pattern. Here, we make a simple comparison between the Application and Parcel modes:
The title | application | parcel |
---|---|---|
Routing control | There are | There is no |
The UI rendering | There are | There are |
Life cycle approach | Single – spa management | User management |
Application scenarios | Aggregation of multiple sub-applications | Use components across frameworks |
Common API Usage
The apis related to single-SPA are already detailed in the official documentation and will not be covered in this article. The following is a list of links to the official website of all single-SPA apis. You can go to the official website to check.
-
Application class API
- registerApplication
- start
- triggerAppChange
- navigateToUrl
- getMountedApps
- getAppNames
- getAppStatus
- unloadApplication
- checkActivityFunctions
- addErrorHandler
- removeErrorHandler
- pathToActiveWhen
- ensureJQuerySupport
- setBootstrapMaxTime
- setMountMaxTime
- setUnmountMaxTime
- setUnloadMaxTime
-
Parcel class API
- mountParcel
- mountRootParcel
- Parcel object
About single-SPA, you need to know more
-
How to switch sub-applications in Application mode
Most popular single-page applications use window.history(window.location.hash) to implement route switching. In a single-page application, we would register the PopState (hashChange) event for the Window object and add the page switching logic to the popState (hashChange) callback. The popState (hashChange) event is triggered when the URL is changed by executing the pushState(replaceState) method, modifying the hash value, and using the browser’s go, back, and forward functions, and then the page is switched.
The switching of sub-applications is also based on the above principles.
When the base application loads a single-SPA, it also registers a PopState (hashChange) event for the Window object. The popState (hashChange) calback is the logic that activates the child application. Popstate (hashChange) is triggered when the base application changes the URL by performing pushState(replaceState), modifying the hash, and using the browser’s go, back, and forward functions. The activation logic of the corresponding child application is executed.
We know that when we use window.history, if we execute the pushState(repalceState) method, it will not trigger the popState event. PushState (replaceState) {pushState(replaceState) {popState (popstate);
// Create a popState event object using the native constructor -popStateEvent. Function createPopStateEvent(state, originalMethodName) {var evt; try { evt = new PopStateEvent("popstate", { state: state }); } catch (err) { evt = document.createEvent("PopStateEvent"); evt.initPopStateEvent("popstate", false, false, state); } evt.singleSpa = true; evt.singleSpaTrigger = originalMethodName; return evt; } // Override the updateState and replaceState methods, using the window.dispatchEvent method, Function patchedUpdateState(updateState, methodName) { return function () { var urlBefore = window.location.href; var result = updateState.apply(this, arguments); var urlAfter = window.location.href; if (! urlRerouteOnly || urlBefore ! == urlAfter) { window.dispatchEvent(createPopStateEvent(window.history.state, methodName)); } return result; }; } / / rewrite pushState method window. History. PushState = patchedUpdateState (window. History. PushState, "pushState"); / / rewrite replaceState method window. History. ReplaceState = patchedUpdateState (window. History. ReplaceState, "replaceState");Copy the code
The popState event can be triggered when the pushState and replaceState methods are executed because single-spa overwrites the pushState and replaceState methods of window.history. When the pushState and replaceState methods are executed, an event object is built using the native method -PopStateEvent, and the popState event is manually triggered by calling the window.dispatchEvent method.
In addition, regarding the routing control of single-SPA, there is one point that needs to be paid attention to. That is, if the sub-application that accesses single-SPA has its own route, it needs to modify the sub-application route, that is, add a prefix to the sub-application route, as follows:
. const router = new VueRouter({ mode: 'history', base: '/app1', routes: [{ path: '/foo', name: 'foo', component: { ... } }, { path: '/bar', name: 'bar', component: { ... } }] }) ...Copy the code
Sub-application app1, if the base application route is ‘/app1’, then all sub-application routes need to be prefixed with ‘/app1’. This is because when the URL changes, the child application is matched first, and then the child application page is matched. If the prefix is not added to the sub-application, the modified URL may not match the sub-application, and the corresponding sub-application page cannot be found.
When a child application switches pages, the child application will not be mounted again despite the URL change.
-
How does single-SPA work
Single-spa has two usage modes: Application and Parcel. The workflow of single-SPA varies depending on the usage mode.
-
Application mode, single- SPA workflow
In application mode, we need to register the child application through registerApplication, and then execute the start method after the base application is mounted, so that the base application can switch the child application according to the URL change and activate the corresponding child application.
The whole working process is as follows:
-
Parcel mode, single- SPA workflow
Compared to the Application pattern, the parcel pattern has a simpler workflow. We just need to get the component’s lifecycle method and mount it directly using the mountRootParcel method.
The mountRootParcel method returns a Parcel instance object containing the update and unmount methods. When we need to update the component, we trigger the component’s Update lifecycle method by calling the update method on the Parcel object directly; When we need to unload the component, call the unmount method of the Parcel object directly.
The second parameter passed in when the mountRootParcel method is executed is the input parameter to the component mount lifecycle method; The parameters passed in when the parcel. Update method is executed are the incoming parameters to the component’s Update lifecycle method.
Components must be mounted, updated, or unmounted with the corresponding root node, so the domElement attribute must be included in the parameter, otherwise an exception will be thrown.
-
-
How is the lifecycle method of the child application/component obtained
In order to make the lifecycle methods of child applications/components available externally, we need to modify the output configuration items of the project webPack configuration file to add the Library and libraryTarget configuration items. The Library defines the name of the export of the child application/component, and the libraryTarget defines how to expose the export.
In the micro-frontend/single-spa/application example above, we define the library as ‘app1’ and the libraryTarget as ‘var’ in app1, Then the base application can obtain the export of the child application through window.app1 and get the life cycle method of the child application.
The libraryTarget configuration item has many values, which can be configured according to the actual needs.
-
How to determine if a child application (component) is also activated (mounted)
Each subapplication has a STATUS field that represents the phases in the subapplication lifecycle:
-
NOT_LOADED: not loaded or to be loaded
Not loaded (to be loaded) The default state of each child application, meaning that the main application has not obtained the bootstrap, mount, unmount, or unload methods of the child application.
The next phase of NOT_LOADED is LOAD_SOURCE_CODE.
-
LOAD_SOURCE_CODE: loads the source code
At this stage, the single-SPA executes the loadApp method provided during the registration of the child application to dynamically obtain the entry JS file of the child application, and then executes to get the bootstrap, mount, unmount, and unload methods of the child application.
The next stage of LOAD_SOURCE_CODE is NOT_BOOTSTRAPPED.
-
NOT_BOOTSTRAPPED: not started/to be started
After obtaining the bootstrap, mount, unmount, unload, and update methods of the child application, single-SPA adds these methods to the child application object. After that, the child app is not_bootstrapped and waiting to be started.
The next stage of NOT_BOOTSTRAPPED is BOOTSTRAPPING.
-
BOOTSTRAPPING: The child application is being started
Once the child application is activated, it enters the BOOTSTRAPPING phase. If the child application provides a bootstrap method, the bootstrap method is triggered.
The next stage of BOOTSTRAPPING is NOT_MOUNTED.
-
NOT_MOUNTED: Not mounted or to be mounted
After the child application is started, the NOT_MOUNTED phase is automatically entered.
The next stage of NOT_MOUNTED is MOUNTING.
-
MOUNTING: The child application is being mounted
At this stage, the mount method provided by the child application is automatically triggered.
MOUNTING is the next stage of MOUNTING
-
MOUNTED: The child application has been MOUNTED
After the child application is MOUNTED, the child application state changes to MOUNTED.
-
UNMOUNTING
If a child application needs to be uninstalled, the state of the child application becomes UNMOUNTING.
At this point, the unmount method of the child application is executed.
-
UNMOUNTED
After the child application is UNMOUNTED, the child application is in the UNMOUNTED state.
-
LOAD_ERROR
If the child application fails to be loaded, the child application status changes to LOAD_ERROR.
The components and their child applications have the same status, including NOT_BOOTSTRAPPED, BOOTSTRAPPING, NOT_MOUNTED, MOUNTED, UNMOUNTING, UNMOUNTED, and UPDATING. That is, when the parcel. Update method is executed, the component state changes to UPDATING.
The UPDATING state exists only for parcel components.
If the child application or component is in the MOUNTED state, it indicates that the child application or component is activated.
The status value of the child application can be obtained by getAppStatus. The value of the status of the component. Obtain this value from parcel. GetStatus
-
-
How do the sub-applications/components communicate
When using single-SPA, it is inevitable to encounter communication between child applications/components. Common communication mainly includes communication between parent and parcel components, communication between parcel components, communication between base applications and child applications, and communication between child applications.
-
Communication between parent and Parcel components
Communication between the parent and the Parcel component is simple; the parent can pass values to the Parcel component via the props property.
The specific way is: In the mount phase, the parent can use the value passed to the Parcel component as the second parameter when mountRootParcel is executed. This parameter will be used as an input parameter when the parcel component mount method is executed, so that the parcel component can get the value passed by the parent component. The same goes for the update phase, when the parent performs parcel. Update, the parameters passed in are used as incoming parameters when the Parcel component’s update method executes.
We can add a method to the value passed by the parent to the Parcel component that can be executed when the Parcel component needs to notify the parent of an update.
-
Communication between Parcel components
Communication between parcel components is also easy to implement, and is essentially communication between parcel components and their parent components. A Parcel component can trigger an update to its parent component via a parent delivery method, which then triggers an update to another parcel component.
-
Communication between base applications and sub-applications
When the base application defines the routing registry, it defines a customProps for each child application. This customProps is used as an input to the mount method of the child application. In a child application, customProps(or a value within customProps) can be used as the shared state of the child application (using vuex, mobx, redux, etc.). This way, when the base application modifies the customProps, the child application can be notified and then updated.
We can also add a method to cusProps that can be executed when a child application needs to notify the base application of an update.
-
Communication between sub-applications
The same goes for communication between sub-applications, which is essentially communication between the base application and sub-applications.
-
-
Why does the child application/component lifecycle method have to return a Promise object
For example, we need to define a mount method for a child application/component as follows:
Export function mount(props) {return promise.resolve ().then() => {// function mount(props)... })}Copy the code
In single-SPA, the specific logic triggered by the sub-application/component lifecycle method is as follows:
function reasonableTime(appOrParcel, lifecycle) { ... return new Promise(function (resolve, reject) { var finished = false; var errored = false; // Return a promise object appOrParcel[lifecycle](getProps(appOrParcel)). Then (function (val) {finished = true; resolve(val); }).catch(function (val) { finished = true; reject(val); }); . }); }... ReasonableTime (appOrParcel, "Mount ").then(function () {// Mount appOrParcel. Status = Mounted; . }).catch(function (err) { ... });Copy the code
In summary, the lifecycle method of the child application/child component needs to return a Promise object that can be in the Resolved state, otherwise the single-SPA will fail to execute the lifecycle method of the child application.
-
Single-spa life cycle hooks
Single-spa defines a number of lifecycle hooks that help us perform custom operations in the child application/component lifecycle. These hooks include: ‘single-spa:before-first-mount’, ‘single-spa:before-first-mount’, ‘single-spa:before-no-app-change’, ‘single-spa:before-app-change ‘ ‘, ‘single-spa:before-routing-event’, ‘single-spa:before-mount-routing-event’, ‘single-spa:no-app-change’, ‘single-spa:app-c ‘ Hange ‘, ‘single – spa: routing – event’.
For example, single-spa:before-first-mount. Before-first-mount indicates that the child application/component is mounted for the first time. The method is as follows:
// In callback we can customize window.addeventListener ('single-spa:before-first-mount', event => {... })Copy the code
Single-spa :before-first-mount triggers the single-SPA :before-first-mount event before the child application/component is mounted for the first time as follows:
window.dispatch(new customEvent("single-spa:before-first-mount")); Copy the code
-
single-spa:before-first-mount
Single-spa triggers before the child application/component is first mounted, and does not trigger again after that.
-
single-spa:first-mount
Single-spa fires after the first mount of the child application/component, and does not fire again after that.
Fisrt-mount is triggered only after the child application/component is mounted.
-
single-spa:before-no-app-change
In application mode, changing the URL triggers the switchover of sub-applications. If there is no subapplication in the routing registry that matches the current URL, the single-SPA :before-no-app-change event is triggered.
-
single-spa:before-app-change
In contrast to single-spa:before-no-app-change, the single-spa:before-app-change event will be triggered if there is a subapplication matching the current URL in the routing registry when changing the URL causes the subapplication to switch.
-
single-spa:before-routing-event
In application mode, the single-SPA :before-routing-event event is triggered after hashChange and PopState are triggered.
-
single-spa:before-mount-routing-event
In application mode, after the old child application is uninstalled, the new child application is triggered before being mounted.
-
single-spa:app-change
In application mode, this function is triggered after the old child application is uninstalled and the new child application is mounted.
Single-spa :before-app-change triggers, single-SPA :app-change triggers.
-
single-spa:no-app-change
In application mode, if single-spa: before-no-app-change triggers, then the final single-SPA: no-app-change triggers.
-
single-spa:routing-even
In application mode, after single-SPA :no-app-change/single-SPA :no-app-change is triggered, single-SPA :routing-event is triggered.
-
-
NPM package for use with single-SPA
In a real project, we can use some NPM packages in conjunction with single-SPA to improve development efficiency. Commonly used NPM packages are single-SPA-Vue, single-SPA-react and so on.
-
single-spa-vue
When a child application/component is connected to a single-SPA, we need to add mount logic in the mount method, unmount logic in the unmount method, and update logic in the update method. If we have more than one child application/component to access single-SPA, we need to write the same logic for each child application/component. This behavior is anathema to any aspiring programmer. In this case, we would certainly want to wrap the same logic abstraction and make it reusable, and single-SPA-Vue is one such NPM package.
Single-spa-vue allows us to quickly define mount, unmount, and update methods for each vUe-based sub-application/component.
Specific usage see: single-SPa-vue
-
single-spa-react
Single-spa-vue does the same thing as single-SPA-Vue. It helps us quickly define mount, unmount, and update methods for each subapplication/component we develop based on React.
See “Single-SPa-react” for details
-
other
In addition, there are NPM packages that can be used together with: single-spa-Angular, single-spa-AngularJS, single-SPA-cycle, etc. For details, see: ecosystem.
-
Single – the shortage of the spa
Although single-SPA has provided us with a complete set of micro front-end solutions, there are still many problems we need to face and solve in the actual project. For example, complex child application loading logic, JS and CSS isolation between applications, side effects left by child application switchover, child application state recovery, communication between child applications, and child application preloading.
-
Complex subapplication loading logic
The subapplication loading process in a real project is much more complex than that in our example – micro-frontend/ single-SPA /application. We need to consider many points, such as:
- Loading static resources such as images and CSS;
- The names and quantities of entry files generated by packages of different sub-applications are inconsistent.
- The hash value of the package file is changed due to the update of the child application.
- Lazy loading;
For the first problem, we can solve it by packaging all the images and CSS in the child application as JS code. But then, optimizations like STANDALONE CSS packaging and parallel resource loading are lost.
For the second and third problems, we can generate a manifest file when the child application is packaged, which contains the name of the entry file for the child application internally, and then save the manifest. When building the routing registry in the base application, the entry file of the child application can be obtained by dynamically obtaining the manifest file corresponding to the child application.
For the last lazy loading problem, we can add publicPath configuration to the sub-application configuration file to complete the sub-application resource path. (See Application – Subapplication Modification section).
-
Js and CSS isolation between applications
Since there are multiple applications (base application + child application, child application + child application), it is important that global variables (such as window) and styles in each application do not interfere with each other.
To solve this problem, we usually use a naming convention to prefix the global variables and styles of each application according to the sub-application. This approach can solve problems, but it relies heavily on artificial constraints, is time-consuming, laborious, and prone to bugs.
-
Legacy side effects of child application switching
Although the subapplication provides unmount lifecycle methods for the base application to call when switching subapplications, there are still some side effects, such as:
- SetInterval defined by the child application
- Events registered by the child application through window.addeventListener;
- The child application adds modified properties to the Window object;
- Dom nodes dynamically added by child applications, such as style nodes;
These side effects exist, some of which will cause memory leaks, some of which will affect the next child application, so we need to clear these side effects when we switch the child application.
-
The subapplication status is restored. Procedure
Sometimes, we need to restore the previous state of the child application when remounting the child application, such as the window changed before the child application, the style added dynamically by the child application, and so on. This requires that we store the above states when the child application is unmounted and then restore the stored state when the child application is remounted.
-
Communication between applications
With single-SPA, communication between applications is also a major concern. We have explained this in the section on how the sub-applications/components communicate with each other, and those who are interested can review it.
-
Child application preloading
When using single-SPA for micro-front-end development, we can also apply the optimization method of resource preloading. In the process of working on the current sub-application, we can use idle time to load the resources needed by other sub-applications in advance, so that the next sub-application can be opened quickly.
These are common problems in the development process of micro front end. Although we can solve through relevant means, but we hope that the framework can help us solve, make micro front-end development more simple and convenient.
So is there a framework that can help us solve these problems?
The answer is yes. Qiankun is such a framework. It has been redeveloped on the basis of single-SPA, which can help us solve the above mentioned problems well. Qiankun is one of the most beautiful places in the world. It is also one of the most beautiful places in the world.
conclusion
This is the end of the single-SPA study. This paper mainly introduces the usage of single-SPA, some key knowledge points and the shortcomings of single-SPA. The length is long, read more time, I hope to give you help, if you have questions or mistakes, welcome to put forward, learn together, progress together, 😁.
Single-spa source annotation
To complete this article, refer to the following documents:
- Single – spa website
- Single – SPA micro front end framework from beginner to master