Recently, I was responsible for a new projectqiankun
Write an article to share some problems and thoughts in the actual combat.
Example code: https://github.com/fengxianqi/qiankun-example 。
Online demo: http://qiankun.fengxianqi.com/
Separate access to online sub applications:
- subapp/sub-vue
- subapp/sub-react
Why use Qiankun
A functional requirement of the project is to embed an existing tool within the company, which is deployed independently and can be used withReact
What is the main technology selection of our projectvue
Therefore, we need to consider the scheme of embedding pages. There are two main routes:
iframe
programmeqiankun
Micro front end solution
Both solutions can meet our needs and are feasible. Have to say,iframe
Although the scheme is ordinary, it is practical and low cost,iframe
The solution can cover most of the micro front-end business requirements, andqiankun
The technical requirements are higher.
Technical students also have a strong demand for their own growth, so when both can meet the business needs, we hope to apply some newer technologies and toss some unknown things, so we decided to chooseqiankun
。
Project architecture
Background system is generally up and down or left and right layout. In the figure below, pink is the base, which is only responsible for head navigation, and green is the whole sub application attached. Click the head navigation to switch the sub application.
Referring to the official examples code, there is a base under the root directory of the projectmain
And other sub applicationssub-vue
,sub-react
The initial directory structure is as follows:
├ - common // common module
├ - Main // base
├ -- sub react // react
└ - sub Vue // Vue subapplication
The base isvue
Build, sub applications arereact
andvue
。
Base configuration
The main base is built by vue-cli3. It is only responsible for rendering the navigation and Issuing the login state. It provides a container div for the sub application to mount. The base should be kept simple (Qiankun’s official demo is even built directly with native HTML). It should not do business related operations.
This library only needs to be introduced in the base, and themain.js
In order to facilitate the management, we put the configuration of sub applications in:main/src/micro-app.js
Next.
const microApps = [
{
name: 'sub-vue',
entry: '//localhost:7777/',
activeRule: '/sub-vue',
Container: '# subapp viewer', // div mounted by sub application
props: {
Routerbase: '/ sub Vue' // issue the route to the sub application, and the sub application defines the route in the Qiankun environment according to the value
}
},
{
name: 'sub-react',
entry: '//localhost:7788/',
activeRule: '/sub-react',
Container: '# subapp viewer', // div mounted by sub application
props: {
routerBase: '/sub-react'
}
}
]
export default microApps
And then in thesrc/main.js
Introduction in
import Vue from 'vue';
import App from './App.vue';
import { registerMicroApps, start } from 'qiankun';
import microApps from './micro-app';
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
}).$mount('#app');
registerMicroApps(microApps, {
beforeLoad: app => {
console.log('before load app.name====>>>>>', app.name)
},
beforeMount: [
app => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
},
],
afterMount: [
app => {
console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name);
}
],
afterUnmount: [
app => {
console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
},
],
});
start();
stayApp.vue
, need to declaremicro-app.js
The configuration of the sub application mount div (note that the ID must be consistent), and the base layout is related, roughly as follows:
<template>
<div id="layout-wrapper">
< div class = "layout header" > head navigation < / div >
<div id="subapp-viewport"></div>
</div>
</template>
In this way, the base is configured. After the project starts, the child application will be mounted to<div id="subapp-viewport"></div>
Medium.
Sub application configuration
1、 Vue subapplication
Create a new one at the root of the project with Vue clisub-vue
The name of the child application is better than that of the parent applicationsrc/micro-app.js
The name of the configuration in is consistent (this can be used directlypackage.json
Mediumname
As output).
- newly added
vue.config.js
The port of devserver is changed to be consistent with the configuration of the main application, and cross domain is addedheaders
andoutput
to configure.
// package.json The name of should be consistent with the main application
const { name } = require('../package.json')
module.exports = {
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
}
},
devServer: {
port: process.env.VUE_ APP_ Port, // Vue in. Env_ APP_ Port = 7788, consistent with the configuration of the parent application
headers: {
'access control allow origin': '*' // cross domain response header when master app gets child application
}
}
}
- newly added
src/public-path.js
(function() {
if (window.__POWERED_BY_QIANKUN__) {
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line no-undef
__webpack_public_path__ = `//localhost:${process.env.VUE_APP_PORT}/`;
return;
}
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
})();
src/router/index.js
Only routes are exposed,new Router
Change tomain.js
It is stated in.- reform
main.js
, introduce thepublic-path.js
, rewrite render, add lifecycle function, etc., and finally as follows:
Import '. / public path' // note that public path needs to be introduced
import Vue from 'vue'
import App from './App.vue'
import routes from './router'
import store from './store'
import VueRouter from 'vue-router'
Vue.config.productionTip = false
let instance = null
function render (props = {}) {
const { container, routerBase } = props
const router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? routerBase : process.env.BASE_URL,
mode: 'history',
routes
})
instance = new Vue({
router,
store,
render: (h) => h(App)
}).$mount(container ? container.querySelector('#app') : '#app')
}
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
export async function bootstrap () {
console.log('[vue] vue app bootstraped')
}
export async function mount (props) {
console.log('[vue] props from main framework', props)
render(props)
}
export async function unmount () {
instance.$destroy()
instance.$el.innerHTML = ''
instance = null
}
At this point, the Vue sub application of the basic version is configuredrouter
andvuex
No need, can be removed.
2、 React subapplication
- adopt
npx create-react-app sub-react
Create a new react application. - newly added
.env
File additionPORT
The variable and port number should be consistent with those configured by the parent application. - Why not
eject
For all webpack configurations, we can use the react app rewired scheme to copy the webpack.
- first
npm install react-app-rewired --save-dev
- newly build
sub-react/config-overrides.js
const { name } = require('./package.json');
module.exports = {
webpack: function override(config, env) {
//To solve the problem that the main application will hang up after access: https://github.com/umijs/qiankun/issues/340
config.entry = config.entry.filter(
(e) => !e.includes('webpackHotDevClient')
);
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
return config;
},
devServer: (configFunction) => {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
config.open = false;
config.hot = false;
config.headers = {
'Access-Control-Allow-Origin': '*',
};
return config;
};
},
};
- newly added
src/public-path.js
。
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
- reform
index.js
, introducepublic-path.js
, add lifecycle functions, etc.
import './public-path'
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
function render() {
ReactDOM.render(
<App />,
document.getElementById('root')
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
*Bootstrap is called only once when the micro application is initialized. The mount hook will be called directly when the micro application re enters the next time. Bootstrap will not be triggered repeatedly.
*Generally, we can initialize some global variables here, such as the application level cache that will not be destroyed in the unmount phase.
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
*The mount method is called every time the application enters. Usually, we trigger the application's rendering method here
*/
export async function mount(props) {
console.log(props);
render();
}
/**
*The method that will be called every time the application is cut out / unloaded. Usually, we will unload the application instance of the micro application here
*/
export async function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
/**
*Optional life cycle hook, which takes effect only when the micro application is loaded in the loadmicroapp mode
*/
export async function update(props) {
console.log('update props', props);
}
serviceWorker.unregister();
So far, the react sub application of the basic version is configured.
Advanced
Global state management
qiankun
Through initglobalstate, onglobalstate change and setglobalstate, the global state management of the master application is realized, and then, by default, the global state management of the master application is implementedprops
Pass the communication method to the child application. Let’s take a look at the official example usage:
Main application:
// main/src/main.js
import { initGlobalState } from 'qiankun';
//Initialize state
const initialState = {
User: {} // user information
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
//State: state after change; prev state before change
console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
Sub application:
//To obtain the communication method from the lifecycle mount, props has two APIs, onglobalstatechange and setglobalstate, by default
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
//State: state after change; prev state before change
console.log(state, prev);
});
props.setGlobalState(state);
}
These two pieces of code are not difficult to understand, the parent-child application throughonGlobalStateChange
This method is used to communicate, which is actually a publish subscribe design pattern.
OK, the official example usage is very simple and sufficient. The syntax of pure JavaScript does not involve any Vue or react. Developers can customize it freely.
If we use the official example directly, the data will be loose and the call will be complex, and all sub applications will have to declareonGlobalStateChange
Monitor the status, and then pass thesetGlobalState
Update the data.
Therefore, it is necessary for usMake further encapsulation design for data state。 The author mainly considers the following points:
- The main application should be concise and simple. For the sub application, the data sent by the main application is a very pure one
object
In order to better support sub applications of different frameworks, the main application does not needvuex
。 - Vue child applications should be able to inherit the data from the parent application and support independent operation.
Sub application inmount
The declaration cycle can obtain the latest data issued by the main application, and then register the data to a file namedglobal
In the vuex module of the global module, the child application updates the data through the action action of the global module, and automatically synchronizes back to the parent application at the same time.
Therefore, for sub applications,It doesn’t have to know whether it is a Qiankun sub application or a stand-alone application. It just has a name calledglobal
It can update data through action, and no longer need to care whether to synchronize to the parent application(synchronized actions are encapsulated inside the method, and the caller doesn’t care)Support sub application to start development independently。
- The application of react is the same.
State encapsulation of main application
The main application maintains oneinitialState
It is aobject
Type, which will be distributed to sub applications.
// main/src/store.js
import { initGlobalState } from 'qiankun';
import Vue from 'vue'
//Initial state of the parent application
// Vue.observable To make initialstate responsive: https://cn.vuejs.org/v2/api/#Vue -observable。
let initialState = Vue.observable({
user: {},
});
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((newState, prev) => {
//State: state after change; prev state before change
console.log('main change', JSON.stringify(newState), JSON.stringify(prev));
for (let key in newState) {
initialState[key] = newState[key]
}
});
//Define a method to get the state and send it to the sub application
actions.getGlobalState = (key) => {
//If there is a key, it means to take a child object under the globalstate
//No key means all
return key ? initialState[key] : initialState
}
export default actions;
Here are two things to note:
Vue.observable
This is to make the state of the parent application responsive if it is not used Vue.observable Package level, it is just a pure object, which can be obtained by sub applications, but will lose its responsiveness,This means that the page will not be updated after the data changes。getGlobalState
Method, this isControversialWe have a discussion on GitHub: https://github.com/umijs/qiankun/pull/729 。
On the one hand, the author thinks thatgetGlobalState
It’s not necessary,onGlobalStateChange
It’s enough.
On the other hand, the author and other students who mentioned PR feel it necessary to provide onegetGlobalState
The reason is that the get method is more convenient to use. The sub application does not need to listen to StateChange events all the time. It only needs to be initialized once through getglobalstate at the first mount. Here, the author insists that the parent application issue a getglobalstate method.
Since getglobalstate is not officially supported, it is necessary to issue the method through props when registering child applications
import store from './store';
const microApps = [
{
name: 'sub-vue',
entry: '//localhost:7777/',
activeRule: '/sub-vue',
},
{
name: 'sub-react',
entry: '//localhost:7788/',
activeRule: '/sub-react',
}
]
const apps = microApps.map(item => {
return {
...item,
Container: '# subapp viewer', // div mounted by sub application
props: {
routerBase: item.activeRule , // issue basic route
getGlobalState: store.getGlobalState //Issue getglobalstate method
},
}
})
export default microApps
State encapsulation of Vue subapplication
As mentioned above, the child application will register the state issued by the parent application as aglobal
In order to facilitate reuse, we encapsulate the following:
// sub-vue/src/store/global-register.js
/**
*
*@ param {vuex instance} store
*@ param {props issued by Qiankun} props
*/
function registerGlobalModule(store, props = {}) {
if (!store || !store.hasModule) {
return;
}
//Gets the initialized state
const initState = props.getGlobalState && props.getGlobalState() || {
menu: [],
user: {}
};
//The data of the parent application is stored in the child application, and the namespace is fixed to global
if (!store.hasModule('global')) {
const globalModule = {
namespaced: true,
state: initState,
actions: {
//The child application changes the state and notifies the parent application
setGlobalState({ commit }, payload) {
commit('setGlobalState', payload);
commit('emitGlobalState', payload);
},
//Initialization, which is only used to synchronize the data of the parent application when mount
initGlobalState({ commit }, payload) {
commit('setGlobalState', payload);
},
},
mutations: {
setGlobalState(state, payload) {
// eslint-disable-next-line
state = Object.assign(state, payload);
},
//Notify parent app
emitGlobalState(state) {
if (props.setGlobalState) {
props.setGlobalState(state);
}
},
},
};
store.registerModule('global', globalModule);
} else {
//The parent application data is synchronized once every mount
store.dispatch('global/initGlobalState', initState);
}
};
export default registerGlobalModule;
main.js
Add the use of global module in
import globalRegister from './store/global-register'
export async function mount(props) {
console.log('[vue] props from main framework', props)
globalRegister(store, props)
render(props)
}
As you can see, the vuex module will call theinitGlobalState
Initialize the state issued by the parent application, and provide thesetGlobalState
Method for external call, internal automatic notification synchronization to the parent application. The sub application is used in Vue page as follows:
export default {
computed: {
...mapState('global', {
user: state => state.user , // get the user information of the parent application
}),
},
methods: {
...mapActions('global', ['setGlobalState']),
update () {
this.setGlobalState ('user ', {Name:' Zhang San '})
}
},
};
In this way, an effect is achieved: the child application does not need to know the existence of Qiankun, it only knows that there is such a global module to store information, the communication between father and son is encapsulated in the method itself, it only cares about its own information storage.
PS: this scheme also has disadvantages, because the child application will synchronize the state issued by the parent application only when it is mounted. Therefore, it is only suitable for the architecture where only one child application is mounted at a time (not suitable for the coexistence of multiple child applications); if the parent application data changes and the child application does not trigger mount, the latest data of the parent application cannot be synchronized back to the child application. If you want to achieve the coexistence of multiple child applications and the parent dynamically transfers the child, the child application still needs to use the one provided by Qiankun
onGlobalStateChange
API monitoring line, there is a better plan for students to share a discussion. The program just meets the current project requirements of the author, so it is enough. Please encapsulate it according to your own business needs.
Sub application switching loading processing
When the sub application is loaded for the first time, it is equivalent to loading a new project, which is still relatively slow. Therefore, loading has to be added.
In the official example, loading processing is done, but it needs to be introduced additionallyimport Vue from 'vue/dist/vue.esm'
This will increase the package size of the main application (about 100kb increased by comparison). A loading increases 100k, obviously the cost is a little unacceptable, so we need to consider a better way.
Our main application is built with Vue, and Qiankun provides a loader method to obtain the loading status of sub applications. Therefore, it is natural to think of:staymain.js
When the instance is loaded, the instance will be displayed in response.Next, select a loading component
- If the main application uses element UI or other frameworks, it can directly use the loading component provided by UI library.
- If the main application does not introduce a UI Library in order to keep it simple, you can consider writing a loading component by yourself, or find a small loading library, such as nprogress, which I will use here.
npm install --save nprogress
The next step is to figure out how to transfer the loading status to the main applicationApp.vue
。 Through the author’s experiments, it is found that,new Vue
The Vue instance returned by theinstance.$children[0]
To changeApp.vue
Data, so change itmain.js
:
//CSS with nprogress
import 'nprogress/nprogress.css'
import microApps from './micro-app';
//Get instance
const instance = new Vue({
render: h => h(App),
}).$mount('#app');
//Define the loader method. When loading changes, the variable is assigned to App.vue Isloading in data of
function loader(loading) {
if (instance && instance.$children) {
//Instance. $children [0] is App.vue In this case, change it directly App.vue Isloading of
instance.$children[0].isLoading = loading
}
}
//Add loader method to sub application configuration
let apps = microApps.map(item => {
return {
...item,
loader
}
})
registerMicroApps(apps);
start();
PS: Qiankun’s registermorapps method also monitors the life cycle of the child application, such as beforeload and aftermount. Therefore, these methods can also be used to record the loading status, but the better usage is to pass it through the loader parameter.
Transformation of main application App.vue , monitoring through WatchisLoading
<template>
<div id="layout-wrapper">
< div class = "layout header" > head navigation < / div >
<div id="subapp-viewport"></div>
</div>
</template>
<script>
import NProgress from 'nprogress'
export default {
name: 'App',
data () {
return {
isLoading: true
}
},
watch: {
isLoading (val) {
if (val) {
NProgress.start()
} else {
this.$nextTick(() => {
NProgress.done()
})
}
}
},
components: {},
created () {
NProgress.start()
}
}
</script>
So far, the effect of loading is realized. althoughinstance.$children[0].isLoading
It seems that the operation is more coquettish, but it does cost much less than the official examples (the volume increase is almost 0). If there is a better way, please share it in the comments section.
Extract public code
Inevitably, some methods or tool classes are needed by all sub applications. It is certainly not easy to maintain if each sub application copies a copy, so it is necessary to extract public code to one place.
Create a new one in the root directorycommon
The folder is used to store public code, such as shared by multiple Vue sub applicationsglobal-register.js
Or reusablerequest.js
andsdk
And so on. The code is not posted here. Please look at the demo directly.
After public code extraction, how to use other applications? Common can be published as an NPM private package. The NPM private package has the following organizational forms:
- NPM points to the local file address:
npm install file:../common
。 Create a common directory directly in the root directory, and then NPM directly depends on the file path. - NPM points to private git warehouse:
npm install git+ssh://xxx-common.git
。 - Publish to NPM private server.
This demo uses the first method because the base and sub applications are all assembled in a git repository. However, in actual application, it is released to NPM private server, because we will split the base and sub application into independent sub warehouses to support independent development, as will be discussed later.
It should be noted that since common does not pass through Babel and Polly, the referent should explicitly specify that the module needs to be compiled when the webpack is packaged, such as that of Vue sub application vue.config.js It is necessary to add the following sentence:
module.exports = {
transpileDependencies: ['common'],
}
Independent application development support
A very important concept of micro front end is splitting, which is the idea of divide and conquer. It divides all business into independent and operational modules.
Is too laggy and stuck for the whole system. A product may only involve one of its sub applications, so the development of the system can only start with the sub application involved, and the independent development of the N is needed. Therefore, it is necessary to support the independent development of the sub application. If you want to support it, you will encounter the following problems:
- How to maintain the login status of sub applications?
- When the base is not started, how to get the data and capability of the base?
When the base is running, the login status and user information are stored on the base, and then the base is sent to the sub application through props. However, if the base does not start and only the sub application starts independently, the sub application can not get the required user information through props. Therefore, the solution can only be that both parent and child applications must implement the same set of login logic. In order to be reusable, the login logic can be encapsulated in common, and then the login related logic can be added to the logic that the sub application runs independently.
// sub-vue/src/main.js
import { store as commonStore } from 'common'
import store from './store'
if (!window.__POWERED_BY_QIANKUN__) {
//Here is the environment for the sub application to run independently to realize the login logic of the sub application
//When running independently, it also registers a store module named Global
commonStore.globalRegister(store)
//After the simulation login, the user information is stored in the global module
Const userinfo = {Name: 'I am a stand-alone runtime, and my name is Zhang San'} // assume the user information retrieved after login
store.commit('global/setGlobalState', { user: userInfo })
render()
}
// ...
export async function mount (props) {
console.log('[vue] props from main framework', props)
commonStore.globalRegister(store, props)
render(props)
}
// ...
!window.__POWERED_BY_QIANKUN__
Indicates that the child application is not inqiankun
The independent runtime. At this time, we still need to register aglobal
The sub application can also get the user’s information from the global module, so as to smooth the environment difference between Qiankun and independent runtime.
PS: we wrote about it earlier
global-register.js
It is very clever and can support two environments at the same time, so the above can be accessed throughcommonStore.globalRegister
Direct reference.
Sub application independent warehouse
With the development of the project, there may be more and more sub applications. If the sub application and the base are assembled in the same git warehouse, it will become more and more bloated.
If the project has CI / CD, only the code of a certain sub application is modified, but the code submission will trigger the construction of all sub applications at the same time, which is unreasonable.
At the same time, if the development of some business sub applications is cross departmental and cross team, how to divide the authority management of code warehouse is a problem.
Based on the above problems, we have to consider migrating each application to a separate git repository. Since we have independent warehouse, the project may not be placed in the same directory, so the previousnpm i file:../common
It is not applicable to install the common by using the method, so it is better to publish it to the company’s NPM private server or use git address form.
qiankun-example
In order to better display, we still put all the applications in the same git warehouse. Please don’t copy them.
Post aggregation management of sub application independent warehouse
After the sub application is independent of GIT warehouse, it can start and develop independently. At this time, there will be problems:The development environment is independent, can not see the whole picture of the application。
Although it is better to focus on a certain sub application during development, there is always a time when the whole project needs to run. For example, when multiple sub applications need to rely on each other, it is necessary to have an aggregate management of GIT repository for all sub applications. The aggregate warehouse requires that all dependencies (including sub applications) can be installed with one click and the whole project can be started with one click.
Three schemes are considered here
- use
git submodule
。 - use
git subtree
。 - Simply put all the sub warehouses in the aggregate directory and
.gitignore
Drop it. - Use lerna management.
git submodule
andgit subtree
Both of them are good sub warehouse management schemes, but the disadvantage is that the aggregation library has to synchronize the changes every time the sub application changes.
Considering that not everyone will use the aggregate warehouse, when the sub warehouse is independently developed, it will not be actively synchronized to the aggregate library. Students who use the aggregate library will have to do synchronization frequently, which is time-consuming and labor-consuming, which is not particularly perfect.
So the third scheme is more in line with the current situation of the author’s team. The aggregate library is equivalent to an empty directory. Under this directory, clone all sub warehouses, andgitignore
In this way, the aggregate library can avoid synchronous operation.
Since all sub warehouses have been ignored, the aggregate library clone will still be an empty directory. At this time, we can write a scriptscripts/clone-all.sh
Write the clone command of all sub warehouses:
#Zicangku I
git clone [email protected]
#Zicangku II
git clone [email protected]
Then initialize one in the aggregate librarypackage.json
, scripts plus:
"scripts": {
"clone:all": "bash ./scripts/clone-all.sh",
},
In this way, after the GIT clone aggregate library is down, thenpm run clone:all
You can click one key to clone all the sub warehouses.
As mentioned above, the aggregation library should be able to install and start the whole project with one click. We refer to Qiankun’s examples and use NPM run all to do this.
- Aggregation library installation
npm i npm-run-all -D
。 - Aggregate Library’s package.json Add install and start commands:
"scripts": {
...
"install": "npm-run-all --serial install:*",
"install:main": "cd main && npm i",
"install:sub-vue": "cd sub-vue && npm i",
"install:sub-react": "cd sub-react && npm i",
"start": "npm-run-all --parallel start:*",
"start:sub-react": "cd sub-react && npm start",
"start:sub-vue": "cd sub-vue && npm start",
"start:main": "cd main && npm start"
},
npm-run-all
Of--serial
It means to execute one by one in sequence,--parallel
Means to run in parallel at the same time.
One click installationnpm i
, one click Startnpm start
。
Vscode eslint configuration
If vscode is used and the eslint plug-in is used for automatic repair, because the project is in a non root directory, eslint cannot take effect. Therefore, you need to specify the working directory of eslint:
// .vscode/settings.json
{
"eslint.workingDirectories": [
"./main",
"./sub-vue",
"./sub-react",
"./common"
],
"eslint.enable": true,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"search.useIgnoreFiles": false,
"search.exclude": {
"**/dist": true
},
}
Sub applications jump to each other
In addition to clicking on the menu at the top of the page to switch to sub applications, we also need to ask sub applications to jump in other sub applications. This will involve the display of the active status of the top menusub-vue
Switch tosub-react
In this case, the top menu needs to set thesub-react
Change to active state. There are two options:
- The jump action of the child application is thrown up to the parent application, and the parent application does the real jump, so that the parent application knows to change the activation state and has sub components
$emit
The meaning of the event to the parent component. - Parent app monitor
history.pushState
Event. When the route is found to be changed, the parent application knows whether to change the activation state.
becauseqiankun
For the time being, there is no API to encapsulate the event thrown by the child application to the parent application, such as iframepostMessage
Therefore, scheme 1 is a little difficult, but the activation state can be put into the state management. The child application can synchronize the parent application by changing the value in vuex. The practice is feasible but not very good, and the maintenance of state is a little complicated in state management.
So we choose scheme two. The sub application jumps throughhistory.pushState(null, '/sub-react', '/sub-react')
Therefore, the parent application tries to listen when mountedhistory.pushState
That’s fine. becausehistory.popstate
Can only listenback/forward/go
But you can’t listenhistory.pushState
, so you need to do an extra global copyhistory.pushState
event.
// main/src/App.vue
export default {
methods: {
bindCurrent () {
const path = window.location.pathname
if (this.microApps.findIndex(item => item.activeRule === path) >= 0) {
this.current = path
}
},
listenRouterChange () {
const _wr = function (type) {
const orig = history[type]
return function () {
const rv = orig.apply(this, arguments)
const e = new Event(type)
e.arguments = arguments
window.dispatchEvent(e)
return rv
}
}
history.pushState = _wr('pushState')
window.addEventListener('pushState', this.bindCurrent)
window.addEventListener('popstate', this.bindCurrent)
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('pushState', this.bindCurrent)
window.removeEventListener('popstate', this.bindCurrent)
})
}
},
mounted () {
this.listenRouterChange()
}
}
performance optimization
Each sub application is a complete application, and each Vue sub application is packagedvue/vue-router/vuex
。 From the perspective of the whole project, it is equivalent to packaging those modules many times, which will be very wasteful, so we can further optimize the performance here.
The first thing we can think of is through webpackexternals
Or the main application sends out the common module for reuse.
However, it should be noted that if all sub applications share the same module, in the long run, it is not conducive to the upgrade of sub applications, and it is difficult to achieve the best of both worlds.
Now I think the better way is: the main application can distribute some modules for its own use, and the sub application can give priority to the modules distributed by the main application. When the main application is not found, it will load it by itself; the child application can also directly use the latest version instead of the one distributed by the parent application.
This scheme refers to the practice and summary of Qiankun micro front-end solution – how to share public plug-ins among subprojects. The idea is very complete. You can have a look. This function has not been added to this project for the time being.
deploy
Now almost no articles related to the deployment of Qiankun on the Internet can be found. It may be that there is nothing easy to say. But for those who are not familiar with it, what is the best deployment scheme for Qiankun deployment? So it is necessary to talk about the deployment scheme of the author here for your reference.
The scheme is as follows:
Considering that there may be routing conflicts when the primary application and the sub application share the domain name, the sub applications may be added continuously. Therefore, we put all the sub applications in thexx.com/subapp/
In this secondary directory, the root path/
Leave it to the main application.
The steps are as follows:
- The main application and all the sub applications are packaged with a HTML, CSS, JS, static, which is uploaded to the server by directory, and the sub applications are put in a unified way
subapp
Under the table of contents, finally as follows:
├── main
│ └── index.html
└── subapp
├── sub-react
│ └── index.html
└── sub-vue
└── index.html
- Configure nginx. The expected value is
xx.com
The root path points to the main application,xx.com/subapp
For sub applications, you only need to write a copy of the configuration of sub applications, and you don’t need to change the nginx configuration when adding new sub applications. The following should be the simplest nginx configuration for micro application deployment.
server {
listen 80;
server_name qiankun.fengxianqi.com;
location / {
Root / data / Web / Qiankun / main; # the directory where the main application is located
index index.html;
try_files $uri $uri/ /index.html;
}
location /subapp {
alias /data/web/qiankun/subapp;
try_files $uri $uri/ /index.html;
}
}
nginx -s reload
Then it’s OK.
In this paper, an online demo is presented
Whole station (main application) http://qiankun.fengxianqi.com/
Separate access to sub applications:
- Subapp / sub Vue, observe the change of vuex data.
- subapp/sub-react
Problems encountered
1、 After the react sub application is started, the main application will hang up after rendering for the first time
The hot overload of the child application actually caused the parent application to hang up directly. At that time, I was completely confused. Fortunately, we found the relevant issues / 340, that is, disable hot overloading when copying the webpack of react (adding the following configuration to disable the hot overload will result in no hot overload. The react application has to be manually refreshed during development, isn’t it a bit uncomfortable…) :
module.exports = {
webpack: function override(config, env) {
//To solve the problem that the main application will hang up after access: https://github.com/umijs/qiankun/issues/340
config.entry = config.entry.filter(
(e) => !e.includes('webpackHotDevClient')
);
// ...
return config;
}
};
2、 Uncaught error: application ‘XX’ died in status skip_ BECAUSE_ BROKEN: [qiankun] Target container with #subapp-viewport not existed while xx mounting!
It is completely normal during local dev development. This problem will only appear when the page is opened for the first time after deployment. It will be normal after F5 refresh. It can only be repeated once after clearing the cache. This bug has been bothering for days.
The error message is very clear, that is, the DOM used to load the sub application does not exist when the main application is mounting the XX sub application. So when we first thought that Vue was the master application,#subapp-viewport
You haven’t had time to render, so try to make sure the main app is workingmount
Then register the sub application.
//Main application main.js
new Vue({
render: h => h(App),
mounted: () => {
//After mounted, register the sub application
renderMicroApps();
},
}).$mount('#root-app');
But this method can’t work, even if setTimeout is used. We need to find another way.
Finally, through debugging step by step, it is found that the project loaded a section of JS of Gaud map, which will be used when loading for the first timedocument.write
To copy the entire HTML, it results in an error that the ා subapp viewer does not exist. Therefore, it is necessary to find a way to remove the JS file.
Episode: why does our project load the Gaud map JS? We didn’t use it in our project. At this time, we fell into a misunderstanding: Qiankun belongs to Ali, and Gaode belongs to Ali. Qiankun won’t load Gao De’s JS dynamically during rendering to do some data collection? I’m very ashamed to have this idea for an open source project… In fact, it is because our partners who write component library templates forget to remove debugging
public/index.html
This JS was used. At that time, I also went to comment on issue (covering my face and crying). I want to tell you that when you encounter a bug, you should check yourself first, and don’t easily question others.
last
This article from the beginning of the construction to the deployment of a very complete sharing of the entire architecture of some ideas and practices, I hope to help you. As an example, the best practice is not the best one, but the best one.
Example code: https://github.com/fengxianqi/qiankun-example 。
Online demo: http://qiankun.fengxianqi.com/
Separate access to online sub applications:
- subapp/sub-vue
- subapp/sub-react
Finally, like the students of this article, please also give a praise and little star encouragement, thank you very much for seeing here.
Some reference articles
- Practice of micro front end in Xiaomi CRM system
- Practice of micro front end
- Maybe the most perfect micro front end solution you’ve ever seen
- The practice of micro delivery group in the front end
- Practice and summary of Qiankun micro front end scheme