前言#
作为独立开发者,我有一个以个人 IP 为核心的自建站 yrobot.top,还有一些列的产品页(工具库,c 端产品)。我的期望是能够将这些产品页和自建站整合在一起。 这样浏览者的体验会好很多(比如页面一致性,主题统一) 但是很多产品比如工具库的详情和开发测试页是重合的,所以就存在 在 xxx 仓库的 页面 需要嵌入到 yrobot.top/app/xxx 页面里。
本文就是探讨以何种方案来解决这个需求会更优雅
微前端的核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,同时可以解决一些 iframe 的潜在问题。
期望#
- 主应用和子应用独立部署
- 简化 产品开发页 的开发流程 (直接引入主站资源,就可以不用考虑主题、nav bar、footer、section 样式)
- 主应用和子应用在同一域名下,如:yrobot.top/app/svg-inline,yrobot.top/app/auto-scroll
- 统一埋点等逻辑
- 全局配置体验统一:主题、语言
难点#
- 产品开发页怎么快速 渲染 Navbar Footer,获取主站 主题、语言
- 主站和产品详情页分开开发部署,怎么在 主站 关联 产品页,产品页内 展示 主站的组件
- 需要解决跨域带来的问题:主站和产品页的域名不同,最简单的情况下 主站是 yrobot.top,产品页是 yrobot.github.io/svg-inline
- css 样式怎么共享
方案思考#
需求基本是微前端要解决的问题,只是我的需求相比一些公司场景,反而不需要一些需求,如跨 vue 和 react 搭建,权限问题,css、js 隔离 等。
所以方案主要还是围绕微前端的方案来调研和思考。
Single-Spa#
Single-Spa 通过提供一个统一的生命周期管理机制,解决了微前端应用的加载、切换等问题。
优点#
- 支持多种前端框架(React、Angular、Vue 等)的混合使用
- 提供了完善的生命周期管理
- 支持动态加载和卸载微应用
- 提供了插件机制,方便扩展
缺点#
- js 沙箱隔离没有实现
- 需要在主应用和微应用之间进行一定的协调和集成,增加了开发复杂度
基本使用#
// 主应用 index.js
import singleSpaReact, { registerApplication, start } from "single-spa-react";
import App from "./App";
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
domElementGetter: () => document.getElementById("root"),
});
registerApplication(
"subapp1",
() => import("./subapp1/index.js"),
() => location.pathname.startsWith("/subapp1")
);
start();
// 微应用 subapp1/index.js
import singleSpaReact from "single-spa-react";
import App from "./App";
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
});
QianKun#
QianKun 基于 Single-Spa,它在 Single-Spa 的基础上提供了更加丰富的功能,如应用间通信、样式隔离等,进一步降低了微前端应用的集成难度。
优点#
- 在 Single-Spa 的基础上提供了更加丰富的功能,如应用间通信、样式隔离等
- 使用 JS 沙箱,确保微应用之间 变量/事件 不冲突
- 支持资源预加载,加速微应用打开速度
- 支持动态加载和卸载微应用,提高了系统的灵活性
缺点#
- 不支持 Vite
基本使用#
// 主应用 index.js
import { registerMicroApps, start } from "qiankun";
registerMicroApps([
{
name: "reactApp",
entry: "//localhost:3000",
container: "#container",
activeRule: "/app-react",
},
]);
start();
// 微应用
import App from "./App";
function render(props) {
const { container } = props;
ReactDOM.render(
<App />,
container
? container.querySelector("#root")
: document.querySelector("#root")
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
Mirco-App#
Micro-App 是一个基于 Web Components 标准的微前端框架,它通过原生的 Web Components 技术实现了微应用的隔离和通信。
优点#
- 基于原生 Web Components 标准,不依赖于特定的前端框架
- 通过原生的 Web Components 技术实现了微应用的隔离和通信
缺点#
- 依赖于 Web Components 标准
- 相比 Single-Spa 和 QianKun 的功能相对较少
Module Federation#
Module Federation 是 Webpack 5 引入的一项新特性,它允许在构建时将应用程序的某些部分作为独立的模块进行共享,从而实现了微前端应用的动态加载和通信。
https://module-federation.io/guide/start/features.html
优点#
- 基于 Webpack 5 的原生特性,无需引入额外的框架或库
- 可以在构建时动态地决定哪些模块需要共享
- 发布模式自由
- 支持 ts 支持(Module Federation 2 开始支持 dynamic type hinting
缺点#
- 无版本管理
- 目前 2.0 只支持 webpack 和 rsbuild,不支持 vite: 目前可以用 vite + rsbuild 配合暴露 MF 包,具体参看 demo
- @originjs/vite-plugin-federation 不支持 2.0 且有很多问题,作者也停更一段时间了
- rolldown roadmap 计划支持 module federation,https://github.com/rolldown/rolldown/discussions/153#discussioncomment-8720087
基本使用#
// 主应用的 Webpack 配置
const {
ModuleFederationPlugin,
} = require("@module-federation/enhanced/webpack");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: {
remote_app: "remote_app@http://localhost:3000/remoteEntry.js",
},
}),
],
};
// 在主应用中使用远程模块
import RemoteApp from "remote-app/App";
// ...
// 远程应用的 Webpack 配置
const {
ModuleFederationPlugin,
} = require("@module-federation/enhanced/webpack");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "remote_app",
filename: "remoteEntry.js",
exposes: {
"./App": "./src/App.js",
},
}),
],
};
每个页面单独构建
单页应用的每个页面都是在单独的构建中从容器暴露出来的。主体应用程序(application shell)也是独立构建,会将所有页面作为远程模块来引用。通过这种方式,可以单独部署每个页面。在更新路由或添加新路由时部署主体应用程序。主体应用程序将常用库定义为共享模块,以避免在页面构建中出现重复。
方案选择#
重新看一下 期望 ,主要可以总结为以下几点:
- 主应用和子应用 独立开发,独立部署,共用 hostname
- 子应用快速引入使用主应用 组件 与 逻辑
- 主应用和子应用 共享状态:js、css
主要是对于 2、3 两点,Module Federation 可以直接暴露 js、css,其他方案包含 js 和 css 隔离,反而造成了一些不必要的麻烦。
方案demo#
建立一个 demo,包含多个 repo。一个是由 next.js 构建的主站 repo,另一个是由 webpack/rsbuild/vite/next.js 构建的产品 repo(生成页面和 npm 包)
参看 demo 代码: https://github.com/Yrobot/module-federation-demo