# e-singlespa
**Repository Path**: ymcdhr/e-singlespa
## Basic Information
- **Project Name**: e-singlespa
- **Description**: single-spa、webpack模块联邦等微前端架构
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2021-09-10
- **Last Updated**: 2021-11-15
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
## Single-Spa
### Single-Spa Demos
1. Html
2. Vue2
3. React
### SytemJS 有什么好处?为什么使用它?
1. 支持加载多种规范的文件
- 支持加载 ESModule、CommonJS、AMD、UMD、全局脚本等(加载 AMD、UMD 时,SystemJS 也依赖于 amd.js)
2. 支持按需/动态加载,分析依赖等
- 使用 System.import() 动态加载模块,加载过程中会分析依赖继续加载;
### Single-Spa 微前端的核心价值
1. 独立开发
2. 独立部署
3. 独立的技术栈
4. 无刷新切换页面(SPA方式,路由劫持 + 应用加载)
5. Single-Spa 本身没有实现 js、css隔离(Qiankun 已实现)
### Single-Spa 的应用类型

#### 1. 容器应用
- 首页模板文件:index.ejs
- 配置文件:root.config.js ,负责加载微应用
#### 2. 应用程序/沙箱
- 应用程序 Application,主要用于封装微应用(没有UI界面的)
- 沙箱 Parcel,主要用于封装微应用(共享UI界面)
#### 3. 共享模块
- utility module,主要用于微应用之间通信,共享数据等
### 创建容器应用
1. 全局安装
```
> npm install --global create-single-spa
```
2. 初始化项目
```
> create-single-spa
```
3. 创建容器应用

4. 启动测试
```
> cd container
> npm run start
```
### 创建普通微应用
- 启动微应用独立调试:npm run dev:standalone
- 启动微应用集成调试:npm run dev
1. 微应用:改造入口js:加入single-spa 生命周期方法
- 使用 async/await 语法糖:
```js
// 在此文件中定义应用的single-spa生命周期
// 注册的应用会经过下载(loaded)、初始化(initialized)、被挂载(mounted)、卸载(unmounted)和unloaded(被移除)等过程
// bootstrap, mount, and unmount的实现是必须的,unload则是可选的
let spaContainer = null
export async function bootstrap() {
console.log("应用正在启动")
}
export async function mount() {
console.log("应用正在挂载")
spaContainer = document.createElement("div")
spaContainer.id = "spaContainer"
spaContainer.innerHTML = "Hello sample"
document.body.appendChild(spaContainer)
}
export async function unmount() {
console.log("应用正在卸载")
document.body.removeChild(spaContainer)
}
```
- 使用 Promise 写法:
```js
// 在此文件中定义应用的single-spa生命周期
// 注册的应用会经过下载(loaded)、初始化(initialized)、被挂载(mounted)、卸载(unmounted)和unloaded(被移除)等过程
// bootstrap, mount, and unmount的实现是必须的,unload则是可选的
let spaContainer = null
// 1、启动应用
export function bootstrap(props) {
return Promise
.resolve()
.then(() => {
// One-time initialization code goes here
console.log('bootstrapped!')
});
}
// 2、挂载应用
export function mount(props) {
return Promise
.resolve()
.then(() => {
// Do framework UI rendering here
spaContainer = document.createElement("div")
spaContainer.id = "sampleContainer"
spaContainer.innerHTML = "Hello sample"
document.body.appendChild(spaContainer)
});
}
// 3、卸载应用
export function unmount(props) {
return Promise
.resolve()
.then(() => {
// Do framework UI unrendering here
console.log('unmounted!');
document.body.removeChild(spaContainer)
});
}
```
2. 微应用:改造webpack配置:
- webpack 配置继承 webpack-config-single-spa 插件的配置,该插件源码简单,建议直接参考 node_modules 中的源码。
- devServer 如果使用的时候,这个版本会报错因为没有 client 属性;建议完全覆盖之后再使用,如下代码(此问题升级webpack-dev-server到4.0.0即可)。
- devServer 如果使用的时候,容器加载该服务的 js 文件始终找不到声明周期函数;所以先打包文件到目录,然后使用 serve 插件提供服务调试。(此问题升级webpack-dev-server到4.0.0即可)。
- webpack配置:
```js
const singleSpaDefaults = require("webpack-config-single-spa")
const { merge } = require("webpack-merge")
const path = require("path")
module.exports = (webpackConfigEnv, argv) => {
// 1、在这里配置single-spa
// https://single-spa.js.org/docs/create-single-spa/#webpack-config-single-spa
const defaultConfig = singleSpaDefaults({
orgName: "myspa",
projectName: "sample"
})
// 2、在这里配置原始项目的webpack,打包必须要为"system"方式
const config = merge(defaultConfig, {
// ...
})
// 3、覆盖 webpack-config-single-spa 里面的配置,不然会报错!可能是版本问题。
config.devServer = {
// historyApiFallback: true,
// 配置跨域访问
headers: {
"Access-Control-Allow-Origin": "*",
},
// client: {
// webSocketURL: {
// hostname: "localhost",
// },
// },
allowedHosts: ["localhost"],
contentBase: path.resolve(__dirname,'dist'),
host: "localhost",
port: "9001"
}
return config
}
```
- Npm Scripts启动命令:
```
"scripts": {
"dev":"webpack serve --port 9001",
"dev:standalone":"webpack serve --port 9001 --env standalone"
},
```
3. 容器:主入口加载微应用打包文件:
```html
```
4. 容器:注册微应用:
```js
import { registerApplication, start } from "single-spa";
registerApplication({
name: "@myspa/sample",
app: () => System.import("@myspa/sample"),
activeWhen: "/sample",
});
start({
urlRerouteOnly: true,
});
```
### 创建 React 微应用(既有项目升级)
1. 项目安装
- 微应用:使用single-spa官方工具创建react项目:creat-single-spa
- 微应用:根据提示一步步操作即可,很简单!
2. 项目调试
- 启动微应用独立调试:npm run dev(普通加载方式)如果微应用中使用了其它的公共微应用的API,可能会使用SystemJS,这种方式就不合适了
- 启动微应用独立调试:npm run start:standalone(systemjs加载方式)
- 启动微应用集成调试:npm run start
#### 1. 将 React 微应用集成到 Single-spa 容器
1、 微应用:安装两插件:
```
single-spa-react
webpack-config-single-spa-react 或者 webpack-config-single-spa-react-ts
```
2、微应用:添加react入口生命周期函数:
```
import React from "react"
import ReactDOM from "react-dom"
import singleSpaReact from "single-spa-react"
import App from "./App"
// ReactDOM.render(
// ,
// document.getElementById("root")
// )
// 1、使用single-spa的声明周期方法替换原生react入口
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
errorBoundary (err: any, info: any, props: any) {
// Customize the root error boundary for your microfrontend here.
return
页面发生了错误!{err}
},
// 如果不使用模板生成index.html,则无需设置root节点
// single-spa会自动创建一个div节点:single-spa-application:@myspa/react
// domElementGetter: function () :any {
// return document.getElementById("root")
// }
})
// 2、导出生命周期
console.log("lifecycles:", lifecycles)
export const bootstrap = lifecycles.bootstrap
export const mount = lifecycles.mount
export const unmount = lifecycles.unmount
```
3、微应用:添加webpack相关配置:
- 安装插件:webpack-config-single-spa-react,该插件源码简单可直接查看;其中最主要的是将react、react-dom剔除打包文件;
- 安装插件:webpack-config-single-spa-react-ts,如果使用了ts就需要安装此插件;
```js
if (!webpackConfigEnv.standalone) {
config.externals.push("react", "react-dom");
}
```
- webpack配置:插件中默认了很多配置,无需大改
- (1) webpack 配置继承 webpack-config-single-spa-react-ts 插件的配置,该插件源码简单,建议直接参考 node_modules 中的源码。
- (2)devServer 如果使用的时候,老版本会报错因为没有 client 属性(升级webpack-dev-server到4.0.0即可)。
```
// webpack.single-spa.js
const singleSpaDefaults = require("webpack-config-single-spa-react-ts");
const { merge } = require("webpack-merge")
module.exports = (webpackConfigEnv, argv) => {
// 1、在这里配置single-spa
// https://single-spa.js.org/docs/create-single-spa/#webpack-config-single-spa-react
const defaultConfig = singleSpaDefaults({
orgName: "myspa",
projectName: "react",
webpackConfigEnv,
argv,
// disableHtmlGeneration: true // 是否生成html文件
})
// 2、在这里配置原始项目的webpack
let config = merge(defaultConfig, {
// ...
})
// 单独启动调试微应用时不要externals
// react、react-dom 已经在配置 singleSpaDefaults 中去掉了
if (!webpackConfigEnv.standalone) {
config.externals.push("react-router-dom")
}
return config
}
```
- npm scripts 启动应用调试:
```
"start": "webpack serve --port 9002 --config webpack.single-spa.js",
```
4、容器:指定微应用路径:
```
```
5、容器:引入微应用公共资源:
```
```
6、容器:添加微应用使用的DOM,添加到index.ejs模板的body中
```
```
7、容器:注册微应用到容器:
```js
registerApplication({
name: "@myspa/react",
app: () => System.import("@myspa/react"),
activeWhen: "/react"
});
```
#### 2. 单独开发/调试 React 微应用
- 注意:
- react示例没有使用single-spa工具生成,而是自定义了webpack配置;
- react示例单独启动调试时可以使用systemjs,也可以不用systemjs;
- (1)如果单独调试时使用systemjs,那么需要single-spa工具来创建react应用更简便。
- (2)如果不用systemjs,需要自己单独配置一个入口html和js。
**方案一:不使用systemjs**
1. 使用不同的入口js区分是single-spa微应用,还是本地单独开发调试的应用
```
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
ReactDOM.render(
,
document.getElementById("root")
)
```
2. 使用不同的webpack打包配置,以下是单独开发调试的webpack配置
```
// webpack.dev.js
const path = require("path");
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const config = {
mode: "development",
entry: "./src/index.tsx",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
devtool: "source-map",
plugins: [
new HtmlWebpackPlugin({
title: 'index首页',
// minify: {
// removeAttributeQuotes: true // 移除属性的引号
// },
inject: true, // script是否至于body底部
template: './public/entry.html',
filename: 'index.html',
cache: false,
// chunks: ['index'], // 指定加载js文件,默认全部加载
// showErrors: true, // 如果 webpack 编译出现错误,webpack会将错误信息包裹在一个 pre 标签内,属性的默认值为 true ,也就是显示错误信息。
})
],
devServer: {
host: "localhost",
port: "8002",
open: true,
contentBase: path.resolve(__dirname, "dist")
}
};
module.exports = merge(baseConfig, config);
```
3. npm scripts 配置不同,以下是单独开发调试的启动命令
```
"dev": "webpack serve --config webpack.dev.js",
```
**方案二:使用systemjs,** 和集成是的配置基本一样;只是用standalone来区分是否要externals
1. npm scripts
```
"start:standalone": "webpack serve --port 9002 --config webpack.single-spa.js --env standalone",
```
2. webpack配置:
```
// webpack.single-spa.js
const singleSpaDefaults = require("webpack-config-single-spa-react-ts");
const { merge } = require("webpack-merge")
module.exports = (webpackConfigEnv, argv) => {
// 1、在这里配置single-spa
// https://single-spa.js.org/docs/create-single-spa/#webpack-config-single-spa-react
const defaultConfig = singleSpaDefaults({
orgName: "myspa",
projectName: "react",
webpackConfigEnv,
argv,
// disableHtmlGeneration: true // 是否生成html文件
})
// 2、在这里配置原始项目的webpack
let config = merge(defaultConfig, {
// ...
})
// 单独启动调试微应用时不要externals
// react、react-dom 已经在配置 singleSpaDefaults 中去掉了
if (!webpackConfigEnv.standalone) {
config.externals.push("react-router-dom")
}
return config
}
```
#### 3. 集成 React 微应用路由到容器
1. 微应用:App.tsx 入口APP中引入路由组件
```js
import React from "react"
import {
BrowserRouter,
Route,
Redirect,
Link,
Switch
} from "react-router-dom"
import Home from "./pages/Home"
import About from "./pages/About"
const App = () => {
return
}
export default App
```
2. 微应用:webpack 配置排除 react-router-dom
```js
externals: ["react-router-dom"]
```
3. 容器:引入 react-router-dom
```js
```
4. 容器:测试访问:
- http://localhost:9000/react
- http://localhost:9000/react/home
- http://localhost:9000/react/about
#### 4. React 微应用中组件的动态导入
1. 最新的Webpack动态导入的分片名称已经不按照id来命名了,不再存在以前的命名冲突问题!(可配置,参考 Webpack5 相关文档)

2. 示例代码:注意模块的加载方式,export default 需要解构出 default才是组件或者模块
```
import React,{ useState, useEffect, Component } from 'react'
// 动态导入组件:
// 如果导入的组件是esm的export default导出的,需要解构其default值
function Lazy () {
const [lazyComponent, setLazyComponent] = useState()
useEffect(() => {
import("./Lazy").then(({ default: Component }) => {
setLazyComponent(Component)
})
}, [])
return lazyComponent
}
function Home () {
return (
Home
{Lazy && }
)
}
export default Home
```
3. React.lazy() 可以让动态导入看起来像普通的组件导入,但是它必须和Suspense 组件配合使用
```
import React,{ useState, useEffect, Suspense } from 'react'
// React.lazy() 可以让动态导入看起来像普通的组件导入,但是它必须和Suspense 组件配合使用
const Lazy = React.lazy(() => import("./Lazy"))
function Home () {
return (
Home
Loading...
}>
{Lazy && }
)
}
export default Home
```
### 创建 Vue 微应用(使用工具直接创建)
- 启动微应用独立调试:npm run serve:standalone(systemjs加载方式)
- 启动微应用集成调试:npm run serve
#### 1. 将 Vue 微应用集成到 Single-spa容器
1. 微应用:使用single-spa官方工具创建vue项目:creat-single-spa

2. 微应用:webpack配置项中排除vue、vue-router,新增vue.config.js文件,添加如下配置:
```
// vue.config.js
module.exports = {
chainWebpack: config => {
// 获取参数不为:standalone
if(process.VUE_CLI_SERVICE.mode !== "standalone"){
config.externals(["vue","vue-router"])
}
}
}
```
3.容器:index.ejs中添加对vue、vue-router的导入;注意:该vue.js文件是amd方式打包,需要需要引用amd.js;umd方式打包的文件不需要amd.js!
```
```
4.微应用:添加微应用启动命令
```
"scripts": {
"serve": "vue-cli-service serve --port 9003",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"serve:standalone": "vue-cli-service serve --mode standalone"
},
```
5.容器:集成微应用到容器中来,在index.ejs中添加打包好的app.js文件;
```
```
6.容器:注册微应用到容器中来:
```
registerApplication({
name: "@myspa/vue",
app: () => System.import("@myspa/vue"),
activeWhen: "/vue"
});
```
7.容器:注释内容访问限制语句;否则会报如下错误:
```
```

#### 2. 单独开发/调试 Vue 微应用
1. 执行启动命令:npm run serve:standalone
2. webpack 中进行判断,区分集成启动和单独启动:这个vue示例和react示例不同,react是自定义的配置,没有使用systemjs;这个vue独立启动和部署用了systemjs,可以对比一下!
```js
// vue.config.js
module.exports = {
chainWebpack: config => {
// 获取参数不为:standalone
if(process.VUE_CLI_SERVICE.mode !== "standalone"){
config.externals(["vue","vue-router"])
}
}
}
```
#### 3. 集成微应用路由到容器
### parcel(共享UI模块应用)
1. parcel应用的作用:创建一个公用的UI组件应用;例如有两个react微应用用到了同一个UI组件,那么可以创建一个parcel应用。
### utility modules(共享js模块应用)
1. 公共使用的库 通常都直接放到了 systemjs-importmap 中;
2. 公共使用的自定义方法,数据,模块等通常放到 utility modules 中,当成一个微应用最终也放到 systemjs-importmap 中。
#### 创建共享应用
1. 微应用:执行命令:create-single-spa

2. 微应用:启动命令
```
"start": "webpack serve --port 9005",
```
3. 容器:index.ejs中导入打包后的微应用:
```js
```
4. 容器:无需注册微应用!
5. 微应用:在utils应用中导出公用模块、方法

#### 导入共享应用
1. 微应用:在其他微应用中导入公用模块、方法(在微应用中使用System.import()动态导入公共模块);特别注意的是,在集成环境和单独运行都要保证正常。
- 注意:systemJS的导入方法返回一个promise
- 注意:如果在typescript中使用systemjs,需要安装插件:npm install --save @types/systemjs

一个问题:如上代码,单独启动react微应用时,因为html是webpack生成的;所以也没有特别指定"@myspa/utils"的路径映射关系,因为会报错!
解决方案:可以针对单独的调试环境添加配置项,如下图;具体的配置项参考:https://github.com/single-spa/standalone-single-spa-webpack-plugin#usage

2. 容器:启动容器应用,测试react微应用中调用utils微应用:

3. 微应用:启动微应用,单独运行正常:npm run start:standalone

### Single-Spa 跨应用通信
1. **方案一:utility modules** (微应用共享同一个 utility modules 里面的模块或数据)
- 需要使用的时候才使用 System.import() 动态加载?不不不,可以在微应用加载的时候就 System.import() 该共享应用,然后再需要的时候使用里面的方法或数据即可!
- 配置示例:
- (1)在 utility-app 中导出共享模块:

- (2)在微应用中导入模块并使用:

2. **方案二:rxjs + utility modules** (微应用之间通信)
- rxjs:是一个使用发布订阅模式实现的状态管理器库:https://cn.rx.js.org/manual/overview.html#h12
- rxjs:这种方案支持:
- (1)单个微应用切换;(存在历史广播,比如微应用发布广播后切换到另一个订阅者微应用中,它也会触发回调)
- (2)多个微应用同时打开时数据共享;(一个微应用订阅,另一个微应用发布广播)
- 配置示例:
```
//(1)在 index.ejs 模板文件中添加 rxjs
// systemjs-importmap
"rxjs": "https://cdn.jsdelivr.net/npm/rxjs@6.6.3/bundles/rxjs.umd.min.js"
//(2)在微应用 utils-app 中创建广播
// 利用 rxjs 跨应用共享数据
import { ReplaySubject } from "rxjs"
export const sharedRxjsState = new ReplaySubject()
// (3) react/vue 微应用中订阅广播,触发时会执行订阅的方法:console.log;
useEffect(() => {
let subjection: any = null
// @myspa/utils => //localhost:9005/myspa-utils-app.js
System.import("@myspa/utils").then(utilsApp => {
if(utilsApp){
// 1、utilsApp 共享数据和方法
console.log("共享数据 commonApi:", utilsApp.commonApi)
// 2、utilsApp + rxjs 微应用之间通信:react 中添加订阅,使用发布订阅(这里是自己订阅了自己)
subjection = utilsApp.sharedRxjsState.subscribe(console.log)
}
}, err => console.log(err))
// 取消订阅
return () => subjection.unsubscribe()
}, [])
//(4)react/vue 其它微应用中发布广播,所有订阅者都会接收;(而且会有历史广播,比如微应用发布广播后切换到另一个订阅者微应用中,它也会触发广播)
{/* 触发所有订阅该方法的微应用执行 console.log */}
```
3. **方案三:自定义状态 + utility modules** (微应用之间通信) qiankun 是怎么做的?
- **考虑场景1:** 单个微应用切换的时候,切换前后微应用的通信?共享同一个 utils-app 中的状态就行,切换到另一个微应用时使用共享数据;
- **考虑场景2:** 多个微应用同时存在一个SPA中,微应用之间如何通信?共享同一个 utils-app 中的状态 + 发布订阅,一个微应用发布后,通知另一个订阅的微应用更新状态。
- (1)这个和rxjs有点儿不同的是: **rxjs历史订阅时怎么做到的?**
- (2)自定义的只能多个微应用同时打开是才触发更新(必须是先订阅再发布),**rxjs则是触发后切换到另一个应用也更新?即订阅的时候已经发布过了!!**
```js
// utilsApp 中的数据可以被微应用修改
// 3、自定义 State 共享数据:
export let stateList = {
myState: {
name: "名称",
age: 18
}
}
// react 微应用中
// 3、自定义state:可以修改
console.log("共享数据自定义 State:", utilsApp.stateList)
utilsApp.stateList.myState.age = 20
```
4. **方案四:通过url通信、LocalStorage、Cookie等本地缓存**
5. 应用场景:
- 1. 单点登录?记录登录状态?
- 2. ...
### Layout 布局引擎
1. Layout 引擎的基本使用:
- 允许使用组件的方式声明顶层路由,并且提供了更加便捷的路由API用来注册应用(内部实际上还是调用了 registerApplication,只是不需要一个个去注册微应用)。
- 如果所有微应用需要一个统一的入口页面,可以使用布局引擎;如下:模板文件统一使用 template 定义路由。
```html
// index.ejs 模板文件中
```
- 配置文件,替换单个微应用注册
```js
// myspa-root.config.js
import { constructApplications, constructRoutes } from "single-spa-layout"
// 1、Layout 引擎配置:
// 获取路由配置对象:获取 template 里面的路由
const routes = constructRoutes(document.querySelector("#single-spa-layout"))
// 获取路由信息数组
const applications = constructApplications({
routes,
loadApp({ name }) {
return System.import(name)
}
})
// 遍历路由信息注册应用
applications.forEach(registerApplication)
```
2. Layout 引擎是否能和乾坤HTML Entry一样,加载不同微应用的 HTML 组件?【遗留问题:貌似不行】
3. Layout 引擎如果不能单独加载 html 文件, **那如果微应用需要资源预加载 prefetch 怎么办** ?全都塞到同一个index.ejs?应该不合理哟!【遗留问题】
### Single-Spa + SRR 如果破?
---
## Webpack 模块联邦
- [参考文档](https://gitee.com/ymcdhr/e-code/tree/master/07-01-%E5%BE%AE%E5%89%8D%E7%AB%AF/note)
- [示例源码](https://gitee.com/ymcdhr/e-code/tree/master/07-01-%E5%BE%AE%E5%89%8D%E7%AB%AF/code/module-federations)
### Webpack 模块联邦文件加载流程
1. 微应用文件加载流程

2. 容器应用文件加载流程

3. 文件加载顺序分析

### 模块联邦与 Single-Spa 的差异
#### **加载方式**
1. **模块联邦**
- 容器应用在 bootstrap.js 中调用微应用中的 mount 方法来加载微应用(灵活度比 Single-Spa 更高)
```js
(1)webpack 配置 ModuleFederationPlugin 之后,打包到 bundle.js,容器应用才能通过 mount 找到微应用)
(2)容器应用可以是一个 React,然后使用这个 React + history 库路由来加载微应用,根据路由匹配调用 mount 即可加载微应用;
```
- 容器应用也可以使用 lazy + import() 动态按需加载,可以结合路由动态加载;
- 所有微应用共用一个 index.html(需要预埋微应用用到的div)
2. **Single-Spa**
- 容器应用就是一个 Single-Spa 入口,System.import("root.config.js") 首先加载注册应用的配置文件;
- 容器应用利用 SystemJS + 框架自己的路由,动态按需加载微应用(匹配到路由就加载),微应用有生命周期函数;
- 所有微应用共用一个 index.html(需要预埋微应用用到的div)
#### **集成规范**
1. **模块联邦**
- 利用 webpack 将微应用打包到 bunlde,默认为自执行函数(可配置,微应用通常是es module开发,打包为自执行函数的形式)
2. **Single-Spa**
- 利用 SystemJS,容器应用必须是 SystemJS,容器应用可以加载不同规范的微应用;
#### **路由配置**
1. **模块联邦**
- 容器路由使用 BrowserHistory
- 微应用路由使用 MemoeryHistory,为了避免和容器路有冲突,(使用库history,且需要同步为应用和容器路由)
2. **Single-Spa**
- 容器应用使用框架的路由 + SystemJS 来加载微应用(框架重写了history、hash路由方式,所以不会跟微应用路由冲突?)
- 微应用可以单独使用其自身的子路由(集成和独立实用都用自身的路由);
#### **共享模块**
1. **模块联邦**
- 在 webpack 配置 shared 打包 package.json 中的共享模块或者指定模块;
- 共享模块统一打包到 bundle 或者使用 import() 动态加载。
2. **Single-Spa**
- 定义到 systemjs-importmap 中(utility-module 算是自定义的共享模块)
- 微应用用到共享模块就在加载微应用时,同时 System.import() 共享模块
#### **跨应用通信**
1. **模块联邦**
- 可以考虑自定义一个共享模块:发布订阅模式 + 自定义State(稍微麻烦一点)
2. **Single-Spa**
- 发布订阅模式 + 共享模块/utility-module(rxjs + utility-module 也行)
#### **JS隔离、CSS隔离**
1. **模块联邦**
- 目前并没有很好的方案
2. **Single-Spa**
- 目前并没有很好的方案,css 可以用 css in js 方案,qiankun 有解决方案(见qiankun)
#### 模块联邦与 Single-Spa 如何配合使用?
### 基本使用
#### 容器应用
1. **容器应用初始化**
- 创建应用结构
- 创建 index.html(root为微应用需要用到的id)
- 修改容器入口:App.js
- 修改容器启动文件:bootstrap.js => 加载微应用的 mount 方法,挂载微应用
- 配置 webpack
2. **容器应用加载微应用**
- 微应用中配置 Webpack ModuleFederation
```js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin")
// 放到 plugins 中:
new ModuleFederationPlugin({
name: "marketing",
filename: "remoteEntry.js",
exposes: {
"./MarketingApp": "./src/bootstrap"
}
}
```
- 容器应用中配置 Webpack ModuleFederation
```js
const ModuleFederationPlugin =
require("webpack/lib/container/ModuleFederationPlugin")
new ModuleFederationPlugin({
name: "container",
remotes: {
marketing: "marketing@http://localhost:8081/remoteEntry.js"
}
})
```
3. 容器应用中创建挂载微应用的组件,在容器src/components/XxxApp.js
```js
// Container/components/MarketingApp.js
import React, { useRef, useEffect } from "react"
import { mount } from "marketing/MarketingApp"
export default function MarketingApp() {
const ref = useRef()
useEffect(() => {
mount(ref.current)
}, [])
return
}
```
4. 容器应用中,入口 App.js 导入微应用组件
```js
// Container/App.js
import React from "react"
import MarketingApp from "./components/MarketingApp"
export default function App() {
return
}
```
#### 微应用(React 为例)
1. **微应用初始化**
- 创建应用结构
- 创建 index.html(root为单独部署的id,和容器应用保持一致)
- 配置 webpack
- 添加 npm scripts 启动命令:"start": "webpack serve"
2. **微应用入口创建挂载方法**
- 在微应用入口文件,创建挂载方法(类似于 Single-Spa 的生命周期函数)
```js
// bootstrap.js
import React from "react"
import ReactDOM from "react-dom"
function mount(el) {
ReactDOM.render(Marketing works
, el)
}
if (process.env.NODE_ENV === "development") {
const el = document.querySelector("#dev-marketing")
if (el) mount(el)
}
export { mount }
```
3. 微应用创建路由
### 共享模块
#### 有哪些共享库
1. 例如:dependencies 下的这些文件都需要共享
```js
"dependencies": {
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router-dom": "^5.2.0"
}
```
2. 可以通过查询 package.json 的文件名来进行共享
#### 基础配置
1. 容器应用中配置 webpack 共享
2. 微应用中也要配置 webpack 共享
```js
const packageJson = require("./package.json")
new ModuleFederationPlugin({
shared: packageJson.dependencies // 也可以写数组:["faker"]
})
```
#### 版本冲突
1. 添加以下配置会使用高本版的依赖,同时在低版本的控制台进行提示
```
shared: {
faker: {
singleton: true
}
}
```
### 路由配置
#### 基本配置示例

1. 容器应用需要使用 BrowserHistory 路由
2. 微应用需要使用内存中的 MemoeryHistory 路由
- 为防止容器应用和微应用同时操作 url 而产生冲突,在微前端架构中,只允许容器应用更新 url;微应用不允许更新 url,MemoryHistory 是基于内存的路由,不会改变浏览器地址栏中的 url。
- 如果不同的技术栈的微应用需要传达有关路由的相关信息,应该尽可能的使用通用的方式;MemoryHistory 在 React 和 Vue 中都有提供。
#### 容器路由,切换微应用
1. 容器应用的路由配置
```js
// Container/App.js
import { Router, Route, Switch } from "react-router-dom"
import { createBrowserHistory } from "history"
const history = createBrowserHistory()
export default function App() {
return (
)
}
```
#### 微应用路由,切换微应用中的组件
1. 微应用的路由配置:在入口文件创建 MemoryHistory
```js
// Marketing/bootstrap.js
import { createMemoryHistory } from "history"
function mount(el) {
const history = createMemoryHistory()
ReactDOM.render(, el)
}
```
2. 微应用的路由规则
```js
// Marketing/app.js
import { Router, Route, Switch } from "react-router-dom"
export default function App({ history }) {
return (
)
}
```
#### 微应用的路由切换通知容器应用更新地址栏
1. 微应用的路由 MemoryHistory 是基于内存的(不会切换地址栏),需要在切换时通知容器更新路由地址
2. 微应用路由同步示例
- 容器应用调用微应用传递的 mount 方法,再传递 onNavigate 方法给微应用;利用自身的 history 更新路由:
```js
// Container/components/MarketingApp.js
import { useHistory } from "react-router-dom"
const history = useHistory()
mount(ref.current, {
onNavigate({ pathname: nextPathname }) {
const { pathname } = history.location
if (pathname !== nextPathname) {
history.push(nextPathname)
}
}
})
```
- 微应用中需要在 mount 方法监听路由切换时,传递路由地址:
```js
// Marketing/bootstrap.js
function mount(el, { onNavigate }) {
if (onNavigate) history.listen(onNavigate)
}
```
#### 容器应用路由切换通知微应用切换组件
1. 容器应用路由切换时只会匹配到微应用,但微应用路由不会响应切换组件;
2. 容器应用路由同步示例
- 微应用 mount 方法中返回一个配置对象,其中有一个方法;拿到容器的地址,使用微应用的 history 进行更新
```js
// Marketing/bootstrap.js
function mount(el, { onNavigate }) {
return {
onParentNavigate({ pathname: nextPathname }) {
const { pathname } = history.location
if (pathname !== nextPathname) {
history.push(nextPathname)
}
}
}
}
```
- 容器应用监听路由切换时,传递路由地址
// Container/components/MarketingApp.js
const { onParentNavigate } = mount()
if (onParentNavigate) {
history.listen(onParentNavigate)
}
#### 独立调试的微应用路由配置
1. 独立调试时,mount 方法需要和容器应用调用时保持一致,保持不报错!
2. 独立调试时,微应用的路由应该使用 BrowserHistory ,需要兼容一下(可以使用默认值);
```js
// 如果当前为本地开发环境,路由依然使用 BrowserHistory,所以在调用 mount 方法时传递
defaultHistory 以做区分。
// Marketing/bootstrap.js
if (process.env.NODE_ENV === "development") {
if (el) mount(el, { defaultHistory: createBrowserHistory() })
}
// 在 mount 方法内部判断 defaultHistory 是否存在,如果存在就用 defaultHistory,否则就用
MemoryHistory。
// Marketing/bootstrap.js
function mount(el, { onNavigate, defaultHistory }) {
const history = defaultHistory || createMemoryHistory()
}
```
### 微应用的懒加载
1. 默认情况:所有的微应用都在初始访问时被加载,这样导致初始访问的时间加长;
2. 配置示例;
- 在容器应用中使用懒加载,配置在加载微应用的 app.js 中:
```js
// Container/app.js
import React, { lazy, Suspense } from "react"
import Progress from "./components/Progress"
const MarketingApp = lazy(() => import("./components/MarketingApp"))
function App () {
return (
}>
)
}
```
- 加载进度条组件
```js
import React from "react"
import { makeStyles } from "@material-ui/core/styles"
import LinearProgress from "@material-ui/core/LinearProgress"
const useStyles = makeStyles(theme => ({
root: {
width: "100%",
"& > * + *": {
marginTop: theme.spacing(2)
}
}
}))
export default function Progress() {
const classes = useStyles()
return (
)
}
```