Skip to content

Commit

Permalink
✨ support dynamic append in multiple mode (#427)
Browse files Browse the repository at this point in the history
* ✨ support dynamic append in multiple mode

* 🎨 pretty code

* 🎨 remove unused code
  • Loading branch information
kuitos authored Apr 15, 2020
1 parent 8da977d commit 7d046d1
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 52 deletions.
22 changes: 22 additions & 0 deletions examples/main/multiple.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>qiankun multiple demo</title>
<style>
body {
width: 100vw;
height: 100vh;
display: flex;
justify-content: space-around;
align-items: center;
}
</style>
</head>
<body>
<div id="react15">react loading...</div>
<div id="vue">vue loading...</div>

<script src="./multiple.js"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions examples/main/multiple.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { loadMicroApp } from '../../es';

const app1 = loadMicroApp(
{ name: 'react15', entry: '//localhost:7102', container: '#react15' },
{
singular: false,
sandbox: {
// strictStyleIsolation: true,
},
},
);

const app2 = loadMicroApp(
{ name: 'vue', entry: '//localhost:7101', container: '#vue' },
{
singular: false,
sandbox: {
// strictStyleIsolation: true,
},
},
);
1 change: 1 addition & 0 deletions examples/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "index.js",
"scripts": {
"start": "parcel index.html --port 7099",
"start:multiple": "parcel multiple.html --port 7099",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
Expand Down
25 changes: 10 additions & 15 deletions examples/react15/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,27 @@ import './public-path';
import 'antd/dist/antd.min.css';
import './index.css';

function storeTest(props) {
props.onGlobalStateChange((value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev), true);
props.setGlobalState({
ignore: props.name,
user: {
name: props.name,
},
});
}

export async function bootstrap() {
console.log('[react15] react app bootstraped');
}

export async function mount(props) {
console.log('[react15] props from main framework', props);
storeTest(props);

ReactDOM.render(<App />, document.getElementById('react15Root'));
const { container } = props;
ReactDOM.render(
<App />,
container ? container.querySelector('#react15Root') : document.getElementById('react15Root'),
);
import('./dynamic.css').then(() => {
console.log('[react15] dynamic style load');
});
}

export async function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById('react15Root'));
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(
container ? container.querySelector('#react15Root') : document.getElementById('react15Root'),
);
}

if (!window.__POWERED_BY_QIANKUN__) {
Expand Down
5 changes: 3 additions & 2 deletions examples/vue/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ Vue.use(ElementUI);
let router = null;
let instance = null;

function render() {
function render(props = {}) {
const { container } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/',
mode: 'history',
Expand All @@ -25,7 +26,7 @@ function render() {
router,
store,
render: h => h(App),
}).$mount('#app');
}).$mount(container ? container.querySelector('#app') : '#app');
}

if (!window.__POWERED_BY_QIANKUN__) {
Expand Down
1 change: 1 addition & 0 deletions examples/vue/vue.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ module.exports = {
},
// 自定义webpack配置
configureWebpack: {
devtool: 'source-map',
resolve: {
alias: {
'@': resolve('src'),
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"homepage": "https://github.com/kuitos/qiankun#readme",
"dependencies": {
"@babel/runtime": "^7.5.5",
"import-html-entry": "^1.6.0-1",
"import-html-entry": "^1.6.0-2",
"lodash": "^4.17.11",
"single-spa": "^5.3.1",
"tslib": "^1.10.0"
Expand Down
40 changes: 40 additions & 0 deletions src/sandbox/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @author Kuitos
* @since 2020-04-13
*/

import { isConstructable } from '../utils';

const boundValueSymbol = Symbol('bound value');

export function getTargetValue(target: any, value: any): any {
/*
仅绑定 !isConstructable && isCallable 的函数对象,如 window.console、window.atob 这类。目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断
@warning 这里不要随意替换成别的判断方式,因为可能触发一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于调用了 top window 对象触发的安全异常)
*/
if (typeof value === 'function' && !isConstructable(value)) {
if (value[boundValueSymbol]) {
return value[boundValueSymbol];
}

const boundValue = value.bind(target);
// some callable function has custom fields, we need to copy the enumerable props to boundValue. such as moment function.
Object.keys(value).forEach(key => (boundValue[key] = value[key]));
Object.defineProperty(value, boundValueSymbol, { enumerable: false, value: boundValue });
return boundValue;
}

return value;
}

const proxyGetterMap = new Map<WindowProxy, Record<PropertyKey, any>>();

export function getProxyPropertyGetter(proxy: WindowProxy, property: PropertyKey) {
const getters = proxyGetterMap.get(proxy) || ({} as Record<string, any>);
return getters[property as string];
}

export function setProxyPropertyGetter(proxy: WindowProxy, property: PropertyKey, getter: () => any) {
const prevGetters = proxyGetterMap.get(proxy) || {};
proxyGetterMap.set(proxy, { ...prevGetters, [property]: getter });
}
6 changes: 3 additions & 3 deletions src/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* @since 2019-04-11
*/
import { Freer, Rebuilder, SandBox } from '../interfaces';
import { patchAtBootstrapping, patchAtMounting } from './patchers';
import LegacySandbox from './legacy/sandbox';
import { patchAtBootstrapping, patchAtMounting } from './patchers';
import ProxySandbox from './proxySandbox';
import SnapshotSandbox from './snapshotSandbox';

Expand Down Expand Up @@ -38,7 +38,7 @@ export function createSandbox(appName: string, elementGetter: () => HTMLElement
}

// some side effect could be be invoked while bootstrapping, such as dynamic stylesheet injection with style-loader, especially during the development phase
const bootstrappingFreers = patchAtBootstrapping(appName, elementGetter, sandbox.proxy);
const bootstrappingFreers = patchAtBootstrapping(appName, elementGetter, sandbox.proxy, singular);

return {
proxy: sandbox.proxy,
Expand All @@ -64,7 +64,7 @@ export function createSandbox(appName: string, elementGetter: () => HTMLElement

/* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/
// render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用
mountingFreers = patchAtMounting(appName, elementGetter, sandbox.proxy);
mountingFreers = patchAtMounting(appName, elementGetter, sandbox.proxy, singular);

/* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------------*/
// 存在 rebuilder 则表明有些副作用需要重建
Expand Down
56 changes: 51 additions & 5 deletions src/sandbox/patchers/dynamicHeadAppend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { isFunction } from 'lodash';
import { checkActivityFunctions } from 'single-spa';
import { frameworkConfiguration } from '../../apis';
import { Freer } from '../../interfaces';
import { getTargetValue, setProxyPropertyGetter } from '../common';

const styledComponentSymbol = Symbol('styled-component');
const styledComponentSymbol = 'Symbol(styled-component-qiankun)';
const attachProxySymbol = 'Symbol(attach-proxy-qiankun)';

declare global {
interface HTMLStyleElement {
Expand Down Expand Up @@ -52,12 +54,14 @@ function setCachedRules(element: HTMLStyleElement, cssRules: CSSRuleList) {
* @param appWrapperGetter
* @param proxy
* @param mounting
* @param singular
*/
export default function patch(
appName: string,
appWrapperGetter: () => HTMLElement | ShadowRoot,
proxy: Window,
mounting = true,
singular = true,
): Freer {
let dynamicStyleSheetElements: Array<HTMLLinkElement | HTMLStyleElement> = [];

Expand All @@ -69,6 +73,13 @@ export default function patch(
case STYLE_TAG_NAME: {
const stylesheetElement: HTMLLinkElement | HTMLStyleElement = newChild as any;

if (!singular) {
// eslint-disable-next-line no-shadow
const { appWrapperGetter, dynamicStyleSheetElements } = element[attachProxySymbol];
dynamicStyleSheetElements.push(stylesheetElement);
return rawAppendChild.call(appWrapperGetter(), stylesheetElement) as T;
}

// check if the currently specified application is active
// While we switch page from qiankun app to a normal react routing page, the normal one may load stylesheet dynamically while page rendering,
// but the url change listener must to wait until the current call stack is flushed.
Expand All @@ -89,9 +100,19 @@ export default function patch(
case SCRIPT_TAG_NAME: {
const { src, text } = element as HTMLScriptElement;

let realAppWrapperGetter = appWrapperGetter;
let realProxy = proxy;

if (!singular) {
// eslint-disable-next-line no-shadow
const { appWrapperGetter, proxy } = element[attachProxySymbol];
realAppWrapperGetter = appWrapperGetter;
realProxy = proxy;
}

const { fetch } = frameworkConfiguration;
if (src) {
execScripts(null, [src], proxy, { fetch }).then(
execScripts(null, [src], realProxy, { fetch }).then(
() => {
// we need to invoke the onload event manually to notify the event listener that the script was completed
// here are the two typical ways of dynamic script loading
Expand All @@ -115,12 +136,12 @@ export default function patch(
);

const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`);
return rawAppendChild.call(appWrapperGetter(), dynamicScriptCommentElement) as T;
return rawAppendChild.call(realAppWrapperGetter(), dynamicScriptCommentElement) as T;
}

execScripts(null, [`<script>${text}</script>`], proxy).then(element.onload, element.onerror);
execScripts(null, [`<script>${text}</script>`], realProxy).then(element.onload, element.onerror);
const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun');
return rawAppendChild.call(appWrapperGetter(), dynamicInlineScriptCommentElement) as T;
return rawAppendChild.call(realAppWrapperGetter(), dynamicInlineScriptCommentElement) as T;
}

default:
Expand All @@ -139,6 +160,31 @@ export default function patch(
return rawHeadRemoveChild.call(this, child) as T;
};

if (!singular) {
setProxyPropertyGetter(proxy, 'document', () => {
return new Proxy(document, {
get(target: Document, property: PropertyKey): any {
if (property === 'createElement') {
return function createElement(tagName: string, options?: any) {
const element = document.createElement(tagName, options);

if (tagName?.toLowerCase() === 'style' || tagName?.toLowerCase() === 'script') {
Object.defineProperty(element, attachProxySymbol, {
value: { name: appName, proxy, appWrapperGetter, dynamicStyleSheetElements },
enumerable: false,
});
}

return element;
};
}

return getTargetValue(document, (<any>target)[property]);
},
});
});
}

return function free() {
HTMLHeadElement.prototype.appendChild = rawHeadAppendChild;
HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild;
Expand Down
6 changes: 4 additions & 2 deletions src/sandbox/patchers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,25 @@ export function patchAtMounting(
appName: string,
elementGetter: () => HTMLElement | ShadowRoot,
proxy: Window,
singular: boolean,
): Freer[] {
return [
patchTimer(),
patchWindowListener(),
patchHistoryListener(),
patchDynamicAppend(appName, elementGetter, proxy),
patchDynamicAppend(appName, elementGetter, proxy, true, singular),
];
}

export function patchAtBootstrapping(
appName: string,
elementGetter: () => HTMLElement | ShadowRoot,
proxy: Window,
singular: boolean,
): Freer[] {
return [
process.env.NODE_ENV === 'development'
? patchDynamicAppend(appName, elementGetter, proxy, false)
? patchDynamicAppend(appName, elementGetter, proxy, false, singular)
: () => () => noop,
];
}
Loading

1 comment on commit 7d046d1

@github-actions
Copy link

Choose a reason for hiding this comment

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

Deploy preview for website ready!

Built with commit 7d046d1

https://https://qiankun-qbhpxk29l.now.sh

Please sign in to comment.