From 7167767f293fdc6c8d091b05c7ce211d18201038 Mon Sep 17 00:00:00 2001 From: "wuji.xwt" Date: Sun, 21 Jun 2020 17:11:54 +0800 Subject: [PATCH] feat: init rax-render --- README.md | 2 +- packages/rax-render/README.md | 49 ++ packages/rax-render/build.json | 11 + packages/rax-render/package.json | 52 ++ .../rax-render/src/comp/visualDom/index.css | 19 + .../rax-render/src/comp/visualDom/index.jsx | 23 + packages/rax-render/src/context/appContext.js | 4 + packages/rax-render/src/engine/base.jsx | 512 ++++++++++++++++++ .../rax-render/src/engine/blockEngine.jsx | 83 +++ packages/rax-render/src/engine/compEngine.jsx | 103 ++++ packages/rax-render/src/engine/index.jsx | 122 +++++ packages/rax-render/src/engine/pageEngine.jsx | 90 +++ packages/rax-render/src/engine/tempEngine.jsx | 67 +++ packages/rax-render/src/hoc/compFactory.js | 71 +++ packages/rax-render/src/hoc/compWrapper.js | 15 + packages/rax-render/src/index.jsx | 5 + 16 files changed, 1227 insertions(+), 1 deletion(-) create mode 100644 packages/rax-render/README.md create mode 100644 packages/rax-render/build.json create mode 100644 packages/rax-render/package.json create mode 100644 packages/rax-render/src/comp/visualDom/index.css create mode 100644 packages/rax-render/src/comp/visualDom/index.jsx create mode 100644 packages/rax-render/src/context/appContext.js create mode 100644 packages/rax-render/src/engine/base.jsx create mode 100644 packages/rax-render/src/engine/blockEngine.jsx create mode 100644 packages/rax-render/src/engine/compEngine.jsx create mode 100644 packages/rax-render/src/engine/index.jsx create mode 100644 packages/rax-render/src/engine/pageEngine.jsx create mode 100644 packages/rax-render/src/engine/tempEngine.jsx create mode 100644 packages/rax-render/src/hoc/compFactory.js create mode 100644 packages/rax-render/src/hoc/compWrapper.js create mode 100644 packages/rax-render/src/index.jsx diff --git a/README.md b/README.md index 3007afc49..34ff152c5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ #### 创建新包: -- `./create.sh ` +- `./scripts/create.sh ` #### 跑起来: diff --git a/packages/rax-render/README.md b/packages/rax-render/README.md new file mode 100644 index 000000000..3d66912a5 --- /dev/null +++ b/packages/rax-render/README.md @@ -0,0 +1,49 @@ +# Rax Renderer + +Rax 渲染模块。 + +## 安装 + +``` +$ npm install @ali/lowcode-engine-rax-renderer --save +``` + +## 使用 + +```js +import { createElement, render } from 'rax'; +import DriverUniversal from 'driver-universal'; +import RaxRenderer from '@ali/lowcode-engine-rax-renderer'; + +const components = { + View, + Text +}; + +const schema = { + componentName: 'Page', + fileName: 'home', + children: [ + { + componentName: 'View', + children: [ + { + componentName: 'Text', + props: { + type: 'primary' + }, + children: ['Welcome to Your Rax App'] + } + ] + } + ] +}; + +render( + , + document.getElementById('root'), { driver: DriverUniversal } +); +``` diff --git a/packages/rax-render/build.json b/packages/rax-render/build.json new file mode 100644 index 000000000..3edf14380 --- /dev/null +++ b/packages/rax-render/build.json @@ -0,0 +1,11 @@ +{ + "plugins": [ + [ + "build-plugin-rax-component", + { + "type": "rax", + "targets": ["web"] + } + ] + ] +} diff --git a/packages/rax-render/package.json b/packages/rax-render/package.json new file mode 100644 index 000000000..90d0070ba --- /dev/null +++ b/packages/rax-render/package.json @@ -0,0 +1,52 @@ +{ + "name": "@ali/lowcode-engine-rax-renderer", + "version": "0.1.0", + "description": "Rax renderer for Ali lowCode engine", + "main": "lib/index.js", + "module": "lib/index.js", + "miniappConfig": { + "main": "lib/miniapp/index", + "main:wechat": "lib/wechat-miniprogram/index" + }, + "files": [ + "dist", + "es", + "lib", + "src", + "types" + ], + "keywords": [ + "low-code", + "lowcode", + "Rax" + ], + "engines": { + "npm": ">=3.0.0" + }, + "peerDependencies": { + "rax": "^1.1.0", + "prop-types": "^15.7.2" + }, + "scripts": { + "start": "build-scripts start", + "build": "build-scripts build", + "prepublish": "npm run build" + }, + "dependencies": { + "@ali/iceluna-sdk": "^1.0.7-beta.12", + "classnames": "^2.2.6", + "debug": "^4.1.1", + "lodash.isempty": "^4.4.0", + "rax-find-dom-node": "^1.0.1", + "rax-text": "^1.1.6", + "rax-view": "^1.0.0" + }, + "devDependencies": { + "@alib/build-scripts": "^0.1.0", + "build-plugin-rax-component": "^0.1.4" + }, + "publishConfig": { + "registry": "https://registry.npm.alibaba-inc.com" + }, + "license": "MIT" +} diff --git a/packages/rax-render/src/comp/visualDom/index.css b/packages/rax-render/src/comp/visualDom/index.css new file mode 100644 index 000000000..a9b944a7f --- /dev/null +++ b/packages/rax-render/src/comp/visualDom/index.css @@ -0,0 +1,19 @@ +.visual-dom .panel-container { + box-sizing: border-box; + border: 1px solid #e9e9e9; +} + +.visual-dom .panel-container .title { + display: block; + font-size: 12px; + color: #333; + background-color: #ebecf0; + line-height: 28px; + padding: 0 12px; + border-bottom: 1px solid #e9e9e9; +} + +.visual-dom .panel-container .content { + min-height: 20px; + padding: 5px; +} diff --git a/packages/rax-render/src/comp/visualDom/index.jsx b/packages/rax-render/src/comp/visualDom/index.jsx new file mode 100644 index 000000000..7d480098a --- /dev/null +++ b/packages/rax-render/src/comp/visualDom/index.jsx @@ -0,0 +1,23 @@ +import { Component } from 'rax'; +import View from 'rax-view'; +import Text from 'rax-text'; +import './index.css'; + +export default class VisualDom extends Component { + static displayName = 'VisualDom'; + static defaultProps = { + children: null + }; + render() { + const { children, title, label, text, __componentName } = this.props; + + return ( + + + {title || label || text || __componentName} + {children} + + + ); + } +} diff --git a/packages/rax-render/src/context/appContext.js b/packages/rax-render/src/context/appContext.js new file mode 100644 index 000000000..b1fce29c4 --- /dev/null +++ b/packages/rax-render/src/context/appContext.js @@ -0,0 +1,4 @@ +import { createContext } from 'rax'; + +const context = createContext({}); +export default context; diff --git a/packages/rax-render/src/engine/base.jsx b/packages/rax-render/src/engine/base.jsx new file mode 100644 index 000000000..eedb51d87 --- /dev/null +++ b/packages/rax-render/src/engine/base.jsx @@ -0,0 +1,512 @@ +import { Component, createElement } from 'rax'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import View from 'rax-view'; +import DataHelper from '@ali/iceluna-sdk/lib/utils/dataHelper'; +import { + forEach, + getValue, + parseData, + parseExpression, + isEmpty, + isSchema, + isFileSchema, + isJSExpression, + isJSSlot, + isJSFunction, + transformArrayToMap, + checkPropTypes, + generateI18n, + acceptsRef, +} from '@ali/iceluna-sdk/lib/utils'; +import VisualDom from '../comp/visualDom'; +import AppContext from '../context/appContext'; +import CompWrapper from '../hoc/compWrapper'; + +const debug = Debug('engine:base'); +const DESIGN_MODE = { + EXTEND: 'extend', + BORDER: 'border', + PREVIEW: 'preview', +}; +const OVERLAY_LIST = ['Dialog', 'Overlay']; +let scopeIdx = 0; + +export default class BaseEngine extends Component { + static dislayName = 'base-engine'; + static propTypes = { + locale: PropTypes.string, + messages: PropTypes.object, + __appHelper: PropTypes.object, + __components: PropTypes.object, + __componentsMap: PropTypes.object, + __ctx: PropTypes.object, + __schema: PropTypes.object + }; + static defaultProps = { + __schema: {} + }; + static contextType = AppContext; + + constructor(props, context) { + super(props, context); + this.appHelper = props.__appHelper; + this.__compScopes = {}; + const { locale, messages } = props; + this.i18n = generateI18n(locale, messages); + this.__bindCustomMethods(props); + } + + async getSnapshotBeforeUpdate() { + this.__setLifeCycleMethods('getSnapshotBeforeUpdate', arguments); + } + + async componentDidMount() { + this.reloadDataSource(); + this.__setLifeCycleMethods('componentDidMount', arguments); + } + + async componentDidUpdate() { + this.__setLifeCycleMethods('componentDidUpdate', arguments); + } + + async componentWillUnmount() { + this.__setLifeCycleMethods('componentWillUnmount', arguments); + } + + async componentDidCatch(e) { + this.__setLifeCycleMethods('componentDidCatch', arguments); + console.warn(e); + } + + reloadDataSource = () => { + return new Promise((resolve, reject) => { + debug('reload data source'); + if (!this.__dataHelper) { + this.__showPlaceholder = false; + return resolve(); + } + this.__dataHelper + .getInitData() + .then(res => { + this.__showPlaceholder = false; + if (isEmpty(res)) { + this.forceUpdate(); + return resolve(); + } + this.setState(res, resolve); + }) + .catch(err => { + if (this.__showPlaceholder) { + this.__showPlaceholder = false; + this.forceUpdate(); + } + reject(err); + }); + }); + }; + + __setLifeCycleMethods = (method, args) => { + const lifeCycleMethods = getValue(this.props.__schema, 'lifeCycles', {}); + if (lifeCycleMethods[method]) { + try { + return lifeCycleMethods[method].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 => { + if (!customMethodsList.includes(item)) { + delete this[item]; + } + }); + this.__customMethodsList = customMethodsList; + forEach(__schema.methods, (val, key) => { + this[key] = val.bind(this); + }); + }; + + __generateCtx = ctx => { + const { pageContext, compContext } = this.context; + const obj = { + page: pageContext, + component: compContext, + ...ctx + }; + forEach(obj, (val, key) => { + this[key] = val; + }); + }; + + __parseData = (data, ctx) => { + const { __ctx } = this.props; + return parseData(data, ctx || __ctx || this); + }; + + __initDataSource = (props = this.props) => { + const schema = props.__schema || {}; + const appHelper = props.__appHelper; + const dataSource = (schema && schema.dataSource) || {}; + this.__dataHelper = new DataHelper(this, dataSource, appHelper, config => this.__parseData(config)); + this.dataSourceMap = this.__dataHelper.dataSourceMap; + // 设置容器组件占位,若设置占位则在初始异步请求完成之前用loading占位且不渲染容器组件内部内容 + this.__showPlaceholder = + this.__parseData(schema.props && schema.props.autoLoading) && + (dataSource.list || []).some(item => !!this.__parseData(item.isInit)); + }; + + __render = () => { + const schema = this.props.__schema; + this.__setLifeCycleMethods('render'); + + const engine = this.context.engine; + 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 => { + this.__ref = ref; + }; + + __createDom = () => { + const { __schema, __ctx, __components = {} } = this.props; + const self = {}; + self.__proto__ = __ctx || this; + return this.__createVirtualDom(__schema.children, self, { + schema: __schema, + Comp: __components[__schema.componentName] + }); + }; + + // 将模型结构转换成react Element + // schema 模型结构 + // self 为每个渲染组件构造的上下文,self是自上而下继承的 + // parentInfo 父组件的信息,包含schema和Comp + // idx 若为循环渲染的循环Index + __createVirtualDom = (schema, self, parentInfo, idx) => { + if (!schema) return null; + // rax text prop 兼容处理 + if (schema.componentName === 'Text') { + if (typeof schema.props.text === 'string') { + schema = Object.assign({}, schema); + schema.children = [schema.props.text]; + } + } + + const { __appHelper: appHelper, __components: components = {}, __componentsMap: componentsMap = {} } = + this.props || {}; + const { engine } = this.context || {}; + if (isJSExpression(schema)) { + return parseExpression(schema, self); + } + 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, idx) => + this.__createVirtualDom(item, self, parentInfo, item && item.__ctx && item.__ctx.lunaKey ? '' : idx) + ); + } + + //解析占位组件 + if (schema.componentName === 'Flagment' && schema.children) { + let tarChildren = isJSExpression(schema.children) ? parseExpression(schema.children, self) : schema.children; + return this.__createVirtualDom(tarChildren, self, parentInfo); + } + + if (schema.$$typeof) { + return schema; + } + if (!isSchema(schema)) return null; + let Comp = components[schema.componentName] || View; + + if (schema.loop !== undefined) { + return this.__createLoopVirtualDom( + { + ...schema, + loop: parseData(schema.loop, self) + }, + self, + parentInfo, + idx + ); + } + const condition = schema.condition === undefined ? 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上下文,需要手动生成一个lunaKey + schema.__ctx = { + lunaKey: `luna${++scopeIdx}` + }; + scopeKey = schema.__ctx.lunaKey; + } else { + // 需要判断循环的情况 + scopeKey = schema.__ctx.lunaKey + (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 = isFileSchema(schema) + ? { + __schema: schema, + __appHelper: appHelper, + __components: components, + __componentsMap: componentsMap + } + : {}; + if (engine && engine.props.designMode) { + otherProps.__designMode = engine.props.designMode; + } + const componentInfo = componentsMap[schema.componentName] || {}; + const props = this.__parseProps(schema.props, self, '', { + schema, + Comp, + componentInfo: { + ...componentInfo, + props: transformArrayToMap(componentInfo.props, 'name') + } + }); + // 对于可以获取到ref的组件做特殊处理 + if (!acceptsRef(Comp)) { + Comp = CompWrapper(Comp); + } + otherProps.ref = ref => { + const refProps = props.ref; + if (refProps && typeof refProps === 'string') { + this[refProps] = ref; + } + engine && engine.props.onCompGetRef(schema, ref); + }; + // scope需要传入到组件上 + if (scopeKey && this.__compScopes[scopeKey]) { + props.__scope = this.__compScopes[scopeKey]; + } + if (schema.__ctx && schema.__ctx.lunaKey) { + if (!isFileSchema(schema)) { + engine && engine.props.onCompGetCtx(schema, self); + } + props.key = props.key || `${schema.__ctx.lunaKey}_${schema.__ctx.idx || 0}_${idx !== undefined ? idx : ''}`; + } else if (typeof idx === 'number' && !props.key) { + props.key = idx; + } + const renderComp = props => ( + + {(!isFileSchema(schema) && + !!schema.children && + this.__createVirtualDom( + isJSExpression(schema.children) ? parseExpression(schema.children, self) : schema.children, + self, + { + schema, + Comp + } + )) || + null} + + ); + //设计模式下的特殊处理 + 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 ( +
+ {renderComp({ ...props, ...overlayProps })} +
+ ); + } + // 虚拟dom显示 + if (componentInfo && 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 }); + }; + + __createLoopVirtualDom = (schema, self, parentInfo, idx) => { + 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, i) => { + const loopSelf = { + [itemArg]: item, + [indexArg]: i + }; + loopSelf.__proto__ = self; + return this.__createVirtualDom( + { + ...schema, + loop: undefined + }, + loopSelf, + parentInfo, + idx ? `${idx}_${i}` : i + ); + }); + }; + + __createContextDom = (childCtx, currCtx) => { + return ( + + {context => { + this.context = context; + this.__generateCtx(currCtx); + this.__render(); + return ( + + {this.__createDom()} + + ); + }} + + ); + }; + + __parseProps = (props, self, path, info) => { + const { schema, Comp, componentInfo = {} } = info; + const propInfo = getValue(componentInfo.props, path); + const propType = propInfo && propInfo.extra && propInfo.extra.propType; + const ignoreParse = schema.__ignoreParse || []; + const checkProps = value => { + if (!propType) return value; + return checkPropTypes(value, path, propType, componentInfo.name) ? value : undefined; + }; + + const parseReactNode = (data, params) => { + if (isEmpty(params)) { + return checkProps(this.__createVirtualDom(data, self, { schema, Comp })); + } else { + return checkProps(function() { + const args = {}; + if (Array.isArray(params) && params.length) { + params.map((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 => { + 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); + } + + if (isJSFunction(props)) { + props = props.value; + } + if (isJSSlot(props)) { + const { params, value } = props; + if (!isSchema(value) || isEmpty(value)) return undefined; + return parseReactNode(value, params); + } + // 兼容通过componentInfo判断的情况 + if (isSchema(props)) { + return parseReactNode(props); + } else if (Array.isArray(props)) { + return checkProps(props.map((item, idx) => this.__parseProps(item, self, path ? `${path}.${idx}` : idx, info))); + } else if (typeof props === 'function') { + return checkProps(props.bind(self)); + } else if (props && typeof props === 'object') { + if (props.$$typeof) return checkProps(props); + const res = {}; + forEach(props, (val, key) => { + if (key.startsWith('__')) { + res[key] = val; + return; + } + res[key] = this.__parseProps(val, self, path ? `${path}.${key}` : key, info); + }); + return checkProps(res); + } else if (typeof props === 'string') { + return checkProps(props.trim()); + } + return checkProps(props); + }; + + get utils() { + return this.appHelper && this.appHelper.utils; + } + get constants() { + return this.appHelper && this.appHelper.constants; + } + get history() { + return this.appHelper && this.appHelper.history; + } + get location() { + return this.appHelper && this.appHelper.location; + } + get match() { + return this.appHelper && this.appHelper.match; + } + + render() { + return null; + } +} diff --git a/packages/rax-render/src/engine/blockEngine.jsx b/packages/rax-render/src/engine/blockEngine.jsx new file mode 100644 index 000000000..2067d0d67 --- /dev/null +++ b/packages/rax-render/src/engine/blockEngine.jsx @@ -0,0 +1,83 @@ +import { createElement } from 'rax'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import classnames from 'classnames'; +import { isSchema, getFileCssName } from '@ali/iceluna-sdk/lib/utils'; +import BaseEngine from './base'; + +const debug = Debug('engine:block'); + +export default class BlockEngine extends BaseEngine { + static dislayName = 'block-engine'; + static propTypes = { + __schema: PropTypes.object + }; + static defaultProps = { + __schema: {} + }; + + static getDerivedStateFromProps(props, state) { + debug(`block.getDerivedStateFromProps`); + const func = props.__schema.lifeCycles && props.__schema.lifeCycles.getDerivedStateFromProps; + if (func) { + return func(props, state); + } + return null; + } + + constructor(props, context) { + super(props, context); + this.__generateCtx(); + const schema = props.__schema || {}; + this.state = this.__parseData(schema.state || {}); + this.__initDataSource(props); + this.__setLifeCycleMethods('constructor', arguments); + debug(`block.constructor - ${schema.fileName}`); + } + + async getSnapshotBeforeUpdate() { + super.getSnapshotBeforeUpdate(...arguments); + debug(`block.getSnapshotBeforeUpdate - ${this.props.__schema.fileName}`); + } + async componentDidMount() { + super.componentDidMount(...arguments); + debug(`block.componentDidMount - ${this.props.__schema.fileName}`); + } + async componentDidUpdate() { + super.componentDidUpdate(...arguments); + debug(`block.componentDidUpdate - ${this.props.__schema.fileName}`); + } + async componentWillUnmount() { + super.componentWillUnmount(...arguments); + debug(`block.componentWillUnmount - ${this.props.__schema.fileName}`); + } + async componentDidCatch() { + await super.componentDidCatch(...arguments); + debug(`block.componentDidCatch - ${this.props.__schema.fileName}`); + } + + render() { + const { __schema } = this.props; + + if (!isSchema(__schema, true) || __schema.componentName !== 'Block') { + return '区块schema结构异常!'; + } + + debug(`block.render - ${__schema.fileName}`); + + const { id, className, style } = this.__parseData(__schema.props); + + return ( +
+ {this.__createContextDom({ + blockContext: this + })} +
+ ); + } +} diff --git a/packages/rax-render/src/engine/compEngine.jsx b/packages/rax-render/src/engine/compEngine.jsx new file mode 100644 index 000000000..4a0cf54b0 --- /dev/null +++ b/packages/rax-render/src/engine/compEngine.jsx @@ -0,0 +1,103 @@ +import { createElement } from 'rax'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import classnames from 'classnames'; +import { isSchema, getFileCssName } from '@ali/iceluna-sdk/lib/utils'; +import BaseEngine from './base'; + +const debug = Debug('engine:comp'); + +export default class CompEngine extends BaseEngine { + static dislayName = 'comp-engine'; + static propTypes = { + __schema: PropTypes.object + }; + static defaultProps = { + __schema: {} + }; + + static getDerivedStateFromProps(props, state) { + debug(`comp.getDerivedStateFromProps`); + const func = props.__schema.lifeCycles && props.__schema.lifeCycles.getDerivedStateFromProps; + if (func) { + return func(props, state); + } + return null; + } + + constructor(props, context) { + super(props, context); + this.__generateCtx({ + component: this + }); + const schema = props.__schema || {}; + this.state = this.__parseData(schema.state || {}); + this.__initDataSource(props); + this.__setLifeCycleMethods('constructor', arguments); + debug(`comp.constructor - ${schema.fileName}`); + } + + async getSnapshotBeforeUpdate() { + super.getSnapshotBeforeUpdate(...arguments); + debug(`comp.getSnapshotBeforeUpdate - ${this.props.__schema.fileName}`); + } + async componentDidMount() { + super.componentDidMount(...arguments); + debug(`comp.componentDidMount - ${this.props.__schema.fileName}`); + } + async componentDidUpdate() { + super.componentDidUpdate(...arguments); + debug(`comp.componentDidUpdate - ${this.props.__schema.fileName}`); + } + async componentWillUnmount() { + super.componentWillUnmount(...arguments); + debug(`comp.componentWillUnmount - ${this.props.__schema.fileName}`); + } + async componentDidCatch(e) { + super.componentDidCatch(...arguments); + debug(`comp.componentDidCatch - ${this.props.__schema.fileName}`); + } + + render() { + const { __schema } = this.props; + + if (!isSchema(__schema, true) || __schema.componentName !== 'Component') { + return '自定义组件schema结构异常!'; + } + + debug(`comp.render - ${__schema.fileName}`); + + const { id, className, style, noContainer } = this.__parseData(__schema.props); + + if (noContainer) { + return this.__createContextDom( + { + compContext: this, + blockContext: this + }, + { + component: this + } + ); + } + + return ( +
+ {this.__createContextDom( + { + compContext: this, + blockContext: this + }, + { + component: this + } + )} +
+ ); + } +} diff --git a/packages/rax-render/src/engine/index.jsx b/packages/rax-render/src/engine/index.jsx new file mode 100644 index 000000000..e969f1a91 --- /dev/null +++ b/packages/rax-render/src/engine/index.jsx @@ -0,0 +1,122 @@ +import { Component, createElement } from 'rax'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import * as isEmpty from 'lodash.isempty'; +import findDOMNode from 'rax-find-dom-node'; +import { isFileSchema, goldlog } from '@ali/iceluna-sdk/lib/utils'; +import AppContext from '../context/appContext'; +import Page from './pageEngine'; +import CustomComp from './compEngine'; +import Block from './blockEngine'; +import Temp from './tempEngine'; + +const debug = Debug('engine:entry'); +const ENGINE_COMPS = { + Page, + Component: CustomComp, + Block, + Temp, +}; +export default class Engine extends Component { + static dislayName = 'engine'; + static propTypes = { + appHelper: PropTypes.object, + components: PropTypes.object, + componentsMap: PropTypes.object, + designMode: PropTypes.string, + suspended: PropTypes.bool, + schema: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + onCompGetRef: PropTypes.func, + onCompGetCtx: PropTypes.func + }; + static defaultProps = { + appHelper: null, + components: {}, + componentsMap: {}, + designMode: '', + suspended: false, + schema: {}, + onCompGetRef: () => {}, + onCompGetCtx: () => {} + }; + + constructor(props, context) { + super(props, context); + this.state = {}; + debug(`entry.constructor - ${props.schema && props.schema.componentName}`); + } + + async componentDidMount() { + goldlog( + 'EXP', + { + action: 'appear', + value: !!this.props.designMode + }, + 'engine' + ); + debug(`entry.componentDidMount - ${this.props.schema && this.props.schema.componentName}`); + } + + async componentDidUpdate() { + debug(`entry.componentDidUpdate - ${this.props.schema && this.props.schema.componentName}`); + } + + async componentWillUnmount() { + debug(`entry.componentWillUnmount - ${this.props.schema && this.props.schema.componentName}`); + } + + async componentDidCatch(e) { + console.warn(e); + } + + shouldComponentUpdate(nextProps) { + return !nextProps.suspended; + } + + __getRef = ref => { + this.__ref = ref; + if (ref) { + this.props.onCompGetRef(this.props.schema, ref, true); + } + }; + + render() { + const { schema, designMode, appHelper, components, componentsMap } = this.props; + if (isEmpty(schema)) { + return null; + } + if (!isFileSchema(schema)) { + return '模型结构异常'; + } + debug('entry.render'); + const allComponents = { ...ENGINE_COMPS, ...components }; + const Comp = allComponents[schema.componentName]; + if (Comp) { + return ( + + + + ); + } + return null; + } +} + +Engine.findDOMNode = findDOMNode; diff --git a/packages/rax-render/src/engine/pageEngine.jsx b/packages/rax-render/src/engine/pageEngine.jsx new file mode 100644 index 000000000..25eed613c --- /dev/null +++ b/packages/rax-render/src/engine/pageEngine.jsx @@ -0,0 +1,90 @@ +import { createElement } from 'rax'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import classnames from 'classnames'; +import { isSchema, getFileCssName } from '@ali/iceluna-sdk/lib/utils'; +import BaseEngine from './base'; + +const debug = Debug('engine:page'); + +export default class PageEngine extends BaseEngine { + static dislayName = 'page-engine'; + static propTypes = { + __schema: PropTypes.object + }; + static defaultProps = { + __schema: {} + }; + + static getDerivedStateFromProps(props, state) { + debug(`page.getDerivedStateFromProps`); + const func = props.__schema.lifeCycles && props.__schema.lifeCycles.getDerivedStateFromProps; + if (func) { + return func(props, state); + } + return null; + } + + constructor(props, context) { + super(props, context); + this.__generateCtx({ + page: this + }); + const schema = props.__schema || {}; + this.state = this.__parseData(schema.state || {}); + this.__initDataSource(props); + this.__setLifeCycleMethods('constructor', arguments); + + debug(`page.constructor - ${schema.fileName}`); + } + + async getSnapshotBeforeUpdate() { + super.getSnapshotBeforeUpdate(...arguments); + debug(`page.getSnapshotBeforeUpdate - ${this.props.__schema.fileName}`); + } + async componentDidMount() { + super.componentDidMount(...arguments); + debug(`page.componentDidMount - ${this.props.__schema.fileName}`); + } + async componentDidUpdate() { + super.componentDidUpdate(...arguments); + debug(`page.componentDidUpdate - ${this.props.__schema.fileName}`); + } + async componentWillUnmount() { + super.componentWillUnmount(...arguments); + debug(`page.componentWillUnmount - ${this.props.__schema.fileName}`); + } + async componentDidCatch() { + await super.componentDidCatch(...arguments); + debug(`page.componentDidCatch - ${this.props.__schema.fileName}`); + } + + render() { + const { __schema } = this.props; + if (!isSchema(__schema, true) || __schema.componentName !== 'Page') { + return '页面schema结构异常!'; + } + debug(`page.render - ${__schema.fileName}`); + + const { id, className, style } = this.__parseData(__schema.props); + + return ( +
+ {this.__createContextDom( + { + pageContext: this, + blockContext: this + }, + { + page: this + } + )} +
+ ); + } +} diff --git a/packages/rax-render/src/engine/tempEngine.jsx b/packages/rax-render/src/engine/tempEngine.jsx new file mode 100644 index 000000000..d76e01c69 --- /dev/null +++ b/packages/rax-render/src/engine/tempEngine.jsx @@ -0,0 +1,67 @@ +import { createElement } from 'rax'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import { isSchema } from '@ali/iceluna-sdk/lib/utils'; +import AppContext from '../context/appContext'; +import BaseEngine from './base'; + +const debug = Debug('engine:temp'); +export default class TempEngine extends BaseEngine { + static dislayName = 'temp-engine'; + static propTypes = { + __ctx: PropTypes.object, + __schema: PropTypes.object + }; + static defaultProps = { + __ctx: {}, + __schema: {} + }; + + constructor(props, context) { + super(props, context); + this.state = {}; + this.cacheSetState = {}; + debug(`temp.constructor - ${props.__schema.fileName}`); + } + + componentDidMount() { + const ctx = this.props.__ctx; + if (!ctx) return; + const setState = ctx.setState; + this.cacheSetState = setState; + ctx.setState = (...args) => { + setState.call(ctx, ...args); + setTimeout(() => this.forceUpdate(), 0); + }; + debug(`temp.componentDidMount - ${this.props.__schema.fileName}`); + } + componentDidUpdate(prevProps, prevState, snapshot) { + debug(`temp.componentDidUpdate - ${this.props.__schema.fileName}`); + } + componentWillUnmount() { + const ctx = this.props.__ctx; + if (!ctx || !this.cacheSetState) return; + ctx.setState = this.cacheSetState; + delete this.cacheSetState; + debug(`temp.componentWillUnmount - ${this.props.__schema.fileName}`); + } + componentDidCatch(e) { + console.warn(e); + debug(`temp.componentDidCatch - ${this.props.__schema.fileName}`); + } + + render() { + const { __schema, __ctx } = this.props; + if (!isSchema(__schema, true) || __schema.componentName !== 'Temp') { + return '下钻编辑schema结构异常!'; + } + + debug(`temp.render - ${__schema.fileName}`); + + return ( +
+ {this.__createDom()} +
+ ); + } +} diff --git a/packages/rax-render/src/hoc/compFactory.js b/packages/rax-render/src/hoc/compFactory.js new file mode 100644 index 000000000..a28ca36a5 --- /dev/null +++ b/packages/rax-render/src/hoc/compFactory.js @@ -0,0 +1,71 @@ +import { Component, createElement } from 'rax'; +import PropTypes from 'prop-types'; +import AppHelper from '@ali/iceluna-sdk/lib/utils/appHelper'; +import { forEach, isFileSchema } from '@ali/iceluna-sdk/lib/utils'; +import CompEngine from '../engine/compEngine'; +import BlockEngine from '../engine/blockEngine'; +import AppContext from '../context/appContext'; + +export default function compFactory(schema, components = {}, componentsMap = {}, config = {}) { + // 自定义组件需要有自己独立的appHelper + const appHelper = new AppHelper(config); + class LNCompView extends Component { + static dislayName = 'luna-comp-factory'; + static version = config.version || '0.0.0'; + static contextType = AppContext; + static propTypes = { + forwardedRef: PropTypes.func + }; + render() { + if (!schema || schema.componentName !== 'Component' || !isFileSchema(schema)) { + console.warn('自定义组件模型结构异常!'); + return null; + } + const { forwardedRef, ...otherProps } = this.props; + // 低代码组件透传应用上下文 + const ctx = ['utils', 'constants', 'history', 'location', 'match']; + ctx.forEach(key => { + if (!appHelper[key] && this.context && this.context.appHelper && this.context.appHelper[key]) { + appHelper.set(key, this.context.appHelper[key]); + } + }); + // 支持通过context透传国际化配置 + const localeProps = {}; + const { locale, messages } = this.context; + if (locale && messages && messages[schema.fileName]) { + localeProps.locale = locale; + localeProps.messages = messages[schema.fileName]; + } + const props = { + ...schema.defaultProps, + ...localeProps, + ...otherProps, + __schema: schema, + ref: forwardedRef + }; + + return ( + + {context => { + this.context = context; + return ( + + ); + }} + + ); + } + } + + const ResComp = React.forwardRef((props, ref) => ); + forEach(schema.static, (val, key) => { + ResComp[key] = val; + }); + ResComp.version = config.version || '0.0.0'; + return ResComp; +} diff --git a/packages/rax-render/src/hoc/compWrapper.js b/packages/rax-render/src/hoc/compWrapper.js new file mode 100644 index 000000000..4f55a6683 --- /dev/null +++ b/packages/rax-render/src/hoc/compWrapper.js @@ -0,0 +1,15 @@ +import { createElement, Component } from 'rax'; + +export default function(Comp) { + class CompWrapper extends Component { + constructor(props, context) { + super(props, context); + } + + render() { + return ; + } + } + + return CompWrapper; +} diff --git a/packages/rax-render/src/index.jsx b/packages/rax-render/src/index.jsx new file mode 100644 index 000000000..e7b4ba078 --- /dev/null +++ b/packages/rax-render/src/index.jsx @@ -0,0 +1,5 @@ +import Engine from './engine'; + +export { default as Engine } from './engine'; +export { default as CompFactory } from './hoc/compFactory'; +export default Engine;