# 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 的应用类型 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0922/100038_c05dde5b_9130428.png "屏幕截图.png") #### 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. 创建容器应用 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0913/111849_9863ede5_9130428.png "屏幕截图.png") 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
React App:
Home
About
} 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 相关文档) ![输入图片说明](https://images.gitee.com/uploads/images/2021/0913/113438_6b0651b1_9130428.png "屏幕截图.png") 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 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0913/113747_35f95cbd_9130428.png "屏幕截图.png") 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.容器:注释内容访问限制语句;否则会报如下错误: ``` ``` ![输入图片说明](https://images.gitee.com/uploads/images/2021/0913/113700_42fe1c04_9130428.png "屏幕截图.png") #### 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 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0913/114047_17954d1a_9130428.png "屏幕截图.png") 2. 微应用:启动命令 ``` "start": "webpack serve --port 9005", ``` 3. 容器:index.ejs中导入打包后的微应用: ```js ``` 4. 容器:无需注册微应用! 5. 微应用:在utils应用中导出公用模块、方法 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0913/114056_b899d3c8_9130428.png "屏幕截图.png") #### 导入共享应用 1. 微应用:在其他微应用中导入公用模块、方法(在微应用中使用System.import()动态导入公共模块);特别注意的是,在集成环境和单独运行都要保证正常。 - 注意:systemJS的导入方法返回一个promise - 注意:如果在typescript中使用systemjs,需要安装插件:npm install --save @types/systemjs ![输入图片说明](https://images.gitee.com/uploads/images/2021/0913/114141_5f97e5de_9130428.png "屏幕截图.png") 一个问题:如上代码,单独启动react微应用时,因为html是webpack生成的;所以也没有特别指定"@myspa/utils"的路径映射关系,因为会报错! 解决方案:可以针对单独的调试环境添加配置项,如下图;具体的配置项参考:https://github.com/single-spa/standalone-single-spa-webpack-plugin#usage ![输入图片说明](https://images.gitee.com/uploads/images/2021/0913/114153_e2e4b38a_9130428.png "屏幕截图.png") 2. 容器:启动容器应用,测试react微应用中调用utils微应用: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0913/114203_9cc8ba55_9130428.png "屏幕截图.png") 3. 微应用:启动微应用,单独运行正常:npm run start:standalone ![输入图片说明](https://images.gitee.com/uploads/images/2021/0913/114212_8d20b03c_9130428.png "屏幕截图.png") ### Single-Spa 跨应用通信 1. **方案一:utility modules** (微应用共享同一个 utility modules 里面的模块或数据) - 需要使用的时候才使用 System.import() 动态加载?不不不,可以在微应用加载的时候就 System.import() 该共享应用,然后再需要的时候使用里面的方法或数据即可! - 配置示例: - (1)在 utility-app 中导出共享模块: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0921/211533_1e6bab78_9130428.png "屏幕截图.png") - (2)在微应用中导入模块并使用: ![输入图片说明](https://images.gitee.com/uploads/images/2021/0921/234147_bb945291_9130428.png "屏幕截图.png") 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. 微应用文件加载流程 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0922/113957_86763172_9130428.png "屏幕截图.png") 2. 容器应用文件加载流程 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0922/114022_bc47f949_9130428.png "屏幕截图.png") 3. 文件加载顺序分析 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0922/114041_2147f20a_9130428.png "屏幕截图.png") ### 模块联邦与 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 } } ``` ### 路由配置 #### 基本配置示例 ![输入图片说明](https://images.gitee.com/uploads/images/2021/0922/114251_9ea26e58_9130428.png "屏幕截图.png") 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 (   
      
) } ```