diff --git a/packages/react-provider/CHANGELOG.md b/packages/react-provider/CHANGELOG.md index 0575970db..da649feed 100644 --- a/packages/react-provider/CHANGELOG.md +++ b/packages/react-provider/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.7-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-react-provider@1.0.6-0...@ali/lowcode-react-provider@1.0.7-0) (2020-09-02) @@ -14,6 +15,10 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ## [1.0.6-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-react-provider@1.0.5-0...@ali/lowcode-react-provider@1.0.6-0) (2020-09-02) +### Features + +* 增加错误边界&pageReady钩子 ([3bb2fc1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/3bb2fc1)) + diff --git a/packages/react-provider/package.json b/packages/react-provider/package.json index d24bf1aa2..f4441334d 100644 --- a/packages/react-provider/package.json +++ b/packages/react-provider/package.json @@ -26,6 +26,7 @@ "license": "MIT", "dependencies": { "@ali/lowcode-runtime": "^1.0.7-0", + "@ali/lowcode-utils": "^1.0.7", "@recore/router": "^1.0.11", "react": "^16", "react-dom": "^16" diff --git a/packages/react-provider/src/components/LazyComponent/index.tsx b/packages/react-provider/src/components/LazyComponent/index.tsx new file mode 100644 index 000000000..4cf20a341 --- /dev/null +++ b/packages/react-provider/src/components/LazyComponent/index.tsx @@ -0,0 +1,81 @@ +import { Component } from 'react'; +import { app, Provider } from '@ali/lowcode-runtime'; + +interface IProps { + getPageData: () => any; + context: Provider; + [key: string]: any; +} + +interface IState { + schema: object | null; + hasError: boolean; +} + +export default class LazyComponent extends Component { + constructor(props: IProps) { + super(props); + this.state = { + schema: null, + hasError: false, + }; + } + + static getDerivedStateFromError() { + return { hasError: true }; + } + + async componentDidMount() { + const { getPageData, context } = this.props; + if (getPageData && !this.state.schema) { + try { + const schema = await getPageData(); + this.setState({ schema }); + context.emitPageReady(); + } catch (err) { + this.setState({ hasError: true }); + this.exeAfterCatch(err.message, err.stack); + } + } + } + + exeAfterCatch(error: any, errorInfo?: any) { + const { afterCatch } = app.getErrorBoundary() || {}; + if (typeof afterCatch === 'function') { + afterCatch.call(this, error, errorInfo); + } + } + + componentDidCatch(error: any, errorInfo: any) { + this.exeAfterCatch(error, errorInfo); + } + + render() { + const { schema, hasError } = this.state; + if (hasError) { + const { fallbackUI: ErrorView } = app.getErrorBoundary() || {}; + if (!ErrorView) { + return ''; + } + return ; + } + + const { getPageData, ...restProps } = this.props; + const Renderer = app.getRenderer(); + const Loading = app.getLoading(); + if (!Renderer || !schema) { + if (!Loading) { + return null; + } + // loading扩展点 + return ; + } + return ( + : null} + {...restProps} + /> + ) + } +} diff --git a/packages/react-provider/src/lazy-component.tsx b/packages/react-provider/src/lazy-component.tsx deleted file mode 100644 index b20fd2bf8..000000000 --- a/packages/react-provider/src/lazy-component.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Component, createElement } from 'react'; -import { app } from '@ali/lowcode-runtime'; - -interface IProps { - getPageData: () => any; - [key: string]: any; -} - -interface IState { - schema: object | null; -} - -export default class LazyComponent extends Component { - constructor(props: IProps) { - super(props); - this.state = { - schema: null, - }; - } - - async componentDidMount() { - const { getPageData } = this.props; - if (getPageData && !this.state.schema) { - const schema = await getPageData(); - this.setState({ schema }); - } - } - - render() { - const { getPageData, ...restProps } = this.props; - const { schema } = this.state; - const Renderer = app.getRenderer(); - const Loading = app.getLoading(); - if (!Renderer || !schema) { - if (!Loading) { - return null; - } - // loading扩展点 - return createElement(Loading); - } - return createElement(Renderer as any, { schema, loading: Loading ? createElement(Loading) : null, ...restProps }); - } -} diff --git a/packages/react-provider/src/provider.tsx b/packages/react-provider/src/provider.tsx index de4d97352..94c37a4c9 100644 --- a/packages/react-provider/src/provider.tsx +++ b/packages/react-provider/src/provider.tsx @@ -2,13 +2,14 @@ import { createElement, ReactType, ReactElement } from 'react'; import ReactDOM from 'react-dom'; import { Router } from '@recore/router'; import { app, Provider } from '@ali/lowcode-runtime'; -import LazyComponent from './lazy-component'; +import { AppHelper } from '@ali/lowcode-utils'; +import LazyComponent from './components/LazyComponent'; export default class ReactProvider extends Provider { // 定制构造根组件的逻辑,如切换路由机制 createApp() { - const RouterView = this.getRouterView(); - let App; + let RouterView = this.getRouterView(); + let App: any; const layoutConfig = this.getLayoutConfig(); if (!layoutConfig || !layoutConfig.componentName) { App = (props: any) => (RouterView ? createElement(RouterView, { ...props }) : null); @@ -16,7 +17,7 @@ export default class ReactProvider extends Provider { } const { componentName: layoutName, props: layoutProps } = layoutConfig; const { content: Layout, props: extraLayoutProps } = app.getLayout(layoutName) || {}; - const sectionalRender = this.isSectionalRender(); + const sectionalRender = this.isSectionalRender; if (!sectionalRender && Layout) { App = (props: any) => createElement( @@ -31,14 +32,21 @@ export default class ReactProvider extends Provider { } runApp(App: any, config: any) { - ReactDOM.render(, document.getElementById(config?.containerId || 'App')); + const containerId = config?.containerId || 'App'; + let container = document.getElementById(containerId); + if (!container) { + container = document.createElement('div'); + document.body.appendChild(container); + container.id = containerId; + } + ReactDOM.render(, container); } // 内置实现 for 动态化渲染 - getRouterView(): ReactType | null { + getRouterView(): ReactType { const routerConfig = this.getRouterConfig(); if (!routerConfig) { - return null; + return () => null; } const routes: Array<{ path: string; @@ -73,11 +81,15 @@ export default class ReactProvider extends Provider { defined: { keepAlive: true }, }); } + const appHelper = new AppHelper(); + appHelper.set('utils', this.getUtils()); + appHelper.set('constants', this.getConstants()); const RouterView = (props: any) => { return createElement(Router as any, { routes, components: this.getComponents(), utils: this.getUtils(), + appHelper, componentsMap: this.getComponentsMapObj(), ...props, }); @@ -89,15 +101,16 @@ export default class ReactProvider extends Provider { if (!pageId) { return null; } - if (this.getlazyElement(pageId)) { - return this.getlazyElement(pageId); + if (this.getLazyElement(pageId)) { + return this.getLazyElement(pageId); } else { const lazyElement = createElement(LazyComponent as any, { getPageData: async () => await this.getPageData(pageId), key: pageId, + context: this, ...props, }); - this.setlazyElement(pageId, lazyElement); + this.setLazyElement(pageId, lazyElement); return lazyElement; } } diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md index 243e43945..b930929c4 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -3,21 +3,32 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [1.0.7-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-runtime@1.0.6-0...@ali/lowcode-runtime@1.0.7-0) (2020-09-02) - - **Note:** Version bump only for package @ali/lowcode-runtime + -## [1.0.6-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-runtime@1.0.5-0...@ali/lowcode-runtime@1.0.6-0) (2020-09-02) +## [1.0.6-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-runtime@1.0.5-0...@ali/lowcode-runtime@1.0.6-0) (2020-08-31) +### Bug Fixes + +* 取消Provider的校验 ([a1755f2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/a1755f2)) + + +### Features + +* 兼容通过api更新属性的场景 ([b58d59b](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/b58d59b)) +* 增加错误边界&pageReady钩子 ([3bb2fc1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/3bb2fc1)) +* 增加若干钩子方法 ([eb55712](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/eb55712)) +* 支持多个provider ([642c62e](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/642c62e)) +* 支持多个Provider ([e35357f](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/e35357f)) -**Note:** Version bump only for package @ali/lowcode-runtime ## [1.0.5-0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-runtime@1.0.4-0...@ali/lowcode-runtime@1.0.5-0) (2020-08-20) diff --git a/packages/runtime/index.d.ts b/packages/runtime/index.d.ts deleted file mode 100644 index 76ca8e969..000000000 --- a/packages/runtime/index.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -type HistoryMode = 'browser' | 'hash'; - -interface ComponentsMap { - [key: string]: any; -} - -interface UtilsMap { - [key: string]: any; -} - -export interface AppConfig { - history?: HistoryMode; - globalComponents?: ComponentsMap; - globalUtils?: UtilsMap; - containerId?: string; -} - -export function run(Component: any, config?: AppConfig | (() => AppConfig)): any; diff --git a/packages/runtime/src/core/container.ts b/packages/runtime/src/core/container.ts index 809394bf6..f5652e117 100644 --- a/packages/runtime/src/core/container.ts +++ b/packages/runtime/src/core/container.ts @@ -5,11 +5,17 @@ export interface ILayoutOptions { props?: any; } +export interface IErrorBoundaryConfig { + fallbackUI: any; + afterCatch?: (...rest: any) => any +} + export default class Container { private renderer: any = null; private layouts: { [key: string]: { content: any; props: any } } = {}; private loading: any = null; - private provider: any; + private errorBoundary: IErrorBoundaryConfig = { fallbackUI: () => '', afterCatch: () => {} }; + private providers: { [key: string]: Provider; } = {}; registerRenderer(renderer: any): any { this.renderer = renderer; @@ -33,12 +39,19 @@ export default class Container { this.loading = component; } + registerErrorBoundary(config: IErrorBoundaryConfig) { + if (!config) { + return; + } + this.errorBoundary = config; + } + registerProvider(CustomProvider: any) { - if (Provider.isPrototypeOf(CustomProvider)) { - this.provider = new CustomProvider(); - } else { - const identifier = (CustomProvider && CustomProvider.name) || 'registered Provider'; - throw new Error(`${identifier} is not a child class of Provider`); + try { + const p = new CustomProvider(); + this.providers[p.getContainerId()] = p; + } catch (error) { + console.error(error.message); } } @@ -57,7 +70,19 @@ export default class Container { return this.loading; } - getProvider() { - return this.provider; + getErrorBoundary(): any { + return this.errorBoundary; + } + + getProvider(id?: string) { + if (!id) { + for (const key in this.providers) { + if (Object.prototype.hasOwnProperty.call(this.providers, key)) { + return this.providers[key]; + } + } + } else { + return this.providers[id]; + } } } diff --git a/packages/runtime/src/core/index.ts b/packages/runtime/src/core/index.ts index de70f9229..720e9e471 100644 --- a/packages/runtime/src/core/index.ts +++ b/packages/runtime/src/core/index.ts @@ -1,5 +1,5 @@ -import Container, { ILayoutOptions } from './container'; -import { IProvider } from './provider'; +import Container, { ILayoutOptions, IErrorBoundaryConfig } from './container'; +import Provider from './provider'; import runApp from './runApp'; class App { @@ -29,20 +29,28 @@ class App { this.container.registerProvider(CustomProvider); } + registerErrorBoundary(config: IErrorBoundaryConfig) { + this.container.registerErrorBoundary(config); + } + getLayout(componentName: string) { return this.container.getLayout(componentName); } - getRenderer(): any | null { + getRenderer(): any { return this.container.getRenderer(); } - getLoading(): any | null { + getLoading(): any { return this.container.getLoading(); } - getProvider(): IProvider { - return this.container.getProvider(); + getErrorBoundary(): IErrorBoundaryConfig { + return this.container.getErrorBoundary(); + } + + getProvider(id?: string): Provider | undefined { + return this.container.getProvider(id); } } diff --git a/packages/runtime/src/core/provider.ts b/packages/runtime/src/core/provider.ts index fc116e4ef..5268797ab 100644 --- a/packages/runtime/src/core/provider.ts +++ b/packages/runtime/src/core/provider.ts @@ -101,32 +101,20 @@ export interface I18n { type Locale = 'zh-CN' | 'en-US'; -export interface IProvider { - init(): void; - ready(): void; - onReady(cb: any): void; - async(): Promise; - getAppData(): Promise; - getPageData(pageId: string): Promise; - getLazyComponent(pageId: string, props: any): any; - createApp(): void; - runApp(App: any, config: IAppConfig): void; -} - -export default class Provider implements IProvider { - private components: IComponents = {}; - private utils: IUtils = {}; - private constants: IConstants = {}; - private routes: IRouterConfig | null = null; - private layout: ILayoutConfig | null = null; - private componentsMap: IComponentMap[] = []; - private history: HistoryMode = 'hash'; - private containerId = ''; - private i18n: I18n | null = null; - private homePage = ''; - private lazyElementsMap: { [key: string]: any } = {}; - private sectionalRender = false; - private emitter: EventEmitter = new EventEmitter(); +export default class Provider { + emitter: EventEmitter = new EventEmitter(); + components: IComponents = {}; + utils: IUtils = {}; + constants: IConstants = {}; + routes: IRouterConfig | null = null; + layout: ILayoutConfig | null = null; + componentsMap: IComponentMap[] = []; + history: HistoryMode = 'hash'; + containerId = ''; + i18n: I18n | null = null; + homePage = ''; + lazyElementsMap: { [key: string]: any } = {}; + isSectionalRender = false; constructor() { this.init(); @@ -150,19 +138,18 @@ export default class Provider implements IProvider { this.registerUtils(utils); this.registerContants(constants); resolve({ - history, - components, - utils, - containerId, + history: this.getHistory(), + components: this.getComponents(), + utils: this.getUtils(), + containerId: this.getContainerId(), }); } catch (err) { - reject(err.message); + reject(err); } }); } init() { - console.log('init'); // 默认 ready,当重载了init时需手动触发 this.ready() this.ready(); } @@ -181,6 +168,50 @@ export default class Provider implements IProvider { this.emitter.on('ready', cb); } + emitPageReady() { + this.emitter.emit('pageReady'); + } + + emitPageEnter() { + this.emitter.emit('pageEnter'); + } + + emitPageUpdate() { + this.emitter.emit('pageUpdate'); + } + + emitPageLeave() { + this.emitter.emit('pageLeave'); + } + + onPageReady(cb: (params?: any) => void) { + this.emitter.on('pageReady', cb); + return () => { + this.emitter.removeListener('pageReady', cb); + }; + } + + onPageEnter(cb: (params?: any) => void) { + this.emitter.on('pageEnter', cb); + return () => { + this.emitter.removeListener('pageEnter', cb); + }; + } + + onPageUpdate(cb: (params?: any) => void) { + this.emitter.on('pageUpdate', cb); + return () => { + this.emitter.removeListener('pageUpdate', cb); + }; + } + + onPageLeave(cb: (params?: any) => void) { + this.emitter.on('pageLeave', cb); + return () => { + this.emitter.removeListener('pageLeave', cb); + }; + } + getAppData(): any { throw new Error('Method called "getAppData" not implemented.'); } @@ -244,7 +275,7 @@ export default class Provider implements IProvider { this.routes = config; } - setHistory(config: HistoryMode | undefined) { + setHistory(config: HistoryMode | undefined): any { if (!config) { return; } @@ -265,7 +296,7 @@ export default class Provider implements IProvider { this.i18n = i18n; } - setlazyElement(pageId: string, cache: any) { + setLazyElement(pageId: string, cache: any) { if (!pageId || !cache) { return; } @@ -278,10 +309,6 @@ export default class Provider implements IProvider { } } - setSectionalRender() { - this.sectionalRender = true; - } - getComponents() { return this.components; } @@ -333,7 +360,7 @@ export default class Provider implements IProvider { } getContainerId() { - return this.containerId; + return this.containerId || 'App'; } getI18n(locale?: Locale) { @@ -347,14 +374,10 @@ export default class Provider implements IProvider { return this.homePage; } - getlazyElement(pageId: string) { + getLazyElement(pageId: string) { if (!pageId) { return; } return this.lazyElementsMap[pageId]; } - - isSectionalRender() { - return this.sectionalRender; - } } diff --git a/packages/runtime/src/core/runApp.ts b/packages/runtime/src/core/runApp.ts index 3dea9fb92..6c6cc2318 100644 --- a/packages/runtime/src/core/runApp.ts +++ b/packages/runtime/src/core/runApp.ts @@ -13,7 +13,7 @@ export interface IUtils { [key: string]: any; } -export type HistoryMode = 'browser' | 'hash'; +export type HistoryMode = 'browser' | 'hash' | 'BROWSER' | 'HASH'; export interface IAppConfig { history?: HistoryMode; @@ -36,6 +36,16 @@ export default function runApp() { } const App = provider.createApp(); provider.runApp(App, config); + }).catch((err: Error) => { + console.error(err.message); + const { fallbackUI, afterCatch } = app.getErrorBoundary() || {}; + if (typeof afterCatch === 'function') { + afterCatch(err.message, err.stack); + } + if (!fallbackUI) { + return; + } + provider.runApp(fallbackUI, {}); }); }); }