背景
4 年前前公司的 CTO 和我说,要不像后端一样用微前端方案做?我当时一头雾水,现在有微前端?用 iframe 不就行了!
当我入职蚂蚁半年后,团队同学推出了微前端技术 qiankun。
微前端目前已经很成熟了,就不再累述,有不了解的同学先自行查阅。
目的
-
现有项目快速集成微前端
-
微前端架构 single-spa 和 qiankun 的优劣
-
了解微前端的实现原理,手动实现简版微前端
开干
上篇我们先通过示例项目入手,了解一下微前端的大致接入过程。 我遇到的大部分微前端场景都是技术栈相同,但开发团队不同,所以我们用最朴素的 React 作为示例,其它技术栈类似,如果有需求,后面会补充。示例代码已上层 GitHub github.com/lxfu1/react… ,强烈建议先 clone 下来对照看。
创建子项目(react-app1)
既然是 React 项目,那我们选用 Create React App(下文简称CRA) 脚手架初始化项目。
step1: 创建目录 creat-spa-app 并进入
mkdir creat-spa-app
cd ./creat-sp-app
step2: 初始化项目,选用 typescript 模板
npx create-react-app react-app1 --template typescript
step3: eject 配置项
CRA 默认情况下是不展示打包相关配置的,但由于我们后续需要修改,所以需要手动释放出来,进入 react-app1 目录执行 npm run eject 即可。
step4: 启动项目,执行 npm start 即可启动项目,默认 3000 端口
step5: 魔改项目,项目正常会有路由跳转等,我们简单修改一下代码,新增路由react1 和 react1/about
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
import "./App.css";
function Home() {
return <span>Home1</span>;
}
const About = () => {
return <span>About1</span>;
};
function App() {
return (
<Router>
<div className="container">
<div className="menu">
<Link to="/react1">Home</Link>
<Link to="/react1/about">About</Link>
</div>
<Routes>
<Route path="/react1/about" element={<About />}></Route>
<Route path="/react1" element={<Home />}></Route>
</Routes>
</div>
</Router>
);
}
export default App;
step6: 魔改,虽然子应用已经可以正常启动开发,但由于需要集成到主应用,所以我们需要做如下几个修改点
- 新增启动方式
start:micro用于设置环境变量
"scripts": {
"start:micro": "cross-env PORT=3000 MICRO=true node scripts/start.js",
"start": "cross-env PORT=3000 node scripts/start.js",
},
- micro 模式下不用在浏览器打开子应用,显得多余
if (!process.env.MICRO) {
openBrowser(urls.localUrlForBrowser);
}
3.修改打包配置
a. 因为需要固定的输出路径,我们去掉 output.filename 的 hash 模式
b. 由于需要结合 systemjs 使用,output.libraryTarget 需要修改为 system
c. external 掉和主包重复的一些包
- 入口文件做区别,新增微前端配置
singleSpaReact,后续会手动实现 singleSpaReact。
import React from "react";
import ReactDOM from "react-dom";
import ReactDOMCilent from "react-dom/client";
import RootComponent from "./App";
import singleSpaReact from "single-spa-react";
// @ts-ignore
// 正常开发模式
if (!window.System) {
const root = ReactDOMCilent.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<RootComponent />
</React.StrictMode>
);
}
// 微前端模式
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: RootComponent,
errorBoundary() {
// https://reactjs.org/docs/error-boundaries.html
return <div>This renders when a catastrophic error occurs</div>;
},
});
export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;
创建子项目(react-app2)
以同样的步骤创建 react-app2,并新增路由react2 和 react2/about,由于 3000 端口已经被占用,我们需要调整一下端口,用 3001。
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3001;
页面示例:
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
function Home() {
return <span>Home2</span>;
}
const About = () => {
return <span>About2</span>;
};
function App() {
return (
<Router>
<div className="container">
<div className="menu">
<Link to="/react2">Home</Link>
<Link to="/react2/about">About</Link>
</div>
<Routes>
<Route path="/react2/about" element={<About />}></Route>
<Route path="/react2" element={<Home />}></Route>
</Routes>
</div>
</Router>
);
}
export default App;
启动:
创建主应用(root-config)
进入主应用前,建议大家先去简单看下 sigle-spa 的用法
我们直接使用官网提供的 示例 进行修改即可,重点关注下 <script type="systemjs-importmap"> 内的代码,这里结合 System 进行子应用的注册。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Single React SPA</title>
<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';">
<meta name="importmap-type" content="systemjs-importmap" />
<link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@latest/lib/system/single-spa.min.js" as="script">
<script type="systemjs-importmap">
{
"imports": {
"@org/root-config": "http://localhost:9000/org-root-config.js",
"@ant/react-app1": "http://localhost:3000/ant-react-app.js",
"@ant/react-app2": "http://localhost:3001/ant-react-app.js"
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
<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.3/dist/extras/amd.min.js"></script>
</head>
<body>
<div class="navbar">
<a onclick="singleSpaNavigate('/react1')"> React app1</a>
<a onclick="singleSpaNavigate('/react2')"> React app2</a>
</div>
<script>
System.import('@org/root-config');
</script>
<import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>
主要作用有两点:
-
启动主应用:简单的 webpack-server
-
注册子应用并监听路由变化
import { registerApplication, start } from "single-spa";
registerApplication({
name: "micro1", // 应用名,不重复即可
app: () => window.System.import("@ant/react-app1"), // 应用挂载
activeWhen: ["/react1"], // 监听路由变化
});
registerApplication({
name: "micro2",
app: () => window.System.import("@ant/react-app2"),
activeWhen: ["/react2"],
});
start({
urlRerouteOnly: true,
});
启动
由于主应用依赖子应用,所以需要先启动子应用。
step1:
cd ./react-app1
npm run start:micro
step2:
cd ./react-app2
npm run start:micro
step3:
cd ./root-config
npm run start
step4:
总结
到此,我们的微前端应用就初步实现了,由于打包用的是 webpack5,省略了一些配置,同时因为微前端需要唯一入口,一些动态加载等配置需要去掉。
不过大家应该也发现了一些问题:
- 子应用为什么要添加
react1react2的路由前缀? - 子应用 2 自身没有样式,访问子应用 1 之后为什么携带了 css 样式?