qiankun微前端接入
子应用
增加 public-path.js
为了解决微应用打包之后 runtime publicPath 主要解决的是微应用动态载入的 脚本、样式、图片 等地址不正确的问题。原因是 webpack 加载资源时未使用正确的 publicPath
因此qiankun 将会在微应用 bootstrap 之前注入一个运行时的 publicPath 变量
1.webpack 加载资源时未使用正确的 publicPath。关于运行时 publicPath 的技术细节,可以参考 webpack 文档。
2.runtime publicPath 主要解决的是微应用动态载入的 脚本、样式、图片 等地址不正确的问题。
3.qinkun 将外链样式改成了内联样式,但是字体文件和背景图片的加载路径是相对路径。 而 css 文件一旦打包完成,就无法通过动态修改 publicPath 来修正其中的字体文件和背景图片的路径
1.在src目录下面创建public-path.js文件,如图所示
2.在创建完成的public-path.js 中黏贴如下代码
// 挂载乾坤相关参数
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
修改BrowerRouter
为了区分各个子应用和主应用,也为了方便qiankun能够识别到对应的子应用我们需要对BrowerRouter进行改造增加basename
该base那么需要和entry进行保持一致是为了让improt-entry-html在多个script文件中快速识别出该script是哪一个子应用的script从而让其快速定位到其html
1.在browerRouter中增加一个basename如图所示
2.改造index.tsx
为了让qiankun在加载入口文件时能够获取qiankun运行时传入的参数以及需要做一些区分是否单独应用等
改造代码如下所示
// 改造生命后期
function render(props) {
const { container, store, mainAppHistory, mainAppNavigate } = props;
ReactDOM.render(
<MainAppProvider value={{ mainAppHistory, mainAppNavigate }}>
<BrowserRouter basename={basename} >
<ConfigProvider locale={zhCN} prefixCls='resant'
getPopupContainer={node => {
if (node) {
return node.parentNode as any;
}
return document.body;
}}
>
{
window.__POWERED_BY_QIANKUN__ ? (
<Provider store={store}>
<AliveScope> <App /></AliveScope>
</Provider>
) : <AliveScope> <App /></AliveScope>
}
</ConfigProvider>
</BrowserRouter>
</MainAppProvider>
,
container ? container.querySelector('#resource') : document.querySelector('#resource'));
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('微服务第一次初始化');
}
export async function mount(props) {
console.log('微服务正在挂载中');
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
render(props);
}
export async function unmount(props) {
const { container } = props;
console.log('微服务正在卸载中');
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#resource') : document.querySelector('#resource'));
}
3.改造index.html
修改public目录下面的index.html如图所示
index.html上react挂载点点id需要和步骤2中的container.querySelector('#resource') : document.querySelector('#resource'))保持一致
4.修改craco.config.js
qiankun依赖于system.js进行并不能识别esmodule因此需要将esmodule进行转译为能够识别的amd
主要增加 publicPath, globalObject: 'window',library, libraryTarget: 'umd',chunkLoadingGlobal这些属性,如图所示
各自段解释含义
- libraryTarget: 由于import-html-entry只能识别cmd或者amd等其他模块但是并不能识别es6的模块因此需要将模块进行转换为umd格式
- globalObject: 由于现在js为了统一浏览器和node等js,将顶层对象都设置为global, 而single-spa内部是使用system.js进行实现的,而system.js是通过script进行加载的,因此会挂载到window对象上,而不是global对象上,因此需要修改globalObject为window
- library:除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,library的值必须是唯一的
- chunkLoadingGlobal: 主要是在主应用中进行懒加载过程中无法区分当前chunk是来源于哪一个子应用的
- publicPath:
由于我们公司的部署采用的k8s进行部署,微应用想部署在非根目录时
1、必须配置webpack
构建时的publicPath
2、history路由的微应用需要设置
base` ,值为目录名称,用于独立访问时使用。
3.publicPath需要和永国那边进行商量,因为涉及到了k8s进行打包的时候打包到在非根目录
主应用
qiankun目前是能够夸框架配置的,导致配置非常繁琐,为了解决在react主应用中简化qianklun的配置因此写了如下组件
引入现有组件
1.在common文件目录的components目录下创建一个名为Qiankun的目录
2.在创建好的Qiankun目录中创建MicroApplicationRoute.tsx, 并将下面这段代码黏贴进去
import { forwardRef, useEffect, useImperativeHandle, useRef, ReactNode, useState } from 'react';
import type { Entry, RegistrableApp, MicroAppStateActions } from 'qiankun';
import { loadMicroApp, initGlobalState } from 'qiankun';
import { whites, minWhites } from './white.config';
import store from '@/store/store';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
const formatToHump = (value) => {
return value.replace(/\-(\w)/g, (_, letter) => letter.toUpperCase());
};
const excludeAssetFilter = (assetUrl: string) => {
const assets: any = [].concat(Object.values(whites as any)).concat(Object.values(minWhites as any));
// 子应用不需要重复加载,主应用加载就够了, 微应用资源(css/js) 不被 qiankun 劫持处理
return assets.includes(assetUrl);
// 白名单的资源不解析这样就不会重复加载
};
/** qiankun的配置项 */
interface Options {
singular?: boolean | ((app: RegistrableApp<any>) => Promise<boolean>);
// 是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 false。
fetch?: typeof fetch;
// 自定义的 fetch 方法。
getPublicPath?: (entry: Entry) => string;
// 微应用的 entry 值。
getTemplate?: (template: string) => string;
// 获取模版,一般是html-entry-imort设置的模版
excludeAssetFilter?: (assetUrl: string) => boolean;
// 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被 qiankun 劫持处理
}
interface MicroApplication {
// 微应用的基础信息
name: string;
// 微应用的名称,微应用之间必须确保唯一。
entry: string | { scripts?: string[]; styles?: string[]; html?: string };
// 必选,微应用的入口(详细说明同上)。
container: string | HTMLElement;
// 微应用的容器节点的选择器或者 Element 实例。如container: '#root' 或 container: document.querySelector('#root')。
props?: Record<string, any>;
// 初始化时需要传递给微应用的数据。
//
sandbox?: Sandbox;
// 默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。当配置为 { strictStyleIsolation: true } 时表示开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。
singular?: boolean;
}
type Sandbox = boolean | { strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean }
// 用于加载乾坤的逻辑
function loadMicroApplication(app: MicroApplication, options?: Options): MicrApp {
return loadMicroApp(app, {
...options,
sandbox: {
strictStyleIsolation: true,
experimentalStyleIsolation: true,
},
excludeAssetFilter
} as any,
);
};
interface MicroApplicationRouteProps {
pathname: string; // 指定路由的路径
entry: string; // 指定微应用的html入口
props?: Record<string, any> // 额外传给微应用的参数(主要用于业务的定制化)
fallback?: ReactNode;
onStateChange?: (state, prev) => void;
state?: any;
onMicroAppChange?: (micrApp: any) => void;
};
interface MicrApp {
mount?: () => Promise<null>;
unmount?: () => Promise<null>;
update?: (customProps: Record<string, any>) => Promise<any>;
getStatus?: () => | 'NOT_LOADED' | 'LOADING_SOURCE_CODE' | 'NOT_BOOTSTRAPPED' | 'BOOTSTRAPPING' | 'NOT_MOUNTED' | 'MOUNTING' | 'MOUNTED' | 'UPDATING' | 'UNMOUNTING' | 'UNLOADING' | 'SKIP_BECAUSE_BROKEN' | 'LOAD_ERROR';
loadPromise?: Promise<null>;
bootstrapPromise?: Promise<null>;
mountPromise?: Promise<null>;
unmountPromise?: Promise<null>;
}
// 仅在微应用初始化时执行一次
const actions: MicroAppStateActions = initGlobalState();
const MicroApplicationRoute = ({ pathname, entry, onMicroAppChange, props }: MicroApplicationRouteProps, ref) => {
const realPathname = pathname.indexOf('/') > -1 ? pathname : `/${pathname}`;
// container取路径删除/以后的部分
const name = realPathname.slice(1,);
const microRoot = useRef();
const micrApp = useRef<any>(null);
const [errored, setErrored] = useState<boolean>(false);
// 由于路由一般是中划线,将其转换为符合规范的路由名称
const appName = formatToHump(name);
function loadApp(appName: string, container: string, entry: string, options?: Record<string, any>) {
const props = {
...options,
store,
mainAppHistory: history,
};
micrApp.current = loadMicroApplication({
entry,
props,
name: appName,
container,
});
onMicroAppChange?.(micrApp.current);
const { getStatus } = micrApp.current;
// setLoading(true);
const status = getStatus();
if (status === 'LOADING_SOURCE_CODE') {
micrApp.current.mountPromise.then(() => {
}).catch(() => setErrored(true));
} else if (status === 'LOAD_ERROR') {
console.log('子应用加载失败');
setErrored(false);
} else if (status === 'UNMOUNTING') {
micrApp.current.unmountPromise.then(() => {
setErrored(false);
// 微应用卸载
}).cache(() => {
console.log('子应用在卸载时报错了');
});
}
};
// 暴露一些外部用于执行微应用的钩子函数
useImperativeHandle(ref as any, () => {
const { mount, unmount, update } = micrApp.current || {};
return {
micrAppMount: mount,
micrAppUmount: unmount,
micrAppUpdate: update,
// 考虑到不知道什么时候进行set值,因此抛给业务方方自行调用
setState: (state: any) => actions.setGlobalState(state),
offStateChange: actions.offGlobalStateChange,
};
});
useEffect(() => {
// 挂载微应用;
micrApp.current = loadApp(appName, microRoot.current!, entry, props);
}, [appName, entry, props]);
return (
<>
<div ref={microRoot as any}></div>
{errored && <p>微应用加载出错了</p>}
</>
);
};
const ForwardMicroApplicationRoute = forwardRef(MicroApplicationRoute);
ForwardMicroApplicationRoute.displayName = 'MicroApplicationRoute';
export default ForwardMicroApplicationRoute;
该组件主要是为了简化qiankun的配置项以及方便做一些定制化
3.在创建好的Qiankun目录中创建MicroApplicationRouter.tsx, 并将下面这段代码黏贴进去
import { Children, FC, ReactElement, ReactNode, useEffect, useRef, useState, Suspense, cloneElement } from 'react';
import { Spin } from 'antd';
interface MicroApplicationRouterProps {
childern?: ReactNode;
}
// 过滤出所有为MicroApplicationRoute的组件
const filterRoutChildren = (children: ReactElement) => {
const micrRoutes: any = [];
const routes: any = [];
Children.forEach(children, (child) => {
const { type } = child as any || {};
const { displayName } = type || {};
if (displayName === 'MicroApplicationRoute') {
micrRoutes.push(child);
} else {
routes.push(child);
}
});
return [micrRoutes, routes];
};
// 痛过base进行匹配挂载哪一个组件
const MicroApplicationRouter: FC<MicroApplicationRouterProps> = ({ children }) => {
const routeMap = useRef<Map<string, ReactNode>>(new Map());
const routePathname = location.pathname;
const [micrApp, setMicrApp] = useState<ReactNode>();
const [micrAppInstance, setMicrAppInstance] = useState<any>();
// 获取到所有的微应用和非微应用
const [microRoutes, routes] = filterRoutChildren(children as ReactElement);
const onMicroAppChange = (micrApp: any) => {
setMicrAppInstance(micrApp);
};
// 检测微应用的pathname是不是唯一
useEffect(() => {
if (!Array.isArray(microRoutes)) {
return;
}
routeMap.current.clear();
microRoutes.forEach((route: ReactElement) => {
const { props } = route;
const { pathname } = props || {};
if (routeMap.current.has(pathname)) {
console.error('微应用的pathname必须唯一');
} else {
routeMap.current.set(pathname, route);
}
});
}, [microRoutes]);
useEffect(() => {
const microRoute = microRoutes.filter(route => {
const { props } = route;
const { pathname } = props || {};
return routePathname.includes(pathname);
});
// 如果存在多个相同的,只取第一个;
if (microRoute.length) {
setMicrApp(microRoute[0]);
} else {
setMicrApp(null);
}
}, [microRoutes, routePathname]);
// 主要解决qiankun bug, 加载新的微应用之前需要卸载之前的微应用,不然就报错
useEffect(() => {
return () => {
if (micrAppInstance && typeof micrAppInstance.unmount === 'function') {
micrAppInstance.unmount();
};
};
}, [micrAppInstance]);
return (
<>
<Suspense fallback={(<Spin spinning={true} tip='子应用正在加载中'/>)}>
{
micrApp ? cloneElement(micrApp as any, { onMicroAppChange }) : routes.map(route => {
return route;
})
}
</Suspense>
</>
);
};
export default MicroApplicationRouter;
该组件是为了判断何时加载子应用何时加载父应用以及采用单例设计模式来设计同时只有一个子应用被加载
4.在创建好的Qiankun目录中创建white.config.tsx, 并将下面这段代码黏贴进去
// 微应用白名单
const whites = {
React: 'https://staticres.linhuiba.com/libs/react/17.0.2/react..production.min..js',
ReactDom: 'https://staticres.linhuiba.com/libs/react-dom/17.0.2/react-dom.production.min..js',
History: 'https://staticres.linhuiba.com/libs/history/5.2.0/history.production.min..js',
ReactRouter: 'https://staticres.linhuiba.com/libs/react-router/6.2.1/react-router.production.min..js',
ReactRouterDom: 'https://staticres.linhuiba.com/libs/react-router-dom/6.2.1/react-router-dom.production.min..js',
ReactRedux: 'https://staticres.linhuiba.com/libs/react-redux/7.2.6/react-redux.min.js',
axios: 'https://staticres.linhuiba.com/libs/axios/0.24.0/axios.min.js',
bigdata: 'https://staticres.linhuiba.com/libs/lhb-bigdata/1.0.3/lhb-bigdata.js'
};
// 微应用生产环境白名单
const minWhites = {
React: 'https://staticres.linhuiba.com/libs/react/17.0.2/react.development.js',
ReactDom: 'https://staticres.linhuiba.com/libs/react-dom/17.0.2/react-dom.development.js',
History: 'https://staticres.linhuiba.com/libs/history/5.2.0/history.development.js',
ReactRouter: 'https://staticres.linhuiba.com/libs/react-router/6.2.1/react-router.development.js',
ReactRouterDom: 'https://staticres.linhuiba.com/libs/react-router-dom/6.2.1/react-router-dom.development.js',
ReactRedux: 'https://staticres.linhuiba.com/libs/react-redux/7.2.6/react-redux.js',
axios: 'https://staticres.linhuiba.com/libs/axios/0.24.0/axios.js'
};
export {
whites,
minWhites
};
该白名单是为了告诉qiankun哪些应用不需要进行劫持
pathname需要和之前定义的basename保持一致
entry则需要和永国南边约定好是主应用用来转发到子应用的最好和子应用的publicpath保持一致
子应用需要支持跨域
由于子应用是嵌套在主应用的,就会导致请求其实是从主应用域名中发出去的,之前的主应用是没有对子应用访问服务端的特殊前缀做ngnix转发的因此会导致主应用访问由跨域问题,所以主应用需要解决子应用所有的代理转发前缀,子应用已经在k8s加过的转发需要在主应用上重复加一次
子应用打包和其他打包方式的区别
由于子应用加了publicpath会导致子应用在自己单独运行时是会报错的,以资源微服务为例配置了publicpath后首先ngnix会去寻找该路径是否有代理,如果没有代理久会去判断该路径是否存在,如果存在久会找到当前路径的index.html,如果没有找到那么久报错,因此子应用打包方式和主应用打包方式是不同的,需要和永国进行商量配置,jekens打包配置如下图所示
如何解决样式隔离带来的组件库冲突问题
qiankun 将会自动隔离微应用之间的样式(开启沙箱的情况下),你可以通过手动的方式确保主应用与微应用之间的样式隔离。比如给主应用的所有样式添加一个前缀,或者假如你使用了 ant-design 这样的组件库,你可以通过这篇文档中的配置方式给主应用样式自动添加指定的前缀。
以 antd 为例:
1.修改antd打包前缀
2.配置 antd ConfigProvider
import { ConfigProvider } from 'antd';
f (!window.__POWERED_BY_QIANKUN__) {
ConfigProvider.config({
prefixCls: 'resant',
});
}
<ConfigProvider locale={zhCN} prefixCls='resant'
getPopupContainer={node => {
if (node) {
return node.parentNode as any;
}
return document.body;
}}
>
{
window.__POWERED_BY_QIANKUN__ ? (
<Provider store={store}>
<App />
</Provider>
) : <App />
}
</ConfigProvider>
/**
* 统一定制弹出框
*/
import React from 'react';
import { Modal as ATModal } from 'antd';
import { ModalCustomProps } from './ts-config';
import styles from './index.module.less';
const Modal: React.FC<ModalCustomProps> = (props) => {
const { visible, open, ...restProps } = props;
return (
<ATModal
className={styles['modal']}
{...restProps}
open={visible || open}
getContainer={false} /** 先上线后面再来考虑如何处理微前端开启沙箱带来的问题 */
/>
);
};
export default Modal;
详细文档参考 antd 官方指南。
在最新的 qiankun 版本中,你也可以尝试通过配置
{ sandbox : { experimentalStyleIsolation: true } }
的方式开启运行时的 scoped css 功能,从而解决应用间的样式隔离问题。
如何解决主应用和子应用路由冲突的问题
由于我们公司对路由跳转采用的是订阅发布模式进行监听路由跳转主要是通过document进行订阅发布,因此子应用能够监听到主应用到路由跳转就会导致路由跳转有问题
1.改造app.tsx如图所示
2.改造dispatch Navigate
3.改造onNavigate
如何解决由于运营商动态插入的脚本加载异常导致微应用加载失败的问题
运营商插入的脚本通常会用 async 标记从而避免 block 微应用的加载,这种通常没问题,如:
但如果有些插入的脚本不是被标记成 async 的,这类脚本一旦运行失败,将会导致整个应用被 block 且后续的脚本也不再执行。我们可以通过以下几个方式来解决这个问题:
使用自定义的 getTemplate 方法
通过自己实现的 getTemplate 方法过滤微应用 HTML 模板中的异常脚本
import { start } from 'qiankun';
start({
getTemplate(tpl) {
return tpl.replace('<script src="/to-be-replaced.js"><script>', '');
},
});
如何提取出公共的依赖库?
不要共享运行时,即便所有的团队都是用同一个框架。- 微前端
虽然共享依赖并不建议,但如果你真的有这个需求,你可以在微应用中将公共依赖配置成 external,然后在主应用中导入这些公共依赖。
目前主要是通过webpack的external参数进行提取依赖然后通过公司cdn链接进行引用这些库
如何解决拉取微应用 entry 时 cookie 未携带的问题
因为拉取微应用 entry 的请求都是跨域的,所以当你的微应用是依赖 cookie (如登陆鉴权)的情况下,你需要通过自定义 fetch 的方式,开启 fetch 的 cors 模式:
import { loadMicroApp } from 'qiankun';
loadMicroApp(app, {
fetch(url, ...args) {
// 给指定的微应用 entry 开启跨域请求
if (url === 'http://app.alipay.com/entry.html') {
return window.fetch(url, {
...args,
mode: 'cors',
credentials: 'include',
});
}
return window.fetch(url, ...args);
},
});
如何实现主应用和子应用之间的通信
1.可以通过globalState模式进行通信
2.可以通过主应用的全局store采用redux来进行通信
3.可以通过订阅发布document-event来实现通信
下面主要讲一下qiankun自带的globalState进行通信
主应用
import { useRef } from 'react;
const microAppRef = useRef<any>();
microAppRef.setGlobalState({name: zgl, age: 25})
<MicroApplicationRouter>
{process.env.NODE_ENV === 'production'
? <MicroApplicationRoute
pathname='/resource'
entry='/res-micro-app/'
/>
: <MicroApplicationRoute
pathname='/resource'
ref={microAppRef }
entry='http://localhost:8006'
/>
}
{children}
</MicroApplicationRouter>
子应用
export async function mount(props) {
console.log(props, '6666111');
console.log('微服务正在挂载中');
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
render(props);
}