Skip to main content

2 posts tagged with "微前端"

View All Tags

· 4 min read

以前其实做过类似的项目构建上的优化工作,但比较散乱,希望能做一个系统的整理,至少划分出几个基本的优化方向,以作为后续项目优化的参考

这里的构建优化,暂时针对的是 webpack 构建的单页面应用项目

现有问题修复

当然要优先解决掉项目现有的问题

代码规范配置

这个是为了提高协作效率,主要是新增以下配置

  • .vscode
  • eslint
  • prettier
  • commitlint
  • husky
  • editorConfig

配置详情可以参考我之前整理的文章 https://blog.zhouweibin.top/FEED/create-react-app/

热更新异常

触发热更新时,控制台会报错 React Uncaught ReferenceError: process is not defined

原因是 react-error-overlay 最新版本已支持 webpack5 ,如果要适配 webpack4,插件需要回退到 6.0.9。问题参考 https://stackoverflow.com/questions/70368760/react-uncaught-referenceerror-process-is-not-defined

后面升级到 webpack5,其实也就自动 fix 这个问题了

支持本地单独打包

之前单独打包后,没法用 serve 调试,排查发现是 package.json 的 homepage 改了资源路径,去除后单独构建就正常了

开发体验

  • 增加构建进度条
  • 增加项目版本输出信息
  • 增强 axios 接口封装函数
  • react 组件局部热更。好像暂不支持 webpack5,留意后续的更新吧

构建速度

分析工具:

create-react-app@5(CRA5)默认做了蛮多构建优化的工作了,后面打算研究下源码看看

升级 webpack5

增加了开箱即用的持久化缓存,是 webpack4 的 hardSourcesPlugin/cache-loader/loader cache的有效替代品,rebuild 速度提升明显。当然还有其他的特性和优化,可以参考 webpack5 升级踩坑

升级前后构建耗时参考:

  • 升级前,首次构建 ≈50s,二次构建 ≈18s;build≈110s
  • 升级后,首次构建 ≈42s,二次构建 ≈3.6s;build≈70s

esbuild-loader

替换 babel-loader,但 CRA5 高度配置的情况下不好替换,估计还得 eject 配置,待定

包体积优化

分析工具:

优化方法

  • 移除不用的依赖
  • 大体积包按需引入
    • echarts 按需引入(210k-->69k)
    • antv/G2 按需加载暂不支持,现在是项目使用的最大的包,后续有计划统一换成 echarts
    • loadsh 属于其他模块引入,暂不处理
    • ant-design 增加按需引用 babel-plugin-import 插件,发现实际并没效果,应该是 antd 本身已支持 tree-shaking,参考这个 issue。antd 的样式应该还是可以通过 babel-plugin-import 进行按需加载,不过 antd5 已经用 css-in-js 重构了样式代码

分包

分包(code-spliting)的目的是防止多个文件(比如路由懒加载产生的多个文件)重复打包公共的依赖,比如 react、vue、antd 等,同时也能更好地利用浏览器并行下载资源来加速首屏

怎么合理地分包是个值得探究的问题 ~

其他

最后

项目构建优化这方面感觉还是有很多东西待探索的,而且不局限于单页面应用,持续更新 ing...

· 6 min read

最近把组内的一个比较大的微前端项目做了下重构。之前是基于 single-spa 结合软链接子应用代码来统一打包,存在以下问题:

  • 基座有额外的维护成本。基座需要适配所有子应用的打包配置,比如 less-loader、ts-loader,这样造成的情况是新增应用的成本较大,有未知的修改基座的成本在,因为可能会修改构建配置
  • 软链接子应用代码的形式,造成打包逻辑较为复杂,中途构建出错需要删除干净所有的软链
  • 不支持子项目使用移动端适配插件,比如 px2rem
  • 不支持子项目定义全局样式,会造成 css 污染
  • css 隔离和 js 沙箱配置难度大

综上,重构目标有几点

  • 去除基座的维护成本。将统一打包改为子应用分别打包,但是会相应地增加一些重复的公共依赖
  • 更靠谱的前端编译。优化前端打包流程,去除软链接,减少前端构建出错的情况
  • 更独立的子项目,涉及样式定义、移动端适配等
  • 更容易接入新的子应用,支持 css 隔离和 js 沙箱

重构步骤

应用部署方式

共用一个端口,通过二级目录划分子应用,主应用通过一级目录路由访问对应的子应用

部署方式参考 https://qiankun.umijs.org/zh/cookbook#%E5%A6%82%E4%BD%95%E9%83%A8%E7%BD%B2

基座引入 qiankun

基座不限技术栈,可以基于 CRA5 新建

yarn add qiankun

修改入口文件 index.ts

import { registerMicroApps, start, setDefaultMountApp } from "qiankun";

registerMicroApps([
{
name: "reactApp",
// 需要确保斜线结尾,防止资源加载异常
entry: "/project/app1/",
container: "#app1-root",
activeRule: "/app1",
},
{
name: "vueApp",
entry: "/project/app2/",
container: "#app2-root",
activeRule: "/app2",
},
]);

// 启动 qiankun
start({
// 去除baidu脚本的跨域限制
// 注意:这种方式只支持异步引入baidu sdk,不支持在script标签引入
excludeAssetFilter: (url) => {
return url.indexOf("api.map.baidu.com") !== -1;
},
});
// 设置默认启动应用
setDefaultMountApp("/app1");

调整 index.html

// ...
<div id="root"></div>
<div id="app1-root"></div>
<div id="app2-root"></div>

异步引入 baidu jssdk

去除 script 标签引入的代码

<!--引入百度地图api-->
<script
type="text/javascript"
src="https://api.map.baidu.com/api?v=3.0&ak=你的密钥"
></script>

改成以下方式:

function loadBaiduJssdk(url) {
return new Promise((resolve, reject) => {
const scriptEl = document.createElement("script");
scriptEl.type = "text/javascript";
scriptEl.src = url;
document.body.appendChild(scriptEl);
scriptEl.onload = resolve;
scriptEl.onerror = reject;
});
}
// 异步加载
async function initMap() {
await loadBaiduJssdk("https://api.map.baidu.com/api?v=3.0&ak=你的密钥");
}

子应用配置

CRA5 新增配置

// config-overrides.js
const setOutputForQiankun = () => (config) => {
config.output.library = `${name}`;
config.output.libraryTarget = "umd";
config.output.globalObject = "window";
return config;
};
// ...
module.exports = {
webpack: override(
setOutputForQiankun(),
// ...
)
devServer: overrideDevServer((config) => {
config.historyApiFallback = true;
config.open = false;
config.headers = {
"Access-Control-Allow-Origin": "*",
};
return config;
}),
}

在 src 下新增 pubilc-path.js(ts 也行)

if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

调整入口文件(src/index.ts),暴露钩子函数。注意根节点要与 container 保持一致(也需要调整 public/index.html,默认根节点 id 为 root)

import "public-path.js";
// ...
function getSubRootContainer(container) {
return container
? container.querySelector("#app1-root")
: document.querySelector("#app1-root");
}

function render(props) {
const { container } = props;
ReactDOM.render(<App />, getSubRootContainer(container));
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}

export async function bootstrap() {
console.log("react app bootstraped");
}

export async function mount(props) {
console.log("props from main framework", props);
render(props);
}

export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(getSubRootContainer(container));
}

以上代码基于 react16

react18 可以参考 https://github.com/ice-lab/icestark/issues/581

路由配置

basename 和 qianiun 的配置保持一致,如果是 qiankun 访问,需要追加主应用配置的路由前缀,参考如下

// app.js
// ...
return (
<Provider store={STORE}>
<Router
basename={
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.__POWERED_BY_QIANKUN__ ? "/app1" : process.env.REACT_APP_BASEURL
// 在环境变量中增加路由前缀 REACT_APP_BASEURL
// 和 activeRule 对齐,也就是 /app1
}
<Suspense fallback={null}>
<Routes>
<Route path="/" element={<MainView />}
{/* ...其他路由 */}
{/* 路由重定向 */}
<Route path="*" element={<Navigate to="/" />}
</Routes>
</Suspense>
</Provider>
)

构建加速

因为项目的特殊性,每一次构建都要构建所有子项目,因此可以使用缓存,缓存各项目在主分支上最近可用的 build 包,主要是根据 commitId 判断缓存的有效性

最终达到的效果是只会构建有修改的子项目,其余子项目沿用缓存,无需重新安装依赖和构建

优化 TODO

  • 理论上还能做 node_modules 的缓存
  • qiankun 提取公共依赖,减少子项目重复安装

参考