Skip to content

Commit

Permalink
🎉
Browse files Browse the repository at this point in the history
  • Loading branch information
kuitos committed May 15, 2019
1 parent 9c57b4c commit a9c4e14
Show file tree
Hide file tree
Showing 18 changed files with 818 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
pids
logs
node_modules
npm-debug.log
coverage/
run
dist
public
.DS_Store
.nyc_output
.basement
config.local.js
.umi
.umi-production
.idea/
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
An implementation of [Micro Frontends](https://micro-frontends.org/), based on [single-spa](https://github.com/CanopyTax/single-spa), but made it production-ready.

## Roadmap

- [ ] core lib
- [ ] umi-plugin-single-spa integrated
42 changes: 42 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "qiankun",
"version": "0.0.1",
"description": "An completed implementation of Micro Frontends",
"main": "index.js",
"scripts": {
"build": "npm run build:esm && npm run build:cjs",
"build:esm": "rm -fr esm && tsc",
"build:cjs": "rm -fr lib && tsc -p tsconfig.cjs.json",
"prepush": "tslint ",
"lint": "tslint 'src/**/*.ts'",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/kuitos/qiankun.git"
},
"files": [
"esm",
"lib"
],
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/kuitos/qiankun/issues"
},
"homepage": "https://github.com/kuitos/qiankun#readme",
"dependencies": {
"import-html-entry": "^0.4.11",
"lodash": "^4.17.11",
"single-spa": "^4.3.3",
"tslib": "^1.9.3"
},
"devDependencies": {
"@types/lodash": "^4.14.129",
"@types/node": "^12.0.2",
"husky": "^2.3.0",
"tslint": "^5.16.0",
"tslint-eslint-rules": "^5.4.0",
"typescript": "^3.4.5"
}
}
16 changes: 16 additions & 0 deletions src/effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @author Kuitos
* @since 2019-02-19
*/
import { getMountedApps, navigateToUrl } from 'single-spa';

export function runDefaultMountEffects(defaultAppLink: string) {

window.addEventListener('single-spa:no-app-change', () => {
const mountedApps = getMountedApps();
if (!mountedApps.length) {
navigateToUrl(defaultAppLink);
}
}, { once: true });

}
62 changes: 62 additions & 0 deletions src/hijackers/historyListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* @author Kuitos
* @since 2019-04-11
*/

import { isFunction, noop } from 'lodash';

export default function hijack() {

// FIXME umi unmount feature request
// @see http://gitlab.alipay-inc.com/bigfish/bigfish/issues/1154
let rawHistoryListen = (..._: any[]) => noop;
const historyListeners: Array<typeof noop> = [];
const historyUnListens: Array<typeof noop> = [];

if ((window as any).g_history && isFunction((window as any).g_history.listen)) {

rawHistoryListen = (window as any).g_history.listen.bind((window as any).g_history);

(window as any).g_history.listen = (listener: typeof noop) => {

historyListeners.push(listener);

const unListen = rawHistoryListen(listener);
historyUnListens.push(unListen);

return () => {
unListen();
historyUnListens.splice(historyUnListens.indexOf(unListen), 1);
historyListeners.splice(historyListeners.indexOf(listener), 1);
};
};
}

return function free() {

let rebuild = noop;

/*
还存在余量 listener 表明未被卸载,存在两种情况
1. 应用在 unmout 时未正确卸载 listener
2. listener 是应用 mount 之前绑定的,
第二种情况下应用在下次 mount 之前需重新绑定该 listener
*/
if (historyListeners.length) {
rebuild = () => {
// 必须使用 window.g_history.listen 的方式重新绑定 listener,从而能保证 rebuild 这部分也能被捕获到,否则在应用卸载后无法正确的移除这部分副作用
historyListeners.forEach(listener => (window as any).g_history.listen(listener));
};
}

// 卸载余下的 listener
historyUnListens.forEach(unListen => unListen());

// restore
if ((window as any).g_history && isFunction((window as any).g_history.listen)) {
(window as any).g_history.listen = rawHistoryListen;
}

return rebuild;
};
}
18 changes: 18 additions & 0 deletions src/hijackers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @author Kuitos
* @since 2019-04-11
*/

import hijackHistoryListener from './historyListener';
import hijackTimer from './timer';
import hijackWindowListener from './windowListener';

export function hijack() {

return [
hijackTimer(),
hijackWindowListener(),
hijackHistoryListener(),
];

}
45 changes: 45 additions & 0 deletions src/hijackers/timer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @author Kuitos
* @since 2019-04-11
*/

import { noop } from 'lodash';
import { sleep } from '../utils';

export default function hijack() {

const rawWindowInterval = window.setInterval.bind(window);
const rawWindowTimeout = window.setTimeout.bind(window);
const timerIds: number[] = [];
const intervalIds: number[] = [];

window.setInterval = (...args: any[]) => {
// @ts-ignore
const intervalId = rawWindowInterval(...args);
intervalIds.push(intervalId);
return intervalId;
};

window.setTimeout = (...args: any[]) => {
// @ts-ignore
const timerId = rawWindowTimeout(...args);
timerIds.push(timerId);
return timerId;
};

return function free() {
window.setInterval = rawWindowInterval;
window.setTimeout = rawWindowTimeout;

timerIds.forEach(async id => {
// 延迟 timeout 的清理,因为可能会有动画还没完成
await sleep(500);
window.clearTimeout(id);
});
intervalIds.forEach(id => {
window.clearInterval(id);
});

return noop;
};
}
38 changes: 38 additions & 0 deletions src/hijackers/windowListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @author Kuitos
* @since 2019-04-11
*/

import { noop } from 'lodash';

export default function hijack() {

const listenerMap = new Map<string, EventListenerOrEventListenerObject[]>();
const rawAddEventListener = window.addEventListener;
const rawRemoveEventListener = window.removeEventListener;

window.addEventListener =
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => {
const listeners = listenerMap.get(type) || [];
listenerMap.set(type, [...listeners, listener]);
return rawAddEventListener.call(window, type, listener, options);
};

window.removeEventListener =
(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => {
const storedTypeListeners = listenerMap.get(type);
if (storedTypeListeners && storedTypeListeners.length && storedTypeListeners.indexOf(listener) !== -1) {
storedTypeListeners.splice(storedTypeListeners.indexOf(listener), 1);
}
return rawRemoveEventListener.call(window, type, listener, options);
};

return function free() {

listenerMap.forEach((listeners, type) => listeners.forEach(listener => window.removeEventListener(type, listener)));
window.addEventListener = rawAddEventListener.bind(window);
window.removeEventListener = rawRemoveEventListener.bind(window);

return noop;
};
}
81 changes: 81 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @author Kuitos
* @since 2019-04-25
*/

import importHTML from 'import-html-entry';
import { isFunction } from 'lodash';
import { registerApplication, start as startSpa } from 'single-spa';
import prefetch from './prefetch';
import { genSandbox } from './sandbox';

import { sleep } from './utils';

export type RenderFunction = (props?: { appContent: string, loading: boolean }) => any;
export type ActiveRule = (app: RegistrableApp) => boolean;
export type RegistrableApp = {
appName: string; // 应用名(ID),也可当做应用的产品码来用
entryHTML: string; // 应用入口 HTML
routerPrefix: string; // 应用路由前缀
props?: object;
};

interface Options {
renderFunction: RenderFunction;
activeRule: ActiveRule;
}

export function registerMicroApps(apps: RegistrableApp[], options: Options) {

const { renderFunction, activeRule } = options;

apps.forEach(app => {

const { appName, entryHTML, props = {} } = app;

// TODO 优化:将 prefetch 移到第一个应用 mount 之后
prefetch(entryHTML);

registerApplication(appName,

async () => {

// 获取入口 html 模板及脚本加载器
const { template: appContent, execScripts } = await importHTML(entryHTML);
// 第一次加载设置应用可见区域 dom 结构
// 确保每次应用加载前容器 dom 结构已经设置完毕
renderFunction({ appContent, loading: true });

const { sandbox, mount: mountSandbox, unmount: unmountSandbox } = genSandbox(appName);
// 等待 300 ms,确保菜单切换动画完成
await sleep(300);
// 获取 模块/应用 导出的 lifecycle hooks
const { bootstrap: exportedBootstrap, mount, unmount } = await execScripts(sandbox);
if (!isFunction(exportedBootstrap) || !isFunction(mount) || !isFunction(unmount)) {
throw new Error(`You need to export the functional lifecycles in ${appName} entry`);
}

return {
bootstrap: [exportedBootstrap],
mount: [
mountSandbox,
// 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕
async () => renderFunction({ appContent, loading: false }),
mount,
],
unmount: [
unmount,
unmountSandbox,
],
};
},

() => activeRule(app),
props,
);
});
}

export function start() {
startSpa();
}
23 changes: 23 additions & 0 deletions src/prefetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @author Kuitos
* @since 2019-02-26
*/

import importHTML from 'import-html-entry';
import { noop } from 'lodash';

/**
* 预加载静态资源,不兼容 requestIdleCallback 的浏览器不做任何动作
* @param entryHTML
*/
export default function prefetch(entryHTML: string) {

const requestIdleCallback = window.requestIdleCallback || noop;

requestIdleCallback(async () => {
const { getExternalScripts, getExternalStyleSheets } = await importHTML(entryHTML);
requestIdleCallback(getExternalStyleSheets);
requestIdleCallback(getExternalScripts);
});

}
Loading

0 comments on commit a9c4e14

Please sign in to comment.