feat: added thisRequiredInJSE API to control whether JSExpression expression access context must use this (#702)

This commit is contained in:
絮黎 2022-06-23 14:44:35 +08:00 committed by GitHub
parent 2022308e21
commit da7f77ee91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 154 additions and 22 deletions

View File

@ -228,6 +228,10 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
return this.get('requestHandlersMap') || null; return this.get('requestHandlersMap') || null;
} }
get thisRequiredInJSE(): any {
return engineConfig.get('thisRequiredInJSE') ?? true;
}
@computed get componentsAsset(): Asset | undefined { @computed get componentsAsset(): Asset | undefined {
return this.get('componentsAsset'); return this.get('componentsAsset');
} }

View File

@ -133,6 +133,10 @@ const VALID_ENGINE_OPTIONS = {
type: 'object', type: 'object',
description: '数据源引擎的请求处理器映射', description: '数据源引擎的请求处理器映射',
}, },
thisRequiredInJSE: {
type: 'boolean',
description: 'JSExpression 是否只支持使用 this 来访问上下文变量',
},
}; };
export interface EngineOptions { export interface EngineOptions {
/** /**
@ -248,6 +252,12 @@ export interface EngineOptions {
* *
*/ */
requestHandlersMap?: RequestHandlersMap; requestHandlersMap?: RequestHandlersMap;
/**
* @default true
* JSExpression 使 this 访 'state.xxx' false
*/
thisRequiredInJSE?: boolean;
} }
const getStrictModeValue = (engineOptions: EngineOptions, defaultValue: boolean): boolean => { const getStrictModeValue = (engineOptions: EngineOptions, defaultValue: boolean): boolean => {

View File

@ -219,6 +219,7 @@ class Renderer extends Component<{
onCompGetRef={(schema: any, ref: any) => { onCompGetRef={(schema: any, ref: any) => {
documentInstance.mountInstance(schema.id, ref); documentInstance.mountInstance(schema.id, ref);
}} }}
thisRequiredInJSE={host.thisRequiredInJSE}
documentId={document.id} documentId={document.id}
getNode={(id: string) => documentInstance.getNode(id) as any} getNode={(id: string) => documentInstance.getNode(id) as any}
rendererName="PageRenderer" rendererName="PageRenderer"

View File

@ -189,6 +189,7 @@ class Renderer extends Component<{
setSchemaChangedSymbol={this.setSchemaChangedSymbol} setSchemaChangedSymbol={this.setSchemaChangedSymbol}
getNode={(id: string) => documentInstance.getNode(id) as Node} getNode={(id: string) => documentInstance.getNode(id) as Node}
rendererName="PageRenderer" rendererName="PageRenderer"
thisRequiredInJSE={host.thisRequiredInJSE}
customCreateElement={(Component: any, props: any, children: any) => { customCreateElement={(Component: any, props: any, children: any) => {
const { __id, ...viewProps } = props; const { __id, ...viewProps } = props;
viewProps.componentId = __id; viewProps.componentId = __id;

View File

@ -12,6 +12,7 @@ import {
getValue, getValue,
parseData, parseData,
parseExpression, parseExpression,
parseThisRequiredExpression,
parseI18n, parseI18n,
isEmpty, isEmpty,
isSchema, isSchema,
@ -83,11 +84,13 @@ export default function baseRendererFactory(): IBaseRenderComponent {
getLocale: any; getLocale: any;
setLocale: any; setLocale: any;
styleElement: any; styleElement: any;
parseExpression: any;
[key: string]: any; [key: string]: any;
constructor(props: IBaseRendererProps, context: IBaseRendererContext) { constructor(props: IBaseRendererProps, context: IBaseRendererContext) {
super(props, context); super(props, context);
this.context = context; this.context = context;
this.parseExpression = props?.thisRequiredInJSE ? parseThisRequiredExpression : parseExpression;
this.__beforeInit(props); this.__beforeInit(props);
this.__init(props); this.__init(props);
this.__afterInit(props); this.__afterInit(props);
@ -112,7 +115,7 @@ export default function baseRendererFactory(): IBaseRenderComponent {
if (func) { if (func) {
if (isJSExpression(func) || isJSFunction(func)) { if (isJSExpression(func) || isJSFunction(func)) {
const fn = parseExpression(func, this); const fn = props.thisRequiredInJSE ? parseThisRequiredExpression(func, this) : parseExpression(func, this);
return fn(props, state); return fn(props, state);
} }
@ -193,7 +196,7 @@ export default function baseRendererFactory(): IBaseRenderComponent {
if (fn) { if (fn) {
// TODO, cache // TODO, cache
if (isJSExpression(fn) || isJSFunction(fn)) { if (isJSExpression(fn) || isJSFunction(fn)) {
fn = parseExpression(fn, this); fn = this.parseExpression(fn, this);
} }
if (typeof fn !== 'function') { if (typeof fn !== 'function') {
console.error(`生命周期${method}类型不符`, fn); console.error(`生命周期${method}类型不符`, fn);
@ -219,7 +222,7 @@ export default function baseRendererFactory(): IBaseRenderComponent {
this.__customMethodsList = customMethodsList; this.__customMethodsList = customMethodsList;
forEach(__schema.methods, (val: any, key: string) => { forEach(__schema.methods, (val: any, key: string) => {
if (isJSExpression(val) || isJSFunction(val)) { if (isJSExpression(val) || isJSFunction(val)) {
val = parseExpression(val, this); val = this.parseExpression(val, this);
} }
if (typeof val !== 'function') { if (typeof val !== 'function') {
console.error(`自定义函数${key}类型不符`, val); console.error(`自定义函数${key}类型不符`, val);
@ -414,7 +417,7 @@ export default function baseRendererFactory(): IBaseRenderComponent {
const { __appHelper: appHelper, __components: components = {} } = this.props || {}; const { __appHelper: appHelper, __components: components = {} } = this.props || {};
if (isJSExpression(schema)) { if (isJSExpression(schema)) {
return parseExpression(schema, scope); return this.parseExpression(schema, scope);
} }
if (isI18nData(schema)) { if (isI18nData(schema)) {
return parseI18n(schema, scope); return parseI18n(schema, scope);
@ -434,7 +437,7 @@ export default function baseRendererFactory(): IBaseRenderComponent {
const _children = this.getSchemaChildren(schema); const _children = this.getSchemaChildren(schema);
// 解析占位组件 // 解析占位组件
if (schema?.componentName === 'Fragment' && _children) { if (schema?.componentName === 'Fragment' && _children) {
const tarChildren = isJSExpression(_children) ? parseExpression(_children, scope) : _children; const tarChildren = isJSExpression(_children) ? this.parseExpression(_children, scope) : _children;
return this.__createVirtualDom(tarChildren, scope, parentInfo); return this.__createVirtualDom(tarChildren, scope, parentInfo);
} }
@ -496,7 +499,7 @@ export default function baseRendererFactory(): IBaseRenderComponent {
let scopeKey = ''; let scopeKey = '';
// 判断组件是否需要生成scope且只生成一次挂在this.__compScopes上 // 判断组件是否需要生成scope且只生成一次挂在this.__compScopes上
if (Comp.generateScope) { if (Comp.generateScope) {
const key = parseExpression(schema.props?.key, scope); const key = this.parseExpression(schema.props?.key, scope);
if (key) { if (key) {
// 如果组件自己设置key则使用组件自己的key // 如果组件自己设置key则使用组件自己的key
scopeKey = key; scopeKey = key;
@ -647,7 +650,7 @@ export default function baseRendererFactory(): IBaseRenderComponent {
_children.forEach((_child: any) => { _children.forEach((_child: any) => {
const _childVirtualDom = this.__createVirtualDom( const _childVirtualDom = this.__createVirtualDom(
isJSExpression(_child) ? parseExpression(_child, scope) : _child, isJSExpression(_child) ? this.parseExpression(_child, scope) : _child,
scope, scope,
{ {
schema, schema,
@ -754,7 +757,7 @@ export default function baseRendererFactory(): IBaseRenderComponent {
return checkProps(props); return checkProps(props);
} }
if (isJSExpression(props)) { if (isJSExpression(props)) {
props = parseExpression(props, scope); props = this.parseExpression(props, scope);
// 只有当变量解析出来为模型结构的时候才会继续解析 // 只有当变量解析出来为模型结构的时候才会继续解析
if (!isSchema(props) && !isJSSlot(props)) return checkProps(props); if (!isSchema(props) && !isJSSlot(props)) return checkProps(props);
} }

View File

@ -60,6 +60,7 @@ export default function rendererFactory(): IRenderComponent {
schema: {} as RootSchema, schema: {} as RootSchema,
onCompGetRef: () => { }, onCompGetRef: () => { },
onCompGetCtx: () => { }, onCompGetCtx: () => { },
thisRequiredInJSE: true,
}; };
static findDOMNode = findDOMNode; static findDOMNode = findDOMNode;

View File

@ -128,6 +128,11 @@ export interface IRendererProps {
faultComponent?: IGeneralComponent; faultComponent?: IGeneralComponent;
/** 设备信息 */ /** 设备信息 */
device?: string; device?: string;
/**
* @default true
* JSExpression 使 this 访
*/
thisRequiredInJSE?: boolean;
} }
export interface IRendererState { export interface IRendererState {
@ -148,12 +153,13 @@ export interface IBaseRendererProps {
__host?: BuiltinSimulatorHost; __host?: BuiltinSimulatorHost;
__container?: any; __container?: any;
config?: Record<string, any>; config?: Record<string, any>;
designMode?: 'live' | 'design'; designMode?: 'design';
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
id?: string | number; id?: string | number;
getSchemaChangedSymbol?: () => boolean; getSchemaChangedSymbol?: () => boolean;
setSchemaChangedSymbol?: (symbol: boolean) => void; setSchemaChangedSymbol?: (symbol: boolean) => void;
thisRequiredInJSE?: boolean;
documentId?: string; documentId?: string;
getNode?: any; getNode?: any;
/** /**

View File

@ -242,7 +242,7 @@ export function transformStringToFunction(str: string) {
* @param self scope object * @param self scope object
* @returns funtion * @returns funtion
*/ */
export function parseExpression(str: any, self: any) { export function parseExpression(str: any, self: any, thisRequired = false) {
try { try {
const contextArr = ['"use strict";', 'var __self = arguments[0];']; const contextArr = ['"use strict";', 'var __self = arguments[0];'];
contextArr.push('return '); contextArr.push('return ');
@ -259,14 +259,18 @@ export function parseExpression(str: any, self: any) {
if (inSameDomain() && (window.parent as any).__newFunc) { if (inSameDomain() && (window.parent as any).__newFunc) {
return (window.parent as any).__newFunc(tarStr)(self); return (window.parent as any).__newFunc(tarStr)(self);
} }
const code = `with($scope || {}) { ${tarStr} }`; const code = `with(${thisRequired ? '{}' : '$scope || {}'}) { ${tarStr} }`;
return new Function('$scope', code)(self); return new Function('$scope', code)(self);
} catch (err) { } catch (err) {
logger.error('parseExpression.error', err, str, self); logger.error('parseExpression.error', err, str, self?.__self ?? self);
return undefined; return undefined;
} }
} }
export function parseThisRequiredExpression(str: any, self: any) {
return parseExpression(str, self, true);
}
/** /**
* capitalize first letter * capitalize first letter
* @param word string to be proccessed * @param word string to be proccessed

View File

@ -994,6 +994,24 @@ exports[`JSExpression JSExpression props with loop 1`] = `
</div> </div>
`; `;
exports[`JSExpression JSExpression props with loop, and thisRequiredInJSE is true 1`] = `
<div
className="lce-page"
style={Object {}}
>
<div
className="div-ut"
forwardRef={[Function]}
name1="1"
/>
<div
className="div-ut"
forwardRef={[Function]}
name1="2"
/>
</div>
`;
exports[`JSExpression JSFunction props 1`] = ` exports[`JSExpression JSFunction props 1`] = `
<div <div
className="lce-page" className="lce-page"

View File

@ -145,7 +145,9 @@ describe('JSExpression', () => {
] ]
}; };
getComp(schema, components.Div).then(({ component, inst }) => { getComp(schema, components.Div, {
thisRequiredInJSE: false,
}).then(({ component, inst }) => {
// expect(inst[0].props.visible).toBeTruthy(); // expect(inst[0].props.visible).toBeTruthy();
expect(inst.length).toEqual(2); expect(inst.length).toEqual(2);
[1, 2].forEach((i) => { [1, 2].forEach((i) => {
@ -157,6 +159,50 @@ describe('JSExpression', () => {
}); });
}); });
it('JSExpression props with loop, and thisRequiredInJSE is true', (done) => {
const schema = {
componentName: 'Page',
props: {},
state: {
isShowDialog: true,
},
children: [
{
componentName: "Div",
loop: [
{
name: '1',
},
{
name: '2'
}
],
props: {
className: "div-ut",
name1: {
type: 'JSExpression',
value: 'this.item.name',
},
name2: {
type: 'JSExpression',
value: 'item.name',
},
}
}
]
};
getComp(schema, components.Div).then(({ component, inst }) => {
expect(inst.length).toEqual(2);
[0, 1].forEach((i) => {
expect(inst[i].props[`name1`]).toBe(i + 1 + '');
expect(inst[i].props[`name2`]).toBe(undefined);
})
componentSnapshot = component;
done();
});
});
// it('JSFunction props with loop', (done) => { // it('JSFunction props with loop', (done) => {
// const schema = { // const schema = {
// componentName: 'Page', // componentName: 'Page',

View File

@ -15,9 +15,11 @@ import {
isString, isString,
serializeParams, serializeParams,
parseExpression, parseExpression,
parseThisRequiredExpression,
parseI18n, parseI18n,
parseData, parseData,
} from '../../src/utils/common'; } from '../../src/utils/common';
import logger from '../../src/utils/logger';
describe('test isSchema', () => { describe('test isSchema', () => {
it('should be false when empty value is passed', () => { it('should be false when empty value is passed', () => {
@ -335,19 +337,55 @@ describe('test parseExpression ', () => {
const result = parseExpression(mockExpression, { scopeValue: 1 }); const result = parseExpression(mockExpression, { scopeValue: 1 });
expect(result({ param1: 2 })).toBe((1 + 2 + 5)); expect(result({ param1: 2 })).toBe((1 + 2 + 5));
}); });
it('[success] JSExpression handle without this use scopeValue', () => {
const mockExpression = {
"type": "JSExpression",
"value": "state"
};
const result = parseExpression(mockExpression, { state: 1 });
expect(result).toBe((1));
});
it('[success] JSExpression handle without this use scopeValue', () => {
const mockExpression = {
"type": "JSExpression",
"value": "this.state"
};
const result = parseExpression(mockExpression, { state: 1 });
expect(result).toBe((1));
});
}); });
describe('test parseExpression ', () => { describe('test parseThisRequiredExpression', () => {
it('can handle JSExpression', () => { it('can handle JSExpression', () => {
const mockExpression = { const mockExpression = {
"type": "JSExpression", "type": "JSExpression",
"value": "function (params) { return this.scopeValue + params.param1 + 5;}" "value": "function (params) { return this.scopeValue + params.param1 + 5;}"
}; };
const result = parseExpression(mockExpression, { scopeValue: 1 }); const result = parseThisRequiredExpression(mockExpression, { scopeValue: 1 });
expect(result({ param1: 2 })).toBe((1 + 2 + 5)); expect(result({ param1: 2 })).toBe((1 + 2 + 5));
}); });
});
it('[error] JSExpression handle without this use scopeValue', () => {
const mockExpression = {
"type": "JSExpression",
"value": "state.text"
};
const fn = logger.error = jest.fn();
parseThisRequiredExpression(mockExpression, { state: { text: 'text' } });
expect(fn).toBeCalledWith('parseExpression.error', new ReferenceError('state is not defined'), {"type": "JSExpression", "value": "state.text"}, {"state": {"text": "text"}});
});
it('[success] JSExpression handle without this use scopeValue', () => {
const mockExpression = {
"type": "JSExpression",
"value": "this.state"
};
const result = parseThisRequiredExpression(mockExpression, { state: 1 });
expect(result).toBe((1));
});
})
describe('test parseI18n ', () => { describe('test parseI18n ', () => {
it('can handle normal parseI18n', () => { it('can handle normal parseI18n', () => {