Skip to content

Commit

Permalink
feat: 1.x support load umd sub app (#176)
Browse files Browse the repository at this point in the history
* feat: support load umd sub app

* chore: lint

* test: test case for umd loader

* chore: version

* fix: parseUrl

* chore: typo

* [1.x]feat: set loadMode when load micro app (#181)

* feat: set loadMode when load micro app

* chore: mode value
  • Loading branch information
ClarkXia committed Oct 26, 2020
1 parent 011fc34 commit da5b239
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 22 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ice/stark",
"version": "1.5.6",
"version": "1.6.0",
"description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.",
"scripts": {
"build": "rm -rf lib && tsc",
Expand Down
32 changes: 23 additions & 9 deletions src/AppRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { appendAssets, emptyAssets, cacheAssets, getEntryAssets, getUrlAssets }
import { setCache, getCache } from './util/cache';
import { callAppEnter, callAppLeave, cacheApp, isCached, AppLifeCycleEnum } from './util/appLifeCycle';
import { callCapturedEventListeners } from './util/capturedListeners';
import ModuleLoader from './util/umdLoader';

import isEqual = require('lodash.isequal');

Expand Down Expand Up @@ -62,6 +63,9 @@ export interface AppConfig {
component?: React.ReactElement;
render?: (props?: AppRouteComponentProps) => React.ReactElement;
cache?: boolean;
umd?: boolean; // mark if sub-application is an umd module
name?: string; // used to mark a umd module, recommaded config it as same as webpack.output.library
customProps?: object; // custom props passed from framework app to sub app
}

// from AppRouter
Expand All @@ -76,6 +80,7 @@ export interface AppRouteProps extends AppConfig {
) => boolean;
componentProps?: AppRouteComponentProps;
clearCacheRoot?: () => void;
moduleLoader?: ModuleLoader;
}

export function converArray2String(list: string | (string | PathData)[]) {
Expand Down Expand Up @@ -121,6 +126,8 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta

private prevAppConfig: AppConfig = null;

private rootElement: HTMLElement;

static defaultProps = {
exact: false,
strict: false,
Expand Down Expand Up @@ -221,9 +228,9 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta

// reCreate rootElement to remove sub-application instance,
// rootElement is created for render sub-application
const rootElement: HTMLElement = this.reCreateElementInBase(rootId);
this.rootElement = this.reCreateElementInBase(rootId);

setCache('root', rootElement);
setCache('root', this.rootElement);

this.loadNextApp();
};
Expand All @@ -240,7 +247,16 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta
cache,
sandbox,
path,
umd,
name,
customProps,
} = this.props;
// set loadMode when load micro app
if (umd) {
setCache('loadMode', 'umd');
} else {
setCache('loadMode', sandbox ? 'sandbox' : 'script');
}
if (sandbox) {
if (typeof sandbox === 'function') {
// eslint-disable-next-line new-cap
Expand Down Expand Up @@ -280,14 +296,12 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta
!cached && handleLoading(true);

const currentAppConfig: AppConfig = this.triggerOnAppEnter();

try {
let appAssets = null;
if (entry || entryContent) {
// entry for fetch -> process -> append
const rootElement = getCache('root');
appAssets = await getEntryAssets({
root: rootElement,
root: this.rootElement,
entry,
href: location.href,
entryContent,
Expand All @@ -298,7 +312,7 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta
appAssets = getUrlAssets(urls);
}
if (appAssets && !cached) {
await appendAssets(appAssets, this.appSandbox);
await appendAssets(appAssets, name || assetsCacheKey, umd, this.appSandbox);
}
// if AppRoute is unmounted, or current app is not the latest app, cancel all operations
if (this.unmounted || this.prevAppConfig !== currentAppConfig) return;
Expand All @@ -314,7 +328,7 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta
console.warn('[icestark] please trigger app unmount manually via registerAppLeave, app path: ', path);
}
// trigger sub-application render
callAppEnter();
callAppEnter({ container: this.rootElement, customProps });

// cancel loading after handleAssets
handleLoading(false);
Expand Down Expand Up @@ -342,7 +356,7 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta
* reset this.prevAppConfig
*/
triggerPrevAppLeave = (): void => {
const { onAppLeave, triggerLoading } = this.props;
const { onAppLeave, triggerLoading, customProps } = this.props;
if (this.appSandbox) {
this.appSandbox.clear();
this.appSandbox = null;
Expand All @@ -356,7 +370,7 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta
}
// reset loading state when leave app
triggerLoading(false);
callAppLeave();
callAppLeave({ customProps, container: this.rootElement });
};

/**
Expand Down
13 changes: 9 additions & 4 deletions src/util/appLifeCycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export enum AppLifeCycleEnum {
AppLeave = 'appLeave',
}

interface AppLifecycleProps {
container: HTMLElement;
customProps: object;
}

export function cacheApp(cacheKey: string) {
[AppLifeCycleEnum.AppEnter, AppLifeCycleEnum.AppLeave].forEach(lifeCycle => {
const lifeCycleCacheKey = `cache_${cacheKey}_${lifeCycle}`;
Expand All @@ -28,25 +33,25 @@ export function isCached(cacheKey: string) {
return !!getCache(`cache_${cacheKey}_${AppLifeCycleEnum.AppEnter}`);
}

export function callAppEnter() {
export function callAppEnter(props?: AppLifecycleProps) {
const appEnterKey = AppLifeCycleEnum.AppEnter;
const registerAppEnterCallback = getCache(appEnterKey);

if (registerAppEnterCallback) {
registerAppEnterCallback();
registerAppEnterCallback(props);
setCache(appEnterKey, null);
}
}

export function callAppLeave() {
export function callAppLeave(props?: AppLifecycleProps) {
// resetCapturedEventListeners when app change, remove react-router/vue-router listeners
resetCapturedEventListeners();

const appLeaveKey = AppLifeCycleEnum.AppLeave;
const registerAppLeaveCallback = getCache(appLeaveKey);

if (registerAppLeaveCallback) {
registerAppLeaveCallback();
registerAppLeaveCallback(props);
setCache(appLeaveKey, null);
}
}
64 changes: 64 additions & 0 deletions src/util/global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// fork: https://github.com/systemjs/systemjs/blob/master/src/extras/global.js

// safari unpredictably lists some new globals first or second in object order
let firstGlobalProp;
let secondGlobalProp;
let lastGlobalProp;
let noteGlobalKeys = [];
const isIE11 = typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Trident') !== -1;

function shouldSkipProperty(p, globalWindow) {
// eslint-disable-next-line no-prototype-builtins
return !globalWindow.hasOwnProperty(p)
|| !isNaN(p) && p < (globalWindow as any).length
|| isIE11 && globalWindow[p] && typeof window !== 'undefined' && globalWindow[p].parent === window;
}

export function getGlobalProp (globalWindow) {
let cnt = 0;
let lastProp;
// eslint-disable-next-line no-restricted-syntax
for (const p in globalWindow) {
// do not check frames cause it could be removed during import
if (shouldSkipProperty(p, globalWindow))
// eslint-disable-next-line no-continue
continue;
if (cnt === 0 && p !== firstGlobalProp || cnt === 1 && p !== secondGlobalProp)
return p;
cnt++;
lastProp = p;
}
if (lastProp !== lastGlobalProp) {
return lastProp;
} else {
// polyfill for UC browser which lastprops will alway be window
// eslint-disable-next-line no-restricted-syntax
for (const p in globalWindow) {
if (!noteGlobalKeys.includes(p)) {
lastProp = p;
}
}
return lastProp;
}
}

export function noteGlobalProps (globalWindow) {
// alternatively Object.keys(global).pop()
// but this may be faster (pending benchmarks)
firstGlobalProp = undefined;
secondGlobalProp = undefined;
noteGlobalKeys = Object.keys(globalWindow);
// eslint-disable-next-line no-restricted-syntax
for (const p in globalWindow) {
// do not check frames cause it could be removed during import
if (shouldSkipProperty(p, globalWindow))
// eslint-disable-next-line no-continue
continue;
if (!firstGlobalProp)
firstGlobalProp = p;
else if (!secondGlobalProp)
secondGlobalProp = p;
lastGlobalProp = p;
}
return lastGlobalProp;
}
25 changes: 17 additions & 8 deletions src/util/handleAssets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import Sandbox from '@ice/sandbox';
import * as urlParse from 'url-parse';
import { AppLifeCycleEnum } from './appLifeCycle';
import { setCache } from './cache';
import { PREFIX, DYNAMIC, STATIC, IS_CSS_REGEX } from './constant';
import { warn, error } from './message';
import ModuleLoader from './umdLoader';

const winFetch = window.fetch;
const COMMENT_REGEX = /<!--.*?-->/g;
Expand All @@ -10,6 +14,7 @@ const STYLE_REGEX = /<style\b[^>]*>([^<]*)<\/style>/gi;
const LINK_HREF_REGEX = /<link\b[^>]*href=['"]?([^'"]*)['"]?\b[^>]*>/gi;
const CSS_REGEX = new RegExp([STYLE_REGEX, LINK_HREF_REGEX].map((reg) => reg.source).join('|'), 'gi');
const STYLE_SHEET_REGEX = /rel=['"]stylesheet['"]/gi;
const moduleLoader = new ModuleLoader();

export enum AssetTypeEnum {
INLINE = 'inline',
Expand Down Expand Up @@ -146,7 +151,7 @@ export function getUrlAssets(urls: string[]) {
}

const cachedScriptsContent: object = {};
function fetchScripts(jsList: Asset[], fetch: Fetch = winFetch) {
export function fetchScripts(jsList: Asset[], fetch: Fetch = winFetch) {
return Promise.all(jsList.map((asset) => {
const { type, content } = asset;
if (type === AssetTypeEnum.INLINE) {
Expand All @@ -157,7 +162,7 @@ function fetchScripts(jsList: Asset[], fetch: Fetch = winFetch) {
}
}));
}
export async function appendAssets(assets: Assets, sandbox?: Sandbox) {
export async function appendAssets(assets: Assets, cacheKey: string, umd: boolean, sandbox?: Sandbox) {
const jsRoot: HTMLElement = document.getElementsByTagName('head')[0];
const cssRoot: HTMLElement = document.getElementsByTagName('head')[0];

Expand All @@ -167,8 +172,13 @@ export async function appendAssets(assets: Assets, sandbox?: Sandbox) {
await Promise.all(
cssList.map((asset, index) => appendCSS(cssRoot, asset, `${PREFIX}-css-${index}`)),
);

if (sandbox && !sandbox.sandboxDisabled) {

if (umd) {
const moduleInfo = await moduleLoader.execModule({ jsList, cacheKey }, sandbox);
// set app lifecycle after exec umd module
setCache(AppLifeCycleEnum.AppEnter, moduleInfo?.mount);
setCache(AppLifeCycleEnum.AppLeave, moduleInfo?.unmount);
} else if (sandbox && !sandbox.sandboxDisabled) {
const jsContents = await fetchScripts(jsList);
// excute code by order
jsContents.forEach(script => {
Expand All @@ -190,12 +200,11 @@ export async function appendAssets(assets: Assets, sandbox?: Sandbox) {
}

export function parseUrl(entry: string): ParsedConfig {
const a = document.createElement('a');
a.href = entry;
const { origin, pathname } = urlParse(entry);

return {
origin: a.origin,
pathname: a.pathname,
origin,
pathname,
};
}

Expand Down
91 changes: 91 additions & 0 deletions src/util/umdLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Sandbox from '@ice/sandbox';
import { getGlobalProp, noteGlobalProps } from './global';
import { Asset, fetchScripts } from './handleAssets';

export interface StarkModule {
cacheKey: string;
jsList: Asset[];
mount?: (Component: any, targetNode: HTMLElement, props?: any) => void;
unmount?: (targetNode: HTMLElement) => void;
};

export interface ImportTask {
[cacheKey: string]: Promise<string[]>;
};

export type PromiseModule = Promise<Response>;

export interface Fetch {
(input: RequestInfo, init?: RequestInit): Promise<Response>;
}

export default class ModuleLoader {
private importTask: ImportTask = {};

load(starkModule: StarkModule): Promise<string[]> {
const { jsList, cacheKey } = starkModule;
if (this.importTask[cacheKey]) {
// return promise if current module is pending or resolved
return this.importTask[cacheKey];
}
const task = fetchScripts(jsList);
this.importTask[cacheKey] = task;
return task;
}

clearTask() {
this.importTask = {};
}

execModule(starkModule: StarkModule, sandbox?: Sandbox) {
return this.load(starkModule).then((sources) => {
const globalWindow = getGobalWindow(sandbox);
const { cacheKey } = starkModule;
let libraryExport = '';
// excute script in order
try {
sources.forEach((source, index) => {
const lastScript = index === sources.length - 1;
if (lastScript) {
noteGlobalProps(globalWindow);
}
// check sandbox
if (sandbox?.execScriptInSandbox) {
sandbox.execScriptInSandbox(source);
} else {
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval
// eslint-disable-next-line no-eval
(0, eval)(source);
}
if (lastScript) {
libraryExport = getGlobalProp(globalWindow);
}
});
} catch (err) {
console.error(err);
}
const moduleInfo = libraryExport ? (globalWindow as any)[libraryExport] : ((globalWindow as any)[cacheKey] || {});
// remove moduleInfo from globalWindow in case of excute multi module in globalWindow
if ((globalWindow as any)[libraryExport]) {
delete globalWindow[libraryExport];
}
return moduleInfo;
});
}
};

/**
* Get globalwindow
*
* @export
* @param {Sandbox} [sandbox]
* @returns
*/
export function getGobalWindow(sandbox?: Sandbox) {
if (sandbox?.getSandbox) {
sandbox.createProxySandbox();
return sandbox.getSandbox();
}
// FIXME: If run in Node environment
return window;
}
Loading

0 comments on commit da5b239

Please sign in to comment.