Microfront -Vue3.0 practice
What is a micro front end
Micro front end is to split different functions into multiple sub applications according to different dimensions . Load these subapplications through the main application , The core lies in dismantling , Closing after dismantling .
Why use micro front end
- Ignore technology stack differences
- Subapplications can be packaged and deployed separately
- Compatible with old code
How to land
Single-Spa Realization
Single-SPA It's a front-end microservice JavaScript Front end solutions ( There is no processing style isolation 、js Isolation ) It realizes route hijacking and application loading .
Previous picture , This is based on Single-Spa To achieve the effect of :
- Create two vue engineering ( I'm through vue cli Created )
vue create parent-app // Create a parent app
vue create child-app // Create subapplication
Only... Is selected for the two project customization bable and vueRouter
Copy code
-
Transform sub applications
Subapplications need to expose bootstrap mount unmount Method Copy code
- Subapplication installation
single-spa-vue
:npm insatll sigle-spa-vue
, staymain.js
Introduced insidesingle-spa-vue
,singleSpa It will be repackaged Vue, The returned object contains three methods that the subapplication needs to export .
- newly build
vue.config.js
file ,output The setting is to package the output as umd Format , Referenced by parent app ,deveServer Set the port number of the boot
- Subapplication installation
-
Transform the parent application
-
Parent app installation
single-spa
:npm install single-spa
, The parent app needs to register and mount the child app .introduce
single-spa
,registerApplication
It's a registered sub app , The first parameter is the name of the subapplication , The second parameter is a method , It is mainly the result of loading sub application package , Pay attention to the order of loading. Load the common module first , In the load app.js, Otherwise, the following error will appear . The return value contains three methods exported by the subapplication .The third parameter is the condition that triggers the loader application : When the route matches to the beginning
subapp
Just load the sub application . Of course, you can pass in custom parameters , Specific reference This web site .start The method is to load the registered child application .
- App.vue You need to provide a mount point for the subapplication ,
id
Follow the applicationappOptions
Ofel
Attributes correspond to .
- App.vue You need to provide a mount point for the subapplication ,
You can basically run here , But there are still some problems : The route of sub application can't be clicked and The picture in the sub route doesn't show .
-
-
Detail correction
The above problem is that the routing jump for the sub application is not based on its own jump and the path of the request resource is wrong ( The picture doesn't show )
Add... To the routing configuration of the subapplication base Options , The solution is the problem of routing jump
main.js
Add the following judgment to the list , If referenced by the main application , The resource request path is mainly configuredThat's based on
Single-Spa
The micro front-end application has been realized
Single-Spa defects
be based on single-spa There are several problems in the implementation of micro front end , Style isolation and js Isolation . In the animation above , When loading subapplications , The style of the parent application is affected . Share global objects (window) It causes the pollution of the global object , Next, we will solve these two problems .
Pattern isolation
- BEM(Block Element Modifier) Contract project prefix ( An agreement is just an agreement )
- CSS-Modules Generate non conflicting selector names when packing
- Shadow DOM The real sense of isolation
- Css-in-js
All of the above methods can solve the problem that styles are not isolated , With Shadow DOM
For example :
Upper figure :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Pattern isolation </title>
</head>
<body>
<div>
<p> I am a P label </p>
<div id="shadow"></div>
</div>
<script>
let shadowDOM = document.getElementById('shadow').attachShadow({mode:'closed'})
let pEle = document.createElement('p')
let shadowStyles = document.createElement('style')
let commonStyles = document.createElement('style')
shadowStyles.textContent = 'p{color:red}'
commonStyles.textContent = 'p{color:green}'
pEle.innerText = ' I am a shadow P'
shadowDOM.appendChild(pEle)
shadowDOM.appendChild(shadowStyles)
document.body.appendChild(commonStyles)
</script>
</body>
</html>
Copy code
Shadow API yes dom The object has ,shadow The elements inside are inaccessible to the outside world
Js Isolation
The sandbox : Snapshot sandbox ( Single application sandbox , Shallow copy ) Agent sandbox ( Use sandbox more ,es6 proxy)
The purpose of sandbox is to achieve logical isolation , The logic change of one application will not affect other applications
Single application
Only one application instance can use shallow copy to record the snapshot
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>js Sandbox isolation </title>
</head>
<body>
<script>
class SnapshotSandbox{
constructor(){
this.proxy = window
this.modifyProps = {} // Record the change
this.snapshot = {}
this.active()
}
active(){
for(const prop in window){
this.snapshot[prop] = window[prop]
}
Object.keys(this.modifyProps).forEach(p=>{
window[p] = this.modifyProps[p]
})
}
inactive(){
for(const prop in window){
if(this.snapshot[prop] !== window[prop]){
this.modifyProps[prop] = window[prop]
window[prop] = this.snapshot[prop] // Make sure it doesn't affect
}
}
}
}
let sandbox = new SnapshotSandbox()
function start(window){
window.a = 1,window.b = 2
console.log(window.a,window.b)
sandbox.inactive()
console.log(window.a,window.b)
sandbox.active()
console.log(window.a,window.b)
}
start(sandbox.proxy)
</script>
</body>
</html>
Copy code
Multiple applications
If there are multiple application instances , Can use es6 proxy
Realization js Isolation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Multiple applications JS Isolation </title>
</head>
<body>
<script>
class ProxySandbox{
constructor(){
const rawWindow = window
const fakeWindow = {}
const proxy = new Proxy(fakeWindow,{
set(target, p ,value){
target[p] = value
return true
},
get(target, p){
return target[p] || rawWindow[p]
}
})
this.proxy = proxy
}
}
let sandbox1 = new ProxySandbox()
let sandbox2 = new ProxySandbox()
window.a = 1
function start(window,value){
window.a = value
console.log(window.a)
}
start(sandbox1.proxy,'hello sandbox1')
start(sandbox2.proxy,'hello sandbox2')
</script>
</body>
</html>
Copy code
Bingo......
QianKun Realization
qiankun be based on Single-SPA, Provide out of the box API(single-spa + sandbox + import-html-entry) Do it , Technology stack independent , Easy access ( image iframe).
The image above is based on qinakun2.0^ + vue3.0^ + antd-vue + mockJs + TS And so on , A main application + Two sub applications ( My code and life snapshot ), Of course, two sub applications can also run independently . Mainly from the following aspects to share :
- Lord 、 The construction of sub application
- Communication between applications
- mock data
- According to the environment (local、develop、 product) Different developments of mock data
- Access control
Building the main 、 Subapplication
structure
Build the main application
main.ts
import { createApp, h } from 'vue'
import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start } from 'qiankun'
import { message } from 'ant-design-vue'
import actions from './shared/actions'
import loadBaseUiComponent from './plugins/antd'
import loadOneUiComponent from './library/ui/install'
import OneUi from './library/ui'
import App from './App.vue'
import router from './router'
import store from './store'
let app: any = null
const msg = {
data: [],
uiComponent: OneUi,
actions,
}
function render({ appContent, loading }: { appContent?: any; loading?: any } = {}): void {
if (!app) {
app = createApp({
data() {
return {
content: appContent,
loading,
}
},
render() {
return h(App, {
props: {
content: this.content,
loading: this.loading,
},
})
},
})
} else {
app.content = appContent
app.loading = loading
}
app.use(router).use(store)
loadBaseUiComponent(app)
loadOneUiComponent(app)
app.mount('#contain')
}
render()
function getActiveRule(routerPrefix: string) {
return (location: { pathname: string }) => location.pathname.startsWith(routerPrefix)
}
registerMicroApps( // Register subapplication
[
{
name: 'sub-app-code',
entry: '//localhost:8895',
container: '#sub-app-view',
activeRule: getActiveRule('/code'), // The conditions of activator application
props: msg,
},
{
name: 'sub-app-life',
entry: '//localhost:8099',
container: '#sub-app-view',
activeRule: getActiveRule('/life'),
props: msg,
},
],
{
beforeLoad: [
(currentApp) => {
console.log('before load', currentApp)
return Promise.resolve()
},
], // Callback before mount
beforeMount: [
(currentApp) => {
console.log('before mount', currentApp)
return Promise.resolve()
},
], // Callback after mount
afterUnmount: [
(currentApp) => {
console.log('after unload', currentApp)
return Promise.resolve()
},
],
},
)
setDefaultMountApp('/code')
runAfterFirstMounted(() => {})
start()
Copy code
App.vue
<div class="sub-app-view" id="sub-app-view" style="height: 100%"></div> // Provide mount points
Copy code
be relative to single-spa
Build the main application in the implementation ,qiankun
It can dynamically obtain the results of sub application packaging
Build sub applications
main.ts
Three King Kong bootstrap mount unmount
/* eslint-disable no-underscore-dangle */
import { createApp } from 'vue'
import loadBaseUiComponent from './plugins/antd'
import loadOneUiComponent from './library/ui/install'
import './utils/micro/public-path'
import App from './App.vue'
import actions from './shared/actions'
import router from './router'
import store from './store'
let instance: any = null
export async function bootstrap(props = {}) {
console.log(props)
}
export async function mount(props: any) {
console.log(props)
instance = createApp(App)
.use(router)
.use(store)
loadBaseUiComponent(instance)
loadOneUiComponent(instance)
instance.mount('#sub-app-code')
}
export async function unmount() {
instance.$destroy?.()
instance = null
}
// eslint-disable-next-line no-unused-expressions
window.__POWERED_BY_QIANKUN__ || mount({})
Copy code
vue.config.js
And single-spa
The difference is that you need to set up cross domain
devServer: {
// host: '0.0.0.0',
hot: true,
disableHostCheck: true,
port,
overlay: {
warnings: false,
errors: true
},
headers: {
'Access-Control-Allow-Origin': '*' // Cross domain setup
}
},
configureWebpack: {
devServer: {
open: true,
},
output: {
// Package sub applications as umd Library format
library: 'sub-app-code',
libraryTarget: 'umd'
},
},
Copy code
router The configuration also depends on whether it is in qiankun Dynamic settings in the environment
const router = createRouter({
history: createWebHistory(__qiankun__ ? '/code' : '/'), // todo /code It needs to be distributed by the main application
routes,
})
Copy code
single-spa
Build sub applications for __webpack_public_path__
Set it up , stay qiankun
You have to set it up in , Pull away to utils/micro/public-path.js
Next
/* eslint-disable camelcase */
/* eslint-disable no-underscore-dangle */
// eslint-disable-next-line no-underscore-dangle
if (window.__POWERED_BY_QIANKUN__) { // window.__POWERED_BY_QIANKUN__ Whether it is qiankun Signs of the environment
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
Copy code
This one is based on qiankun
The micro front-end application is completed
Communication between applications
I use the official actions signal communication
,qiankun
The interior provides initGlobalState
Method is used to register MicroAppStateActions
Examples are used for communication , This example has three methods , Namely :
setGlobalState
: Set upglobalState
- When setting a new value , It will be carried out internallyShallow examination
, If you findglobalState
A notification is triggered when a change occurs , Notice to allThe observer
function .onGlobalStateChange
: registerThe observer
function - Respond toglobalState
change , stayglobalState
Trigger when a change occursThe observer
function .offGlobalStateChange
: CancelThe observer
function - The instance no longer respondsglobalState
change .
The effect is globalState
After change , A prompt box will appear
-
establish actions example ( Operation in the main application )
shared/actions.ts
import { initGlobalState, MicroAppStateActions } from 'qiankun' const initialState = {} const actions: MicroAppStateActions = initGlobalState(initialState) export default actions Copy code
-
Observe globalState change
main.ts
function render({ appContent, loading }: { appContent?: any; loading?: any } = {}): void { ... actions.onGlobalStateChange((state, preState) => { console.log('new', state, 'old', preState) message.success(` New news reminds :${state.message}`) // When globalState A prompt box will appear for the change }) ... } Copy code
-
Send out actions
At present, only the main application uses
actions
, You can't communicate alone solo, We need toactions
Distribute to subapplications , It's also very simple when you need to pass it to the subapplication props Just pass one more parametermain.ts
const msg = { data: [], uiComponent: OneUi, actions, //actions example } registerMicroApps( [ { name: 'sub-app-code', entry: '//localhost:8895', container: '#sub-app-view', activeRule: getActiveRule('/code'), props: msg, // Down to the subapplication }, { name: 'sub-app-life', entry: '//localhost:8099', container: '#sub-app-view', activeRule: getActiveRule('/life'), props: msg, }, ], ... ) Copy code
It should be noted that , In the sub application, we need to build
actions class
, Receive from the main applicationactions
class Actions { actions = { onGlobalStateChange: (args: any) => args, setGlobalState: (args: any) => args, } /** * Set up actions */ // eslint-disable-next-line no-unused-vars setActions(actions: { onGlobalStateChange: (args: any) => any; setGlobalState: (args: any) => any }) { this.actions = actions } onGlobalStateChange(args: any) { return this.actions.onGlobalStateChange({ ...args }) } setGlobalState(args: any) { return this.actions.setGlobalState({ ...args }) } } const actions = new Actions() export default actions Copy code
Sub application of
main.ts
mount Method callexport async function mount(props: any) { if (props.actions) { actions.setActions(props.actions) actions.setGlobalState({ message: 'BBB' }) } ... } Copy code
Bingo...
mock Data and requests are intercepted according to different environments
It uses
mockjs
. Create these three files in the root directory to set the flags of different environmentsfor example : .env.dev-local
VUE_APP_CURRENTMODE = 'dev-local' VUE_APP_ENV = ' Local development environment ' VUE_APP_MOCK = true Copy code
Of course, it needs to be set up npm script, For example, I want to run the local environment command :
npm run dev-local
, It reads.env.dev-local
Add the values to the global environment variables ."dev-local": "vue-cli-service serve --mode dev-local", "dev-remote": "vue-cli-service serve --mode dev-remote", Copy code
Then it can be opened dynamically according to the environment you want mock 了 , Here's how I do it , stay
main.ts
It callsuseMock()
function useMock() { console.log('process.env', process.env) if (process.env.VUE_APP_MOCK) { // eslint-disable-next-line global-require require('../mock') } } Copy code
Access control
Request routing permission can be unified in the main application , And then it's distributed to the subapplication , You can control the permissions .
I didn't write
summary : Subapplications can be built separately , Load dynamically at run time . Lord 、 The subapplication is completely decoupled , Technology stack independent , It's protocol access ( Subapplication must export bootstrap、mount、unmount Method )
Copy code
ending
Why not use iframe
Users will lose their current state when they refresh the page
Source code : qiankun edition single-spa edition More articles