Skip to content

Commit

Permalink
Merge pull request #120 from ice-lab/release/1.5.3
Browse files Browse the repository at this point in the history
release/1.5.3
  • Loading branch information
ClarkXia committed May 14, 2020
2 parents 2bbb696 + 55e9d38 commit dfe44bc
Show file tree
Hide file tree
Showing 12 changed files with 297 additions and 72 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ice/stark",
"version": "1.5.2",
"version": "1.5.3",
"description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.",
"scripts": {
"build": "rm -rf lib && tsc",
Expand Down Expand Up @@ -38,7 +38,7 @@
"react": ">=15.0.0"
},
"dependencies": {
"@ice/sandbox": "^1.0.3",
"@ice/sandbox": "^1.0.4",
"lodash.isequal": "^4.5.0",
"path-to-regexp": "^1.7.0",
"url-parse": "^1.1.9"
Expand Down
2 changes: 1 addition & 1 deletion packages/icestark-module/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ice/stark-module",
"version": "1.0.1",
"version": "1.1.0",
"description": "toolkit for load standard micro-module",
"main": "lib/index.js",
"scripts": {
Expand Down
49 changes: 32 additions & 17 deletions packages/icestark-module/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { getGlobalProp, noteGlobalProps } from './global';

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

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

export type PromiseModule = Promise<Response>;
Expand All @@ -21,13 +21,15 @@ export interface Fetch {
export default class ModuleLoader {
private importTask: ImportTask = {};

load(starkModule: StarkModule, fetch: Fetch = window.fetch): Promise<string> {
load(starkModule: StarkModule, fetch: Fetch = window.fetch): Promise<string[]> {
const { url, name } = starkModule;
if (this.importTask[name]) {
// return promise if current module is pending or resolved
return this.importTask[name];
}
const task = fetch(url).then((res) => res.text());
const urls = Array.isArray(url) ? url : [url];

const task = Promise.all(urls.map((scriptUrl) => fetch(scriptUrl).then((res) => res.text())));
this.importTask[name] = task;
return task;
}
Expand All @@ -37,27 +39,40 @@ export default class ModuleLoader {
}

execModule(starkModule: StarkModule, sandbox?: Sandbox) {
return this.load(starkModule).then((source) => {
return this.load(starkModule).then((sources) => {
let globalWindow = null;
if (sandbox?.getSandbox) {
sandbox.createProxySandbox();
globalWindow = sandbox.getSandbox();
} else {
globalWindow = window;
}
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);
}
const { name } = starkModule;
const libraryExport = getGlobalProp(globalWindow);

return (globalWindow as any)[name] || (globalWindow as any)[libraryExport] || {};
let libraryExport = '';
// excute script in order
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);
}
});
const moduleInfo = libraryExport ? (globalWindow as any)[libraryExport] : ((globalWindow as any)[name] || {});
// remove moduleInfo from globalWindow in case of excute multi module in globalWindow
if ((globalWindow as any)[libraryExport]) {
delete globalWindow[libraryExport];
}
return moduleInfo;
});
}
};
150 changes: 127 additions & 23 deletions packages/icestark-module/src/modules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type ISandbox = boolean | SandboxProps | SandboxContructor;
let globalModules = [];
let importModules = {};

const IS_CSS_REGEX = /\.css(\?((?!\.js$).)+)?$/;
export const moduleLoader = new ModuleLoader();

export const registerModules = (modules: StarkModule[]) => {
Expand Down Expand Up @@ -38,21 +39,15 @@ export function renderComponent(Component: any, props = {}): React.ReactElement
*/
const defaultMount = (Component: any, targetNode: HTMLElement, props?: any) => {
console.warn('Please set mount, try run react mount function');
try {
ReactDOM.render(renderComponent(Component, props), targetNode);
// eslint-disable-next-line no-empty
} catch(err) {}
ReactDOM.render(renderComponent(Component, props), targetNode);
};

/**
* default unmount function
*/
const defaultUnmount = (targetNode: HTMLElement) => {
console.warn('Please set unmount, try run react unmount function');
try {
ReactDOM.unmountComponentAtNode(targetNode);
// eslint-disable-next-line no-empty
} catch(err) {}
ReactDOM.unmountComponentAtNode(targetNode);
};

function createSandbox(sandbox: ISandbox) {
Expand All @@ -69,6 +64,65 @@ function createSandbox(sandbox: ISandbox) {
return moduleSandbox;
}

/**
* parse url assets
*/
export const parseUrlAssets = (assets: string | string[]) => {
const jsList = [];
const cssList = [];
(Array.isArray(assets) ? assets : [assets]).forEach(url => {
const isCss: boolean = IS_CSS_REGEX.test(url);
if (isCss) {
cssList.push(url);
} else {
jsList.push(url);
}
});

return { jsList, cssList };
};


export function appendCSS(
name: string,
url: string,
root: HTMLElement | ShadowRoot = document.getElementsByTagName('head')[0],
): Promise<string> {
return new Promise<string>((resolve, reject) => {
if (!root) reject(new Error(`no root element for css assert: ${url}`));

const element: HTMLLinkElement = document.createElement('link');
element.setAttribute('module', name);
element.rel = 'stylesheet';
element.href = url;

element.addEventListener(
'error',
() => {
console.error(`css asset loaded error: ${url}`);
return resolve();
},
false,
);
element.addEventListener('load', () => resolve(), false);

root.appendChild(element);
});
}

/**
* remove css
*/

export function removeCSS(name: string, node?: HTMLElement | Document) {
const linkList: NodeListOf<HTMLElement> = (node || document).querySelectorAll(
`link[module=${name}]`,
);
linkList.forEach(link => {
link.parentNode.removeChild(link);
});
}

/**
* return globalModules
*/
Expand All @@ -77,30 +131,50 @@ export const getModules = function () {
};

/**
* mount module function
* load module source
*/
export const mountModule = async (targetModule: StarkModule, targetNode: HTMLElement, props: any = {}, sandbox?: ISandbox) => {
const { name } = targetModule;

export const loadModule = async(targetModule: StarkModule, sandbox?: ISandbox) => {
const { name, url } = targetModule;
let moduleSandbox = null;
if (!importModules[name]) {
const { jsList, cssList } = parseUrlAssets(url);
moduleSandbox = createSandbox(sandbox);
const moduleInfo = await moduleLoader.execModule(targetModule, moduleSandbox);
const moduleInfo = await moduleLoader.execModule({ name, url: jsList }, moduleSandbox);
importModules[name] = {
moduleInfo,
moduleSandbox,
moduleCSS: cssList,
};
}

const moduleInfo = importModules[name].moduleInfo;
const { moduleInfo, moduleCSS } = importModules[name];

if (!moduleInfo) {
console.error('load or exec module faild');
return;
const errMsg = 'load or exec module faild';
console.error(errMsg);
return Promise.reject(new Error(errMsg));
}

const mount = targetModule.mount || moduleInfo?.mount || defaultMount;
const component = moduleInfo.default || moduleInfo;

// append css before mount module
if (moduleCSS.length) {
await Promise.all(moduleCSS.map((css: string) => appendCSS(name, css)));
}

return {
mount,
component,
};
};

/**
* mount module function
*/
export const mountModule = async (targetModule: StarkModule, targetNode: HTMLElement, props: any = {}, sandbox?: ISandbox) => {
const { mount, component } = await loadModule(targetModule, sandbox);
return mount(component, targetNode, props);
};

Expand All @@ -112,7 +186,7 @@ export const unmoutModule = (targetModule: StarkModule, targetNode: HTMLElement)
const moduleInfo = importModules[name]?.module;
const moduleSandbox = importModules[name]?.moduleSandbox;
const unmount = targetModule.unmount || moduleInfo?.unmount || defaultUnmount;

removeCSS(name);
if (moduleSandbox?.clear) {
moduleSandbox.clear();
}
Expand All @@ -123,11 +197,25 @@ export const unmoutModule = (targetModule: StarkModule, targetNode: HTMLElement)
/**
* default render compoent, mount all modules
*/
export class MicroModule extends React.Component<any, {}> {
export class MicroModule extends React.Component<any, { loading: boolean }> {
private moduleInfo = null;

private mountNode = null;

private unmout = false;

static defaultProps = {
loadingComponent: null,
handleError: () => {},
};

constructor(props) {
super(props);
this.state = {
loading: false,
};
}

componentDidMount() {
this.mountModule();
}
Expand All @@ -140,23 +228,39 @@ export class MicroModule extends React.Component<any, {}> {

componentWillUnmount() {
unmoutModule(this.moduleInfo, this.mountNode);
this.unmout = true;
}

mountModule() {
async mountModule() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { sandbox, moduleInfo, wrapperClassName, wrapperStyle, ...rest } = this.props;
const { sandbox, moduleInfo, wrapperClassName, wrapperStyle, loadingComponent, handleError, ...rest } = this.props;
this.moduleInfo = moduleInfo || getModules().filter(m => m.name === this.props.moduleName)[0];
if (!this.moduleInfo) {
console.error(`Can't find ${this.props.moduleName} module in modules config`);
return;
}

mountModule(this.moduleInfo, this.mountNode, rest, sandbox);
this.setState({ loading: true });
try {
const { mount, component } = await loadModule(this.moduleInfo, sandbox);
this.setState({ loading: false });
if (mount && component) {
if (this.unmout) {
unmoutModule(this.moduleInfo, this.mountNode);
} else {
mount(component, this.mountNode, rest);
}
}
} catch (err) {
this.setState({ loading: false });
handleError(err);
}
}

render() {
const { wrapperClassName, wrapperStyle } = this.props;
return (<div className={wrapperClassName} style={wrapperStyle} ref={ref => this.mountNode = ref} />);
const { loading } = this.state;
const { wrapperClassName, wrapperStyle, loadingComponent } = this.props;
return loading ? loadingComponent
: <div className={wrapperClassName} style={wrapperStyle} ref={ref => this.mountNode = ref} />;
}
};

Expand Down
Loading

0 comments on commit dfe44bc

Please sign in to comment.