1. 什么是微前端
微前端是一种软件架构,可以将前端应用拆分成一些更小的能够独立开发部署的微型应用。然后将这些微应用进行组合使其成为整体的应用的架构模式。
微前端架构类似于架构,但不同的是,组件不能独立构建和发布,但是微前端中的应用是可以的。
微前端架构与框架无关,每个微应用都可以使用不同的框架。
2.微前端架构带来的价值
- 允许增量迁移和增量开发。
项目从一个框架迁移到另一个框架是非常花费时间和艰巨的任务。例如:一个使用AngularJS开发的项目,在后续迭代过程中由于人力,或者是主流框架变了,想换成react,或者vue?这个时候怎么办呢?是继续使用AngularJS还是用react/vue呢?直接迁移是不可能的,在新的框架中完全重写也不太现实。使用微前端架构就可以解决问题,在保留原有项目的同时,可以完全使用新的框架开发新的需求,然后再使用微前端架构将旧的项目和新的项目进行整合。这样既可以使产品得到更好的用户体验,也可以使团队成员在技术上得到进步,产品开发成本也降到的最低。
迁移是一项非常耗时且艰难的任务,例如:有一个管理系统使用 AngularJS 开发维护已经有三年时间,但是随时间的推移和团队成员的变更,无论从开发成本还是用人需求上,AngularJS 已经不能满足要求,于是团队想要更新技术栈,AngularJS不再是主流的框架,想换成react,或者vue开发,想在其他框架中实现新的需求,但是现有项目怎么办?直接迁移是不可能的,在新的框架中完全重写也不太现实。使用微前端架构就可以解决问题,在保留原有项目的同时,可以完全使用新的框架开发新的需求,然后再使用微前端架构将旧的项目和新的项目进行整合。这样既可以使产品得到更好的用户体验,也可以使团队成员在技术上得到进步,产品开发成本也降到的最低。
-
允许应用中的小块区域独立发布和构建
当应用足够庞大时,应用的发布和构建就会变的非常耗时,有的应用仅构建就需要花费10多分钟,构建完成,还需要发布,这也需要时间,但是使用了微前端架构之后,一个大的应用,可以被拆分成成一个一个小的应用,可以独立构建,独立发布的应用。哪个部分发生了变化,就独立修改,独立构建发布,而不是将整个应用构建发布。 -
允许不同的团队选择自己擅长的技术栈
因为微前端架构与框架无关,当一个应用由多个团队进行开发时,每个团队都可以使用自己擅长的技术栈进行开发,也就是它允许适当的让团队决策使用哪种技术,从而使团队协作变得不再僵硬。
微前端的使用场景:
- 拆分巨型应用,使应用变得更加可维护
- 兼容历史应用,实现增量开发
3.如何实现微前端
-
多个微应用如何进行组合 ?
实际上微前端架构中的每一个应用,最终都会被打包成模块,在浏览器当中通过加载这些模块,来运行不同的微应用。通过模块化的方式运行不同的微应用,可以方防止微应用之间的js,css的发生冲突。在微前端架构中,除了多个微应用以外,还存在一个容器应用,我们要将每个微应用都被注册到容器应用中。通过容器应用管理微应用的加载,运行以及卸载。 -
如何防止微应用与微应用之间发生路由冲突?
在微前端架构中,当路由发生变化时,容器应用首先会拦截路由的变化,根据路由匹配微应用,当匹配到微应用以后,再启动微应用当中的路由,这样就可以防止微应用与微应用之间路由发生冲突。 -
微应用与微应用之间如何实现状态共享 ?
在微应用中可以通过【发布订阅模式】实现状态共享,例如:使用 RxJS库。 RxJS不仅仅可以在ng里面使用,RxJS可以在任何框架中使用。
通过【发布订阅模式】可以轻松的实现微应用与微应用之间状态共享 。
- 微应用与微应用之间【如何实现框架和库的共享】?
例如:react框架本身,vue框架本身。因为,多个微应用之间很多时候需要使用同一个框架,或者是库。在微前端的架构中我们要使用模块化的方式去加载应用,在最新的模块化规范当中,新增了 import-map 特性,这个特性可以允许我们使用网络模块,而不是一定将模块下载到本地,我们只需要提前配置好模块名字与对应的模块地址就可以了。这样每个微应用在引用公共模块时,都可以引用提前配置好的公共模块。这样就可以解决多个微应用之间框架共享的问题了。当然后,还需要微应用本身的webpack配置,通过webpack中的externals属性告诉webpack在打包应用时,哪些模块是不需要被打包的。
通过 import-map 和 webpack 中的 externals 属性。
4.Systemjs的基本使用
1.Systemjs是什么?
Systemjs是一个动态模块加载器。因为在微前端架构中每一个应用都会被打包成一个模块,我们在浏览器🀄️加载他,运行它,但是到目前为止,浏览器还不支持模块化,或者说支持成都还不够好,这个时候我们就需要用到一个库,通过这个库,让浏览器可以加载动态模块。
在开发阶段我们可以使用 ES 模块规范,难道我们要使用systemjs库了之后,就要使用systemjs规范么?不是这样的,在开发阶段我们就是用es规范就行了。我们的应用通常都会使用webpack打包,在使用webpack打包的时候我们可以告诉webpack,我们要将es模块规范转换为systemjs能够支持的模块规范就可以了。
在微前端架构中,微应用被打包为模块,但浏览器不支持模块化,需要使用 systemjs 实现浏览器中的模块化。
systemjs 是一个用于实现模块化的 JavaScript 库,有属于自己的模块化规范。
在开发阶段我们可以使用 ES 模块规范,然后使用 webpack 将其转换为 systemjs 支持的模块。
案例:
- mkdir system-react
- cd system-react
- npm install
- npm start
package.json
{
"name": "systemjs-react",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"babel-loader": "^8.2.2",
"html-webpack-plugin": "^4.5.1",
"webpack": "^5.17.0",
"webpack-cli": "^4.4.0",
"webpack-dev-server": "^3.11.2"
}
}
webpack.config.js
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "index.js",
path: path.join(__dirname, "build"),
// 打包的的时候转换为system支持的模式 (一定要写,否则包react的错)
libraryTarget: "system"
},
devtool: "source-map",
devServer: {
port: 9000,
contentBase: path.join(__dirname, "build"),
historyApiFallback: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"]
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
inject: false //
})
],
// 哪些模块不需要打包
externals: ["react", "react-dom", "react-router-dom"]
}
src/App.js
import React from "react"
export default function App() {
return <div>App works</div>
}
src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>systemjs-react</title>
<!-- index.js文件中引入的React,react-dom,react-router-dom就是来自这个地方配置的。 -->
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js",
"react-router-dom": "https://cdn.jsdelivr.net/npm/react-router-dom@5.2.0/umd/react-router-dom.min.js"
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/system.min.js"></script>
</head>
<body>
<div id="root"></div>
<!-- System.import:引入模块。当前文件夹下面的index.js -->
<script>
System.import("./index.js")
</script>
</body>
</html>
src/index.js
import React from "react"
import ReactDOM from "react-dom"
import App from "./App.js"
ReactDOM.render(<App />, document.getElementById("root"))
5.single-spa概述
single-spa 是一个实现微前端架构的框架。
在 single-spa 框架中为我们提供了三种类型的微前端应用
:
-
single-spa-application / parcel:
普通的微应用
。 微前端架构中的微应用,可以使用 vue、react、angular 等框
架进行开发。这种微应用是和路由相关联的。例如:当我们访问“/a”地址的时候,加载哪一个微应用,当访问“/b”的时候,加载哪一个微应用。
parcel这种微应用不和路由进行关联。主要用于跨应用共享UI组件。 -
single-spa root config:创建微架构中的
容器应用
。我们通过容器应用管理普通的微应用。 -
utility modules:公共模块应用,
不是渲染UI组件的,他是用于跨应用共享 javascript 逻辑的微应用
。
6. 使用create-single-spa脚手架工具创建容器应用
- 安装 single-spa 脚手架工具: npm install create-single-spa@2.0.3 -g
- 查看脚手架信息:npm info create-single-spa
- 创建微前端应用目录: mkdir workspace && cd “$_”
- 创建微前端容器应用: create-single-spa
1.应用文件夹填写 container
2.应用选择 single-spa root config
3.组织名称填写 study
组织名称可以理解为团队名称,微前端架构允许多团队共同开发应用,组织名称可以标识应用
由哪个团队开发。
应用名称的命名规则为 @组织名称/应用名称 ,比如 @study/todos - 启动应用: npm start
- 访问应用: localhost:9000
- 默认代码解析
Root-config.js
index.ejs: 是模版文件。只有这一个模版文件,其他的应用当中是没有这个文件的。
study-root-config.js : 容器应用的入口文件。
workspace/container/src/study-root-config.js
import { registerApplication, start } from "single-spa"
/*
registerApplication:注册微前端应用
1. name: 字符串类型, 当前注册的微应用名称是什么。
结构如: "@组织名称/应用名称" 。例如:@single-spa/welcome single-spa是组织名称,welcome是应用名称
3. app: 函数类型, 返回的必须是Promise, 通过 systemjs 引用打包好的微前端应用模块代码 (umd),是一个线上地址
4. activeWhen: 路由匹配时激活应用
*/
registerApplication({
name: "@single-spa/welcome",
app: () => System.import( "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js" ),
// 当前这个微应用什么时候激活,例如:当浏览器地址栏中访问“/”时激活
activeWhen: ["/"]
})
// registerApplication({
// name: "@study/navbar",
// app: () => System.import("@study/navbar"),
// activeWhen: ["/"]
// });
// start:启动微前端应用
// start 方法必须在 single spa 的配置文件中调用
// 在调用 start 之前, 应用会被加载, 但不会初始化, 挂载或卸载.
start({
// 是否可以通过 history.pushState() 和 history.replaceState() 更改触发 single-spa 路由 // true 不允许 false 允许
urlRerouteOnly: true
})
index.ejs
<!-- 导入微前端容器应用 -->
<script> System.import("@study/root-config") </script>
<!--
import-map-overrides 可以覆盖导入映射 当前项目中用于配合 single-spa Inspector 调试工具使用. 可以手动覆盖项目中的 JavaScript 模块加载地址, 用于调试.
-->
<import-map-overrides-full show-when-local-storage="devtools" dev-libs> </import-map-overrides-full>
<!-- 模块加载器 -->
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/system.min.js"> </script>
<!-- systemjs 用来解析 AMD 模块的插件 -->
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.0/dist/extras/amd.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>
<!-- 用于支持 Angular 应用 -->
<script src="https://cdn.jsdelivr.net/npm/zone.js@0.10.3/dist/zone.min.js"> </script>
<!-- single-spa 预加载 -->
<link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@5.8.3/lib/system/single- spa.min.js" as="script" />
<!-- JavaScript 模块下载地址 此处可放置微前端项目中的公共模块 -->
<script type="systemjs-importmap">
{ "imports": { "single-spa": "https://cdn.jsdelivr.net/npm/single- spa@5.8.3/lib/system/single-spa.min.js" } }
</script>
文件目录结构:
7.创建不基于框架的微应用
- 创建lagou项目的微应用
mkdir container lagou && cd container lagou
package.json
{
"name": "lagou",
"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"
}
}
webpack.config.js
const singleSpaDefaults = require("webpack-config-single-spa")
const { merge } = require("webpack-merge")
module.exports = () => {
const defaultConfig = singleSpaDefaults({
orgName: "study",
projectName: "lagou"
})
return merge(defaultConfig, {
devServer: {
port: 9001
}
})
}
在应用入口文件中导出微应用所需的生命周期函数,生命周期函数必须返回promise,所以每个函数时用async 修饰返回promise
src/study-lagou.js
let lagouContainer = null
// 启动
export async function bootstrap() {
console.log("应用正在启动")
}
// 挂载
export async function mount() {
console.log("应用正在挂载")
lagouContainer = document.createElement("div")
lagouContainer.id = "lagouContainer"
lagouContainer.innerHTML = "Hello Lagou"
document.body.appendChild(lagouContainer)
}
// 卸载
export async function unmount() {
console.log("应用正在卸载")
document.body.removeChild(lagouContainer)
}
2. 在微前端容器应用中注册lagou微前端应用
container/src/study-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: ["/"]
// }
// )
// 第2种方式书写
registerApplication(
"@single-spa/welcome",
() =>
System.import(
"https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
),
location => location.pathname === "/" // 当访问/lago的时候会出现welcome应用的页面,那么需要设置location
)
// 注册lago应用
+ registerApplication({
+ name: "@study/lagou", // 微应用名
+ app: () => System.import("@study/lagou"),
+ activeWhen: ["/lagou"] // 当前这个微应用在什么时候激活
})
// 启动微应用
start({
// 配置对象
urlRerouteOnly: true
})
- 在模板文件中指定模块访问地址
container/src/index.ejs
<script type="systemjs-importmap">
{
"imports": {
"@study/root-config": "//localhost:9000/study-root-config.js",
+ "@study/lagou": "//localhost:9001/study-lagou.js",
}
}
</script>
- 修改默认应用代码(当访问/lago的时候会出现welcome应用的页面,那么需要设置location)
如下视图:
// 注意: 参数的传递方式发生了变化, 原来是传递了一个对象, 对象中有三项配置, 现在是传递了三 个参数
registerApplication(
"@single-spa/welcome",
() => System.import( "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js" ),
location => location.pathname === "/" // 当访问/lago的时候会出现welcome应用的页面,那么需要设置location
)
启动lagou:npm start
访问:localhost://9000/lago
8.创建基于react框架的微应用
1.基础create-single-spa创建react项目
目录结构:
2.修改应用端口 && 启动应用
todos/package.json
{ "scripts": { "start": "webpack serve --port 9002", } }
3.注册todos应用,将 React 项目的入口文件注册到基座应用中.
container/src/study-root-config.js
registerApplication({
name: "@study/todos",
app: () => System.import("@study/todos"),
activeWhen: ["/todos"]
})
4.指定微前端应用模块的引用地址
container/src/index.ejs
<!--在注册应用时 systemjs 引用了 @study/todos 模块, 所以需要配置该模块的引用地址 -->
<script type="systemjs-importmap">
{
"imports": {
"@study/root-config": "//localhost:9000/study-root-config.js",
"@study/todos": "//localhost:9002/study-todos.js"
}
}</script>
5.指定公共库的访问地址
默认情况下,应用中的 react 和 react-dom 没有被 webpack 打包, single-spa 认为它是公共库,
不应该单独打包。
container/src/index.ejs
<script type="systemjs-importmap">
{ "imports":
{
"single-spa": "https://cdn.jsdelivr.net/npm/single- spa@5.8.3/lib/system/single-spa.min.js",
"react": "https://cdn.jsdelivr.net/npm/react@17.0.1/umd/react.production.min.js", "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.1/umd/react- dom.production.min.js",
"react-router-dom": "https://cdn.jsdelivr.net/npm/react-router- dom@5.2.0/umd/react-router-dom.min.js"
}
}
</script>
6.微前端 React 应用入口文件代码解析
todos/src/study-todos.js
// react、react-dom 的引用是 index.ejs 文件中 import-map 中指定的版本
import React from "react"
import ReactDOM from "react-dom"
// single-spa-react 用于创建使用 React 框架实现的微前端应用
import singleSpaReact from "single-spa-react"
// 用于渲染在页面中的根组件
import rootComponent from "./root.component"
// 指定根组件的渲染位置
const domElementGetter = () => document.getElementById("todosContainer")
// 错误边界函数
const errorBoundary = () => <div>发生错误时此处内容将会被渲染</div>
// 创建基于 React 框架的微前端应用, 返回生命周期函数对象
const lifecycles = singleSpaReact({ React, ReactDOM, rootComponent, domElementGetter, errorBoundary })
// 暴露必要的生命周期函数
export const { bootstrap, mount, unmount } = lifecycles
7.路由配置
todos/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="/todos">
<div>{props.name}</div>
<div>
<Link to="/home">Home</Link>
<Link to="/about">About</Link>
</div>
<Switch>
<Route path="/home"> <Home />
</Route> <Route path="/about"> <About /> </Route>
<Route path="/"> <Redirect to="/home" /> </Route>
</Switch>
</BrowserRouter>
) }
todos/src/pages/About.js
import React from "react"
const About = () => {
return <div>About works</div>
}
export default About
todos/src/pages/Hone.js
import React from "react"
const About = () => {
return <div>HOME</div>
}
export default About
8.修改 webpack 配置
const { merge } = require("webpack-merge")
const singleSpaDefaults = require("webpack-config-single-spa-react")
module.exports = (webpackConfigEnv, argv) => {
const defaultConfig = singleSpaDefaults({
orgName: "study",
projectName: "todos",
webpackConfigEnv, argv
})
return merge(
defaultConfig,
+ { externals: ["react-router-dom"] })
}
9.创建基于Vue框架的微应用
-
创建vue微应用realworld:
创建应用: create-single-spa
1.项目文件夹填写 realworld
2.框架选择 Vue
3.生成 Vue 2 项目 -
提取 vue && vue-router
vue.config.js
module.exports = { chainWebpack: config => { config.externals(["vue", "vue-router"]) } }
container/index.ejs
<script type="systemjs-importmap"> { "imports": { "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js", "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue- router.min.js" } }</script>
- 修改启动命令 && 启动应用
realworld/package.json
"scripts": {
"start": "vue-cli-service serve --port 9003",
},
npm start
- 在containe中注册vue的微应用
container/src/study-root-config.js
registerApplication({
name: "@study/realworld",
app: () => System.import("@study/realworld"),
activeWhen: ["/realworld"]
})
- 指定微前端应用模块的引用地址
container/src/index.ejs
<script type="systemjs-importmap">
{
"imports": {
"@study/root-config": "//localhost:9000/study-root-config.js",
"@study/lagou": "//localhost:9001/study-lagou.js",
"@study/todos": "//localhost:9002/study-todos.js",
+ "@study/realworld": "//localhost:9003/js/app.js",
}
}
</script>
解决上面的报错添加amd.min.js库
container/src/index.ejs
<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.0/dist/extras/amd.min.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.0/dist/extras/amd.min.js"></script>
<% } %>
解决上面的报错注释meta: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';"
/> -->
- 配置路由
realword/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 Foo = { template: "<div>Foo</div>" }
const Bar = { template: "<div>Bar</div>" }
const routes = [
{ path: "/foo", component: Foo },
{ path: "/bar", component: Bar }
]
const router = new VueRouter({ routes, mode: "history", base: "/realworld" })
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#lifecyle-props
name: this.name,
mountParcel: this.mountParcel,
singleSpa: this.singleSpa
}
})
}
}
})
export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmount
realword/App.js
<template>
<div id="app">
<div>
<router-link to="/foo">foo</router-link>
<router-link to="/bar">bar</router-link>
<button @click="handleClick">button</button>
</div>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "App",
components: {
},
}
</script>
<style></style>
9.创建Parcel应用
Parcel 用来创建公共 UI,这个公共UI并不是应用内部的公共UI,指的是跨框架跨应用的。
涉及到跨框架共享 UI 时需要使用Parcel。
Parcel 的定义可以使用任何 single-spa 支持的框架,它也是单独的应用,需要单独启动,但是它不关联
路由。
Parcel 应用的模块访问地址也需要被添加到 import-map 中,其他微应用通过 System.import 方法进行
引用。
需求:创建 navbar parcel,在不同的框架应用中使用它。
1.使用 React 创建 Parcel 应用 create-single-spa
navbar/study-navbar.js
import React from "react"
import ReactDOM from "react-dom"
import singleSpaReact from "single-spa-react"
import Root from "./root.component"
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
navbar/root.component.js
import React from "react"
import { BrowserRouter, Link } from "react-router-dom"
export default function Root(props) {
return (
<BrowserRouter>
<div>
<Link to="/">@single-spa/welcome</Link>{" "}
<Link to="/lagou">@study/lagou</Link>{" "}
<Link to="/todos">@study/todos</Link>{" "}
<Link to="/realworld">@study/realworld</Link>
</div>
</BrowserRouter>
)
}
- 在 webpack 配置文件中去除 react-router-dom
navbar/webpack.config.js
externals: ["react-router-dom"]
- 指定端口,启动应用
navbar/package.json
"scripts": { "start": "webpack serve --port 9004", }
- 在模板文件中指定应用模块地址
container/index.ejs
{ "imports": { "@study/navbar": "//localhost:9004/study-navbar.js" } }
- 在 React 应用中使用它
todos/src/root-component.js
import React from "react"
+ import Parcel from "single-spa-react/parcel"
import {
BrowserRouter,
Route,
Link,
Redirect,
Switch
} from "react-router-dom"
import Home from "./Home"
import About from "./About"
export default function Root(props) {
return (
<BrowserRouter basename="/todos">
+ <Parcel config={System.import("@study/navbar")} />
<div>
<Link to="/home">Home</Link>
<Link to="/about">About</Link>
</div>
<Switch>
<Route path="/home">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/">
<Redirect to="/home" />
</Route>
</Switch>
</BrowserRouter>
)
}
- 在 Vue 应用中使用它
realword/src/App.vue
<template>
<div id="app">
<div>
<Parcel :config="parcelConfig" :mountParcel="mountParcel" />
<router-link to="/foo">foo</router-link>
<router-link to="/bar">bar</router-link>
<button @click="handleClick">button</button>
</div>
<router-view></router-view>
</div>
</template>
<script>
+ import Parcel from "single-spa-vue/dist/esm/parcel"
+ import { mountRootParcel } from "single-spa"
export default {
name: "App",
components: {
+ Parcel
},
data() {
return {
+ parcelConfig: window.System.import("@study/navbar"),
+ mountParcel: mountRootParcel
}
},
methods: {
}
</script>
<style></style>
realword/vue.config.js
module.exports = {
chainWebpack: config => {
+ config.externals(["vue", "vue-router", "single-spa"])
}
}
10. 创建跨框架共享的JavaScript逻辑
用于放置跨应用共享的 JavaScript 逻辑,它也是独立的应用,需要单独构建单独启动。
-
创建tools应用: create-single-spa
1 文件夹填写 tools
2.应用选择 in-browser utility module (styleguide, api cache, etc)
-
修改端口,启动应用
"scripts": {
"start": "webpack serve --port 9005",
}
3.在模板文件中声明应用模块访问地址
container/index.ejs
<script type="systemjs-importmap">
{
"imports": {
"@study/root-config": "//localhost:9000/study-root-config.js",
"@study/lagou": "//localhost:9001/study-lagou.js",
"@study/todos": "//localhost:9002/study-todos.js",
"@study/realworld": "//localhost:9003/js/app.js",
"@study/navbar": "//localhost:9004/study-navbar.js",
+ "@study/tools": "//localhost:9005/study-tools.js",
}
}
</script>
- 应用中导出方法
tools/src/study-tools.js
export function sayHello(who) {
console.log(`%c${who} sayHello`, "color:skyblue")
}
- 在react项目中引用
todos/src/Home.js
import React, { useState, useEffect } from "react"
// 加载公共模块
+ function useToolsModule() {
const [toolsModule, setToolsModule] = useState()
useEffect(() => {
System.import("@study/tools").then(setToolsModule)
}, [])
return toolsModule
}
const Home = () => {
+ const toolsModule = useToolsModule()
+ if (toolsModule) toolsModule.sayHello("todos")
return <div>Todos home works</div> }
export default Home
- 在 Vue 应用中使用该方法
<template>
<div id="app">
<div>
<Parcel :config="parcelConfig" :mountParcel="mountParcel" />
<router-link to="/foo">foo</router-link>
<router-link to="/bar">bar</router-link>
+ <button @click="handleClick">button</button>
</div>
<router-view></router-view>
</div>
</template>
<script>
import Parcel from "single-spa-vue/dist/esm/parcel"
import { mountRootParcel } from "single-spa"
export default {
name: "App",
components: {
Parcel
},
data() {
return {
parcelConfig: window.System.import("@study/navbar"),
mountParcel: mountRootParcel
}
},
methods: {
+ async handleClick() {
const toolsModule = await window.System.import("@study/tools")
toolsModule.sayHello("@study/realworld")
}
},
}
</script>
<style></style>
11.实现跨应用通讯 - 基于发布订阅模式
跨应用通信可以使用 RxJS,因为它无关于框架,也就是可以在任何其他框架中使用。
- 在 index.ejs 文件中添加 rxjs 的 import-map
<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://cdn.jsdelivr.net/npm/react@17.0.1/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@17.0.1/umd/react-dom.production.min.js",
"react-router-dom": "https://cdn.jsdelivr.net/npm/react-router-dom@5.2.0/umd/react-router-dom.min.js",
"vue": "https://cdn.jsdelivr.net/npm/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://cdn.jsdelivr.net/npm/rxjs@6.6.3/bundles/rxjs.umd.min.js"
}
}
</script>
- 在 tools 中导出一个 ReplaySubject,它可以广播历史消息,就算应用是动态加载进来
的,也可以接收到数据。
tools/src/study-tools.js
+ import { ReplaySubject } from "rxjs"
export function sayHello(who) {
console.log(`%c${who} sayHello`, "color:skyblue")
}
+ export const sharedSubject = new ReplaySubject()
- 在 React 应用中订阅它
// 订阅
useEffect(() => { let subjection = null
if (toolsModule) {
subjection = toolsModule.sharedSubject.subscribe(console.log) }
return () => subjection.unsubscribe()
}, [toolsModule])
// 发送广播
<button onClick={() => toolsModule.sharedSubject.next("Hello Hello Hello")}>
button
</button>
- 在 Vue 应用中订阅它
// 订阅
async mounted() {
let toolsModule = await window.System.import("@study/tools") toolsModule.sharedSubject.subscribe(console.log)
}
12.布局引擎的使用方式
允许使用组件的方式声明顶层路由,并且提供了更加便捷的路由API用来注册应用。
- 下载布局引擎 npm install single-spa-layout@1.3.1
- 构建路由
<body>
+ <template id="single-spa-layout">
<single-spa-router>
<application name="@study/navbar""></application>
<route default>
<application name="@single-spa/welcome"></application>
</route>
<!-- 布局引擎的使用方式:path: 当前要访问的地址 -->
<route path="lagou">
// name: 应用名称
<application name="@study/lagou"></application>
</route>
<route path="todos">
<application name="@study/todos"></application>
</route>
<route path="realworld">
<application name="@study/realworld"></application>
</route>
</single-spa-router>
</template>
<main></main>
<script>
System.import("@study/root-config")
</script>
<div id="root"></div>
<import-map-overrides-full
show-when-local-storage="devtools"
dev-libs
></import-map-overrides-full>
</body>
+ <script type="systemjs-importmap"> { "imports": { "@single-spa/welcome": "https://unpkg.com/single-spa- welcome/dist/single-spa-welcome.js" } }</script>
- 获取路由信息 && 注册应用
container/study-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)
start({ urlRerouteOnly: true })