Construction and application of single-spa micro front-end framework

Construction and application of single-spa micro front-end framework

This article will be divided into several parts, mainly to build a micro front-end application that can be used in enterprise-level projects

Micro front-end framework: single-spa , single-spa-vue

Project framework: vue2 + elementui

single-spa is a good micro distal the base frame, qiankun framework is based on single-spa achieved in single-spa based on a layer made package, also addresses the single-spa few defects.
Because single-spa is a basic micro-front-end framework, it will be very easy to understand its implementation principles and then look at other micro-front-end frameworks.
This tutorial is based on single-spa , and after the end of this tutorial, a micro-front-end practice based on qiankun will be released.

Demo address demno (the server is not good, loading is a bit slow)

Source address: single-spa-main single-spa-child-one single-spa-child-two

The source code will be updated in real time. If there is a discrepancy with the article, the source code shall prevail (ui is ugly, forbearance)

Introduction to this article: This demo will have a main project
single-spa-main
, Two subprojects
single-spa-child-one(spa1)
,
single-spa-child-two(spa2)
, The main project will include two sub-projects, and the sub-projects
spa1
A submodule
spa2
,for
spa1
Introduced
spa2
It's just an idea, and the problems that will arise in the future are not clear yet

End of nonsense, start

In the first step, we first create a main project

vue create single-spa-main duplicated code

After creating the project, we need to make a transformation to the project

//vue.config.js const StatsPlugin = require ( 'stats-webpack-plugin' ) module .exports = { publicPath : process.env.VUE_APP_PUBLIC_PATH, outputDir : 'dist' , assetsDir : 'static' , runtimeCompiler : true , productionSourceMap : false , devServer : { port : 9000 , hot : true , headers :{ 'Access- Control-Allow-Origin' : '*' } }, configureWebpack : config => ({ output : { library : 'singleMain' , libraryTarget : 'window' }, plugins : [ new StatsPlugin( 'manifest.json' , { chunkModules : false , entrypoints : true , source : false , chunks : false , modules : false , assets : false , children : false , exclude : [ /node_modules/ ] }) ], }), chainWebpack : config => {}, css : { extract : false , loaderOptions : { postcss : { plugins : [ require ( 'postcss-selector-namespace' )({ namespace (css) { IF (css.includes ( "normalize.css" )) return '' return '.single-Spa-main' } }) ] } } } } Copy code
library
,
libraryTarget
, The current project is linked to
window
on
stats-webpack-plugin
The role is to write the statistics of the build to a file
manifest.json
, Students who are interested in this plugin can take a closer look at this api
Mainly talk about
postcss-selector-namespace
This api, the function of this api is to add a scope restriction to the front of the css, because the principle of the micro front end is to insert the code under a label of the main project, so directly css needs to add a parent whose scope is valid. , So as not to mess up the css, if the setting is successful, start the project, the css of the main project will have the prefix just set before, so as to achieve the effect of css isolation.

The main project is temporarily transformed here

In the second step, we create a subproject

vue create single-spa-child- one duplicated code

Similarly, we will carry out a transformation to this project, the transformation method is basically the same, just pay attention to the name.

//vue.config.js const StatsPlugin = require ( 'stats-webpack-plugin' ) module .exports = { publicPath : process.env.VUE_APP_PUBLIC_PATH, outputDir : 'dist' , assetsDir : 'static' , runtimeCompiler : true , productionSourceMap : false , devServer : { port : process.env.VUE_APP_PUBLIC_PORT, hot : true , headers : { 'Access-Control-Allow-Origin' : '*' } }, configureWebpack : config => ({ output : { library : ' singleChild1 ' , libraryTarget : 'window' }, plugins : [ new StatsPlugin( 'manifest.json' , { chunkModules : false , entrypoints : true , source : false , chunks : false , modules : false , assets : false , children : false , exclude : [ /node_modules/ ] }) ], }), chainWebpack : config => {}, css : { extract : false , loaderOptions : { postcss : { plugins : [ require ( 'postcss-selector-namespace' )({ namespace () { return '#singleChild1' } }) ] } } } } Copy code

In the same way, create a
single-spa-child-two

The project creation is over, then we continue to transform the project

The first step is to register the sub-projects in the main project

First create a singlespaMain.js and import it in main. The function of this file is to register sub-projects
SinglespaMain.js// Import {registerApplication, Start} from 'SINGLE-Spa' Import Axios from 'Axios' Import eventRoot from './eventRoot' //Load sub-applications remotely 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, firstScript) }) } /* * getManifest: remotely load the manifest.json file and parse the js that needs to be loaded * */ const getManifest = ( url, bundle ) => new Promise ( ( resolve ) => { axios.get(url).then( async res => { const {data} = res const {entrypoints, publicPath} = data const assets = entrypoints[bundle].assets for ( let i = 0 ; i <assets.length; i++) { await createScript(publicPath + assets[i]).then( () => { if (i === assets.length- 1 ) { resolve() } }) } }) }) //list of sub-applications const apps = [ { //child application name name : 'singleChild1' , //child application loading function is a promise app : async () => { let childModule = null await getManifest( ` ${process.env.VUE_APP_CHILD_ONE}/manifest.json? v = ${ new Date ().getTime()} ` , 'app' ).then( () => { childModule = window .singleChild1 }) return childModule }, //When the route meets the conditions (returns true), activate (mount) the child application activeWhen : location => { return location.pathname.startsWith( '/singleChild1' ) }, //The object passed to the child application customProps : { baseUrl : '/ singleChild1 ' , eventRoot} }, { //child application name name : 'singleChild2' , //child application loading function is a promise app : async () => { let childModule = null await getManifest( ` ${process.env.VUE_APP_CHILD_TWO}/manifest.json? v = ${ new Date ().getTime()} ` , 'app' ).then( () => { childModule = window .singleChild2 }) return childModule }, //When the route meets the conditions (returns true), activate (mount) the child application activeWhen : location => { return location.pathname.startsWith( '/singleChild2' ) }, //The object passed to the child application customProps : { baseUrl : '/ singleChild2 ' , eventRoot} } ] apps.forEach( item => registerApplication(item)) start() Copy code
getManifest
Function fit
createScript
Function, will load the js packaged out of the sub-project
We create one in the main project
eventRoot
, As an event communication tool for the entire project, the internal implementation is a new vue coming out through
customProps
, Pass down step by step

Note that it is passed
baseUrl
This field, this field is used to specify the basic routing of the sub-item. Since the sub-item may be used in different places, the routing of this page will be different. If the routing is written down in the sub-item, the routing will be Coupling, which is not conducive to the application of multiple projects, in
spa1
Project references
spa2
When I come back to focus on this application

This is
mnifest
Requested data

Next, transform the routing

Router.js// Import Vue from 'VUE' Import VueRouter from 'VUE-Router' Import the HelloWorld from '@/views/HelloWorld.vue' Vue.use(VueRouter) const routes = [ { path : '/' , name : 'HelloWorld' , component : HelloWorld, children : [ { path : 'singleChild1*' , name : 'singleChild1' , meta : { keepAlive : true } }, { path : 'singleChild2*' , name : 'singleChild2' , meta : { keepAlive : true } } ] } ] export default new VueRouter({ mode : 'history' , routes }) Copy code
The routing changes are basically very small, add under the routing that needs to load the sub-item
children
That's it

The next step is
helloword
This component

< template > < div class = "hello" > < el-tabs v-model = "tabsname" > < el-tab-pane label = "User Management" name = "first" > User Management < div id = "singleChild1" > </div > </el-tab-pane > < el-tab-pane label = "Configuration Management" name = "second" > Configuration management </el-tab-pane > < el-tab-pane label = "Role Management" name = "third" > Role management < div id = "singleChild2" > </div > </el-tab-pane > < el-tab-pane label = "Timing task compensation" name = "fourth" > Timing task compensation </el-tab-pane > </el-tabs > </div > </template > Copy code
This page only needs to put
id
Just add the label to the corresponding position

The transformation of the main project can basically come to an end here, first transform the sub-projects to make this application run

Start with
spa2
Start because
spa1
Will also be introduced inside
spa2
,and so
spa1
Save for the last modification

Main.js// Import Vue from 'VUE' Import the App from './App.vue' Import singleSpaVue from 'SINGLE-Spa-VUE' Import routerList from './router' Import registerRouter from './util/registerRouter' Vue.config.productionTip = false const appOptions = { el : '# singleChild2 ' , render : h => h(App) } //Support the application to run and deploy independently, not dependent on the base application if (! window .singleSpaNavigate) { delete appOptions.el appOptions.router = registerRouter( '' , routerList) new Vue(appOptions).$mount( '#app' ) } //Based on the base application, export the life cycle function let vueLifecycle = '' export function bootstrap ( {baseUrl, eventRoot} ) { console .log( ' singleChild2 bootstrap' , baseUrl) appOptions.router = registerRouter(baseUrl, routerList) Vue.prototype.eventRoot = eventRoot vueLifecycle = singleSpaVue({ Vue, appOptions }) return vueLifecycle.bootstrap( () => {}) } export function mount () { console .log( ' singleChild2 mount' ) return vueLifecycle.mount( () => {}) } export function unmount () { console .log( ' singleChild2 unmount' ) return vueLifecycle.unmount( () => {}) } Copy code
//router.js import the HelloWorld from '@/views/HelloWorld.vue' export default [ { path : '/' , name : 'HelloWorld' , component : HelloWorld } ] Copy code
RegisterRouter.js// Import Vue from 'VUE' Import VueRouter from 'VUE-Router' export default function ( baseUrl = '' , routerList ) { Vue.use(VueRouter) return new VueRouter({ base : baseUrl || '' , mode : 'history' , routes : routerList }) } Copy code
Start two projects and enter the corresponding
tab
Download, the page can be loaded

Add cross-project events directly here

//spa2 //HelloWorld.vue <template> < div class = "hello" > < h1 > spa2 </h1 > < button @ click = "qiehuan" > xiu~~Switch </button > </div > </template> < script > export default { name : 'HelloWorld' , methods : { qiehuan ( ) { this .eventRoot.$emit( 'goFourth' ) } } } </script > <-! The Add "scoped" attribute to limit CSS to the this the Component only -> < style scoped > </style > Copy the code

It will go up when clicked
emit
For an event, we can monitor this event in other projects.

//spa-main //HelloWorld.vue <template> < div class = "hello" > < el-tabs v-model = "tabsname" :before-leave = "qiehuan" > < el-tab-pane label = "User Management" name = "first" > User Management < div id = "singleChild1" > </div > </el-tab-pane > < el-tab-pane label = "Configuration Management" name = "second" > Configuration management </el-tab-pane > < el-tab-pane label = "Role Management" name = "third" > Role management < div id = "singleChild2" > </div > </el-tab-pane > < el-tab-pane label = "Timing task compensation" name = "fourth" > Timing task compensation </el-tab-pane > </el-tabs > </div > </template> < script > import eventRoot from '@/eventRoot' export default { name : 'HelloWorld' , data () { return { tabsname : 'second' } }, created () { eventRoot.$on( 'goFourth' , this .goFourth) }, methods : { qiehuan (changeName) { if (changeName === 'first' ) { this .$router.push({ path : '/ singleChild1 ' }) } else if (changeName === 'third' ) { this .$router.push({ path : '/ singleChild2 ' }) } else if ( this .tabsname === 'first' || this .tabsname === 'third' ) { this .$router.push({ path : '/' }) } }, goFourth () { this .qiehuan( 'fourth' ) this .tabsname = 'fourth' } } } </script > < style scoped > .hello { text-align : center; } </style > Copy code

Let's proceed
spa1
The transformation of the project, because in our plan
spa1
Not only a sub-project, but also
spa2
Imported as a sub-module, so
spa1
It is both a sub-project and a main project

SPA1// //main.js Import Vue from 'VUE' Import the App from './App.vue' Import singleSpaVue from 'SINGLE-Spa-VUE' Import routerList from './router' Import registerRouter from './util/registerRouter' import singlespaSpa1 from './singlespaSpa1' import eventRoot from './eventRoot' Vue.config.productionTip = false const appOptions = { el : '# singleChild1 ' , render : h => h(App) } //Support the application to run and deploy independently, not dependent on the base application if (! window .singleMain) { delete appOptions.el appOptions.router = registerRouter( '' , routerList) singlespaSpa1( '' , eventRoot) Vue.prototype.eventRoot = eventRoot new Vue(appOptions).$mount( '#app' ) } //Based on the base application, export the life cycle function let vueLifecycle = '' export function bootstrap ( {baseUrl, eventRoot} ) { console .log( ' singleChild1 bootstrap' , baseUrl) appOptions.router = registerRouter(baseUrl, routerList) singlespaSpa1(baseUrl, eventRoot) Vue.prototype.eventRoot = eventRoot vueLifecycle = singleSpaVue({ Vue, appOptions }) return vueLifecycle.bootstrap( () => {}) } export function mount () { console .log( ' singleChild1 mount' ) return vueLifecycle.mount( () => {}) } export function unmount () { console .log( ' singleChild1 unmount' ) return vueLifecycle.unmount( () => {}) } Copy code
SinglespaSpa1// Import {registerApplication, Start} from 'SINGLE-Spa' Import Axios from 'Axios' //Load sub-applications remotely 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, firstScript) }) } /* * getManifest: remotely load the manifest.json file and parse the js that needs to be loaded * */ const getManifest = ( url, bundle ) => new Promise ( ( resolve ) => { axios.get(url).then( async res => { const {data} = res const {entrypoints, publicPath} = data const assets = entrypoints[bundle].assets for ( let i = 0 ; i <assets.length; i++) { await createScript(publicPath + assets[i]).then( () => { if (i === assets.length- 1 ) { resolve() } }) } }) }) //List of sub-applications const apps = function ( baseUrl, eventRoot ) { return [ { //child application name name : 'singleChild2' , //child application loading function is a promise app : async () => { let childModule = null await getManifest( ` ${process.env.VUE_APP_CHILD_TWO}/manifest.json? v = ${ new Date ().getTime()} `,'app').then(() =>{ childModule = window .singleChild2 }) return childModule }, //When the route meets the conditions (returns true), activate (mount) the sub-application activeWhen : location => { return location.pathname.startsWith( ` ${baseUrl}/spa1spa2` ) }, //The object customProps passed to the sub-application : { baseUrl : ` ${baseUrl}/spa1spa2` , eventRoot} } ] } export default function ( baseUrl, eventRoot ) { apps(baseUrl, eventRoot).forEach( item => registerApplication(item)) start() } Copy code
Router.js// Import Vue from 'VUE' Import the HelloWorld from '@/views/HelloWorld.vue' Import Test1 from '@/views/test1.vue' export default [ { path : '/' , name : 'HelloWorld' , component : HelloWorld, children : [ { path : 'spa1spa2' , name : ' singleChild2 ' , component : Vue.component( ' singleChild2 ' , { render : h => h( 'div' , { attrs : { id : 'singleChild2' } }) }), meta : { keepAlive : true } } ] }, { path : '/test1' , name : 'test1' , component : Test1 } ] Copy code
<!-- HelloWorld.vue --> < template > < div class = "hello" > < h1 > spa1 </h1 > < button @ click = "showSpa2" > showSpa2 </button > < router-view/> < button @ click = "go" > gotest </button > </div > </template > < script > export default { name : 'HelloWorld' , methods : { showSpa2 () { this .$router.push({ name : ' singleChild2 ' }) }, go () { this .$router.push({ name : 'test1' }) } } } </script > <!-- Add "scoped" attribute to limit CSS to this component only --> < style scoped > .hello { font-size : 30px ; } </style > Copy code
I won t say much about the same places, but different places:
1. Mainly
singlespaSpa1
The file output of this loading submodule becomes a function, because the loading of the submodule depends on a
baseUrl
, Of the subproject
baseurl
Passed over from the previous project again, and the overall
eventRoot
Need to be passed, so here it becomes a function form
2. It is judged here whether it is necessary to run independently and needs to be mounted with the main project
on window
of
library
Judgment, that is
window.singleMain
, Because our project also introduced
single-spa
,and so
window.singleSpaNavigate
Will definitely exist
3. Why does the routing not use the * form of the main project? It is not recommended to use this form, it will make a lot of judgments on the page.
router-view
it's the best
4. Here you need to create one
eventRoot
, Used to realize communication with sub-modules without relying on the main project
If the startup is ok, the page should look like this

Click on
showSpa2
after

This article is over, and there will be an article on optimization later. The above is just the most basic code configuration. If the project is complex, it will be transformed according to your own needs.