前言
终于要对我们公司的“巨石”项目动手改造了,该项目是基于umi@3.5.27框架搭建的,目前已经有上百个路由了,每次在这上面搬砖都是一次痛苦的煎熬: 项目启动时,偶尔还会报
当然这个问题是有办法解决的,可以在启动命令加上:NODE_OPTIONS=--max_old_space_size=4096,比如
"dev": "NODE_OPTIONS=--max_old_space_size=4096 UMI_ENV=dev umi dev"
如果说上一个问题还能通过骚操作解决掉,那这个问题一定不能忍:每次热更新我同事都会开玩笑说:去上个WC让子弹再飞一会 -_-
基于上诉原因,我们决定对这个项目进行微前端改造,期间踩过很多坑,在此记录下,供有需要的看官使用。
系列文章传送门:
【实战】umi3.x+qiankun+模块联邦 搭建微前端应用(二)— 集成qiankun
【实战】umi3.x+qiankun+模块联邦 搭建微前端应用(三)— 项目部署
项目需求及技术手段
- 因为这个项目已经上线很久了,我们的基本的原则是不能改变用户的操作习惯,这里采用的是 umi3.x 提供的 MicroAppWithMemoHistory,来实现应用的跳转;
- 应用或页面切换希望能保持状态,类似于实现vue的keep-alive效果;
- 各应用之间希望能复用组件及公共方法,那么这里就需要用到webpack5提供的 模块联邦了;
废话不多说,我们开始吧;
初始化项目
首先主应用采用 umi3.x,两个子应用:app1 采用 vue3;app2 采用 umi3.x。
用 pnpm 作为包管理器,因为umi3.x官方还没完全支持,umi4.x已经支持了,因此这里会有坑,后续会提到;
各个项目的初始化流程根据各自官网搭建,此处略。最终生成的目录结构如下:
umi-qiankun
├── README.md
├── config
│ ├── config.ts
│ └── routes.ts
├── package.json
├── pnpm-lock.yaml
├── scripts
│ ├── build.js
│ └── utils.js
├── src
│ └── pages
├── sub-projects
│ ├── app1
│ └── app2
├── tsconfig.json
└── typings.d.ts
这里需要注意两个子应用,放在了 sub-projects 这个目录下了
项目配置
在主应用安装
pnpm add -D @umijs/plugin-qiankun
pnpm add @ant-design/icons @ant-design/pro-layout @umijs/route-utils lodash path-to-regexp
注意 @ant-design/icons @ant-design/pro-layout @umijs/route-utils lodash path-to-regexp 注意这几个如果包管理器采用 npm 或 yarn 时是不用安装,但这个我们采用的是 pnpm 则需要安装,这是 pnpm 没有幽灵依赖造成的
注册子应用
// config/config.ts
qiankun: {
master: {
// 注册子应用信息
apps: [
{
name: 'app1', // 唯一 id
entry: '//localhost:8001', // html entry
},
{
name: 'app2', // 唯一 id
entry: '//localhost:8002', // html entry
},
],
},
},
routes.ts 的配置
// config/routes.ts
export default [
{
path: "/",
name: "首页",
component: "@/pages/home",
},
{
path: "/about",
name: "about",
component: "@/pages/about",
},
{
path: "/app1",
name: "app1",
microApp: "app1", // 这里就是微应用,与上面对应
microUrl:'/', // 自定义字段,后续做子应用内页面跳转有用
},
{
path: "/app2",
name: "app2",
microApp: "app2", // 这里就是微应用,与上面对应
microUrl:'/', // 自定义字段,后续做子应用内页面跳转有用
},
{
path: "/app2_about",
name: "app2-about",
microApp: "app2", // 这里就是微应用,与上面对应
microUrl:'/about', // 自定义字段,后续做子应用内页面跳转有用
},
];
在子应用的配置
这里需要注意的是在 vue3.x 项目中需要安装
pnpm add vite-plugin-qiankun
这个 vite 插件是专门针对 qiankun 微应用使用的
在 main.ts 文件中配置
// vue3 main.ts 无关代码自行省略
// @ts-nocheck
import { createApp } from "vue";
import App from "./App.vue";
import {
renderWithQiankun,
qiankunWindow,
} from "vite-plugin-qiankun/dist/helper";
let router = null;
let instance = null;
renderWithQiankun({
mount(props) {
render(props);
instance.config.globalProperties.$onGlobalStateChange =
props.onGlobalStateChange;
instance.config.globalProperties.$setGlobalState = props.setGlobalState;
},
bootstrap() {
console.log("%c ", "color: green;", "sub-vite2-vue3 app bootstraped");
},
unmount(props: any) {
instance.unmount();
instance._container.innerHTML = "";
instance = null;
router = null;
},
});
function render(props = {}) {
const { container } = props;
instance = createApp(App);
instance.use(router);
instance.mount(container ? container.querySelector("#app") : "#app");
}
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render({});
}
在 app2 项目中依然需要安装
pnpm add -D @umijs/plugin-qiankun
在 config/config.ts 文件中配置
qiankun: {
slave: {},
},
到这里,不出意外的话应该能正常访问各子应用了。接下来我们来思考一个场景,有一个页面或一个子应用需要填写复杂表单,这时候中途如果来回切换页面,那么辛辛苦苦填的一堆数据就没有了。基于这个场景,很自然的衍生出一个需求,如何保持页面状态?
我们梳理一下需求,并在右侧内容区域新增一个页签(类似浏览器的页签)功能,页签左右切换是可以保持状态的,只有当点击左侧菜单时如果页签已经存在则重新加载,如果不存在则新开一个页签。
具体实现:
在主应用下新建 src/layouts/index.tsx
import { useRef } from 'react';
import { LikeOutlined, UserOutlined } from '@ant-design/icons';
import {
PageContainer,
ProLayout,
SettingDrawer,
} from '@ant-design/pro-layout';
import { history, IRouteComponentProps } from 'umi';
import { useState } from 'react';
import Content from '@/components/Layout/Content';
import routes from '../../config/routes';
const defaultRoutes = {
route: {
path: '/',
routes,
},
};
export default (props: IRouteComponentProps) => {
const contentRef = useRef(null);
const [pathname, setPathname] = useState(props.location.pathname);
return (
<div
id="test-pro-layout"
style={{
height: '100vh',
}}
>
<ProLayout
{...defaultRoutes}
location={{
pathname,
}}
menuItemRender={(item, dom) => (
<a
onClick={() => {
setPathname(item.path || '/');
contentRef.current.resetPaneContent(item.path);
}}
>
{dom}
</a>
)}
>
<Content {...props} ref={contentRef} setPathname={setPathname} />
</ProLayout>
</div>
);
};
在主应用新建:src/components/Layout/Content.txt
import { withRouter, history, MicroAppWithMemoHistory } from 'umi';
import {
useState,
useEffect,
cloneElement,
forwardRef,
useImperativeHandle,
Fragment,
} from 'react';
import { Tabs } from 'antd';
const { TabPane } = Tabs;
const Content: any = forwardRef(
({ children, location, setPathname, refInstance, ...rest }, ref) => {
const [cacheChildren, setCacheChildren] = useState(new Map());
const onTabChange = (tab) => {
history.push(tab);
setPathname(tab);
};
useEffect(() => {
if (cacheChildren.has(location.pathname)) return;
const currentRoute = rest.route.routes.find(
(i: any) => i.path === location.pathname,
);
const content = cloneElement(
children,
{},
children.props.children.map((child) => {
return cloneElement(child);
}),
);
const newCacheChildren = cacheChildren.set(location.pathname, {
...currentRoute,
key: 0,
tab: currentRoute.name,
content,
});
setCacheChildren(new Map(newCacheChildren));
}, [location.pathname]);
const resetPaneContent = (path: string) => {
let oldEl = cacheChildren.get(path);
if (cacheChildren.has(path)) {
// 更新当前页签的key,使其可以重新渲染
oldEl.key += 1;
cacheChildren.set(path, oldEl);
setCacheChildren(new Map(cacheChildren));
}
setTimeout(() => {
history.push(path);
}, 30);
};
useImperativeHandle(refInstance, () => ({
resetPaneContent,
}));
return (
<Tabs
hideAdd
activeKey={location.pathname}
onChange={onTabChange}
type="editable-card"
>
{[...cacheChildren.values()].map((pane) => (
<TabPane tab={pane.tab} key={pane.path}>
{pane.microApp ? (
<MicroAppWithMemoHistory
key={pane.key}
name={pane.microApp}
url={pane.microUrl}
></MicroAppWithMemoHistory>
) : (
<Fragment key={pane.key}>{pane.content}</Fragment>
)}
</TabPane>
))}
</Tabs>
);
},
);
const Comp = withRouter(Content);
// 这里的forwardRef与withRouter结合会有问题,因此使用了这种方式进行ref的转发
export default forwardRef((props, ref) => (
<Comp {...props} refInstance={ref} />
));
最终实现效果:
总结
这个章节我们主要是初步搭建了一个微前端的基础框架,包括采用pnpm作为包管理器、主、子应用目录结构的划分、qiankun的配置等,并在此基础上实现了页签功能,使得我们可以保存页面状态。万里长征刚踏出第一步,码字不易,求点赞收藏,更欢迎批评指正。下一个章节继续说说:集成webpack5 模块联邦实现公共组件、公共方法的复用;