single-spa
一、创建容器应用
1.全局安装single-spa脚手架工具:npm i create-single-spa -g
2.新建workspace文件夹,切换到文件夹下,使用create-single-spa
创建:
# 应用是否创建到当前目录,因为首先创建的是一个容器应用,所以新建到container文件夹中
? Directory for new project container
# 创建什么应用,选择root config,也就是容器应用
# 1. single-spa-application / parcel:微前端架构中的微应用,可以使用 vue、react、angular 等框架。
# 2. single-spa root config:创建微前端容器应用。
# 3. utility modules:公共模块应用,非渲染组件,用于跨应用共享 javascript 逻辑的微应用。
? Select type to generate single-spa root config
# 用什么管理 选择npm
? Which package manager do you want to use? npm
# 使用ts吗,暂不使用
? 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) jichen
3.启动应用: cd container
npm start
4.访问应用:localhost:9000,可以看到如下页面:
容器是没有页面的,之所以能看到这个页面,是因为脚手架默认注册了一个微应用。
3.默认代码解析:
container/src/jichen-root-config.js
import { registerApplication, start } from "single-spa";
// 注册微前端应用:
// 1.name: 字符串类型,微前端应用名称"@组织名称/应用名称"
// 2.app: 函数类型,返回Promise,通过systemjs引用打包好的微前端应用模块代码(umd)
// 3.activeWhen: 路由匹配时激活应用
registerApplication({
name: "@single-spa/welcome",
app: () =>
System.import(
"https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
),
activeWhen: ["/"],
});
// registerApplication({
// name: "@jichen/navbar",
// app: () => System.import("@jichen/navbar"),
// activeWhen: ["/"]
// });
// start 方法必须在single spa的配置文件中调用
// 调用 start 之前,应用会被加载,但不会初始化,挂载或卸载
start({
// 是否可以通过history.pushState() 和history.replaceState()更改触发single-spa路由,默认为false, true:不允许
urlRerouteOnly: true,
});
container/src/index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Root Config</title>
<!--
Remove this if you only support browsers that support async/await.
This is needed by babel to share largeish helper code for compiling async/await in older
browsers. More information at https://github.com/single-spa/create-single-spa/issues/112
-->
<script src="https://cdn.jsdelivr.net/npm/regenerator-runtime@0.13.7/runtime.min.js"></script>
<!--
This CSP allows any SSL-enabled host and for arbitrary eval(), but you should limit these directives further to increase your app's security.
Learn more about CSP policies at https://content-security-policy.com/#directive
-->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: localhost:* ws://localhost:*; style-src 'unsafe-inline' https:; object-src 'none';">
<meta name="importmap-type" content="systemjs-importmap" />
<!-- JavaScript 模块下载地址,此处可放置微前端项目中的公共模块 -->
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js"
}
}
</script>
<!-- single-spa 预加载 -->
<link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js" as="script">
<!-- Add your organization's prod import map URL to this script's src -->
<!-- <script type="systemjs-importmap" src="/importmap.json"></script> -->
<% if (isLocal) { %>
<script type="systemjs-importmap">
{
"imports": {
"@jichen/root-config": "//localhost:9000/jichen-root-config.js"
}
}
</script>
<% } %>
<!-- 用于支持 Angular 应用 -->
<!-- <script src="https://cdn.jsdelivr.net/npm/zone.js@0.11.3/dist/zone.min.js"></script> -->
<!-- 用于覆盖通过 import-map 设置的 JavaScript 模块下载地址 -->
<script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
<% if (isLocal) { %>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.js"></script>
<% } else { %>
<!-- 模块加载器 -->
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script>
<!-- systemjs用来解析 AMD 模块的插件 -->
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.min.js"></script>
<% } %>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<main></main>
<!-- 导入微前端容器应用 -->
<script>
System.import('@jichen/root-config');
</script>
<!-- import-map-overrides 可以覆盖导入映射,当前项目中用于配合single-spa Inspector调试工具使用。可以手动覆盖项目中的JS模块加载地址,用于调试 -->
<import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>
二、创建不基于框架的微应用
1.应用初始化,在workspace文件夹下创建qing文件夹,切换到该文件夹下,用于存放改微应用的代码,使用以下package.json文件安装所需依赖:
{
"name": "qing",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"start": "webpack serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.12.10",
"single-spa": "^5.9.0",
"webpack": "^5.8.0",
"webpack-cli": "^4.2.0",
"webpack-config-single-spa": "^2.0.0",
"webpack-dev-server": "^4.0.0-beta.0",
"webpack-merge": "^5.4.0"
}
}
2.安装完依赖后,新建webpack.config.js配置webpack
const singleSpaDefaults = require('webpack-config-single-spa')
const { merge } = require("webpack-merge")
module.exports = () =>{
const defaultConfig = singleSpaDefaults({
// 组织名称
orgName: "jichen",
// 项目名称
projectName: "qing"
})
// 使用merge方法配置微应用默认端口
return merge(defaultConfig, {
devServer: {
port: 9001
}
})
}
3.在qing文件夹下新建src目录,然后再sr目录下新建微应用的入口文件,文件名格式是”组织名称-微应用名称.js”,即jichen-qing.js
,框架要求入口文件中必须导出微前端应用所需的声明周期函数,生命周期函数必须返回Promise:
// 定义一个变量,方便卸载时移除
let qingContainer = null
// 使用async 默认返回的就是Promise对象
export async function bootstrap() {
console.log("应用正在启动。。。");
}
export async function mount() {
console.log("应用正在挂载。。。");
qingContainer = document.createElement('div')
qingContainer.id = "qingContainer"
qingContainer.innerHTML = 'qing wahaha'
document.body.appendChild(qingContainer)
}
export async function unmount() {
console.log("应用正在卸载");
document.body.removeChild(qingContainer)
}
4.在前端容器应用中注册微前端应用,在container/src/jichen-root-config.js文件中添加如下代码:
registerApplication({
name: "@jichen/qing",
app: () => System.import("@jichen/qing"),
activeWhen: ["/qing"]
});
5.在模板文件container/src/index.ejs中指定模板访问地址:
<script type="systemjs-importmap">
{
"imports": {
"@jichen/root-config": "//localhost:9000/jichen-root-config.js",
// 添加访问地址
"@jichen/qing": "//localhost:9001/jichen-qing.js"
}
}
</script>
6.终端中切换到qing目录下,执行npm start
,启动微应用 ,然后访问容器应用地址 localhost:9000/qing ,会发现同时存在了两个微应用(qing和脚手架默认注册的微应用)的内容,这是因为localhost:9000/qing会匹配activeWhen: ["/"]
规则:
修改默认应用代码container/src/jichen-root-config.js:
registerApplication({
name: "@single-spa/welcome",
app: () =>
System.import(
"https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
),
activeWhen: ["/"],
});
改为如下配置:
registerApplication("@single-spa/welcome", () =>
System.import(
"https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
),
location => location.pathname === '/',
);
这时访问localhost:9000/qing和localhost:9000就只显示各自微应用的内容了。
三、创建基于React的微应用
1.创建应用:create-single-spa
? Directory for new project reactApp
? Select type to generate single-spa application / parcel
? Which framework do you want to use? react
? Which package manager do you want to use? npm
? Will this project use Typescript? No
? Organization name (can use letters, numbers, dash or underscore) jichen
? Project name (can use letters, numbers, dash or underscore) reactApp
2.修改应用启动端口reactApp/package.json:
{
"scripts": {
"start": "webpack serve --port 9002",
},
}
3.切换到reactApp目录下,执行npm start
启动应用
4.注册微应用到容器应用container/src/jichen-root-config.js:
registerApplication({
name: "@jichen/reactApp",
app: () => System.import("@jichen/reactApp"),
activeWhen: ["/reactApp"]
});
5.指定微前端应用模块的引用地址:
<script type="systemjs-importmap">
{
"imports": {
"@jichen/root-config": "//localhost:9000/jichen-root-config.js",
"@jichen/qing": "//localhost:9001/jichen-qing.js",
// 添加地址
"@jichen/reactApp": "//localhost:9002/jichen-reactApp.js",
}
}
</script>
6.指定公共库的访问地址,因为single-spa认为react和react-dom属于公共库,不应该单独被webpack打包:
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
// 增加公共库地址
""https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.production.min.js
}
}
</script>
7.微前端React应用入口文件代码:
// 引用的是容器应用index.ejs 文件中import-map指定的版本
import React from "react";
import ReactDOM from "react-dom";
// single-spa-react 用于创建使用React框架实现的微前端应用
import singleSpaReact from "single-spa-react";
// 用于渲染在页面中的根组件
import Root from "./root.component";
// 指定根组件渲染位置,必须在容器应用index.ejs 文件中定义一个id为root的div
const domElementGetter = () => document.getElementById('root')
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: Root,
// 错误边界函数,微应用发生错误时显示
errorBoundary(err, info, props) {
// Customize the root error boundary for your microfrontend here.
return null;
},
});
// 暴露必须的声明周期函数
export const { bootstrap, mount, unmount } = lifecycles;
8.访问localhost:9000/reactApp
,可以看到如下页面,说明创建的react微应用启动成功了:
9.配置路由:
// reactApp/src/root.component.js
import React from "react";
import {BrowserRouter, Switch, Route, Redirect, Link} from "react-router-dom"
import Home from './pages/Home'
import About from './pages/About'
export default function Root(props) {
return (
<BrowserRouter basename="/reactApp">
<div>{props.name}</div>
<div>
<Link to="home">Home</Link>
<Link to="about">About</Link>
</div>
<Switch>
<Route path="/home">
<Home></Home>
</Route>
<Route path="/about">
<About></About>
</Route>
<Route path="/">
<Redirect to="/home" />
</Route>
</Switch>
</BrowserRouter>
);
}
// reactApp/src/pages/Home.js
import React from 'react'
const Home = () => {
return (
<div>Home Works</div>
)
}
export default Home
// reactApp/src/pages/About.js
import React from 'react'
const About = () => {
return (
<div>About Works</div>
)
}
export default About
10.修改webpack配置reactApp/webpack.config.js:
const { merge } = require("webpack-merge");
const singleSpaDefaults = require("webpack-config-single-spa-react");
module.exports = (webpackConfigEnv, argv) => {
const defaultConfig = singleSpaDefaults({
orgName: "jichen",
projectName: "reactApp",
webpackConfigEnv,
argv,
});
return merge(defaultConfig, {
// modify the webpack config however you'd like to by adding to this object
// 编译时不打包react-router-dom框架
externals: ["react-router-dom"]
});
};
可以看到如下效果:
四、创建基于Vue的微应用
1.创建应用
2.在vueapp目录下新建vue.config.js配置webpack:
module.exports = {
chainwebpack: config => {
// 不打包vue和vue-router,将他们作为公共库在容器模板中引入
config.externals(["vue", "vue-router"])
}
}
3.容器应用的模板文件index.ejs引入vue和vue-router:
<script type="systemjs-importmap">
{
"imports": {
// 新增
"vue": "https://cdn.jsdelivr.net/npm/vue/dist/vue.js",
"vue-route": "https://cdn.staticfile.org/vue-router/2.7.0/vue-router.min.js"
}
}
</script>
4.修改启动命令vueapp/package.json:
{
"scripts": {
"start": "vue-cli-service serve --port 9003"
},
}
5.npm start
启动微应用
6.注册微应用到容器
// container/src/jichen-root-config.js
registerApplication({
name: "@jichen/vueapp",
app: () => System.import("@jichen/vueapp"),
activeWhen: ["/vueapp"]
});
7.指定微前端应用模块的引用地址:
访问http://localhost:9003/,提示不是正确的访问路径,也可以看到应该配置的入口是:http://localhost:9003/js/app.js
// container/src/index.ejs
<script type="systemjs-importmap">
{
"imports": {
// 增加
"@jichen/vueapp": "//localhost:9003/js/app.js"
}
}
</script>
8.访问__http://localhost:9000/vueapp__
,看到如下页面就表示成功了,但是有个报错
是个类似跨域安全的报错,需要将container/src/index.ejs文件中的以下代码注释,就不报错了
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: localhost:* ws://localhost:*; style-src 'unsafe-inline' https:; object-src 'none';">
9.配置路由:
// vueapp/src/main.js
import Vue from 'vue';
import VueRouter from 'vue-router'
import singleSpaVue from 'single-spa-vue';
import App from './App.vue';
Vue.use(VueRouter)
Vue.config.productionTip = false;
// 路由组件
const Home = { template: "<div>Home</div>" }
const About = { template: "<div>About</div>" }
// 路由规则
const routes = [
{path: '/home', component: Home},
{path: '/about', component: About}
]
// 路由实例
const router = new VueRouter({routes, mode: "history", base: "vueapp"})
const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
// 路由
router,
//渲染组件
render(h) {
return h(App, {
props: {
// single-spa props are available on the "this" object. Forward them to your component as needed.
// https://single-spa.js.org/docs/building-applications#lifecycle-props
// if you uncomment these, remember to add matching prop definitions for them in your App.vue file.
name: this.name,
mountParcel: this.mountParcel,
singleSpa: this.singleSpa
},
});
},
},
});
// 导出生命周期函数
export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;
// vueapp/src/App.vue
<template>
<div id="app">
<h1>{{ name }}</h1>
<p>
<router-link to="/home">Home</router-link>
<router-link to="/about">About</router-link>
</p>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
props: ["name"]
}
</script>
<style>
</style>
访问http://localhost:9000/vueapp/home可以看到如下效果
五、创建Parcel应用
Parcel用来创建公共UI,涉及跨框架共享UI时,需要使用Parcel。
Parcel的定义可以使用任何single-spa支持的框架,它也是单独的应用,需要单独启动,但它不关联路由。
Paecel应用的模块访问地址也需要被添加到import-map中,其它微应用通过System.import方法进行引用。
需求:使用vue创建一个的Parcel应用navbar。
1.使用create-single-spa
2.在navbar目录下新建vue.config.js配置webpack:
module.exports = {
chainWebpack: config => {
// 不打包vue和vue-router,将他们作为公共库在容器模板中引入
config.externals(["vue", "vue-router"])
}
}
3.修改启动命令navbar/package.json
{
"scripts": {
"start": "vue-cli-service serve --port 9004",
},
}
4.在容器模板文件container/src/index.ejs中指定应用模块地址:
{
"imports": {
"@jichen/root-config": "//localhost:9000/jichen-root-config.js",
"@jichen/qing": "//localhost:9001/jichen-qing.js",
"@jichen/reactApp": "//localhost:9002/jichen-reactApp.js",
"@jichen/vueapp": "//localhost:9003/js/app.js",
// 新增
"@jichen/navbar": "//localhost:9004/js/app.js"
}
}
5.实现navbar功能navbar/src/App.vue:
<template>
<div id="app">
<p>
<router-link to="/">@single-spa/welcome</router-link>
<router-link to="/qing">@jichen/qing</router-link>
<router-link to="/vueapp">@jichen/vueapp</router-link>
<router-link to="/reactapp">@jichen/reactapp</router-link>
</p>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
components: {
}
}
</script>
<style>
</style>
6.在reactapp中使用navbar(reactApp/src/pages/Home.js):
import React from 'react'
import Parcel from "single-spa-react/parcel"
const Home = () => {
return (
<div>
<Parcel config={System.import("@jichen/navbar")} />
</div>
)
}
export default Home
7.在vueapp中使用navbar:
<!-- vueapp/src/App.vue -->
<template>
<div id="app">
<h1>{{ name }}</h1>
<p>
<router-link to="/home">Home</router-link>
<router-link to="/about">About</router-link>
</p>
<p>
<Parcel :config="parcelConfig" :mountParcel="mountParcel" />
</p>
<router-view></router-view>
</div>
</template>
<script>
import Parcel from 'single-spa-vue/dist/esm/parcel';
import { mountRootParcel } from 'single-spa';
export default {
components: {
Parcel,
},
data() {
return {
parcelConfig: window.System.import("@jichen/navbar"),
mountParcel: mountRootParcel
}
},
name: 'App',
props: ["name"]
}
</script>
<style>
</style>
// navbar/src/main.js
import Vue from 'vue';
import VueRouter from 'vue-router'
import singleSpaVue from 'single-spa-vue';
import App from './App.vue';
Vue.config.productionTip = false;
Vue.use(VueRouter)
const routes = []
const router = new VueRouter({routes, mode: "history", base: "/"})
const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
router,
render(h) {
return h(App, {
props: {
// single-spa props are available on the "this" object. Forward them to your component as needed.
// https://single-spa.js.org/docs/building-applications#lifecycle-props
// if you uncomment these, remember to add matching prop definitions for them in your App.vue file.
/*
name: this.name,
mountParcel: this.mountParcel,
singleSpa: this.singleSpa,
*/
},
});
},
},
});
export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;
8.访问http://localhost:9000/vueapp,可以看到如下,可以在vueapp和reactapp中使用navbar的功能:
六、创建utility modules
1.用于放置跨应用共享的JavaScript逻辑,它也是独立的应用,需要单独构建,单独启动。
2.使用create-single-spa
创建:
3.修改启动端口tools/package.json
{
"name": "@jichen/tools",
"scripts": {
"start": "webpack serve --port 9005",
}
}
4.导出共享的方法tools/src/jichen-tools.js
// Anything exported from this file is importable by other in-browser modules.
export function sayHello(who) {
console.log(`%c${who} sayHello`, "color:skyblue")
}
5.容器应用模板中声明模块访问地址container/src/index.ejs
{
"imports": {
"@jichen/root-config": "//localhost:9000/jichen-root-config.js",
"@jichen/qing": "//localhost:9001/jichen-qing.js",
"@jichen/reactApp": "//localhost:9002/jichen-reactApp.js",
"@jichen/vueapp": "//localhost:9003/js/app.js",
"@jichen/navbar": "//localhost:9004/js/app.js",
// 新增
"@jichen/tools": "//localhost:9005/jichen-tools.js"
}
}
6.终端切换到tools目录下,运行npm start
启动应用。
7.在reactapp中使用sayHello方法reactApp/src/pages/About.js
import React, {useEffect, useState} from 'react'
function useToolsModule() {
const [toolsModule, setToolsModule] = useState()
useEffect(() => {
System.import("@jichen/tools").then(setToolsModule)
}, [])
return toolsModule
}
const About = () => {
const toolsModule = useToolsModule()
if(toolsModule) toolsModule.sayHello('react')
return (
<div>About Works</div>
)
}
export default About
访问http://localhost:9000/reactApp/about 可以看到以下结果:
8.vueapp使用sayHello方法vueapp/src/App.vue
<template>
<div id="app">
<h1>{{ name }}</h1>
<p>
<router-link to="/home">Home</router-link>
<router-link to="/about">About</router-link>
</p>
<p>
<Parcel :config="parcelConfig" :mountParcel="mountParcel" />
</p>
<button @click="handleClick">sayHello</button>
<router-view></router-view>
</div>
</template>
<script>
import Parcel from 'single-spa-vue/dist/esm/parcel';
import { mountRootParcel } from 'single-spa';
export default {
components: {
Parcel,
},
data() {
return {
parcelConfig: window.System.import("@jichen/navbar"),
mountParcel: mountRootParcel
}
},
name: 'App',
props: ["name"],
methods: {
async handleClick() {
let toolsModule = await window.System.import("@jichen/tools")
toolsModule.sayHello('vue')
}
},
}
</script>
<style>
</style>
访问 http://localhost:9000/vueapp 点击sayHello按钮,可以看到以下结果:
9.使用RxJS跨应用通信,因为它无关框架,可以在任何框架中使用。
9-1. 在容器应用的模板文件中引入RxJS库,container/src/index.ejs
{
"imports": {
"rxjs": "https://unpkg.com/rxjs/bundles/Rx.min.js"
}
}
9-2. 在tools中导出一个ReplaySubject,可以广播历史消息,就算是动态加载进来的应用也可以接收到数据tools/src/jichen-tools.js
import { ReplaySubject } from 'rxjs'
// Anything exported from this file is importable by other in-browser modules.
export function sayHello(who) {
console.log(`%c${who} sayHello`, "color:skyblue")
}
export const sharedSubject = new ReplaySubject()
9-3. 在reactapp中订阅reactApp/src/pages/About.js
import React, {useEffect, useState} from 'react'
function useToolsModule() {
const [toolsModule, setToolsModule] = useState()
useEffect(() => {
System.import("@jichen/tools").then(setToolsModule)
}, [])
return toolsModule
}
const About = () => {
const toolsModule = useToolsModule()
useEffect(() => {
let subjection = null
if(toolsModule) {
toolsModule.sayHello('react')
// 订阅
subjection = toolsModule.sharedSubject.subscribe(console.log)
}
// 取消订阅
return () => subjection.unsubscribe
})
return (
<div>About Works</div>
)
}
export default About
9-4. 在vueapp中订阅vueapp/src/App.vue
<template>
<div id="app">
<h1>{{ name }}</h1>
<p>
<router-link to="/home">Home</router-link>
<router-link to="/about">About</router-link>
</p>
<p>
<Parcel :config="parcelConfig" :mountParcel="mountParcel" />
</p>
<button @click="handleClick">sayHello</button>
<router-view></router-view>
</div>
</template>
<script>
import Parcel from 'single-spa-vue/dist/esm/parcel';
import { mountRootParcel } from 'single-spa';
export default {
components: {
Parcel,
},
data() {
return {
parcelConfig: window.System.import("@jichen/navbar"),
mountParcel: mountRootParcel
}
},
name: 'App',
props: ["name"],
methods: {
async handleClick() {
let toolsModule = await window.System.import("@jichen/tools")
toolsModule.sayHello('vue')
}
},
async mounted () {
let toolsModule = await window.System.import("@jichen/tools")
toolsModule.sharedSubject.subscribe(console.log)
},
}
</script>
<style>
</style>
9-5. reactapp中定义发起广播的按钮:reactApp/src/pages/About.js
import React, {useEffect, useState} from 'react'
function useToolsModule() {
const [toolsModule, setToolsModule] = useState()
useEffect(() => {
System.import("@jichen/tools").then(setToolsModule)
}, [])
return toolsModule
}
const About = () => {
const toolsModule = useToolsModule()
useEffect(() => {
let subjection = null
if(toolsModule) {
toolsModule.sayHello('react')
subjection = toolsModule.sharedSubject.subscribe(console.log)
}
return () => subjection.unsubscribe
}, [toolsModule])
return (
<div>
About Works
<button onClick={() => subjection.sharedSubject.next('请注意')}>广播</button>
</div>
)
}
export default About
9-6. 访问http://localhost:9000/reactApp/about ,点击广播按钮,可以看到输出,并且切换到vueapp也可以看到历史消息:
9-7. 在vueapp中定义倒车按钮,vueapp/src/App.vue
<template>
<div id="app">
<h1>{{ name }}</h1>
<p>
<router-link to="/home">Home</router-link>
<router-link to="/about">About</router-link>
</p>
<p>
<Parcel :config="parcelConfig" :mountParcel="mountParcel" />
</p>
<button @click="handleClick">sayHello</button>
<button @click="boardCast">倒车</button>
<router-view></router-view>
</div>
</template>
<script>
import Parcel from 'single-spa-vue/dist/esm/parcel';
import { mountRootParcel } from 'single-spa';
export default {
components: {
Parcel,
},
data() {
return {
parcelConfig: window.System.import("@jichen/navbar"),
mountParcel: mountRootParcel
}
},
name: 'App',
props: ["name"],
methods: {
async handleClick() {
let toolsModule = await window.System.import("@jichen/tools")
toolsModule.sayHello('vue')
},
async boardCast () {
let toolsModule = await window.System.import("@jichen/tools")
toolsModule.sharedSubject.next("倒车")
},
},
async mounted () {
let toolsModule = await window.System.import("@jichen/tools")
toolsModule.sharedSubject.subscribe(console.log)
},
}
</script>
<style>
</style>
9-8. 访问http://localhost:9000/vueapp ,点击倒车按钮,切换到reactapp的About页面,可以看到历史消息:
七、布局引擎的使用方式
1.允许使用组件的方式声明顶层路由,并且提供了更加便捷的路由API用来注册应用。
2.切换到container下安装布局引擎npm i single-spa-layout
3.在容器应用的模板文件中构建路由(主要是template中的代码),还有之前创建容器时默认的@single-spa/welcome也需要声明一下,container/src/index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Root Config</title>
<!--
Remove this if you only support browsers that support async/await.
This is needed by babel to share largeish helper code for compiling async/await in older
browsers. More information at https://github.com/single-spa/create-single-spa/issues/112
-->
<script src="https://cdn.jsdelivr.net/npm/regenerator-runtime@0.13.7/runtime.min.js"></script>
<!--
This CSP allows any SSL-enabled host and for arbitrary eval(), but you should limit these directives further to increase your app's security.
Learn more about CSP policies at https://content-security-policy.com/#directive
-->
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: localhost:* ws://localhost:*; style-src 'unsafe-inline' https:; object-src 'none';"> -->
<meta name="importmap-type" content="systemjs-importmap" />
<!-- If you wish to turn off import-map-overrides for specific environments (prod), uncomment the line below -->
<!-- More info at https://github.com/joeldenning/import-map-overrides/blob/master/docs/configuration.md#domain-list -->
<!-- <meta name="import-map-overrides-domains" content="denylist:prod.example.com" /> -->
<!-- Shared dependencies go into this import map. Your shared dependencies must be of one of the following formats:
1. System.register (preferred when possible) - https://github.com/systemjs/systemjs/blob/master/docs/system-register.md
2. UMD - https://github.com/umdjs/umd
3. Global variable
More information about shared dependencies can be found at https://single-spa.js.org/docs/recommended-setup#sharing-with-import-maps.
-->
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
"react": "https://unpkg.com/react@18/umd/react.production.min.js",
"react-dom": "https://unpkg.com/react-dom@18/umd/react-dom.production.min.js",
"react-router-dom": "https://unpkg.com/react-router-dom@5.0.0/umd/react-router-dom.min.js",
"vue": "https://unpkg.com/vue@2.6.10/dist/vue.js",
"vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js",
"rxjs": "https://unpkg.com/rxjs/bundles/Rx.min.js"
}
}
</script>
<link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js" as="script">
<!-- Add your organization's prod import map URL to this script's src -->
<!-- <script type="systemjs-importmap" src="/importmap.json"></script> -->
<% if (isLocal) { %>
<script type="systemjs-importmap">
{
"imports": {
"@jichen/root-config": "//localhost:9000/jichen-root-config.js",
"@jichen/qing": "//localhost:9001/jichen-qing.js",
"@jichen/reactApp": "//localhost:9002/jichen-reactApp.js",
"@jichen/vueapp": "//localhost:9003/js/app.js",
"@jichen/navbar": "//localhost:9004/js/app.js",
"@jichen/tools": "//localhost:9005/jichen-tools.js",
"@single-spa/welcome": "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
}
}
</script>
<% } %>
<!--
If you need to support Angular applications, uncomment the script tag below to ensure only one instance of ZoneJS is loaded
Learn more about why at https://single-spa.js.org/docs/ecosystem-angular/#zonejs
-->
<!-- <script src="https://cdn.jsdelivr.net/npm/zone.js@0.11.3/dist/zone.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
<% if (isLocal) { %>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.js"></script>
<% } else { %>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.min.js"></script>
<% } %>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<template id="single-spa-layout">
<single-spa-router>
<application name="@jichen/navbar"></application>
<route default>
<application name="@single-spa/welcome"></application>
</route>
<route path="qing">
<application name="@jichen/qing"></application>
</route>
<route path="reactApp">
<application name="@jichen/reactApp"></application>
</route>
<route path="vueapp">
<application name="@jichen/vueapp"></application>
</route>
</single-spa-router>
</template>
<main></main>
<div id="root"></div>
<script>
System.import('@jichen/root-config');
</script>
<import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>
4.获取路由信息并注册应用container/src/jichen-root-config.js
import { registerApplication, start } from "single-spa";
import { constructApplications, constructRoutes } from "single-spa-layout"
// 获取路由配置对象
const routes = constructRoutes(document.querySelector("#single-spa-layout"))
// 获取路由信息数组
const applications = constructApplications({
routes,
loadApp({ name }) {
return System.import(name)
}
})
// 遍历路由信息注册应用
applications.forEach(registerApplication)
// registerApplication({
// name: "@single-spa/welcome",
// app: () =>
// System.import(
// "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
// ),
// activeWhen: ["/"],
// });
// registerApplication("@single-spa/welcome", () =>
// System.import(
// "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
// ),
// location => location.pathname === '/',
// );
// registerApplication({
// name: "@jichen/qing",
// app: () => System.import("@jichen/qing"),
// activeWhen: ["/qing"]
// });
// registerApplication({
// name: "@jichen/reactApp",
// app: () => System.import("@jichen/reactApp"),
// activeWhen: ["/reactApp"]
// });
// registerApplication({
// name: "@jichen/vueapp",
// app: () => System.import("@jichen/vueapp"),
// activeWhen: ["/vueapp"]
// });
start({
urlRerouteOnly: true,
});
5.访问http://localhost:9000/ 效果如下,每个微应用都添加了navbar,reactapp和vueapp有两个navbar是因为我们之前手动加过一次。