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

Release 2.1.0 #212

Merged
merged 13 commits into from
Jan 14, 2021
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": "2.0.2",
"version": "2.1.0",
"description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.",
"scripts": {
"install:deps": "rm -rf node_modules && rm -rf ./packages/*/node_modules && yarn install && lerna exec -- npm install",
Expand Down
2 changes: 1 addition & 1 deletion packages/icestark-app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ice/stark-app",
"version": "1.2.0",
"version": "1.2.1",
"description": "icestark-app is a JavaScript library for icestark, used by sub-application.",
"scripts": {
"build": "rm -rf lib && tsc",
Expand Down
37 changes: 37 additions & 0 deletions packages/icestark-app/src/AppLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-disable react/jsx-filename-extension */
import * as React from 'react';
import formatUrl from './util/formatUrl';

export type AppLinkProps = {
to: string;
hashType?: boolean;
replace?: boolean;
message?: string;
children?: React.ReactNode;
} & React.AnchorHTMLAttributes<Element>;

const AppLink = (props: AppLinkProps) => {
const { to, hashType, replace, message, children, ...rest } = props;
const linkTo = formatUrl(to, hashType);
return (
<a
{...rest}
href={linkTo}
onClick={e => {
e.preventDefault();
// eslint-disable-next-line no-alert
if (message && window.confirm(message) === false) {
return false;
}

const changeState = window.history[replace ? 'replaceState' : 'pushState'];

changeState({}, null, linkTo);
}}
>
{children}
</a>
);
};

export default AppLink;
18 changes: 14 additions & 4 deletions packages/icestark-app/src/appHistory.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import formatUrl from './util/formatUrl';

const appHistory = {
push: (url: string) => {
window.history.pushState({}, null, url);
push: (url: string, hashType?: boolean) => {
window.history.pushState(
{},
null,
formatUrl(url, hashType)
);
},
replace: (url: string) => {
window.history.replaceState({}, null, url);
replace: (url: string, hashType?: boolean) => {
window.history.replaceState(
{},
null,
formatUrl(url, hashType)
);
},
};

Expand Down
1 change: 1 addition & 0 deletions packages/icestark-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { default as registerAppEnter } from './registerAppEnter';
export { default as registerAppLeave } from './registerAppLeave';
export { default as appHistory } from './appHistory';
export { default as isInIcestark } from './isInIcestark';
export { default as AppLink } from './AppLink';
10 changes: 10 additions & 0 deletions packages/icestark-app/src/util/formatUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* format url
* @param url
* @param hashType
*/
const formatUrl = (url: string, hashType?: boolean) => {
return (hashType && url.indexOf('#') === -1) ? `#${url}` : url;
};

export default formatUrl;
11 changes: 11 additions & 0 deletions packages/icestark-app/tests/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isInIcestark,
} from '../src/index';
import { setCache, getCache } from '../src/cache';
import formatUrl from '../src/util/formatUrl';

const namespace = 'ICESTARK';

Expand Down Expand Up @@ -132,3 +133,13 @@ describe('isInIcestark', () => {
expect(isInIcestark()).toBe(true);
});
});

describe('formatUrl', () => {
test('formatUrl', () => {
expect(formatUrl('/seller')).toBe('/seller');

expect(formatUrl('#/seller')).toBe('#/seller');

expect(formatUrl('/seller', true)).toBe('#/seller');
})
})
15 changes: 5 additions & 10 deletions src/AppRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as React from 'react';
import renderComponent from './util/renderComponent';
import { AppHistory } from './appHistory';
import { unloadMicroApp, BaseConfig, getAppConfig, createMicroApp, AppConfig } from './apps';
import { unloadMicroApp, BaseConfig, createMicroApp } from './apps';
import { converArray2String } from './AppRouter';
import { PathData } from './util/matchPath';
import { setCache } from './util/cache';
import { callCapturedEventListeners, resetCapturedEventListeners } from './util/capturedListeners';
// eslint-disable-next-line import/order
import isEqual = require('lodash.isequal');
Expand Down Expand Up @@ -41,19 +40,18 @@ export interface AppRouteProps extends BaseConfig {
basename?: string;
render?: (componentProps: AppRouteComponentProps) => React.ReactElement;
path?: string | string[] | PathData[];
loadingApp?: (appConfig: AppConfig) => void;
onAppEnter?: (appConfig: CompatibleAppConfig) => void;
onAppLeave?: (appConfig: CompatibleAppConfig) => void;
}

export type CompatibleAppConfig = Omit<AppRouteProps, 'componentProps' | 'cssLoading' | 'loadingApp' | 'onAppEnter' | 'onAppLeave'>
export type CompatibleAppConfig = Omit<AppRouteProps, 'componentProps' | 'cssLoading' | 'onAppEnter' | 'onAppLeave'>

/**
* Gen compatible app config from AppRoute props
*/
function genCompatibleAppConfig (appRouteProps: AppRouteProps): CompatibleAppConfig {
const appConfig: CompatibleAppConfig = {};
const omitProperties = ['componentProps', 'cssLoading', 'loadingApp', 'onAppEnter', 'onAppLeave'];
const omitProperties = ['componentProps', 'cssLoading', 'onAppEnter', 'onAppLeave'];

Object.keys(appRouteProps).forEach(key => {
if (omitProperties.indexOf(key) === -1) {
Expand Down Expand Up @@ -156,7 +154,7 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta
}

renderChild = (): void => {
const { path, name, rootId, loadingApp, ...rest } = this.props;
const { path, name, rootId, ...rest } = this.props;
// reCreate rootElement to remove sub-application instance,
// rootElement is created for render sub-application
const rootElement: HTMLElement = this.reCreateElementInBase(rootId);
Expand All @@ -166,10 +164,7 @@ export default class AppRoute extends React.Component<AppRouteProps, AppRouteSta
activePath: path,
container: rootElement,
};
setCache('root', rootElement);
if (!getAppConfig(name)) {
loadingApp({ name });
}

createMicroApp(appConfig);
}

Expand Down
18 changes: 11 additions & 7 deletions src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import appHistory from './appHistory';
import renderComponent from './util/renderComponent';
import { ICESTSRK_ERROR, ICESTSRK_NOT_FOUND } from './util/constant';
import { setCache } from './util/cache';
import start, { unload } from './start';
import start, { unload, Fetch, defaultFetch } from './start';
import { matchActivePath, PathData } from './util/matchPath';
import { AppConfig } from './apps';

Expand All @@ -28,6 +28,7 @@ export interface AppRouterProps {
element?: HTMLElement | HTMLLinkElement | HTMLStyleElement | HTMLScriptElement,
) => boolean;
basename?: string;
fetch?: Fetch;
}

interface AppRouterState {
Expand Down Expand Up @@ -65,6 +66,7 @@ export default class AppRouter extends React.Component<AppRouterProps, AppRouter
onAppEnter: () => {},
onAppLeave: () => {},
basename: '',
fetch: defaultFetch,
};

constructor(props: AppRouterProps) {
Expand All @@ -73,12 +75,9 @@ export default class AppRouter extends React.Component<AppRouterProps, AppRouter
url: location.href,
appLoading: '',
};
}

componentDidMount() {
const { shouldAssetsRemove, onAppEnter, onAppLeave } = this.props;
// render NotFoundComponent eventListener
window.addEventListener('icestark:not-found', this.triggerNotFound);
// make sure start invoked before createMicroApp
const { shouldAssetsRemove, onAppEnter, onAppLeave, fetch } = props;
start({
shouldAssetsRemove,
onAppLeave,
Expand All @@ -87,9 +86,15 @@ export default class AppRouter extends React.Component<AppRouterProps, AppRouter
onFinishLoading: this.finishLoading,
onError: this.triggerError,
reroute: this.handleRouteChange,
fetch,
});
}

componentDidMount() {
// render NotFoundComponent eventListener
window.addEventListener('icestark:not-found', this.triggerNotFound);
}

componentWillUnmount() {
this.unmounted = true;
window.removeEventListener('icestark:not-found', this.triggerNotFound);
Expand Down Expand Up @@ -180,7 +185,6 @@ export default class AppRouter extends React.Component<AppRouterProps, AppRouter
name: this.appKey,
componentProps,
cssLoading: appLoading === this.appKey,
loadingApp: this.loadingApp,
onAppEnter: this.props.onAppEnter,
onAppLeave: this.props.onAppLeave,
})}
Expand Down
49 changes: 39 additions & 10 deletions src/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createSandbox, getUrlAssets, getEntryAssets, appendAssets, loadAndAppen
import { getCache, setCache } from './util/cache';
import { AppLifeCycleEnum } from './util/appLifeCycle';
import { loadUmdModule } from './util/umdLoader';
import { globalConfiguration } from './start';
import { globalConfiguration, StartConfiguration } from './start';

interface ActiveFn {
(url: string): boolean;
Expand Down Expand Up @@ -58,11 +58,15 @@ export interface AppConfig extends BaseConfig {
appSandbox?: Sandbox;
}

export type MicroApp = AppConfig & ModuleLifeCycle;
export type MicroApp =
AppConfig
& ModuleLifeCycle
& { configuration?: StartConfiguration };

// cache all microApp
let microApps: MicroApp[] = [];
(window as any).microApps = microApps;

function getAppNames() {
return microApps.map(app => app.name);
}
Expand All @@ -77,7 +81,7 @@ export function getAppStatus(appName: string) {
}

export function registerMicroApp(appConfig: AppConfig, appLifecyle?: AppLifecylceOptions) {
// check appConfig.name
// check appConfig.name
if (getAppNames().includes(appConfig.name)) {
throw Error(`name ${appConfig.name} already been regsitered`);
}
Expand Down Expand Up @@ -131,8 +135,10 @@ export function updateAppConfig(appName: string, config) {

// load app js assets
export async function loadAppModule(appConfig: AppConfig) {
const { onLoadingApp, onFinishLoading, fetch } = getAppConfig(appConfig.name)?.configuration || globalConfiguration;

let lifecycle: ModuleLifeCycle = {};
globalConfiguration.onLoadingApp(appConfig);
onLoadingApp(appConfig);
const appSandbox = createSandbox(appConfig.sandbox);
const { url, container, entry, entryContent, name } = appConfig;
const appAssets = url ? getUrlAssets(url) : await getEntryAssets({
Expand All @@ -141,21 +147,25 @@ export async function loadAppModule(appConfig: AppConfig) {
href: location.href,
entryContent,
assetsCacheKey: name,
fetch,
});
updateAppConfig(appConfig.name, { appAssets, appSandbox });

cacheLoadMode(appConfig);

if (appConfig.umd) {
await loadAndAppendCssAssets(appAssets);
lifecycle = await loadUmdModule(appAssets.jsList, appSandbox);
} else {
await appendAssets(appAssets, appSandbox);
await appendAssets(appAssets, appSandbox, fetch);
lifecycle = {
mount: getCache(AppLifeCycleEnum.AppEnter),
unmount: getCache(AppLifeCycleEnum.AppLeave),
};
setCache(AppLifeCycleEnum.AppEnter, null);
setCache(AppLifeCycleEnum.AppLeave, null);
}
globalConfiguration.onFinishLoading(appConfig);
onFinishLoading(appConfig);
return combineLifecyle(lifecycle, appConfig);
}

Expand Down Expand Up @@ -198,15 +208,33 @@ export function getAppConfigForLoad (app: string | AppConfig, options?: AppLifec
return getAppConfig(name);
};

export async function createMicroApp(app: string | AppConfig, appLifecyle?: AppLifecylceOptions) {
// cache loadMode
export function cacheLoadMode (app: AppConfig) {
const { umd, sandbox } = app;
// cache loadMode
// eslint-disable-next-line no-nested-ternary
const loadMode = umd ? 'umd' : ( sandbox ? 'sandbox' : 'script' );
setCache('loadMode', loadMode);
}

export async function createMicroApp(app: string | AppConfig, appLifecyle?: AppLifecylceOptions, configuration?: StartConfiguration) {
const appConfig = getAppConfigForLoad(app, appLifecyle);
const appName = appConfig && appConfig.name;

// compatible with use inIcestark
const container = (app as AppConfig).container || appConfig?.container;
if (container && !getCache('root')) {
if (container) {
setCache('root', container);
}

if (appConfig && appName) {
// add configuration to every micro app
const userConfiguration = globalConfiguration;
Object.keys(configuration || {}).forEach(key => {
userConfiguration[key] = configuration[key];
});
updateAppConfig(appName, { configuration: userConfiguration });

// check status of app
if (appConfig.status === NOT_LOADED || appConfig.status === LOAD_ERROR ) {
if (appConfig.title) document.title = appConfig.title;
Expand All @@ -219,7 +247,7 @@ export async function createMicroApp(app: string | AppConfig, appLifecyle?: AppL
updateAppConfig(appName, { ...lifeCycle, status: NOT_MOUNTED });
}
} catch (err){
globalConfiguration.onError(err);
userConfiguration.onError(err);
updateAppConfig(appName, { status: LOAD_ERROR });
}
if (lifeCycle.mount) {
Expand Down Expand Up @@ -257,7 +285,8 @@ export async function unmountMicroApp(appName: string) {
const appConfig = getAppConfig(appName);
if (appConfig && (appConfig.status === MOUNTED || appConfig.status === LOADING_ASSETS || appConfig.status === NOT_MOUNTED)) {
// remove assets if app is not cached
emptyAssets(globalConfiguration.shouldAssetsRemove, !appConfig.cached && appConfig.name);
const { shouldAssetsRemove } = getAppConfig(appName)?.configuration || globalConfiguration;
emptyAssets(shouldAssetsRemove, !appConfig.cached && appConfig.name);
updateAppConfig(appName, { status: UNMOUNTED });
if (!appConfig.cached && appConfig.appSandbox) {
appConfig.appSandbox.clear();
Expand Down
10 changes: 10 additions & 0 deletions src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import { AppConfig, getMicroApps, createMicroApp, unmountMicroApp, clearMicroApp
import { emptyAssets, recordAssets } from './util/handleAssets';
import { LOADING_ASSETS, MOUNTED } from './util/constant';

if (!window?.fetch) {
throw new Error('[icestark] window.fetch not found, you need polyfill it');
}

export const defaultFetch = window?.fetch.bind(window);

export type Fetch = typeof window.fetch | ((url: string) => Promise<Response>);

export interface StartConfiguration {
shouldAssetsRemove?: (
assetUrl?: string,
Expand All @@ -31,6 +39,7 @@ export interface StartConfiguration {
onError?: (err: Error) => void;
onActiveApps?: (appConfigs: AppConfig[]) => void;
reroute?: (url: string, type: RouteType | 'init' | 'popstate'| 'hashchange') => void;
fetch?: Fetch;
}

const globalConfiguration: StartConfiguration = {
Expand All @@ -43,6 +52,7 @@ const globalConfiguration: StartConfiguration = {
onError: () => {},
onActiveApps: () => {},
reroute,
fetch: defaultFetch,
};

interface OriginalStateFunction {
Expand Down
Loading