diff --git a/packages/renderer-core/src/hoc/error-boundary.tsx b/packages/renderer-core/src/hoc/error-boundary.tsx new file mode 100644 index 000000000..9c6c2cb11 --- /dev/null +++ b/packages/renderer-core/src/hoc/error-boundary.tsx @@ -0,0 +1,56 @@ +import { cloneEnumerableProperty } from '@alilc/lowcode-utils'; +import adapter from '../adapter'; + +/** + * 为什么要用 cache, + * 因为 __getHOCWrappedComponent 会返回一个新的组件, + * 如果不缓存,会导致组件每次渲染都是一次新的 mount + */ +const componentCache = new Map(); + +export function errorBoundaryWrapper(Comp: any, { schema, baseRenderer }) { + const { createElement, Component, forwardRef } = adapter.getRuntime(); + const { engine } = baseRenderer.context; + const componentCacheId = schema.id; + + if (componentCache.has(componentCacheId)) { + return componentCache.get(componentCacheId); + } + + class Wrapper extends Component { + state = { error: null }; + componentError = false; + componentDidCatch(error: Error) { + this.componentError = true; + this.setState({ error }); + } + + render() { + if (this.componentError) { + this.componentError = false; + return engine.createElement(engine.getFaultComponent(), { + ...this.props, + error: this.state.error, + componentName: schema.componentName, + }); + } + return createElement(Comp, { + ...this.props, + ref: this.props.forwardRef, + }); + } + } + + (Wrapper as any).displayName = Comp.displayName; + + let ErrorBoundary = cloneEnumerableProperty( + forwardRef((props: any, ref: any) => { + return createElement(Wrapper, { ...props, forwardRef: ref }); + }), + Comp, + ); + + componentCache.set(componentCacheId, ErrorBoundary); + + return ErrorBoundary; +} diff --git a/packages/renderer-core/src/renderer/base.tsx b/packages/renderer-core/src/renderer/base.tsx index 216735edc..376da4b31 100644 --- a/packages/renderer-core/src/renderer/base.tsx +++ b/packages/renderer-core/src/renderer/base.tsx @@ -33,6 +33,7 @@ import { import { IBaseRendererProps, INodeInfo, IBaseRenderComponent, IBaseRendererContext, IRendererAppHelper, DataSource } from '../types'; import { compWrapper } from '../hoc'; import { IComponentConstruct, leafWrapper } from '../hoc/leaf'; +import { errorBoundaryWrapper } from '../hoc/error-boundary'; import logger from '../utils/logger'; import isUseLoop from '../utils/is-use-loop'; @@ -705,9 +706,9 @@ export default function baseRendererFactory(): IBaseRenderComponent { */ get __componentHOCs(): IComponentConstruct[] { if (this.__designModeIsDesign) { - return [leafWrapper]; + return [errorBoundaryWrapper, leafWrapper]; } - return []; + return [errorBoundaryWrapper]; } __getSchemaChildrenVirtualDom = (schema: IPublicTypeNodeSchema | undefined, scope: any, Comp: any, condition = true) => { diff --git a/packages/renderer-core/src/renderer/renderer.tsx b/packages/renderer-core/src/renderer/renderer.tsx index 09559b6f8..300b1cd16 100644 --- a/packages/renderer-core/src/renderer/renderer.tsx +++ b/packages/renderer-core/src/renderer/renderer.tsx @@ -105,55 +105,7 @@ export default function rendererFactory(): IRenderComponent { return SetComponent; } - patchDidCatch(SetComponent: any) { - if (!this.isValidComponent(SetComponent)) { - return; - } - if (SetComponent.patchedCatch) { - return; - } - if (!SetComponent.prototype) { - return; - } - SetComponent.patchedCatch = true; - - // Rax 的 getDerivedStateFromError 有 BUG,这里先用 componentDidCatch 来替代 - // @see https://github.com/alibaba/rax/issues/2211 - const originalDidCatch = SetComponent.prototype.componentDidCatch; - SetComponent.prototype.componentDidCatch = function didCatch(this: any, error: Error, errorInfo: any) { - this.setState({ engineRenderError: true, error }); - if (originalDidCatch && typeof originalDidCatch === 'function') { - originalDidCatch.call(this, error, errorInfo); - } - }; - - const engine = this; - const originRender = SetComponent.prototype.render; - SetComponent.prototype.render = function () { - if (this.state && this.state.engineRenderError) { - this.state.engineRenderError = false; - return engine.createElement(engine.getFaultComponent(), { - ...this.props, - error: this.state.error, - componentName: this.props._componentName - }); - } - return originRender.call(this); - }; - if(!(SetComponent.prototype instanceof PureComponent)) { - const originShouldComponentUpdate = SetComponent.prototype.shouldComponentUpdate; - SetComponent.prototype.shouldComponentUpdate = function (nextProps: IRendererProps, nextState: any) { - if (nextState && nextState.engineRenderError) { - return true; - } - return originShouldComponentUpdate ? originShouldComponentUpdate.call(this, nextProps, nextState) : true; - }; - } - } - createElement(SetComponent: any, props: any, children?: any) { - // TODO: enable in runtime mode? - this.patchDidCatch(SetComponent); return (this.props.customCreateElement || createElement)(SetComponent, props, children); } diff --git a/packages/renderer-core/src/types/index.ts b/packages/renderer-core/src/types/index.ts index a49fe8992..afbec272a 100644 --- a/packages/renderer-core/src/types/index.ts +++ b/packages/renderer-core/src/types/index.ts @@ -335,7 +335,6 @@ export interface IRenderComponent { componentDidCatch(e: any): Promise | void; shouldComponentUpdate(nextProps: IRendererProps): boolean; isValidComponent(SetComponent: any): any; - patchDidCatch(SetComponent: any): void; createElement(SetComponent: any, props: any, children?: any): any; getNotFoundComponent(): any; getFaultComponent(): any;