在微前端架构中,微应用被打包为模块,然后在浏览器中加载模块代码。但浏览器对模块化支持不够完善,所有single-spa
中使用SystemJS
处理浏览器中的模块化
SystemJs
浏览器对原生esm的支持
浏览本地支持esm
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Single-spa</title>
</head>
<body>
<div id="app">{{ msg }}</div>
</body>
<script type="importmap">
{
"imports": {
"vue": "https://cdn.jsdelivr.net/npm/vue@3.2.26/dist/vue.esm-browser.js"
}
}
</script>
<script type="module">
import { createApp } from 'vue'
const Hello = {
data() {
return {
msg: 'Hello World'
}
}
}
createApp(Hello).mount('#app')
</script>
</html>
Systemjs在浏览器中使用
浏览器本身支持esm的写法,但是并不是所有浏览器都支持esm。因此,可以引入systemjs
<script type="systemjs-importmap">
{
"imports": {
+ "vue": "https://cdn.jsdelivr.net/npm/vue@3.2.26/dist/vue.global.min.js"
}
}
</script>
+<script type="systemjs-module" src="./js/main.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/systemjs@6.11.0/dist/system.min.js"></script>
</html>
System.register(['vue'], () => {
let Vue
return {
setters: [ret => {
Vue = ret.Vue
}],
execute() {
const Hello = {
data() {
return {
msg: 'Hello World'
}
}
}
Vue.createApp(Hello).mount('#app')
}
}
})
使用webpack构建system模块
webpack配置文件
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
/** @type {import('webpack').Configuration} */
module.exports = {
mode: 'development',
entry: './src/main.jsx',
output: {
filename: '[name].js',
path: path.join(__dirname, 'dist'),
// 告诉webpack输出兼容systemjs兼容代码
libraryTarget: 'system',
},
externals: ['react', 'react-dom'],
resolve: {
extensions: ['.js', '.json', '.jsx'],
},
devtool:'source-map',
devServer: {
port: 8080,
static: {
directory: path.join(__dirname, 'dist'),
},
historyApiFallback: true
},
module: {
rules: [{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-react'
]
}
}
}]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
// 告诉HtmlWebpackPlugin不需要把构建结果自动注入html中
inject: false
})
]
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>systemjs</title>
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.development.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.development.js"
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.11.0/dist/system.min.js"></script>
<script type="systemjs-module" src="./main.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
安装
全局安装
npm i create-single-spa -g
# 创建主应用
create-single-spa
不想全局安装,使用下面命令
npm init create-single-spa
创建主应用
? Directory for new project main
# 选择 root config 表示主应用
? Select type to generate single-spa root config
? Which package manager do you want to use? npm
? Will this project use Typescript? No
? Would you like to use single-spa Layout Engine No
? Organization name (can use letters, numbers, dash or underscore) learn
主应用
// learn-root-config.js
import {
registerApplication,
start
} from "single-spa";
// 注册微应用
registerApplication({
name: "@single-spa/welcome",
app: () =>
System.import(
"https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
),
// 激活微应用的路径
activeWhen: ["/"],
});
registerApplication({
name: "@learn/navbar",
app: () => System.import("@learn/navbar"),
activeWhen: ["/"]
});
start({
urlRerouteOnly: true,
});
微应用
没有框架的微应用
创建一个新目录,用来存放微应用
npm init -y
npm i webpack webpack-cli webpack-dev-server webpack-merge webpack-config-single-spa @babel/core single-spa -D
新建webpack.config.js文件
// webpack.config.js
const {
merge
} = require('webpack-merge')
const singleSpaConfig = require('webpack-config-single-spa')
module.exports = () => {
// 默认入口js文件名称为[orgName]-[orgName]
const config = singleSpaConfig({
orgName: 'learn',
projectName: 'html'
})
return merge(config, {
devServer: {
port: 9001
}
})
}
微应用的入口文件
// 启用
export async function bootstrap() {
console.log('learn bootstrap')
}
// 渲染
export async function mount() {
console.log('learn mount')
render()
}
// 注销
export async function unmount() {
console.log('learn unmount')
}
function render() {
const el = document.createElement('div')
el.innerHTML = '<h1>无框架微应用</h1>'
document.body.appendChild(el)
}
注意,微应用的入口文件必须导出bootstrap、mount、unmount三个函数,且返回值都必须是Promise
主应用中注册
registerApplication({
name: "@learn/html",
app: () => System.import("@learn/html"),
activeWhen: ["/html"]
});
React微应用
npm init single-spa
? Select type to generate single-spa application / parcel
# 之后选择React应用
Vue微应用
npm init single-spa
...
? Select type to generate single-spa application / parcel
# 之后选择Vue应用
Parcel微应用
使用single-spa创建一个微应用
<script type="systemjs-importmap">
{
"imports": {
"@learn/root-config": "//localhost:9000/learn-root-config.js",
"@learn/html":"//localhost:9001/learn-html.js",
"@learn/react":"//localhost:9002/learn-react.js",
"@learn/vue":"//localhost:9003/js/app.js",
"@learn/vue3":"//localhost:9004/js/app.js",
+ "@learn/parcel-vue":"//localhost:9005/js/app.js",
"react":"https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.development.js",
"react-dom":"https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.development.js",
"react-router-dom": "https://cdn.jsdelivr.net/npm/react-router-dom@6.2.1/main.min.js",
"vue": "https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js",
"vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.5.3/dist/vue-router.min.js"
}
}
</script>
注册parcel应用
registerApplication({
name: "@learn/parcel-vue",
app: () => System.import("@learn/parcel-vue"),
activeWhen: ["/parcel-vue"]
})
在vue应用中使用parcel应用
<script>
import Parcel from 'single-spa-vue/dist/esm/parcel'
import { mountRootParcel } from 'single-spa'
export default {
name:'About',
components:{
Parcel
},
data(){
return {
parcelConfig: window.System.import('@learn/parcel-vue'),
mountParcel : mountRootParcel
}
}
}
</script>
<template>
<div class="about">
<h1>This is an about page</h1>
<Parcel :config="parcelConfig" :mountParcel ="mountParcel"/>
</div>
</template>
注意,不要让webpack打包single-spa
在React应用中使用Parcel应用
import Parcel from 'single-spa-react/parcel'
const About = () => {
return (
<div>
<Parcel config={System.import('@learn/parcel-vue')}/>
</div>
)
}
跨应用共享的工具模块
使用single-spa
创建应用时选择工具模块
npm init single-spa
? Directory for new project utility-test
# 选择工具模块
? Select type to generate in-browser utility module (styleguide, api cache, etc)
? Which framework do you want to use? none
? Which package manager do you want to use? npm
? Will this project use Typescript? No
? Organization name (can use letters, numbers, dash or underscore) learn
? Project name (can use letters, numbers, dash or underscore) utility-test
在模块应用导出一个方法
export function add(a, b) {
return a + b
}
在主应用中注册
其他应用调用
const UtilityTest = () => import('@learn/utility-test')
// 注意导出模块采用import导入语应用时异步的,所以需要等模块加载完毕后再调用
UtilityTest().then(ret => {
console.log(ret.add(2,3))
})
应用通信
每个应用应尽可能的自己管理自己的状态
使用rxks
import {Subject} from 'rxjs'
export const subject = new Subject()
subject.subscribe({
next: (v) => console.log(`observerA: ${v}`)
})
registerApplication({
name: "@learn/react",
app: () => System.import("@learn/react"),
activeWhen: ["/react"],
// 将subject传递给微应用
customProps: {
subject
}
});
在微应用中使用
let subject1 = null
// 微应用通过props获取subject
export async function mount(props) {
if (!subject1 && props.subject) {
subject1 = props.subject.subscribe({
next: (v) => console.log(`React observer: ${v}`)
})
setTimeout(() => {
props.subject.next(100)
}, 5000)
}
lifecycles.mount(props)
}
export async function unmount(props) {
if (subject1) {
console.log('取消订阅')
subject1.unsubscribe()
}
lifecycles.mount(props)
}
export const {
bootstrap
} = lifecycles;
封装一个类似qiankun的状态管理
import {
Subject
} from 'rxjs'
export const subject = new Subject()
class GlobalState {
constructor(state) {
this._state = state
this._subject = new Subject()
this.subs = []
}
onGlobalStateChange = (cb, fireimmediately) => {
if (fireimmediately) cb(this._state)
const sub = this._subject.subscribe({
next: v => {
cb(v, this._state)
}
})
this.subs.push(sub)
}
setGlobalState = (state) => {
this._subject.next(state)
this._state = state
}
offGlobalState = () => {
this.subs.forEach(sub => {
sub.unsubscribe()
})
}
}
class ChildState {
constructor(global) {
this._global = global
this.subs = []
}
onGlobalStateChange = (cb, fireimmediately) => {
const global = this._global
if (fireimmediately) cb( global._state)
const sub = global._subject.subscribe({
next: v => {
cb(v, global._state)
}
})
this.subs.push(sub)
}
setGlobalState = (state) => {
this._global._subject.next(state)
this._global._state = state
}
offGlobalState = () => {
this.subs.forEach(sub => {
sub.unsubscribe()
})
}
}
export function initGlobalState(state) {
return new GlobalState(state)
}
export function initChildState(global){
return new ChildState(global)
}
封装ChildState的目的是为了防止子应用卸载后会清除调主应用的状态监听
const global = initGlobalState({
count: 1
})
const childState = new initChildState(global)
registerApplication({
name: "@learn/react",
app: () => System.import("@learn/react"),
activeWhen: ["/react"],
customProps: {
onGlobalStateChange: childState.onGlobalStateChange.bind(global),
setGlobalState: childState.setGlobalState.bind(global),
offGlobalState: childState.offGlobalState.bind(global)
}
});