780 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
};
}