如何实现微前端
- 多个微应用如何进行组合 ?
在微前端架构中,除了存在多个微应用以外,还存在一个容器应用,每个微应用都需要被注册到容器应用中。
微前端中的每个应用在浏览器中都是一个独立的 JavaScript 模块,通过模块化的方式被容器应用启动和运行。
使用模块化的方式运行应用可以防止不同的微应用在同时运行时发生冲突。 - 在微应用中如何实现路由 ?
在微前端架构中,当路由发生变化时,容器应用首先会拦截路由的变化,根据路由匹配微前端应用,当匹配到微应用以后,再启动微应用路由,匹配具体的页面组件。 - 微应用与微应用之间如何实现状态共享 ?
在微应用中可以通过发布订阅模式实现状态共享,比如使用 RxJS。 - 微应用与微应用之间如何实现框架和库的共享?
通过 import-map 和 webpack 中的 externals 属性
Systemjs 模块化解决方案
在微前端架构中,微应用被打包为模块,但浏览器不支持模块化,需要使用 systemjs 实现浏览器中的模块化。
systemjs 是一个用于实现模块化的 JavaScript 库,有属于自己的模块化规范。
在开发阶段我们可以使用 ES 模块规范,然后使用 webpack 将其转换为 systemjs 支持的模块。
案例
通过 webpack 将 react 应用打包为 systemjs 模块,在通过 systemjs 在浏览器中加载模块
npm install webpack@5.17.0 webpack-cli@4.4.0 webpack-dev-server@3.11.2 html-webpackplugin@4.5.1 @babel/core@7.12.10 @babel/cli@7.12.10 @babel/preset-env@7.12.11 @babel/preset-react@7.12.10 babel-loader@8.2.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"),
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"]
}
<!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>
<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>
<script>
System.import("./index.js")
</script>
</body>
</html>
微前端框架 single-spa
single-spa 概述
single-spa 是一个实现微前端架构的框架。
在 single-spa 框架中有三种类型的微前端应用:
- single-spa-application / parcel:微前端架构中的微应用,可以使用 vue、react、angular
等框架。 - single-spa root config:创建微前端容器应用。
- utility modules:公共模块应用,非渲染组件,用于跨应用共享 javascript 逻辑的微应用。
创建容器应用
-
安装 single-spa 脚手架工具:
npm install create-single-spa@2.0.3 -g
-
创建微前端应用目录
mkdir workspace && cd "$_"
-
创建微前端容器应用 create-single-spa
- 应用文件夹填写 container
- 应用选择 single-spa root config
- 组织名称填写 study
应用名称的命名规则为 @组织名称/应用名称 ,比如 @study/todos
-
启动应用: npm start
-
访问应用: localhost:9000
-
默认代码解析
-
Root-config.js
// workspace/container/src/study-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: ["/"] }) // 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-mapoverrides@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/singlespa.min.js" as="script" /> <!-- JavaScript 模块下载地址 此处可放置微前端项目中的公共模块 --> <script type="systemjs-importmap"> { "imports": { "single-spa": "https://cdn.jsdelivr.net/npm/singlespa@5.8.3/lib/system/single-spa.min.js" } } </script>
-
创建基于 React 的微应用
-
创建应用: create-single-spa
- 应用目录输入 todos
- 框架选择 react
-
修改应用端口 && 启动应用
{ "scripts": { "start": "webpack serve --port 9002", } }
-
注册应用,将 React 项目的入口文件注册到基座应用中.
registerApplication({ name: "@study/todos", app: () => System.import("@study/todos"), activeWhen: ["/todos"] })
-
指定微前端应用模块的引用地址
<!-- 在注册应用时 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>
-
指定公共库的访问地址
默认情况下,应用中的 react 和 react-dom 没有被 webpack 打包, single-spa 认为它是公共库,不应该单独打包。<script type="systemjs-importmap"> { "imports": { "single-spa": "https://cdn.jsdelivr.net/npm/singlespa@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/reactdom.production.min.js", "react-router-dom": "https://cdn.jsdelivr.net/npm/react-routerdom@5.2.0/umd/react-router-dom.min.js" } } </script>
-
微前端 React 应用入口文件代码解析
// 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
-
路由配置
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> ) }
-
修改 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"] }) }
创建 Parcel 应用
Parcel 用来创建公共 UI,涉及到跨框架共享 UI 时需要使用 Parcel。
Parcel 的定义可以使用任何 single-spa 支持的框架,它也是单独的应用,需要单独启动,但是它不关联路由。
Parcel 应用的模块访问地址也需要被添加到 import-map 中,其他微应用通过 System.import 方法进行引用。
需求:创建 navbar parcel,在不同的应用中使用它。
-
使用 React 创建 Parcel 应用 create-single-spa
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
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
externals: ["react-router-dom"]
-
指定端口,启动应用
"scripts": { "start": "webpack serve --port 9004", }
-
在模板文件中指定应用模块地址
{ "imports": { "@study/navbar": "//localhost:9004/study-navbar.js" } }
-
在 React 应用中使用它
import Parcel from "single-spa-react/parcel" <Parcel config={System.import("@study/navbar")} />
-
在 Vue 应用中使用它
<Parcel :config="parcelConfig" :mountParcel="mountParcel" /> <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("@study/navbar"), mountParcel: mountRootParcel } } } </script>
创建 utility modules
用于放置跨应用共享的 JavaScript 逻辑,它也是独立的应用,需要单独构建单独启动。
-
创建应用: create-single-spa
- 文件夹填写 tools
- 应用选择 in-browser utility module (styleguide, api cache, etc)
-
修改端口,启动应用
"scripts": { "start": "webpack serve --port 9005", }
-
应用中导出方法
export function sayHello(who) { console.log(`%c${who} Say Hello`, "color: skyblue") }
-
在模板文件中声明应用模块访问地址
<script type="systemjs-importmap"> { "imports": { "@study/tools": "//localhost:9005/study-tools.js" } } </script>
-
在 React 应用中使用该方法
import React, { useEffect, useState } 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 应用中使用该方法
<h1 @click="handleClick">{{ name }}</h1>
async handleClick() { let toolsModule = await window.System.import("@study/tools") toolsModule.sayHello("realworld") }
实现跨应用通信
跨应用通信可以使用 RxJS,因为它无关于框架,也就是可以在任何其他框架中使用。
-
在 index.ejs 文件中添加 rxjs 的 import-map
{ "imports": { "rxjs":"https://cdn.jsdelivr.net/npm/rxjs@6.6.3/bundles/rxjs.umd.min.js" } }
-
.在 utility modules 中导出一个 ReplaySubject,它可以广播历史消息,就算应用是动态加载进来的,也可以接收到数据。
import { ReplaySubject } from "rxjs" export const sharedSubject = new ReplaySubject()
-
在 React 应用中订阅它
useEffect(() => { let subjection = null if (toolsModule) { subjection = toolsModule.sharedSubject.subscribe(console.log) } return () => subjection.unsubscribe() }, [toolsModule])
-
在 Vue 应用中订阅它
async mounted() { let toolsModule = await window.System.import("@study/tools") toolsModule.sharedSubject.subscribe(console.log) }
Layout Engine
允许使用组件的方式声明顶层路由,并且提供了更加便捷的路由API用来注册应用。
-
下载布局引擎 npm install single-spa-layout@1.3.1
-
构建路由
<template id="single-spa-layout"> <single-spa-router> <application name="@study/navbar""></application> <route default> <application name="@single-spa/welcome"></application> </route> <route path="lagou"> <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>
<script type="systemjs-importmap"> { "imports": { "@single-spa/welcome": "https://unpkg.com/single-spawelcome/dist/single-spa-welcome.js" } } </script>
-
获取路由信息 && 注册应用
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 })