1.微前端介绍以应用选型
1.1什么是微前端?
微前端是一种前端架构模式,它将前端应用程序拆分成多个小型的、独立开发、独立部署的子应用,然后将这些子应用组合成一个大型的、复杂的前端应用。每个子应用都有自己的技术栈、独立的代码库、独立的开发、测试和部署流程,并且可以独立运行、测试和部署。
微前端的目的是解决大型前端应用程序在开发、测试和部署等方面的复杂性和困难。通过将前端应用程序拆分成多个独立的子应用,可以实现团队的分工协作、提高开发效率、降低代码耦合性、提高代码可维护性和可测试性。同时,微前端也可以实现应用程序的增量更新、灰度发布、动态加载和懒加载等功能,从而提高应用程序的性能和用户体验。
1.2技术选择
主应用:umi(Ant Design Pro)
子应用:umi(Ant Design Pro)
父应用和子应用其实都是独立的前端项目,父应用可以在内部引入子应用,子应用也可以在自己内部继续引入孙子应用
2.开始使用
2.1配置父应用
首先需要配置父应用,注册子应用的相关信息,这样父应用才能识别子应用并在内部引入。
注册子应用的方式主要有两种:
- 插件注册子应用。
- 运行时注册子应用。
以上两种方式都是在 .umirc.ts
中注册子应用,但是使用的插件不同,从而导致了使用方式的不同。
- 插件注册子应用:
使用插件注册子应用时,需要安装 @umijs/plugin-qiankun
插件,并在 .umirc.ts
中进行配置,如下:
import { defineConfig } from 'umi';
export default defineConfig({
qiankun: {
slave: {},
master: {
apps: [
{
name: 'reactApp', // 唯一 id
entry: 'http://localhost:8091', // html entry
},
],
// sandbox: false,
},
},
});
在 qiankun
配置中,slave
为子应用的配置,可以在这里配置子应用的名称、入口、路由、公共依赖等信息。该插件会自动构建子应用的入口文件、修改 webpack 配置等。
注意:没有.umirc.ts文件就写在config.ts中,上述中如果不添加
slave: {}配置信息,会报错:Unhandled Rejection (Error): register failed, invalid key useQiankunStateForSlave from plugin ../../app.tsx.如果有这个报错请参考这种方法
- 运行时注册子应用:
运行时注册子应用时,不需要安装额外的插件,只需要在 .umirc.ts
中进行配置,如下:
import { defineConfig } from 'umi';
export default defineConfig({
// 省略其他配置
routes: [
{
path: '/',
component: '@/layouts/index',
routes: [
{
path: '/',
component: '@/pages/index',
microApp: 'sub-app', // 运行时注册子应用
},
],
},
],
});
在 routes
中配置 microApp
字段即可运行时注册子应用。在这里可以配置子应用的名称、入口、路由等信息。
两种方式的目的都是为了在主应用中注册子应用,只是使用的插件和配置方式有所不同。如果使用 @umijs/plugin-qiankun
插件,则可以自动构建子应用的入口文件、修改 webpack 配置等,使用起来更加方便。如果不使用插件,则需要手动配置子应用的入口、路由等信息。
2.2配置子应用
子应用需要导出必要的生命周期钩子,供父应用在适当的时机调用。
假设您的子应用项目基于 Umi 开发且引入了 qiankun
插件。如果没有,可以按照此教程进行配置。
修改子应用的 Umi 的配置文件,添加如下内容:
// .umirc.ts
export default {
qiankun: {
slave: {},
},
};
这样,微前端插件会自动在项目中创建好 Qiankun 子应用所需的生命周期钩子和方法.
本地调试时,主应用中点击子应用的路由地址,报错”Unhandled Rejection (TypeError): Failed to fetch”,
要解决这个问题,有两种方法可以尝试:
- 配置子应用的跨域规则
在子应用的配置中,通过配置跨域规则来允许主应用访问子应用的资源。具体来说,可以在子应用的配置文件中(例如 umi 项目的 config/config.ts 或 config/config.prod.ts 文件)加入如下配置:
export default {
// ...
devServer: {
// 允许跨域访问的域名,如果有多个可以用逗号隔开
headers: {
'Access-Control-Allow-Origin': '*',
},
},
// ...
};
这个配置会在子应用启动时,自动为子应用启动一个 devServer 服务,并允许跨域访问。
2.配置主应用代理
export default {
// ...
proxy: {
'/app1': {
target: 'http://localhost:8001', // 子应用1的服务地址
changeOrigin: true,
pathRewrite: {
'^/app1': '',
},
},
'/app2': {
target: 'http://localhost:8002', // 子应用2的服务地址
changeOrigin: true,
pathRewrite: {
'^/app2': '',
},
},
// ...
},
// ...
};
这个配置将会把以 /app1 或 /app2 开头的请求,分别代理到子应用1和子应用2所在的服务上。注意,在使用代理时,要确保子应用的服务已经启动并监听了对应的端口号。
2.3引入子应用
在父应用中引入子应用,插件提供了三种不同实现的方式:
- 路由绑定引入子应用。
<MicroApp />
组件引入子应用。<MicroAppWithMemoHistory />
组件引入子应用。
2.3.1路由绑定引入子应用
手动配置 config.ts
文件中的 routes
项,通过路由的方式绑定子应用。何时使用:
- 子应用包含完整的路由切换逻辑时。
- 父子应用路由相互关联时。
现在,可以配置父应用下的一个子应用"react子应用",路由如下:
{
name: 'react子应用',
path: '/subreact',
microApp: 'reactApp',
microAppProps: {
autoSetLoading: true,
},
routes: [
{
name: 'Welcome',
icon: 'smile',
path: '/subreact/Welcome',
},
{
name: 'table-list',
icon: 'table',
path: '/subreact/list',
// 如果想要将 /subreact/list 下所有子路由都关联给微应用 app1,可以带上 * 通配符
},
],
},
配置好后,子应用的路由 base 会在运行时被设置为主应用中配置的 path
。 例如,在上面的配置中,我们指定了 react子应用 关联的 path 为 /subreact/Welcome,假如 app1 里有一个路由配置为 /user
,当我们想在父应用中访问 /user
对应的页面时,浏览器的 url 需要是 base + /user
,即 /
subreact/user
路径,否则子应用会因为无法匹配到正确的路由而渲染空白或404页面。
可以手动修改路由配置信息:
if (window.__POWERED_BY_QIANKUN__) {
routesData.map(item => {
if (item.path.includes('/')) {
item.path = '/subreact' + item.path
}
if (item.redirect) {
item.redirect = '/subreact' + item.redirect
}
return item
})
}
2.3.2<MicroApp />
组件引入子应用
通过 <MicroApp />
组件加载(或卸载)子应用。何时使用:
- 子应用包含完整的路由切换逻辑时。
- 父子应用路由相互关联时。
现在,我们想在父应用的某个页面中引入子应用 app1
,可以编写代码如下:
import { MicroApp } from 'umi';
export default function Page() {
return <MicroApp name="subreact" />;
};
使用该方式引入子应用时,父子应用的路由将一一对应。例如,当父应用路由为 /some/page
时,子应用路由同样为 /some/page
。切换子应用路由时,父应用将同步切换。
如果父应用的路由包含前缀,可以通过配置 base
属性保证父子应用的路由正确对应。例如,父应用路由为 /
main-react/welcome
时,我们希望子应用的路由为 /subreact/welcome
,可以修改代码如下:
import { MicroApp } from 'umi';
export default function Page() {
return <MicroApp name="reactApp" base="/main-react" />
};
2.3.3<MicroAppWithMemoHistory />
组件引入子应用
通过 <MicroAppWithMemoHistory />
组件加载(或卸载)子应用。何时使用:
- 仅使用子应用的指定路由时。
- 父子应用路由相互独立时。
<MicroAppWithMemoHistory />
组件是 <MicroApp />
组件的变体,您需要显式提供 url
属性作为子应用的路由。当父应用的路由发生变化时,子应用的路由不会改变。
现在,我们想在父应用的某个组件内部引入 subreact 子应用,子应用的路由为 /
subreact/welcome
,可以编写代码如下:
<MicroAppWithMemoHistory name="reactApp" url="/welcome" />
2.4子应用之间跳转
这个我暂时未尝试成功,会报错MicroAppLink 导出错误问题,成功的小伙伴欢迎补充.下面是具体内容:
如果子应用通过路由绑定的方式引入,在其它子应用的内部,可以使用 <MicroAppLink />
跳转到对应的路由。以子应用 app1
和 app2
为例:
// 在 app1 中
import { MicroAppLink } from 'umi';
export default function Page() {
return (
<>
{/* 跳转链接为 /app2/home */}
<MicroAppLink name="app2" to="/home">
<Button>go to app2</Button>
</MicroAppLink>
</>
);
}
在上面的例子中,点击按钮后,父应用的路由变为 /app2/home
,渲染子应用 app2
内部路由为 /home
的页面。同理,如果想要从子应用 app2 回到子应用 app1,可以编写代码如下:
// 在 app2 中
import { MicroAppLink } from 'umi';
export default function Page() {
return (
<>
{/* 跳转链接为 /app1/project/home */}
<MicroAppLink name="app1" to="/home">
<Button>go to app1</Button>
</MicroAppLink>
</>
);
}
您也可以从子应用跳转到父应用的指定路由:
// 在子应用中
import { MicroAppLink } from 'umi';
export default function Page() {
return (
<>
{/* 跳转链接为 /table */}
<MicroAppLink isMaster to="/table">
<Button>go to master app</Button>
</MicroAppLink>
</>
);
}
补充:可以通过路由引用和组件引用同一个子应用
3.子应用生命周期
3.1父应用配置生命周期钩子
在父应用的 src/app.ts
中导出 qiankun
对象进行全局配置,所有的子应用都将实现这些生命周期钩子:
// src/app.ts
export const qiankun = {
lifeCycles: {
// 所有子应用在挂载完成时,打印 props 信息
async afterMount(props) {
console.log(props);
},
},
};
3.2子应用配置生命周期钩子
在子应用的 src/app.ts
中导出 qiankun
对象,实现生命周期钩子。子应用运行时仅支持配置 bootstrap
、mount
和 unmount
钩子:
/ src/app.ts
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
console.log('app1 bootstrap', props);
},
// 应用 render 之前触发
async mount(props) {
console.log('app1 mount', props);
},
// 应用卸载之后触发
async unmount(props) {
console.log('app1 unmount', props);
},
};
会将引用子组件时传递给子组件的函数和参数都打印出来,这个下面组件传递信息会讲到,且props中包括子应用的信息,如下:
4.父子应用通信
父子应用间的通信有两种实现的方法:
- 基于
useModel()
的通信。这是 Umi 推荐的解决方案。 - 基于配置的通信。
4.1基于 useModel()
的通信
该通信方式基于 数据流 插件,此插件已经内置于 @umi/max
解决方案当中。
该通信方式需要子应用基于 Umi 开发且引入了该数据流插件。
关于此插件的详细介绍可见数据流指南。
4.1.1主应用透传数据
如果通过路由的模式引入子应用,则需要在父应用的 src/app.ts
里导出一个名为 useQiankunStateForSlave()
的函数,该函数的返回值将传递给子应用:
// src/app.ts
export function useQiankunStateForSlave() {
const [globalState, setGlobalState] = useState<any>({
slogan: 'Hello MicroFrontend',
});
return {
globalState,
setGlobalState,
};
}
如果通过组件的模式引入子应用,直接将数据以组件参数的形式传递给子应用即可:
import React, { useState } from 'react';
import { MicroApp } from 'umi';
export default function Page() {
const [globalState, setGlobalState] = useState<any>({
slogan: 'Hello MicroFrontend',
});
return (
<MicroApp
name="app1"
globalState={globalState}
setGlobalState={setGlobalState}
/>
);
};
4.1.2子应用消费数据
子应用会自动生成一个全局的 Model,其命名空间为 @@qiankunStateFromMaster
。通过 useModel()
方法,允许子应用在任意组件中获取并消费父应用透传的数据,如下所示:
import { useModel } from 'umi';
export default function Page() {
const masterProps = useModel('@@qiankunStateFromMaster');
return <div>{JSON.stringify(masterProps)}</div>;
};
或者可以通过高阶方法 connectMaster()
来获取并消费父应用透传的数据,如下所示:
import { connectMaster } from 'umi';
function MyPage(props) {
return <div>{JSON.stringify(props)}</div>;
}
export default connectMaster(MyPage);
子应用也可以在生命周期钩子中能够获取并消费得到的 props
属性,根据需求实现对应的生命周期钩子即可。
特别的,当父应用使用 <MicroApp />
或 <MicroAppWithMemoHistory />
组件的方式引入子应用时,会额外向子应用传递一个 setLoading()
方法,允许子应用在合适的时机执行,标记子应用加载为完成状态:
const masterProps = useModel('@@qiankunStateFromMaster');
masterProps.setLoading(false);
// 或者
function MyPage(props) {
props.setLoading(false);
}
connectMaster(MyPage);
注:setLoading()
方法可以和自定义加载配套使用,在主应用中通过组件引入子应用时,自定义loader,如下:
//CustomLoader.tsx
import { Alert, Spin } from 'antd';
import * as React from 'react';
interface ICustomLoaderProps {
loading: boolean
}
const CustomLoader: React.FC<ICustomLoaderProps> = (props) => {
const { loading } = props
return <Spin spinning={loading}>
</Spin>;
};
export default CustomLoader;
import CustomLoader from '@/components/CustomLoader';
<MicroAppWithMemoHistory name="reactApp"
url="/welcome"
loader={(loading) => <CustomLoader loading={loading} />} />
下面例举一下组件引用方式时,子应用通过父应用传递过来的方法修改父应用中的值:
const masterProps = useModel('@@qiankunStateFromMaster');
const onChangeGlobalState = () => {
const updatedGlobalState = {
...masterProps?.globalState,
slogan: '我通过子应用成功修改父应用中的值',
};
masterProps?.setGlobalState(updatedGlobalState);
};
注意:
子应用中使用 useModel
获取到的 masterProps
对象包含了从主应用中传递过来的 globalState
和 setGlobalState
方法。然而,直接使用 setGlobalState
修改 globalState
时可能无法实现更新。
这是因为 masterProps
中包含的 globalState
是主应用中的状态,子应用不能直接修改主应用中的状态。子应用只能通过 setGlobalState
方法向主应用传递更新后的状态,并让主应用自己进行更新。因此,在子应用中使用 setGlobalState
方法时,需要将整个更新后的状态对象作为参数传递给 setGlobalState
方法。而不能直接修改 globalState
对象中的某个属性值。
正确的做法是,在子应用中使用 setGlobalState
方法时,先从 masterProps
中获取当前的 globalState
对象,然后将需要更新的属性值进行修改,最后将整个更新后的 globalState
对象传递给 setGlobalState
方法。
或者可以使用下面这种方法:
import { useModel } from 'umi';
import { Button } from 'antd';
function SubApp() {
const { globalState, setGlobalState } = useModel('@@qiankunStateFromMaster');
const handleClick = () => {
setGlobalState({
...globalState,
slogan: 'New Slogan from Sub App',
});
};
return (
<div>
<h1>{globalState.slogan}</h1>
<Button onClick={handleClick}>更改父应用globalState</Button>
</div>
);
}
export default SubApp;
参考文档:
微前端 | UmiJS