微前端是一种软件架构,可以将前端应用拆分成一些更小的能够独立开发部署的微型应用。然后将这些微应用进行组合使其成为整体的应用的架构模式。
微前端架构类似于架构,但不同的是,组件不能福利构建呵呵发布,但是微前端中的应用是可以的。
微前端架构与框架无关,每个微应用都可以使用不同的框架。
2.微前端架构带来的价值-
允许增量迁移和增量开发。
迁移是一项非常耗时且艰难的任务,比如有一个管理系统使用 AngularJS 开发维护已经有三年时
间,但是随时间的推移和团队成员的变更,无论从开发成本还是用人需求上,AngularJS 已经不能
满足要求,于是团队想要更新技术栈,想在其他框架中实现新的需求,但是现有项目怎么办?直接
迁移是不可能的,在新的框架中完全重写也不太现实。
使用微前端架构就可以解决问题,在保留原有项目的同时,可以完全使用新的框架开发新的需求,
然后再使用微前端架构将旧的项目和新的项目进行整合。这样既可以使产品得到更好的用户体验,
也可以使团队成员在技术上得到进步,产品开发成本也降到的最低。 -
允许应用中的小块独立发布和构建
在目前的单页应用架构中,使用组件构建用户界面,应用中的每个组件或功能开发完成或者bug修
复完成后,每次都需要对整个产品重新进行构建和发布,任务耗时操作上也比较繁琐。
在使用了微前端架构后,可以将不能的功能模块拆分成独立的应用,此时功能模块就可以单独构建
单独发布了,构建时间也会变得非常快,应用发布后不需要更改其他内容应用就会自动更新,这意
味着你可以进行频繁的构建发布操作了。 -
允许不同的团队选择自己擅长的技术栈
因为微前端架构与框架无关,当一个应用由多个团队进行开发时,每个团队都可以使用自己擅长的技术栈进行开发,也就是它允许适当的让团队决策使用哪种技术,从而使团队协作变得不再僵硬。
微前端的使用场景
- 拆分巨型应用,使应用变得更加可维护
- 兼容历史应用,实现增量开发
- 多个微应用如何进行组合 ?
在微前端架构中,除了存在多个微应用以外,还存在一个容器应用,每个微应用都需要被注册到容器应用中。
微前端中的每个应用在浏览器中都是一个独立的 JavaScript 模块,通过模块化的方式被容器应用启动和运行。
使用模块化的方式运行应用可以防止不同的微应用在同时运行时发生冲突。 - 在微应用中如何实现路由 ?
在微前端架构中,当路由发生变化时,容器应用首先会拦截路由的变化,根据路由匹配微前端应用,当匹配到微应用以后,再启动微应用路由,匹配具体的页面组件。 - 微应用与微应用之间如何实现状态共享 ?
在微应用中可以通过发布订阅模式实现状态共享,比如使用 RxJS。 4. 微应用与微应用之间如何实现框架和库的共享?
通过 import-map 和 webpack 中的 externals 属性。
在微前端架构中,微应用被打包为模块,但浏览器不支持模块化,需要使用 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支持的模式
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 App works
}
src/index.html
systemjs-react
src/index.js
import React from "react" import ReactDOM from "react-dom" import App from "./App.js" ReactDOM.render(5.single-spa概述, document.getElementById("root"))
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
- 查看脚手架信息: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
// workspace/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: ["/"]
})
// start 方法必须在 single spa 的配置文件中调用
// 在调用 start 之前, 应用会被加载, 但不会初始化, 挂载或卸载.
start({
// 是否可以通过 history.pushState() 和 history.replaceState() 更改触发 single-spa 路由 // true 不允许 false 允许
urlRerouteOnly: true
})
index.ejs
文件目录结构:
- 创建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
}
})
}
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
- 修改默认应用代码
// 注意: 参数的传递方式发生了变化, 原来是传递了一个对象, 对象中有三项配置, 现在是传递了三 个参数 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
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
5.指定公共库的访问地址
默认情况下,应用中的 react 和 react-dom 没有被 webpack 打包, single-spa 认为它是公共库,
不应该单独打包。
container/src/index.ejs
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 = () => 发生错误时此处内容将会被渲染
// 创建基于 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 (
{props.name}
Home
About
) }
todos/src/pages/About.js
import React from "react"
const About = () => {
return About works
}
export default About
todos/src/pages/Hone.js
import React from "react"
const About = () => {
return HOME
}
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
- 修改启动命令 && 启动应用
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
解决上面的报错添加amd.min.js库
container/src/index.ejs
<% if (isLocal) { %>
+
<% } else { %>
+
<% } %>
解决上面的报错注释meta:container/src/index.ejs
- 配置路由
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: "Foo" }
const Bar = { template: "Bar" }
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
9.创建Parcel应用foo bar
Parcel 用来创建公共 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 (
@single-spa/welcome{" "}
@study/lagou{" "}
@study/todos{" "}
@study/realworld
)
}
- 在 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 (
+ System.import("@study/navbar")} />
Home
About
)
}
- 在 Vue 应用中使用它
realword/src/App.vue
foo bar
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
- 应用中导出方法
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 Todos home works }
export default Home
- 在 Vue 应用中使用该方法
11.实现跨应用通讯 - 基于发布订阅模式foo bar +
跨应用通信可以使用 RxJS,因为它无关于框架,也就是可以在任何其他框架中使用。
- 在 index.ejs 文件中添加 rxjs 的 import-map
- 在 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])
// 发送广播
- 在 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
- 构建路由
+
// name: 应用名称
+
- 获取路由信息 && 注册应用
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 })



