Merge branch 'feat/rax-render' into 'release/0.9.0'

feat: rax renderer

## 说明

- `src/utils` 可以和 react-renderer 的 `utils` 一起迁移到 `packages/utils`,但工作量比较大且需要回归测试,因此此次 PR 没有做处理;
- eslint 错误较多,下次迭代进行处理。

See merge request !863172
This commit is contained in:
康为 2020-06-21 17:58:43 +08:00
commit a9cc29a7fc
33 changed files with 2547 additions and 1 deletions

View File

@ -6,7 +6,7 @@
#### 创建新包:
- `./create.sh <package-name>`
- `./scripts/create.sh <package-name>`
#### 跑起来:

View File

@ -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(
<RaxRenderer
schema={schema}
components={components}
/>,
document.getElementById('root'), { driver: DriverUniversal }
);
```

View File

@ -0,0 +1,11 @@
{
"plugins": [
[
"build-plugin-rax-component",
{
"type": "rax",
"targets": ["web"]
}
]
]
}

View File

@ -0,0 +1,35 @@
import { createElement, render } from 'rax';
import DriverUniversal from 'driver-universal';
import View from 'rax-view';
import Text from 'rax-text';
import { Engine } from '../src/index';
const components = {
View,
Text,
};
const schema = {
componentName: 'Page',
fileName: 'home',
props: {},
children: [
{
componentName: 'View',
props: {},
children: [
{
componentName: 'Text',
props: {
type: 'primary',
},
children: ['Welcome to Your Rax App!'],
},
],
},
],
};
render(<Engine schema={schema} components={components} />, document.getElementById('root'), {
driver: DriverUniversal,
});

View File

@ -0,0 +1 @@
App({});

View File

@ -0,0 +1,6 @@
{
"pages": ["pages/index"],
"window": {
"defaultTitle": "demo"
}
}

View File

@ -0,0 +1 @@
<my-component></my-component>

View File

@ -0,0 +1,4 @@
Page({
onLoad() {},
onShow() {}
});

View File

@ -0,0 +1,6 @@
{
"defaultTitle": "Miniapp Rax Text demo",
"usingComponents": {
"my-component": "../components/Target/index"
}
}

View File

@ -0,0 +1 @@
App({});

View File

@ -0,0 +1,6 @@
{
"pages": ["pages/index"],
"window": {
"title": "demo"
}
}

View File

@ -0,0 +1,4 @@
Page({
onLoad() {},
onShow() {}
});

View File

@ -0,0 +1,6 @@
{
"title": "Wechat MiniProgram Rax Text demo",
"usingComponents": {
"my-component": "../components/Target/index"
}
}

View File

@ -0,0 +1 @@
<my-component></my-component>

View File

@ -0,0 +1,64 @@
{
"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/b3-one": "^0.0.17",
"@ali/bzb-request": "2.6.1",
"@ali/lib-mtop": "^2.5.1",
"classnames": "^2.2.6",
"debug": "^4.1.1",
"events": "^3.0.0",
"fetch-jsonp": "^1.1.3",
"intl-messageformat": "^7.7.2",
"jsonuri": "^2.1.2",
"keymaster": "^1.6.2",
"lodash": "^4.17.11",
"moment": "^2.24.0",
"rax-find-dom-node": "^1.0.1",
"rax-text": "^1.1.6",
"rax-view": "^1.0.0",
"react-is": "^16.10.1",
"serialize-javascript": "^1.7.0",
"whatwg-fetch": "^3.0.0"
},
"devDependencies": {
"@alib/build-scripts": "^0.1.0",
"build-plugin-rax-component": "^0.1.4",
"driver-universal": "^3.1.3"
},
"publishConfig": {
"registry": "https://registry.npm.alibaba-inc.com"
},
"license": "MIT"
}

View File

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

View File

@ -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 (
<View className="visual-dom">
<View className="panel-container">
<Text className="title">{title || label || text || __componentName}</Text>
<View className="content">{children}</View>
</View>
</View>
);
}
}

View File

@ -0,0 +1,4 @@
import { createContext } from 'rax';
const context = createContext({});
export default context;

View File

@ -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 '../utils/dataHelper';
import {
forEach,
getValue,
parseData,
parseExpression,
isEmpty,
isSchema,
isFileSchema,
isJSExpression,
isJSSlot,
isJSFunction,
transformArrayToMap,
checkPropTypes,
generateI18n,
acceptsRef,
} from '../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 schemaComp
// 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 = '';
// scopethis.__compScopes
if (Comp.generateScope) {
const key = parseExpression(schema.props.key, self);
if (key) {
// key使key
scopeKey = key;
} else if (!schema.__ctx) {
// schema__ctxlunaKey
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);
}
}
// scopescope
if (scopeKey && this.__compScopes[scopeKey]) {
const compSelf = { ...this.__compScopes[scopeKey] };
compSelf.__proto__ = self;
self = compSelf;
}
// propscontext
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 => (
<Comp {...props}>
{(!isFileSchema(schema) &&
!!schema.children &&
this.__createVirtualDom(
isJSExpression(schema.children) ? parseExpression(schema.children, self) : schema.children,
self,
{
schema,
Comp
}
)) ||
null}
</Comp>
);
//
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 (
<Div ref={ref} __designMode={engine.props.designMode}>
{renderComp({ ...props, ...overlayProps })}
</Div>
);
}
// 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 (
<AppContext.Consumer>
{context => {
this.context = context;
this.__generateCtx(currCtx);
this.__render();
return (
<AppContext.Provider
value={{
...this.context,
...childCtx
}}
>
{this.__createDom()}
</AppContext.Provider>
);
}}
</AppContext.Consumer>
);
};
__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;
}
}

View File

@ -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 '../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 (
<div
ref={this.__getRef}
className={classnames('luna-block', getFileCssName(__schema.fileName), className, this.props.className)}
id={id}
style={style}
>
{this.__createContextDom({
blockContext: this
})}
</div>
);
}
}

View File

@ -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 '../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 (
<div
ref={this.__getRef}
className={classnames('luna-comp', getFileCssName(__schema.fileName), className, this.props.className)}
id={this.props.id || id}
style={{ ...style, ...this.props.style }}
>
{this.__createContextDom(
{
compContext: this,
blockContext: this
},
{
component: this
}
)}
</div>
);
}
}

View File

@ -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 '../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 (
<AppContext.Provider
value={{
appHelper,
components: allComponents,
componentsMap,
engine: this
}}
>
<Comp
key={schema.__ctx && `${schema.__ctx.lunaKey}_${schema.__ctx.idx || '0'}`}
ref={this.__getRef}
__appHelper={appHelper}
__components={allComponents}
__componentsMap={componentsMap}
__schema={schema}
__designMode={designMode}
{...this.props}
/>
</AppContext.Provider>
);
}
return null;
}
}
Engine.findDOMNode = findDOMNode;

View File

@ -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 '../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 (
<div
ref={this.__getRef}
className={classnames('luna-page', getFileCssName(__schema.fileName), className, this.props.className)}
id={id}
style={style}
>
{this.__createContextDom(
{
pageContext: this,
blockContext: this
},
{
page: this
}
)}
</div>
);
}
}

View File

@ -0,0 +1,67 @@
import { createElement } from 'rax';
import PropTypes from 'prop-types';
import Debug from 'debug';
import { isSchema } from '../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 (
<div ref={this.__getRef} className="luna-temp">
<AppContext.Provider value={{ ...this.context, ...__ctx }}>{this.__createDom()}</AppContext.Provider>
</div>
);
}
}

View File

@ -0,0 +1,71 @@
import { Component, createElement } from 'rax';
import PropTypes from 'prop-types';
import AppHelper from '../utils/appHelper';
import { forEach, isFileSchema } from '../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 (
<AppContext.Consumer>
{context => {
this.context = context;
return (
<CompEngine
{...props}
__appHelper={appHelper}
__components={{ ...components, Component: CompEngine, Block: BlockEngine }}
__componentsMap={componentsMap}
/>
);
}}
</AppContext.Consumer>
);
}
}
const ResComp = React.forwardRef((props, ref) => <LNCompView {...props} forwardedRef={ref} />);
forEach(schema.static, (val, key) => {
ResComp[key] = val;
});
ResComp.version = config.version || '0.0.0';
return ResComp;
}

View File

@ -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 <Comp {...this.props} />;
}
}
return CompWrapper;
}

View File

@ -0,0 +1,5 @@
import Engine from './engine';
export { default as Engine } from './engine';
export { default as CompFactory } from './hoc/compFactory';
export default Engine;

View File

@ -0,0 +1,49 @@
import EventEmitter from 'events';
let instance = null;
EventEmitter.defaultMaxListeners = 100;
export default class AppHelper extends EventEmitter {
static getInstance = () => {
if (!instance) {
instance = new AppHelper();
}
return instance;
};
constructor(config) {
super();
instance = this;
Object.assign(this, config);
}
get(key) {
return this[key];
}
set(key, val) {
if (typeof key === 'string') {
this[key] = val;
} else if (typeof key === 'object') {
Object.keys(key).forEach((item) => {
this[item] = key[item];
});
}
}
batchOn(events, lisenter) {
if (!Array.isArray(events)) return;
events.forEach((event) => this.on(event, lisenter));
}
batchOnce(events, lisenter) {
if (!Array.isArray(events)) return;
events.forEach((event) => this.once(event, lisenter));
}
batchOff(events, lisenter) {
if (!Array.isArray(events)) return;
events.forEach((event) => this.off(event, lisenter));
}
}

View File

@ -0,0 +1,299 @@
import { transformArrayToMap, isJSFunction, transformStringToFunction, clone } from './index';
import { jsonp, mtop, request, get, post, bzb } from './request';
const DS_STATUS = {
INIT: 'init',
LOADING: 'loading',
LOADED: 'loaded',
ERROR: 'error'
};
export default class DataHelper {
constructor(comp, config = {}, appHelper, parser) {
this.host = comp;
this.config = config;
this.parser = parser;
this.ajaxList = (config && config.list) || [];
this.ajaxMap = transformArrayToMap(this.ajaxList, 'id');
this.dataSourceMap = this.generateDataSourceMap();
this.appHelper = appHelper;
}
// 重置configdataSourceMap状态会被重置
resetConfig(config = {}) {
this.config = config;
this.ajaxList = (config && config.list) || [];
this.ajaxMap = transformArrayToMap(this.ajaxList, 'id');
this.dataSourceMap = this.generateDataSourceMap();
return this.dataSourceMap;
}
// 更新config只会更新配置状态保存
updateConfig(config = {}) {
this.config = config;
this.ajaxList = (config && config.list) || [];
const ajaxMap = transformArrayToMap(this.ajaxList, 'id');
// 删除已经移除的接口
Object.keys(this.ajaxMap).forEach(key => {
if (!ajaxMap[key]) {
delete this.dataSourceMap[key];
}
});
this.ajaxMap = ajaxMap;
// 添加未加入到dataSourceMap中的接口
this.ajaxList.forEach(item => {
if (!this.dataSourceMap[item.id]) {
this.dataSourceMap[item.id] = {
status: DS_STATUS.INIT,
load: (...args) => {
return this.getDataSource(item.id, ...args);
}
};
}
});
return this.dataSourceMap;
}
generateDataSourceMap() {
const res = {};
this.ajaxList.forEach(item => {
res[item.id] = {
status: DS_STATUS.INIT,
load: (...args) => {
return this.getDataSource(item.id, ...args);
}
};
});
return res;
}
updateDataSourceMap(id, data, error) {
this.dataSourceMap[id].error = error ? error : undefined;
this.dataSourceMap[id].data = data;
this.dataSourceMap[id].status = error ? DS_STATUS.ERROR : DS_STATUS.LOADED;
}
getInitData() {
const initSyncData = this.parser(this.ajaxList).filter(item => {
if (item.isInit) {
this.dataSourceMap[item.id].status = DS_STATUS.LOADING;
return true;
}
return false;
});
return this.asyncDataHandler(initSyncData).then(res => {
let dataHandler = this.config.dataHandler;
if (isJSFunction(dataHandler)) {
dataHandler = transformStringToFunction(dataHandler.value);
}
if (!dataHandler || typeof dataHandler !== 'function') return res;
try {
return dataHandler.call(this.host, res);
} catch (e) {
console.error('请求数据处理函数运行出错', e);
return;
}
});
}
getDataSource(id, params, otherOptions, callback) {
const req = this.parser(this.ajaxMap[id]);
const options = req.options || {};
if (typeof otherOptions === 'function') {
callback = otherOptions;
otherOptions = {};
}
const { headers, ...otherProps } = otherOptions || {};
if (!req) {
console.warn(`getDataSource API named ${id} not exist`);
return;
}
return this.asyncDataHandler([
{
...req,
options: {
...options,
// 支持参数为array的情况当参数为array时不做参数合并
params:
Array.isArray(options.params) || Array.isArray(params)
? params || options.params
: {
...options.params,
...params
},
headers: {
...options.headers,
...headers
},
...otherProps
}
}
])
.then(res => {
try {
callback && callback(res && res[id]);
} catch (e) {
console.error('load请求回调函数报错', e);
}
return res && res[id];
})
.catch(err => {
try {
callback && callback(null, err);
} catch (e) {
console.error('load请求回调函数报错', e);
}
return err;
});
}
asyncDataHandler(asyncDataList) {
return new Promise((resolve, reject) => {
const allReq = [];
const doserReq = [];
const doserList = [];
const beforeRequest = this.appHelper && this.appHelper.utils && this.appHelper.utils.beforeRequest;
const afterRequest = this.appHelper && this.appHelper.utils && this.appHelper.utils.afterRequest;
const csrfInput = document.getElementById('_csrf_token');
const _tb_token_ = csrfInput && csrfInput.value;
asyncDataList.map(req => {
const { id, type, options } = req;
if (!id || !type) return;
if (type === 'doServer') {
const { uri, params } = options || {};
if (!uri) return;
doserList.push(id);
doserReq.push({ name: uri, package: 'cms', params });
} else {
allReq.push(req);
}
});
if (doserReq.length > 0) {
allReq.push({
type: 'doServer',
options: {
uri: '/nrsService.do',
cors: true,
method: 'POST',
params: {
data: JSON.stringify(doserReq),
_tb_token_
}
}
});
}
if (allReq.length === 0) resolve({});
const res = {};
Promise.all(
allReq.map(item => {
return new Promise(resolve => {
const { type, id, dataHandler, options } = item;
const doFetch = (type, options) => {
this.fetchOne(type, options)
.then(async data => {
if (afterRequest) {
this.appHelper.utils.afterRequest(item, data, undefined, async (data, error) => {
await fetchHandler(data, error);
});
} else {
await fetchHandler(data, undefined);
}
})
.catch(async err => {
if (afterRequest) {
// 必须要这么调用否则beforeRequest中的this会丢失
this.appHelper.utils.afterRequest(item, undefined, err, async (data, error) => {
await fetchHandler(data, error);
});
} else {
await fetchHandler(undefined, err);
}
});
};
const fetchHandler = async (data, error) => {
if (type === 'doServer') {
if (!Array.isArray(data)) {
data = [data];
}
doserList.forEach(async (id, idx) => {
const req = this.ajaxMap[id];
if (req) {
res[id] = await this.dataHandler(id, req.dataHandler, data && data[idx], error);
this.updateDataSourceMap(id, res[id], error);
}
});
} else {
res[id] = await this.dataHandler(id, dataHandler, data, error);
this.updateDataSourceMap(id, res[id], error);
}
resolve();
};
if (type === 'doServer') {
doserList.forEach(item => {
this.dataSourceMap[item].status = DS_STATUS.LOADING;
});
} else {
this.dataSourceMap[id].status = DS_STATUS.LOADING;
}
// 请求切片
if (beforeRequest) {
// 必须要这么调用否则beforeRequest中的this会丢失
this.appHelper.utils.beforeRequest(item, clone(options), options => doFetch(type, options));
} else {
doFetch(type, options);
}
});
})
)
.then(() => {
resolve(res);
})
.catch(e => {
reject(e);
});
});
}
async dataHandler(id, dataHandler, data, error) {
if (isJSFunction(dataHandler)) {
dataHandler = transformStringToFunction(dataHandler.value);
}
if (!dataHandler || typeof dataHandler !== 'function') return data;
try {
return await dataHandler.call(this.host, data, error);
} catch (e) {
console.error('[' + id + ']单个请求数据处理函数运行出错', e);
return;
}
}
fetchOne(type, options) {
let { uri, method = 'GET', headers, params, ...otherProps } = options;
otherProps = otherProps || {};
switch (type) {
case 'mtop':
method && (otherProps.method = method);
return mtop(uri, params, otherProps);
case 'jsonp':
return jsonp(uri, params, otherProps);
case 'bzb':
return bzb(uri, params, {
method,
headers,
...otherProps
});
default:
method = method.toUpperCase();
if (method === 'GET') {
return get(uri, params, headers, otherProps);
} else if (method === 'POST') {
return post(uri, params, headers, otherProps);
}
return request(uri, method, params, headers, otherProps);
}
}
}

View File

@ -0,0 +1,718 @@
import Debug from 'debug';
import _keymaster from 'keymaster';
import { forEach as _forEach, shallowEqual as _shallowEqual } from '@ali/b3-one/lib/obj';
import { serialize as serializeParams } from '@ali/b3-one/lib/url';
import _moment from 'moment';
import 'moment/locale/zh-cn';
import _pick from 'lodash/pick';
import _deepEqual from 'lodash/isEqualWith';
import _clone from 'lodash/cloneDeep';
import _isEmpty from 'lodash/isEmpty';
import _throttle from 'lodash/throttle';
import _debounce from 'lodash/debounce';
import _serialize from 'serialize-javascript';
import * as _jsonuri from 'jsonuri';
import IntlMessageFormat from 'intl-messageformat';
import pkg from '../../package.json';
window.sdkVersion = pkg.version;
export const moment = _moment;
moment.locale('zh-cn');
export const forEach = _forEach;
export const shallowEqual = _shallowEqual;
export const keymaster = _keymaster;
export const pick = _pick;
export const deepEqual = _deepEqual;
export const clone = _clone;
export const isEmpty = _isEmpty;
export const throttle = _throttle;
export const debounce = _debounce;
export const serialize = _serialize;
export const jsonuri = _jsonuri;
export { get, post, jsonp, mtop, request } from './request';
const ReactIs = require('react-is');
const ReactPropTypesSecret = require('prop-types/lib/ReactPropTypesSecret');
const factoryWithTypeCheckers = require('prop-types/factoryWithTypeCheckers');
const PropTypes2 = factoryWithTypeCheckers(ReactIs.isElement, true);
const EXPRESSION_TYPE = {
JSEXPRESSION: 'JSExpression',
JSFUNCTION: 'JSFunction',
JSSLOT: 'JSSlot'
};
const EXPRESSION_REG = /^\{\{(\{.*\}|.*?)\}\}$/;
const hasSymbol = typeof Symbol === 'function' && Symbol['for'];
const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol['for']('react.forward_ref') : 0xead0;
const debug = Debug('utils:index');
const ENV = {
TBE: 'TBE',
WEBIDE: 'WEB-IDE',
VSCODE: 'VSCODE',
WEB: 'WEB'
};
/**
* @name isSchema
* @description 判断是否是模型结构
*/
export function isSchema(schema, ignoreArr) {
if (isEmpty(schema)) return false;
if (!ignoreArr && Array.isArray(schema)) return schema.every(item => isSchema(item));
return !!(schema.componentName && schema.props && (typeof schema.props === 'object' || isJSExpression(schema.props)));
}
export function isFileSchema(schema) {
if (isEmpty(schema)) return false;
return ['Page', 'Block', 'Component', 'Addon', 'Temp'].includes(schema.componentName);
}
// 判断当前页面是否被嵌入到同域的页面中
export function inSameDomain() {
try {
return window.parent !== window && window.parent.location.host === window.location.host;
} catch (e) {
return false;
}
}
export function getFileCssName(fileName) {
if (!fileName) return;
let name = fileName.replace(/([A-Z])/g, '-$1').toLowerCase();
return ('luna-' + name)
.split('-')
.filter(p => !!p)
.join('-');
}
export function isJSSlot(obj) {
return obj && typeof obj === 'object' && EXPRESSION_TYPE.JSSLOT === obj.type;
}
export function isJSFunction(obj) {
return obj && typeof obj === 'object' && EXPRESSION_TYPE.JSFUNCTION === obj.type;
}
export function isJSExpression(obj) {
//兼容两种写法有js构造表达式的情况
const isJSExpressionObj =
obj && typeof obj === 'object' && EXPRESSION_TYPE.JSEXPRESSION === obj.type && typeof obj.value === 'string';
const isJSExpressionStr = typeof obj === 'string' && EXPRESSION_REG.test(obj.trim());
return isJSExpressionObj || isJSExpressionStr;
}
/**
* @name wait
* @description 等待函数
*/
export function wait(ms) {
return new Promise(resolve => setTimeout(() => resolve(true), ms));
}
export function curry(Comp, hocs = []) {
return hocs.reverse().reduce((pre, cur) => {
return cur(pre);
}, Comp);
}
/**
* 获取parent window上的值需要做同域判断
* @param {string} key
*/
export function getParentWinValue(key) {
if (inSameDomain()) {
return window.parent && window.parent[key];
}
}
export function getValue(obj, path, defaultValue) {
if (isEmpty(obj) || typeof obj !== 'object') return defaultValue;
const res = path.split('.').reduce((pre, cur) => {
return pre && pre[cur];
}, obj);
if (res === undefined) return defaultValue;
return res;
}
export function parseObj(schemaStr) {
if (typeof schemaStr !== 'string') return schemaStr;
//默认调用顶层窗口的parseObj,保障new Function的window对象是顶层的window对象
try {
if (inSameDomain() && window.parent.__newFunc) {
return window.parent.__newFunc(`"use strict"; return ${schemaStr}`)();
}
return new Function(`"use strict"; return ${schemaStr}`)();
} catch (err) {
return undefined;
}
}
export function parseSearch(search) {
if (!search || typeof search !== 'string') {
return {};
}
const str = search.replace(/^\?/, '');
const paramStr = str.split('&');
const res = {};
paramStr.forEach(item => {
const regRes = item.split('=');
if (regRes[0] && regRes[1]) {
res[regRes[0]] = decodeURIComponent(regRes[1]);
}
});
return res;
}
export function fastClone(obj) {
return parseObj(serialize(obj, { unsafe: true }));
}
// 更新obj的内容但不改变obj的指针
export function fillObj(receiver = {}, ...suppliers) {
Object.keys(receiver).forEach(item => {
delete receiver[item];
});
Object.assign(receiver, ...suppliers);
return receiver;
}
// 中划线转驼峰
export function toHump(name) {
return name.replace(/\-(\w)/g, function(all, letter) {
return letter.toUpperCase();
});
}
// 驼峰转中划线
export function toLine(name) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase();
}
// 获取当前环境
export function getEnv() {
const userAgent = navigator.userAgent;
const isVscode = /Electron\//.test(userAgent);
if (isVscode) return ENV.VSCODE;
const isTheia = window.is_theia === true;
if (isTheia) return ENV.WEBIDE;
return ENV.WEB;
}
/**
* 合并skeleton配置
* @param {*} 骨架默认配置
* @param {*} 用户自定义配置
*/
export function comboSkeletonConfig(defaultConfig = {}, customConfig) {
const { skeleton, theme, addons, hooks, shortCuts, extensions, constants, utils, i18n } = customConfig || {};
if (skeleton && skeleton.handler && typeof skeleton.handler === 'function') {
return skeleton.handler({
skeleton,
...defaultConfig
});
}
const defaultShortCuts = transformArrayToMap(defaultConfig.shortCuts || [], 'keyboard');
const customShortCuts = transformArrayToMap(shortCuts || [], 'keyboard');
const localeList = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP'];
const i18nConfig = {};
localeList.forEach(key => {
i18nConfig[key] = {
...(defaultConfig.i18n && defaultConfig.i18n[key]),
...(i18n && i18n[key])
};
});
return {
skeleton,
theme: {
...defaultConfig.theme,
...theme
},
addons: {
...defaultConfig.addons,
...addons
},
hooks: [...(defaultConfig.hooks || []), ...(hooks || [])],
shortCuts: Object.values({
...defaultShortCuts,
...customShortCuts
}),
extensions: {
...defaultConfig.extensions,
...extensions
},
constants: {
...defaultConfig.constants,
...constants
},
utils: [...(defaultConfig.utils || []), ...(utils || [])],
i18n: i18nConfig
};
}
/**
* 用于构造国际化字符串处理函数
* @param {*} locale 国际化标识例如 zh-CNen-US
* @param {*} messages 国际化语言包
*/
export function generateI18n(locale = 'zh-CN', messages = {}) {
return (key, values = {}) => {
if (!messages || !messages[key]) return '';
const formater = new IntlMessageFormat(messages[key], locale);
return formater.format(values);
};
}
/**
* 判断当前组件是否能够设置ref
* @param {*} Comp 需要判断的组件
*/
export function acceptsRef(Comp) {
return (
(Comp.$$typeof && Comp.$$typeof === REACT_FORWARD_REF_TYPE) || (Comp.prototype && Comp.prototype.isReactComponent)
);
}
/**
* 黄金令箭埋点
* @param {String} gmKey 为黄金令箭业务类型
* @param {Object} params 参数
* @param {String} logKey 属性串
*/
export function goldlog(gmKey, params = {}, logKey = 'other') {
// vscode 黄金令箭API
const sendIDEMessage = window.sendIDEMessage || getParentWinValue('sendIDEMessage');
const goKey = serializeParams({
sdkVersion: pkg.version,
env: getEnv(),
...params
});
if (sendIDEMessage) {
sendIDEMessage({
action: 'goldlog',
data: {
logKey: `/iceluna.core.${logKey}`,
gmKey,
goKey
}
});
}
window.goldlog && window.goldlog.record(`/iceluna.core.${logKey}`, gmKey, goKey, 'POST');
}
// utils为编辑器打包生成的utils文件内容utilsConfig为数据库存放的utils配置
export function generateUtils(utils, utilsConfig) {
if (!Array.isArray(utilsConfig)) return { ...utils };
const res = {};
utilsConfig.forEach(item => {
if (!item.name || !item.type || !item.content) return;
if (item.type === 'function' && typeof item.content === 'function') {
res[item.name] = item.content;
} else if (item.type === 'npm' && utils[item.name]) {
res[item.name] = utils[item.name];
}
});
return res;
}
// 复制到粘贴板
export function setClipboardData(str) {
return new Promise((resolve, reject) => {
if (typeof str !== 'string') reject('不支持拷贝');
if (navigator.clipboard) {
navigator.clipboard
.writeText(str)
.then(() => {
resolve();
})
.catch(err => {
reject('复制失败,请重试!', err);
});
} else {
const textArea = document.createElement('textarea');
textArea.value = str;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
let successful = document.execCommand('copy');
if (successful) {
document.body.removeChild(textArea);
resolve();
}
} catch (err) {
document.body.removeChild(textArea);
reject('复制失败,请重试!', err);
}
}
});
}
// 获取粘贴板数据
export function getClipboardData() {
return new Promise((resolve, reject) => {
if (window.clipboardData) {
resolve(window.clipboardData.getData('text'));
} else if (navigator.clipboard) {
return navigator.clipboard
.readText()
.then(res => {
resolve(res);
})
.catch(err => {
reject('粘贴板获取失败', err);
});
} else {
reject('粘贴板获取失败');
}
});
}
// 将函数返回结果转成promise形式如果函数有返回值则根据返回值的bool类型判断是reject还是resolve若函数无返回值默认执行resolve
export function transformToPromise(input) {
if (input instanceof Promise) return input;
return new Promise((resolve, reject) => {
if (input || input === undefined) {
resolve();
} else {
reject();
}
});
}
export function moveArrayItem(arr, sourceIdx, distIdx, direction) {
if (
!Array.isArray(arr) ||
sourceIdx === distIdx ||
sourceIdx < 0 ||
sourceIdx >= arr.length ||
distIdx < 0 ||
distIdx >= arr.length
)
return arr;
const item = arr[sourceIdx];
if (direction === 'after') {
arr.splice(distIdx + 1, 0, item);
} else {
arr.splice(distIdx, 0, item);
}
if (sourceIdx < distIdx) {
arr.splice(sourceIdx, 1);
} else {
arr.splice(sourceIdx + 1, 1);
}
return arr;
}
export function transformArrayToMap(arr, key, overwrite = true) {
if (isEmpty(arr) || !Array.isArray(arr)) return {};
const res = {};
arr.forEach(item => {
const curKey = item[key];
if (item[key] === undefined) return;
if (res[curKey] && !overwrite) return;
res[curKey] = item;
});
return res;
}
export function checkPropTypes(value, name, rule, componentName) {
if (typeof rule === 'string') {
rule = new Function(`"use strict"; const PropTypes = arguments[0]; return ${rule}`)(PropTypes2);
}
if (!rule || typeof rule !== 'function') {
console.warn('checkPropTypes should have a function type rule argument');
return true;
}
const err = rule(
{
[name]: value
},
name,
componentName,
'prop',
null,
ReactPropTypesSecret
);
if (err) {
console.warn(err);
}
return !err;
}
export function transformSchemaToPure(obj) {
const pureObj = obj => {
if (Array.isArray(obj)) {
return obj.map(item => pureObj(item));
} else if (typeof obj === 'object') {
// 对于undefined及null直接返回
if (!obj) return obj;
const res = {};
forEach(obj, (val, key) => {
if (key.startsWith('__') && key !== '__ignoreParse') return;
res[key] = pureObj(val);
});
return res;
}
return obj;
};
return pureObj(obj);
}
export function transformSchemaToStandard(obj) {
const standardObj = obj => {
if (Array.isArray(obj)) {
return obj.map(item => standardObj(item));
} else if (typeof obj === 'object') {
// 对于undefined及null直接返回
if (!obj) return obj;
const res = {};
forEach(obj, (val, key) => {
if (key.startsWith('__') && key !== '__ignoreParse') return;
if (isSchema(val) && key !== 'children' && obj.type !== 'JSSlot') {
res[key] = {
type: 'JSSlot',
value: standardObj(val)
};
// table特殊处理
if (key === 'cell') {
res[key].params = ['value', 'index', 'record'];
}
} else {
res[key] = standardObj(val);
}
});
return res;
} else if (typeof obj === 'function') {
return {
type: 'JSFunction',
value: obj.toString()
};
} else if (typeof obj === 'string' && EXPRESSION_REG.test(obj.trim())) {
const regRes = obj.trim().match(EXPRESSION_REG);
return {
type: 'JSExpression',
value: (regRes && regRes[1]) || ''
};
}
return obj;
};
return standardObj(obj, false);
}
export function transformStringToFunction(str) {
if (typeof str !== 'string') return str;
if (inSameDomain() && window.parent.__newFunc) {
return window.parent.__newFunc(`"use strict"; return ${str}`)();
} else {
return new Function(`"use strict"; return ${str}`)();
}
}
export function addCssTag(id, content) {
let styleTag = document.getElementById(id);
if (styleTag) {
styleTag.innerHTML = content;
return;
}
styleTag = document.createElement('style');
styleTag.id = id;
styleTag.class = 'luna-style';
styleTag.innerHTML = content;
document.head.appendChild(styleTag);
}
// 注册快捷
export function registShortCuts(config, appHelper) {
const keyboardFilter = (keymaster.filter = event => {
const eTarget = event.target || event.srcElement;
const tagName = eTarget.tagName;
const isInput = !!(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA');
const isContenteditable = target => {
while (target) {
if (target.contentEditable === 'true') return true;
target = target.parentNode;
}
return false;
};
if (isInput || isContenteditable(eTarget)) {
if (event.metaKey === true && [70, 83].includes(event.keyCode)) event.preventDefault(); //禁止触发chrome原生的页面保存或查找
return false;
} else {
return true;
}
});
const ideMessage = appHelper.utils && appHelper.utils.ideMessage;
//复制
if (!document.copyListener) {
document.copyListener = e => {
if (!keyboardFilter(e) || appHelper.isCopying) return;
const schema = appHelper.schemaHelper && appHelper.schemaHelper.schemaMap[appHelper.activeKey];
if (!schema || !isSchema(schema)) return;
appHelper.isCopying = true;
const schemaStr = serialize(transformSchemaToPure(schema), {
unsafe: true
});
setClipboardData(schemaStr)
.then(() => {
ideMessage && ideMessage('success', '当前内容已复制到剪贴板请使用快捷键Command+v进行粘贴');
appHelper.emit('schema.copy', schemaStr, schema);
appHelper.isCopying = false;
})
.catch(errMsg => {
ideMessage && ideMessage('error', errMsg);
appHelper.isCopying = false;
});
};
document.addEventListener('copy', document.copyListener);
if (getParentWinValue('vscode')) {
keymaster('command+c', document.copyListener);
}
}
//粘贴
if (!document.pasteListener) {
const doPaste = (e, text) => {
if (!keyboardFilter(e) || appHelper.isPasting) return;
const schemaHelper = appHelper.schemaHelper;
let targetKey = appHelper.activeKey;
let direction = 'after';
const topKey = schemaHelper.schema && schemaHelper.schema.__ctx && schemaHelper.schema.__ctx.lunaKey;
if (!targetKey || topKey === targetKey) {
const schemaHelper = appHelper.schemaHelper;
const topKey = schemaHelper.schema && schemaHelper.schema.__ctx && schemaHelper.schema.__ctx.lunaKey;
if (!topKey) return;
targetKey = topKey;
direction = 'in';
}
appHelper.isPasting = true;
const schema = parseObj(text);
if (!isSchema(schema)) {
appHelper.emit('illegalSchema.paste', text);
// ideMessage && ideMessage('error', '当前内容不是模型结构,不能粘贴进来!');
console.warn('paste schema illegal');
appHelper.isPasting = false;
return;
}
appHelper.emit('material.add', {
schema,
targetKey,
direction
});
appHelper.isPasting = false;
appHelper.emit('schema.paste', schema);
};
document.pasteListener = e => {
const clipboardData = e.clipboardData || window.clipboardData;
const text = clipboardData && clipboardData.getData('text');
doPaste(e, text);
};
document.addEventListener('paste', document.pasteListener);
if (getParentWinValue('vscode')) {
keymaster('command+v', e => {
const sendIDEMessage = getParentWinValue('sendIDEMessage');
sendIDEMessage &&
sendIDEMessage({
action: 'readClipboard'
})
.then(text => {
doPaste(e, text);
})
.catch(err => {
console.warn(err);
});
});
}
}
(config || []).forEach(item => {
keymaster(item.keyboard, ev => {
ev.preventDefault();
item.handler(ev, appHelper, keymaster);
});
});
}
// 取消注册快捷
export function unRegistShortCuts(config) {
(config || []).forEach(item => {
keymaster.unbind(item.keyboard);
});
if (getParentWinValue('vscode')) {
keymaster.unbind('command+c');
keymaster.unbind('command+v');
}
if (document.copyListener) {
document.removeEventListener('copy', document.copyListener);
delete document.copyListener;
}
if (document.pasteListener) {
document.removeEventListener('paste', document.pasteListener);
delete document.pasteListener;
}
}
export function parseData(schema, self) {
if (isJSExpression(schema)) {
return parseExpression(schema, self);
} else if (typeof schema === 'string') {
return schema.trim();
} else if (Array.isArray(schema)) {
return schema.map(item => parseData(item, self));
} else if (typeof schema === 'function') {
return schema.bind(self);
} else if (typeof schema === 'object') {
// 对于undefined及null直接返回
if (!schema) return schema;
const res = {};
forEach(schema, (val, key) => {
if (key.startsWith('__')) return;
res[key] = parseData(val, self);
});
return res;
}
return schema;
}
/*全匹配{{开头,}}结尾的变量表达式或者对象类型JSExpression且均不支持省略this */
export function parseExpression(str, self) {
try {
const contextArr = ['"use strict";', 'var __self = arguments[0];'];
contextArr.push('return ');
let tarStr;
//向前兼容,支持标准协议新格式
if (typeof str === 'string') {
const regRes = str.trim().match(EXPRESSION_REG);
tarStr = regRes[1];
} else {
tarStr = (str.value || '').trim();
}
tarStr = tarStr.replace(/this(\W|$)/g, (a, b) => `__self${b}`);
tarStr = contextArr.join('\n') + tarStr;
//默认调用顶层窗口的parseObj,保障new Function的window对象是顶层的window对象
if (inSameDomain() && window.parent.__newFunc) {
return window.parent.__newFunc(tarStr)(self);
}
return new Function(tarStr)(self);
} catch (err) {
debug('parseExpression.error', err, str, self);
return undefined;
}
}
/**
* 判断组件配置中是否有reactNode且type为function的props
* @param {*} componentInfo
*/
export function hasReactNodeFuncProps(componentInfo) {
const isReactNodeFuncProps = config => {
if (config.type === 'ReactNode') {
return config.props && config.props.type === 'function';
} else if (config.type === 'Mixin') {
return config.props && config.props.reactNodeProps && config.props.reactNodeProps.type === 'function';
}
};
return componentInfo && (componentInfo.props || []).some(item => isReactNodeFuncProps(item));
}

View File

@ -0,0 +1,171 @@
import 'whatwg-fetch';
import fetchMtop from '@ali/lib-mtop';
import fetchJsonp from 'fetch-jsonp';
import bzbRequest from '@ali/bzb-request';
import { serialize, buildUrl, parseUrl } from '@ali/b3-one/lib/url';
export function get(dataAPI, params = {}, headers = {}, otherProps = {}) {
headers = {
Accept: 'application/json',
...headers
};
dataAPI = buildUrl(dataAPI, params);
return request(dataAPI, 'GET', null, headers, otherProps);
}
export function post(dataAPI, params = {}, headers = {}, otherProps = {}) {
headers = {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
...headers
};
return request(
dataAPI,
'POST',
headers['Content-Type'].indexOf('application/json') > -1 || Array.isArray(params)
? JSON.stringify(params)
: serialize(params),
headers,
otherProps
);
}
export function request(dataAPI, method = 'GET', data, headers = {}, otherProps = {}) {
switch (method) {
case 'PUT':
case 'DELETE':
headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
...headers
};
data = JSON.stringify(data || {});
break;
}
return new Promise((resolve, reject) => {
if (otherProps.timeout) {
setTimeout(() => {
reject(new Error('timeout'));
}, otherProps.timeout);
}
fetch(dataAPI, {
method,
credentials: 'include',
headers,
body: data,
...otherProps
})
.then(response => {
switch (response.status) {
case 200:
case 201:
case 202:
return response.json();
case 204:
if (method === 'DELETE') {
return {
success: true
};
} else {
return {
__success: false,
code: response.status
};
}
case 400:
case 401:
case 403:
case 404:
case 406:
case 410:
case 422:
case 500:
return response
.json()
.then(res => {
return {
__success: false,
code: response.status,
data: res
};
})
.catch(() => {
return {
__success: false,
code: response.status
};
});
}
return null;
})
.then(json => {
if (json && json.__success !== false) {
resolve(json);
} else {
delete json.__success;
reject(json);
}
})
.catch(err => {
reject(err);
});
});
}
export function jsonp(dataAPI, params = {}, otherProps = {}) {
return new Promise((resolve, reject) => {
otherProps = {
timeout: 5000,
...otherProps
};
fetchJsonp(buildUrl(dataAPI, params), otherProps)
.then(response => response.json())
.then(json => {
if (json) {
resolve(json);
} else {
reject();
}
})
.catch(err => {
reject(err);
});
});
}
export function mtop(dataAPI, params, otherProps = {}) {
fetchMtop.config.subDomain = otherProps.subDomain || 'm';
return fetchMtop.request({
api: dataAPI,
v: '1.0',
data: params,
ecode: otherProps.ecode || 0,
type: otherProps.method || 'GET',
dataType: otherProps.dataType || 'jsonp',
AntiFlood: true, // 防刷
timeout: otherProps.timeout || 20000
});
}
export function bzb(apiCode, params, otherProps = {}) {
// 通过url参数设置小二工作台请求环境
const getUrlEnv = () => {
try {
if (window.parent && window.parent.location.host === window.location.host) {
const urlInfo = parseUrl(window.parent && window.parent.location.href);
return urlInfo && urlInfo.params && urlInfo.params._env;
}
const urlInfo = parseUrl(window.location.href);
return urlInfo && urlInfo.params && urlInfo.params._env;
} catch (e) {
return null;
}
};
otherProps.method = otherProps.method || 'GET';
otherProps.env = getUrlEnv() || otherProps.env || 'prod';
return bzbRequest(apiCode, {
data: params,
...otherProps
});
}