微前端从入门到放弃(上)

422 阅读4分钟

背景

4 年前前公司的 CTO 和我说,要不像后端一样用微前端方案做?我当时一头雾水,现在有微前端?用 iframe 不就行了!

当我入职蚂蚁半年后,团队同学推出了微前端技术 qiankun

微前端目前已经很成熟了,就不再累述,有不了解的同学先自行查阅。

目的

  1. 现有项目快速集成微前端

  2. 微前端架构 single-spa 和 qiankun 的优劣

  3. 了解微前端的实现原理,手动实现简版微前端

开干

上篇我们先通过示例项目入手,了解一下微前端的大致接入过程。 我遇到的大部分微前端场景都是技术栈相同,但开发团队不同,所以我们用最朴素的 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 端口

image.png

step5: 魔改项目,项目正常会有路由跳转等,我们简单修改一下代码,新增路由react1react1/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;

image.png

step6: 魔改,虽然子应用已经可以正常启动开发,但由于需要集成到主应用,所以我们需要做如下几个修改点

  1. 新增启动方式 start:micro 用于设置环境变量
  "scripts": {
    "start:micro": "cross-env PORT=3000 MICRO=true node scripts/start.js",
    "start": "cross-env  PORT=3000 node scripts/start.js",
  },
  1. micro 模式下不用在浏览器打开子应用,显得多余
  if (!process.env.MICRO) {
    openBrowser(urls.localUrlForBrowser);
  }

3.修改打包配置

a. 因为需要固定的输出路径,我们去掉 output.filename 的 hash 模式
b. 由于需要结合 systemjs 使用,output.libraryTarget 需要修改为 system
c. external 掉和主包重复的一些包

image.png

  1. 入口文件做区别,新增微前端配置 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,并新增路由react2react2/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;

启动:

image.png

创建主应用(root-config)

进入主应用前,建议大家先去简单看下 sigle-spa 的用法

image.png

我们直接使用官网提供的 示例 进行修改即可,重点关注下 <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>

主要作用有两点:

  1. 启动主应用:简单的 webpack-server

  2. 注册子应用并监听路由变化

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:

访问 http://localhost:9000

May-26-2022 10-08-22.gif

总结

到此,我们的微前端应用就初步实现了,由于打包用的是 webpack5,省略了一些配置,同时因为微前端需要唯一入口,一些动态加载等配置需要去掉。

不过大家应该也发现了一些问题:

  1. 子应用为什么要添加 react1 react2 的路由前缀?
  2. 子应用 2 自身没有样式,访问子应用 1 之后为什么携带了 css 样式?