Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor-microapp #2853

Merged
merged 5 commits into from
Dec 25, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions examples/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"start:react": "cross-env MODE=react webpack-dev-server",
"start:vue": "cross-env MODE=vue webpack-dev-server",
"start:vue3": "cross-env MODE=vue3 webpack-dev-server",
"start:multiple": "cross-env MODE=multiple webpack-dev-server",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\""
},
"author": "",
"devDependencies": {
Expand All @@ -27,8 +28,8 @@
},
"dependencies": {
"qiankun": "^2.10.10",
"react": "^16.13.1",
bravepg marked this conversation as resolved.
Show resolved Hide resolved
"react-dom": "^16.13.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"vue": "^3.3.9",
"zone.js": "^0.10.2",
"@vue/composition-api": "1.7.2"
Expand Down
64 changes: 46 additions & 18 deletions examples/main/render/ReactRender.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
import React from 'react';
import ReactDOM from 'react-dom';

/**
* 渲染子应用
*/
function Render(props) {
const { loading } = props;

return (
<>
{loading && <h4 className="subapp-loading">Loading...</h4>}
<div id="subapp-viewport" />
</>
);
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom/client';
import { MicroApp } from '../../../packages/ui-bindings/react/dist/esm/';
import '../index.less';

const root = ReactDOM.createRoot(
document.getElementById('subapp-container'),
);
const sidemenu = document.querySelector('.mainapp-sidemenu');

const microApps = [
{ name: 'react15', entry: '//localhost:7102' },
{ name: 'react16', entry: '//localhost:7100' },
];

function App() {
const [appName, setAppName] = useState('');

const handleMenuClick = (e) => {
const app = microApps.find((app) => app.name === e.target.dataset.value);
if (app && app.name !== appName) {
setAppName(app.name);
} else {
console.log('not found any app');
}
}

useEffect(() => {
sidemenu.addEventListener('click', handleMenuClick);

return () => {
sidemenu.removeEventListener('click', handleMenuClick);
}
}, []);

if (appName) {
const appEntry = microApps.find((app) => app.name === appName)?.entry;
return <MicroApp name={appName} entry={appEntry} autoCaptureError />;
}

return null;
}

export default function render({ loading }) {
const container = document.getElementById('subapp-container');
ReactDOM.render(<Render loading={loading} />, container);
function reactRender() {
// 将组件挂载到指定的节点上
root.render(<App />);
}

reactRender();
2 changes: 2 additions & 0 deletions examples/main/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ const path = require('path');

const modeEntryMap = {
multiple: './multiple.js',
react: './render/ReactRender.jsx',
vue: './render/VueRender.js',
vue3: './render/Vue3Render.js',
undefined: './render/VanillaRender.js',
}

const modeHTMLMap = {
multiple: './multiple.html',
react: './index.html',
vue: './index.html',
vue3: './index.html',
undefined: './index.html',
Expand Down
5 changes: 3 additions & 2 deletions examples/react15/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@
},
"browserslist": [
"last 2 Chrome versions"
]
}
],
"repository": "[email protected]:umijs/qiankun.git"
}
2 changes: 1 addition & 1 deletion examples/react16/.rescriptsrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const QiankunPlugin = require('../../packages/webpack-plugin/dist/cjs');
const { QiankunPlugin } = require('../../packages/webpack-plugin/dist/cjs');


module.exports = {
Expand Down
10 changes: 4 additions & 6 deletions packages/ui-bindings/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@
"author": "Bravepg",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.11",
"@qiankunjs/ui-shared": "workspace:^"
"@qiankunjs/ui-shared": "workspace:^",
"lodash": "^4.17.11"
},
"devDependencies": {
"@types/react": "^18.0.0",
"eslint-plugin-react": "^7.33.2",
"qiankun": "workspace:^",
"react": "^18.2.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"peerDependencies": {
"qiankun": "3.0.0-rc.15",
"@qiankunjs/ui-shared": "0.0.0",
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
Expand Down
42 changes: 15 additions & 27 deletions packages/ui-bindings/react/src/MicroApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,15 @@ function useDeepCompare<T>(value: T): T {
}

export const MicroApp = forwardRef((componentProps: Props, componentRef: Ref<MicroAppType | undefined>) => {
const { name, loader, errorBoundary, wrapperClassName, className, ...restProps } = componentProps;

const propsFromParams = omitSharedProps(restProps);
const { name, autoSetLoading, autoCaptureError, wrapperClassName, className, loader, errorBoundary } = componentProps;

const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error>();
const containerRef = useRef<HTMLDivElement>(null);
const microAppRef = useRef<MicroAppType>();

// 未配置自定义 errorBoundary 且开启了 autoCaptureError 场景下,使用插件默认的 errorBoundary,否则使用自定义 errorBoundary
const microAppErrorBoundary =
errorBoundary || (propsFromParams.autoCaptureError ? (e) => <ErrorBoundary error={e} /> : null);
const microAppErrorBoundary = errorBoundary || (autoCaptureError ? (e) => <ErrorBoundary error={e} /> : null);

// 配置了 errorBoundary 才改 error 状态,否则直接往上抛异常
const setComponentError = (e: Error | undefined) => {
Expand All @@ -48,27 +47,21 @@ export const MicroApp = forwardRef((componentProps: Props, componentRef: Ref<Mic
}
};

const containerRef = useRef<HTMLDivElement>(null);
const microAppRef = useRef<MicroAppType>();

useImperativeHandle(componentRef, () => microAppRef.current);

useEffect(() => {
mountMicroApp({
props: componentProps,
void mountMicroApp({
container: containerRef.current!,
setMicroApp(app) {
microAppRef.current = app;
},
setLoading: (l) => {
setLoading(l);
},
microApp: microAppRef.current,
componentProps,
setMicroApp: (app) => (microAppRef.current = app),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

应该是 mountMicroApp 方法返回 microApp 实例,然后调用方 setMicroApp,而不应该是 mountMicroApp 里面做这个事情

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

嗯~听上去更舒服,我重写下

setLoading,
setError: setComponentError,
});

return () => {
const microApp = microAppRef.current;
if (microApp) {
if (microApp && microApp.getStatus() === 'MOUNTED') {
// 微应用 unmount 是异步的,中间的流转状态不能确定,所有需要一个标志位来确保 unmount 开始之后不会再触发 update
microApp._unmounting = true;
unmountMicroApp(microApp).catch((e: Error) => {
Expand All @@ -82,22 +75,17 @@ export const MicroApp = forwardRef((componentProps: Props, componentRef: Ref<Mic
useEffect(() => {
updateMicroApp({
name,
propsFromParams,
getMicroApp() {
return microAppRef.current;
},
setLoading: (l) => {
setLoading(l);
},
key: 'react',
microApp: microAppRef.current,
microAppProps: omitSharedProps(componentProps),
setLoading,
});

return noop;
}, [useDeepCompare(propsFromParams)]);
}, [useDeepCompare(omitSharedProps(componentProps))]);

// 未配置自定义 loader 且开启了 autoSetLoading 场景下,使用插件默认的 loader,否则使用自定义 loader
const microAppLoader =
loader || (propsFromParams.autoSetLoading ? (loadingStatus) => <MicroAppLoader loading={loadingStatus} /> : null);
loader || (autoSetLoading ? (loadingStatus) => <MicroAppLoader loading={loadingStatus} /> : null);

const microAppWrapperClassName = wrapperClassName
? `${wrapperClassName} qiankun-micro-app-wrapper`
Expand Down
65 changes: 36 additions & 29 deletions packages/ui-bindings/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,48 +27,59 @@ export type SharedSlots<T> = {
errorBoundary?: (error: Error) => T;
};

export async function unmountMicroApp(microApp: MicroAppType) {
await microApp.mountPromise.then(() => microApp.unmount());
}

export const omitSharedProps = (props: Partial<SharedProps>) => {
return omit(props, ['wrapperClassName', 'className', 'lifeCycles', 'settings', 'entry', 'name']);
};

export function mountMicroApp({
export async function unmountMicroApp(microApp: MicroAppType) {
await microApp.mountPromise.then(() => microApp.unmount());
}

export async function mountMicroApp({
container,
componentProps,
microApp: prevMicroApp,
setMicroApp,
setLoading,
setError,
setMicroApp,
container,
props,
}: {
container: HTMLDivElement;
microApp?: MicroAppType;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个参数应该叫 prevMicroApp

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

合理 我改下

componentProps: SharedProps;
setMicroApp?: (app: MicroAppType) => void;
setLoading?: (loading: boolean) => void;
setError?: (error?: Error) => void;
setMicroApp?: (app?: MicroAppType) => void;
props: SharedProps;
container: HTMLDivElement;
}) {
const propsFromParams = omitSharedProps(props);
if (!componentProps.name || !componentProps.entry) {
console.error('the name and entry of MicroApp is needed');
bravepg marked this conversation as resolved.
Show resolved Hide resolved
return;
}

// 等待 prevMicroApp 卸载完成
if (prevMicroApp?._unmounting) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

如果 name 改变,等待前一个卸载完成

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里确定下 同一个组件,不同name渲染的时候,需要等下上一个应用卸载完成再 更改路由吗?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

是的,不然的话,上一个卸载在调用 unmount 的时候可能会出现找不到 dom 的情况

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

但是跳转 push 是在 主应用操作的, 而卸载的 promise 是在组件里面 await 的 。现在还应该还是先执行 push

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个只是解决以组件模式切换 name 时候的问题。你说的这个其实不需要关注,因为 push 导致的路由变化是异步的浏览器行为,我们不太可能也不应该等组件卸载完了,再让浏览器路由变化,不然一旦 unmount 时间比较久,浏览器感觉像是卡顿了一样。

await prevMicroApp.unmountPromise;
}

setError?.(undefined);
setLoading?.(true);

const microAppProps = omitSharedProps(componentProps);
const configuration = {
globalContext: window,
...(props.settings || {}),
...(componentProps.settings || {}),
};

const microApp = loadMicroApp(
{
name: props.name,
entry: props.entry,
name: componentProps.name,
entry: componentProps.entry,
container,
props: propsFromParams,
props: microAppProps,
},
configuration,
mergeWith(
{},
props.lifeCycles,
componentProps.lifeCycles,
(v1: LifeCycleFn<Record<string, unknown>>, v2: LifeCycleFn<Record<string, unknown>>) => concat(v1, v2),
),
);
Expand All @@ -77,7 +88,7 @@ export function mountMicroApp({

microApp.mountPromise
.then(() => {
if (props.autoSetLoading) {
if (componentProps.autoSetLoading) {
setLoading?.(false);
}
})
Expand All @@ -98,19 +109,15 @@ export function mountMicroApp({

export function updateMicroApp({
name,
getMicroApp,
microApp,
microAppProps,
setLoading,
propsFromParams,
key,
}: {
name?: string;
getMicroApp?: () => MicroAppType | undefined;
microApp?: MicroAppType;
microAppProps?: Record<string, unknown>;
setLoading?: (loading: boolean) => void;
propsFromParams?: Record<string, unknown>;
key?: string;
}) {
const microApp = getMicroApp?.();

if (microApp) {
if (!microApp._updatingPromise) {
// 初始化 updatingPromise 为 microApp.mountPromise,从而确保后续更新是在应用 mount 完成之后
Expand All @@ -122,7 +129,7 @@ export function updateMicroApp({
const canUpdate = (app: MicroAppType) => app.update && app.getStatus() === 'MOUNTED' && !app._unmounting;
if (canUpdate(microApp)) {
const props = {
...propsFromParams,
...microAppProps,
setLoading(l: boolean) {
setLoading?.(l);
},
Expand All @@ -132,11 +139,11 @@ export function updateMicroApp({
const updatingTimestamp = microApp._updatingTimestamp!;
if (Date.now() - updatingTimestamp < 200) {
console.warn(
`[@qiankunjs/${key}] It seems like microApp ${name} is updating too many times in a short time(200ms), you may need to do some optimization to avoid the unnecessary re-rendering.`,
`[@qiankunjs/ui-shared] It seems like microApp ${name} is updating too many times in a short time(200ms), you may need to do some optimization to avoid the unnecessary re-rendering.`,
);
}

console.info(`[@qiankunjs/${key}] MicroApp ${name} is updating with props: `, props);
console.info(`[@qiankunjs/ui-shared}] MicroApp ${name} is updating with props: `, props);
microApp._updatingTimestamp = Date.now();
}

Expand Down
9 changes: 3 additions & 6 deletions packages/ui-bindings/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,18 @@
"author": "linghaoSu",
"license": "MIT",
"dependencies": {
"@qiankunjs/ui-shared": "workspace:^",
"lodash": "^4.17.11",
"vue-demi": "^0.14.6",
"@qiankunjs/ui-shared": "workspace:^"
"vue-demi": "^0.14.6"
},
"devDependencies": {
"eslint-plugin-vue": "^9.18.1",
"qiankun": "workspace:^",
"vue": "^3.3.9",
"vue2": "npm:[email protected]"
},
"peerDependencies": {
"qiankun": "^3.0.0-rc.15",
"@vue/composition-api": "^1.7.2",
"vue": "^2.0.0 || >=3.0.0",
"@qiankunjs/ui-shared": "^0.0.0"
"vue": "^2.0.0 || >=3.0.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
Expand Down
Loading
Loading