import classnames from 'classnames'; import Debug from 'debug'; import { create as createDataSourceEngine } from '@ali/lowcode-datasource-engine/interpret'; import adapter from '../adapter'; import divFactory from '../components/Div'; import visualDomFactory from '../components/VisualDom'; import contextFactory from '../context'; import { forEach, getValue, parseData, parseExpression, parseI18n, isEmpty, isSchema, isFileSchema, isJSExpression, isJSSlot, isJSFunction, transformArrayToMap, transformStringToFunction, checkPropTypes, getI18n, acceptsRef, getFileCssName, capitalizeFirstLetter, DataHelper, isI18n, isVariable, } from '../utils'; import { IRendererProps, ISchema, IInfo, ComponentModel, IRenderer } from '../types'; import { compWrapper } from '../hoc'; export default function baseRenererFactory() { const { BaseRenderer: customBaseRenderer } = adapter.getRenderers(); if (customBaseRenderer) { return customBaseRenderer; } const { Component, createElement } = adapter.getRuntime(); const Div = divFactory(); const VisualDom = visualDomFactory(); const AppContext = contextFactory(); const debug = Debug('renderer:base'); const DESIGN_MODE = { EXTEND: 'extend', BORDER: 'border', PREVIEW: 'preview', }; const OVERLAY_LIST = ['Dialog', 'Overlay', 'Animate', 'ConfigProvider']; let scopeIdx = 0; return class BaseRenderer extends Component implements IRenderer { static dislayName = 'base-renderer'; static defaultProps = { __schema: {}, }; static contextType = AppContext; __namespace = 'base'; constructor(props: IRendererProps, context: any) { super(props, context); this.__beforeInit(props); this.__init(props); this.__afterInit(props); this.__initDebug(); this.__debug(`constructor - ${props?.__schema?.fileName}`); } __beforeInit(/* props: IRendererProps */) { } __init(props: IRendererProps) { this.appHelper = props.__appHelper; this.__compScopes = {}; this.__instanceMap = {}; this.__bindCustomMethods(props); this.__initI18nAPIs(); } __afterInit(/* props: IRendererProps */) { } static getDerivedStateFromProps(props: IRendererProps, state: any) { debug('getDerivedStateFromProps'); const func = props?.__schema?.lifeCycles?.getDerivedStateFromProps; if (func) { return func(props, state); } return null; } async getSnapshotBeforeUpdate() { this.__setLifeCycleMethods('getSnapshotBeforeUpdate', arguments); this.__debug(`getSnapshotBeforeUpdate - ${this.props?.__schema?.fileName}`); } async componentDidMount() { this.reloadDataSource(); this.__setLifeCycleMethods('componentDidMount', arguments); this.__debug(`componentDidMount - ${this.props?.__schema?.fileName}`); } async componentDidUpdate(...args: any) { this.__setLifeCycleMethods('componentDidUpdate', args); this.__debug(`componentDidUpdate - ${this.props.__schema.fileName}`); } async componentWillUnmount(...args: any) { this.__setLifeCycleMethods('componentWillUnmount', args); this.__debug(`componentWillUnmount - ${this.props?.__schema?.fileName}`); } async componentDidCatch(e: any) { this.__setLifeCycleMethods('componentDidCatch', arguments); console.warn(e); } reloadDataSource = () => new Promise((resolve, reject) => { this.__debug('reload data source'); if (!this.__dataHelper) { this.__showPlaceholder = false; return resolve({}); } this.__dataHelper .getInitData() .then((res: any) => { this.__showPlaceholder = false; if (isEmpty(res)) { this.forceUpdate(); return resolve({}); } this.setState(res, resolve); }) .catch((err: Error) => { if (this.__showPlaceholder) { this.__showPlaceholder = false; this.forceUpdate(); } reject(err); }); }); __setLifeCycleMethods = (method: string, args?: any) => { const lifeCycleMethods = getValue(this.props.__schema, 'lifeCycles', {}); let fn = lifeCycleMethods[method]; if (fn) { // TODO, cache if (isJSExpression(fn) || isJSFunction(fn)) { fn = parseExpression(fn, this); } if (typeof fn !== 'function') { console.error(`生命周期${method}类型不符`, fn); return; } try { return fn.apply(this, args); } catch (e) { console.error(`[${this.props.__schema.componentName}]生命周期${method}出错`, e); } } }; __bindCustomMethods = (props = this.props) => { const { __schema } = props; const customMethodsList = Object.keys(__schema.methods || {}) || []; this.__customMethodsList && this.__customMethodsList.forEach((item: any) => { if (!customMethodsList.includes(item)) { delete this[item]; } }); this.__customMethodsList = customMethodsList; forEach(__schema.methods, (val: any, key: string) => { if (isJSExpression(val) || isJSFunction(val)) { val = parseExpression(val, this); } if (typeof val !== 'function') { console.error(`自定义函数${key}类型不符`, val); return; } this[key] = val.bind(this); }); }; __generateCtx = (ctx: object) => { const { pageContext, compContext } = this.context; const obj = { page: pageContext, component: compContext, ...ctx, }; forEach(obj, (val: any, key: string) => { this[key] = val; }); }; __parseData = (data: any, ctx?: object) => { const { __ctx } = this.props; return parseData(data, ctx || __ctx || this); }; __initDataSource = (props = this.props) => { const schema = props.__schema || {}; const dataSource = (schema && schema.dataSource) || {}; // requestHandlersMap 存在才走数据源引擎方案 if (props?.__appHelper?.requestHandlersMap) { const { dataSourceMap, reloadDataSource } = createDataSourceEngine(dataSource, (this as any), { requestHandlersMap: props.__appHelper.requestHandlersMap, }); this.dataSourceMap = dataSourceMap; this.reloadDataSource = () => new Promise((resolve) => { this.__debug('reload data source'); // this.__showPlaceholder = true; reloadDataSource().then(() => { // this.__showPlaceholder = false; // @TODO 是否需要 forceUpate // this.forceUpdate(); resolve({}); }); }); } else { const appHelper = props.__appHelper; this.__dataHelper = new DataHelper(this, dataSource, appHelper, (config: any) => this.__parseData(config)); this.dataSourceMap = this.__dataHelper.dataSourceMap; this.reloadDataSource = () => new Promise((resolve, reject) => { this.__debug('reload data source'); if (!this.__dataHelper) { // this.__showPlaceholder = false; return resolve({}); } this.__dataHelper .getInitData() .then((res: any) => { // this.__showPlaceholder = false; if (isEmpty(res)) { this.forceUpdate(); return resolve({}); } this.setState(res, resolve); }) .catch((err: Error) => { if (this.__showPlaceholder) { this.__showPlaceholder = false; this.forceUpdate(); } reject(err); }); }); } // 设置容器组件占位,若设置占位则在初始异步请求完成之前用loading占位且不渲染容器组件内部内容 // @TODO __showPlaceholder 的逻辑一旦开启就关不掉,先注释掉了 /* this.__showPlaceholder = this.__parseData(schema.props && schema.props.autoLoading) && (dataSource.list || []).some( (item) => !!this.__parseData(item.isInit), ); */ }; __initI18nAPIs = () => { this.i18n = (key: string, values = {}) => { const { locale, messages } = this.props; return getI18n(key, values, locale, messages); }; this.getLocale = () => this.props.locale; this.setLocale = (loc: string) => this.appHelper?.utils?.i18n?.setLocale && this.appHelper?.utils?.i18n?.setLocale(loc); }; __render = () => { const schema = this.props.__schema; this.__setLifeCycleMethods('render'); const { engine } = this.context; if (engine) { engine.props.onCompGetCtx(schema, this); // 画布场景才需要每次渲染bind自定义方法 if (engine.props.designMode) { this.__bindCustomMethods(); this.dataSourceMap = this.__dataHelper && this.__dataHelper.updateConfig(schema.dataSource); } } }; __getRef = (ref: any) => { const { engine } = this.context; const { __schema } = this.props; ref && engine?.props?.onCompGetRef(__schema, ref); this.__ref = ref; }; getSchemaChildren = (schema: ISchema) => { if (!schema || !schema.props) { return schema?.children; } if (!schema.children) return schema.props.children; if (!schema.props.children) return schema.children; let _children = ([] as ComponentModel[]).concat(schema.children); if (Array.isArray(schema.props.children)) { _children = _children.concat(schema.props.children); } else { _children.push(schema.props.children); } return _children; }; __createDom = () => { const { __schema, __ctx, __components = {} } = this.props; const self: any = {}; self.__proto__ = __ctx || this; const _children = this.getSchemaChildren(__schema); return this.__createVirtualDom(_children, self, ({ schema: __schema, Comp: __components[__schema.componentName], } as IInfo)); }; // 将模型结构转换成react Element // schema 模型结构 // self 为每个渲染组件构造的上下文,self是自上而下继承的 // parentInfo 父组件的信息,包含schema和Comp // idx 若为循环渲染的循环Index __createVirtualDom = (schema: any, self: any, parentInfo: IInfo, idx: string | number = ''): any => { const { engine } = this.context || {}; try { if (!schema) return null; // FIXME if (schema.componentName === 'Text' && typeof schema.props.text === 'string') { schema = { ...schema }; schema.children = [schema.props.text]; } const { __appHelper: appHelper, __components: components = {} } = this.props || {}; if (isJSExpression(schema)) { return parseExpression(schema, self); } if (isI18n(schema)) { return parseI18n(schema, self); } if (isJSSlot(schema)) { return this.__createVirtualDom(schema.value, self, parentInfo); } if (typeof schema === 'string') return schema; if (typeof schema === 'number' || typeof schema === 'boolean') { return schema.toString(); } if (Array.isArray(schema)) { if (schema.length === 1) return this.__createVirtualDom(schema[0], self, parentInfo); return schema.map((item, idy) => this.__createVirtualDom(item, self, parentInfo, item?.__ctx?.lceKey ? '' : idy)); } // FIXME const _children = this.getSchemaChildren(schema); // 解析占位组件 if (schema.componentName === 'Flagment' && _children) { const tarChildren = isJSExpression(_children) ? parseExpression(_children, self) : _children; return this.__createVirtualDom(tarChildren, self, parentInfo); } if (schema.$$typeof) { return schema; } if (!isSchema(schema)) return null; let Comp = components[schema.componentName] || engine.getNotFoundComponent(); if (schema.hidden && engine?.props?.designMode) { return null; } if (schema.loop != null) { const loop = parseData(schema.loop, self); if ((Array.isArray(loop) && loop.length > 0) || isJSExpression(loop)) { return this.__createLoopVirtualDom( { ...schema, loop, }, self, parentInfo, idx, ); } } const condition = schema.condition == null ? true : parseData(schema.condition, self); if (!condition) return null; let scopeKey = ''; // 判断组件是否需要生成scope,且只生成一次,挂在this.__compScopes上 if (Comp.generateScope) { const key = parseExpression(schema.props.key, self); if (key) { // 如果组件自己设置key则使用组件自己的key scopeKey = key; } else if (!schema.__ctx) { // 在生产环境schema没有__ctx上下文,需要手动生成一个lceKey schema.__ctx = { lceKey: `lce${++scopeIdx}`, }; scopeKey = schema.__ctx.lceKey; } else { // 需要判断循环的情况 scopeKey = schema.__ctx.lceKey + (idx !== undefined ? `_${idx}` : ''); } if (!this.__compScopes[scopeKey]) { this.__compScopes[scopeKey] = Comp.generateScope(this, schema); } } // 如果组件有设置scope,需要为组件生成一个新的scope上下文 if (scopeKey && this.__compScopes[scopeKey]) { const compSelf = { ...this.__compScopes[scopeKey] }; compSelf.__proto__ = self; self = compSelf; } // 容器类组件的上下文通过props传递,避免context传递带来的嵌套问题 const otherProps: any = isFileSchema(schema) ? { __schema: schema, __appHelper: appHelper, __components: components, } : {}; if (engine?.props?.designMode) { otherProps.__designMode = engine.props.designMode; } const componentInfo: any = {}; const props: any = this.__parseProps(schema.props, self, '', { schema, Comp, componentInfo: { ...componentInfo, props: transformArrayToMap(componentInfo.props, 'name'), }, }) || {}; // 对于可以获取到ref的组件做特殊处理 if (!acceptsRef(Comp)) { Comp = compWrapper(Comp); } otherProps.ref = (ref: any) => { this.$(props.fieldId, ref); // 收集ref const refProps = props.ref; if (refProps && typeof refProps === 'string') { this[refProps] = ref; } ref && engine?.props?.onCompGetRef(schema, ref); }; // scope需要传入到组件上 if (scopeKey && this.__compScopes[scopeKey]) { props.__scope = this.__compScopes[scopeKey]; } // FIXME 这里清除 key 是为了避免循环渲染中更改 key 导致的渲染重复 props.key = ''; if (schema?.__ctx?.lceKey) { if (!isFileSchema(schema)) { engine?.props?.onCompGetCtx(schema, self); } props.key = props.key || `${schema.__ctx.lceKey}_${schema.__ctx.idx || 0}_${idx !== undefined ? idx : ''}`; } else if (typeof idx === 'number' && !props.key) { props.key = idx; } props.__id = schema.id; if (!props.key) { props.key = props.__id; } let child: any = null; if (/*! isFileSchema(schema) && */_children) { child = this.__createVirtualDom( isJSExpression(_children) ? parseExpression(_children, self) : _children, self, { schema, Comp, }, ); } const renderComp = (props: any) => engine.createElement(Comp, props, child); // 设计模式下的特殊处理 if (engine && [DESIGN_MODE.EXTEND, DESIGN_MODE.BORDER].includes(engine.props.designMode)) { // 对于overlay,dialog等组件为了使其在设计模式下显示,外层需要增加一个div容器 if (OVERLAY_LIST.includes(schema.componentName)) { const { ref, ...overlayProps } = otherProps; return createElement(Div, { ref, __designMode: engine.props.designMode, }, renderComp({ ...props, ...overlayProps })); } // 虚拟dom显示 if (componentInfo?.parentRule) { const parentList = componentInfo.parentRule.split(','); const { schema: parentSchema, Comp: parentComp } = parentInfo; if ( !parentList.includes(parentSchema.componentName) || parentComp !== components[parentSchema.componentName] ) { props.__componentName = schema.componentName; Comp = VisualDom; } else { // 若虚拟dom在正常的渲染上下文中,就不显示设计模式了 props.__disableDesignMode = true; } } } return renderComp({ ...props, ...otherProps }); } catch (e) { return engine.createElement(engine.getFaultComponent(), { error: e, schema, self, parentInfo, idx, }); } }; __createLoopVirtualDom = (schema: any, self: any, parentInfo: IInfo, idx: number | string) => { if (isFileSchema(schema)) { console.warn('file type not support Loop'); return null; } if (!Array.isArray(schema.loop)) return null; const itemArg = (schema.loopArgs && schema.loopArgs[0]) || 'item'; const indexArg = (schema.loopArgs && schema.loopArgs[1]) || 'index'; return schema.loop.map((item: string[], i: number) => { const loopSelf: any = { [itemArg]: item, [indexArg]: i, }; loopSelf.__proto__ = self; return this.__createVirtualDom( { ...schema, loop: undefined, }, loopSelf, parentInfo, idx ? `${idx}_${i}` : i, ); }); }; __parseProps = (props: any, self: any, path: string, info: IInfo): any => { const { schema, Comp, componentInfo = {} } = info; const propInfo = getValue(componentInfo.props, path); // FIXME! 将这行逻辑外置,解耦,线上环境不要验证参数,调试环境可以有,通过传参自定义 const propType = propInfo?.extra?.propType; const ignoreParse = schema.__ignoreParse || []; const checkProps = (value: any) => { if (!propType) return value; return checkPropTypes(value, path, propType, componentInfo.name) ? value : undefined; }; const parseReactNode = (data: any, params: any) => { if (isEmpty(params)) { return checkProps(this.__createVirtualDom(data, self, ({ schema, Comp } as IInfo))); } return checkProps(function () { const args: any = {}; if (Array.isArray(params) && params.length) { params.forEach((item, idx) => { if (typeof item === 'string') { args[item] = arguments[idx]; } else if (item && typeof item === 'object') { args[item.name] = arguments[idx]; } }); } args.__proto__ = self; return self.__createVirtualDom(data, args, { schema, Comp }); }); }; // 判断是否需要解析变量 if ( ignoreParse.some((item: any) => { if (item instanceof RegExp) { return item.test(path); } return item === path; }) ) { return checkProps(props); } if (isJSExpression(props)) { props = parseExpression(props, self); // 只有当变量解析出来为模型结构的时候才会继续解析 if (!isSchema(props) && !isJSSlot(props)) return checkProps(props); } const handleLegaoI18n = (props: any) => props[props.use || 'zh_CN']; // 兼容乐高设计态 i18n 数据 if (isI18n(props)) { const i18nProp = handleLegaoI18n(props); if (i18nProp) { props = i18nProp; } else { return parseI18n(props, self); } } // 兼容乐高设计态的变量绑定 if (isVariable(props)) { props = props.value; if (isI18n(props)) { props = handleLegaoI18n(props); } } if (isJSFunction(props)) { props = transformStringToFunction(props.value); } if (isJSSlot(props)) { const { params, value } = props; if (!isSchema(value) || isEmpty(value)) return undefined; return parseReactNode(value, params); } // 兼容通过componentInfo判断的情况 if (isSchema(props)) { const isReactNodeFunction = !!( propInfo?.type === 'ReactNode' && propInfo?.props?.type === 'function' ); const isMixinReactNodeFunction = !!( propInfo?.type === 'Mixin' && propInfo?.props?.types?.indexOf('ReactNode') > -1 && propInfo?.props?.reactNodeProps?.type === 'function' ); return parseReactNode( props, isReactNodeFunction ? propInfo.props.params : isMixinReactNodeFunction ? propInfo.props.reactNodeProps.params : null, ); } if (Array.isArray(props)) { return checkProps(props.map((item, idx) => this.__parseProps(item, self, path ? `${path}.${idx}` : `${idx}`, info))); } if (typeof props === 'function') { return checkProps(props.bind(self)); } if (props && typeof props === 'object') { if (props.$$typeof) return checkProps(props); const res: any = {}; forEach(props, (val: any, key: string) => { if (key.startsWith('__')) { res[key] = val; return; } res[key] = this.__parseProps(val, self, path ? `${path}.${key}` : key, info); }); return checkProps(res); } if (typeof props === 'string') { return checkProps(props.trim()); } return checkProps(props); }; $(filedId: string, instance?: any) { this.__instanceMap = this.__instanceMap || {}; if (!filedId) { return this.__instanceMap; } if (instance) { this.__instanceMap[filedId] = instance; } return this.__instanceMap[filedId]; } __initDebug = () => { this.__logger = Debug(`renderer:${this.__namespace || 'base'}`); }; __debug = (msg = '') => { if (this.__logger) { this.__logger(`${this.__namespace}.${msg}`); } }; __renderContextProvider = (customProps?: object, children?: any) => { customProps = customProps || {}; children = children || this.__createDom(); return createElement(AppContext.Provider, { value: { ...this.context, blockContext: this, ...customProps, }, children, }); }; __renderContextConsumer = (children: any) => { return createElement(AppContext.Consumer, {}, children); }; __renderComp(Comp: any, ctxProps: object) { const { __schema } = this.props; const data = this.__parseData(__schema?.props); const { className } = data; const { engine } = this.context || {}; if (!engine) { return null; } const child = engine.createElement( Comp || Div, { ...data, ...this.props, ref: this.__getRef, className: classnames(getFileCssName(__schema?.fileName), className, this.props.className), __id: __schema?.id, }, this.__createDom(), ); return this.__renderContextProvider(ctxProps, child); } __renderContent(children: any) { const { __schema } = this.props; const props = this.__parseData(__schema.props); const { id, className, style = {} } = props; return createElement('div', { ref: this.__getRef, className: classnames(`lce-${this.__namespace}`, getFileCssName(__schema.fileName), className, this.props.className), id: this.props.id || id, style: { ...style, ...(typeof this.props.style === 'object' ? this.props.style : {}) }, }, children); } __checkSchema = (schema: ISchema, extraComponents: string | string[] = []) => { if (typeof extraComponents === 'string') { extraComponents = [extraComponents]; } const buitin = capitalizeFirstLetter(this.__namespace); const componentNames = [buitin, ...extraComponents]; return !isSchema(schema, true) || !componentNames.includes(schema.componentName); }; get requestHandlersMap() { return this.appHelper?.requestHandlersMap; } get utils() { return this.appHelper?.utils; } get constants() { return this.appHelper?.constants; } get history() { return this.appHelper?.history; } get location() { return this.appHelper?.location; } get match() { return this.appHelper?.match; } render() { return null; } }; }