From da7f77ee91b3bf441a4a57614872df32d6a1d041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B5=AE=E9=BB=8E?= Date: Thu, 23 Jun 2022 14:44:35 +0800 Subject: [PATCH] feat: added thisRequiredInJSE API to control whether JSExpression expression access context must use this (#702) --- .../designer/src/builtin-simulator/host.ts | 4 ++ packages/editor-core/src/config.ts | 10 ++++ .../src/renderer-view.tsx | 1 + .../src/renderer-view.tsx | 1 + packages/renderer-core/src/renderer/base.tsx | 19 ++++--- .../renderer-core/src/renderer/renderer.tsx | 1 + packages/renderer-core/src/types/index.ts | 8 ++- packages/renderer-core/src/utils/common.ts | 10 +++- .../__snapshots__/renderer.test.tsx.snap | 18 ++++++ .../tests/renderer/renderer.test.tsx | 48 +++++++++++++++- .../renderer-core/tests/utils/common.test.ts | 56 ++++++++++++++++--- 11 files changed, 154 insertions(+), 22 deletions(-) diff --git a/packages/designer/src/builtin-simulator/host.ts b/packages/designer/src/builtin-simulator/host.ts index e4cfc4c96..e40566f57 100644 --- a/packages/designer/src/builtin-simulator/host.ts +++ b/packages/designer/src/builtin-simulator/host.ts @@ -228,6 +228,10 @@ export class BuiltinSimulatorHost implements ISimulatorHost { diff --git a/packages/rax-simulator-renderer/src/renderer-view.tsx b/packages/rax-simulator-renderer/src/renderer-view.tsx index c11c2dfdf..2e8d6e0fc 100644 --- a/packages/rax-simulator-renderer/src/renderer-view.tsx +++ b/packages/rax-simulator-renderer/src/renderer-view.tsx @@ -219,6 +219,7 @@ class Renderer extends Component<{ onCompGetRef={(schema: any, ref: any) => { documentInstance.mountInstance(schema.id, ref); }} + thisRequiredInJSE={host.thisRequiredInJSE} documentId={document.id} getNode={(id: string) => documentInstance.getNode(id) as any} rendererName="PageRenderer" diff --git a/packages/react-simulator-renderer/src/renderer-view.tsx b/packages/react-simulator-renderer/src/renderer-view.tsx index 5647a5447..d978494c0 100644 --- a/packages/react-simulator-renderer/src/renderer-view.tsx +++ b/packages/react-simulator-renderer/src/renderer-view.tsx @@ -189,6 +189,7 @@ class Renderer extends Component<{ setSchemaChangedSymbol={this.setSchemaChangedSymbol} getNode={(id: string) => documentInstance.getNode(id) as Node} rendererName="PageRenderer" + thisRequiredInJSE={host.thisRequiredInJSE} customCreateElement={(Component: any, props: any, children: any) => { const { __id, ...viewProps } = props; viewProps.componentId = __id; diff --git a/packages/renderer-core/src/renderer/base.tsx b/packages/renderer-core/src/renderer/base.tsx index 6a6d622b1..aee020344 100644 --- a/packages/renderer-core/src/renderer/base.tsx +++ b/packages/renderer-core/src/renderer/base.tsx @@ -12,6 +12,7 @@ import { getValue, parseData, parseExpression, + parseThisRequiredExpression, parseI18n, isEmpty, isSchema, @@ -83,11 +84,13 @@ export default function baseRendererFactory(): IBaseRenderComponent { getLocale: any; setLocale: any; styleElement: any; + parseExpression: any; [key: string]: any; constructor(props: IBaseRendererProps, context: IBaseRendererContext) { super(props, context); this.context = context; + this.parseExpression = props?.thisRequiredInJSE ? parseThisRequiredExpression : parseExpression; this.__beforeInit(props); this.__init(props); this.__afterInit(props); @@ -112,7 +115,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { if (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); } @@ -193,7 +196,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { if (fn) { // TODO, cache if (isJSExpression(fn) || isJSFunction(fn)) { - fn = parseExpression(fn, this); + fn = this.parseExpression(fn, this); } if (typeof fn !== 'function') { console.error(`生命周期${method}类型不符`, fn); @@ -219,7 +222,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { this.__customMethodsList = customMethodsList; forEach(__schema.methods, (val: any, key: string) => { if (isJSExpression(val) || isJSFunction(val)) { - val = parseExpression(val, this); + val = this.parseExpression(val, this); } if (typeof val !== 'function') { console.error(`自定义函数${key}类型不符`, val); @@ -414,7 +417,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { const { __appHelper: appHelper, __components: components = {} } = this.props || {}; if (isJSExpression(schema)) { - return parseExpression(schema, scope); + return this.parseExpression(schema, scope); } if (isI18nData(schema)) { return parseI18n(schema, scope); @@ -434,7 +437,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { const _children = this.getSchemaChildren(schema); // 解析占位组件 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); } @@ -496,7 +499,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { let scopeKey = ''; // 判断组件是否需要生成scope,且只生成一次,挂在this.__compScopes上 if (Comp.generateScope) { - const key = parseExpression(schema.props?.key, scope); + const key = this.parseExpression(schema.props?.key, scope); if (key) { // 如果组件自己设置key则使用组件自己的key scopeKey = key; @@ -647,7 +650,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { _children.forEach((_child: any) => { const _childVirtualDom = this.__createVirtualDom( - isJSExpression(_child) ? parseExpression(_child, scope) : _child, + isJSExpression(_child) ? this.parseExpression(_child, scope) : _child, scope, { schema, @@ -754,7 +757,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { return checkProps(props); } if (isJSExpression(props)) { - props = parseExpression(props, scope); + props = this.parseExpression(props, scope); // 只有当变量解析出来为模型结构的时候才会继续解析 if (!isSchema(props) && !isJSSlot(props)) return checkProps(props); } diff --git a/packages/renderer-core/src/renderer/renderer.tsx b/packages/renderer-core/src/renderer/renderer.tsx index bd2b3df8b..59d63f790 100644 --- a/packages/renderer-core/src/renderer/renderer.tsx +++ b/packages/renderer-core/src/renderer/renderer.tsx @@ -60,6 +60,7 @@ export default function rendererFactory(): IRenderComponent { schema: {} as RootSchema, onCompGetRef: () => { }, onCompGetCtx: () => { }, + thisRequiredInJSE: true, }; static findDOMNode = findDOMNode; diff --git a/packages/renderer-core/src/types/index.ts b/packages/renderer-core/src/types/index.ts index b0cc9f996..a3e7f0c36 100644 --- a/packages/renderer-core/src/types/index.ts +++ b/packages/renderer-core/src/types/index.ts @@ -128,6 +128,11 @@ export interface IRendererProps { faultComponent?: IGeneralComponent; /** 设备信息 */ device?: string; + /** + * @default true + * JSExpression 是否只支持使用 this 来访问上下文变量 + */ + thisRequiredInJSE?: boolean; } export interface IRendererState { @@ -148,12 +153,13 @@ export interface IBaseRendererProps { __host?: BuiltinSimulatorHost; __container?: any; config?: Record; - designMode?: 'live' | 'design'; + designMode?: 'design'; className?: string; style?: CSSProperties; id?: string | number; getSchemaChangedSymbol?: () => boolean; setSchemaChangedSymbol?: (symbol: boolean) => void; + thisRequiredInJSE?: boolean; documentId?: string; getNode?: any; /** diff --git a/packages/renderer-core/src/utils/common.ts b/packages/renderer-core/src/utils/common.ts index 437e6077f..8db913f3d 100644 --- a/packages/renderer-core/src/utils/common.ts +++ b/packages/renderer-core/src/utils/common.ts @@ -242,7 +242,7 @@ export function transformStringToFunction(str: string) { * @param self scope object * @returns funtion */ -export function parseExpression(str: any, self: any) { +export function parseExpression(str: any, self: any, thisRequired = false) { try { const contextArr = ['"use strict";', 'var __self = arguments[0];']; contextArr.push('return '); @@ -259,14 +259,18 @@ export function parseExpression(str: any, self: any) { if (inSameDomain() && (window.parent as any).__newFunc) { 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); } catch (err) { - logger.error('parseExpression.error', err, str, self); + logger.error('parseExpression.error', err, str, self?.__self ?? self); return undefined; } } +export function parseThisRequiredExpression(str: any, self: any) { + return parseExpression(str, self, true); +} + /** * capitalize first letter * @param word string to be proccessed diff --git a/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap index 76ed01e02..169ed545c 100644 --- a/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap +++ b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap @@ -994,6 +994,24 @@ exports[`JSExpression JSExpression props with loop 1`] = ` `; +exports[`JSExpression JSExpression props with loop, and thisRequiredInJSE is true 1`] = ` +
+
+
+
+`; + exports[`JSExpression JSFunction props 1`] = `
{ ] }; - getComp(schema, components.Div).then(({ component, inst }) => { + getComp(schema, components.Div, { + thisRequiredInJSE: false, + }).then(({ component, inst }) => { // expect(inst[0].props.visible).toBeTruthy(); expect(inst.length).toEqual(2); [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) => { // const schema = { // componentName: 'Page', diff --git a/packages/renderer-core/tests/utils/common.test.ts b/packages/renderer-core/tests/utils/common.test.ts index a67842ed6..6fac55024 100644 --- a/packages/renderer-core/tests/utils/common.test.ts +++ b/packages/renderer-core/tests/utils/common.test.ts @@ -1,10 +1,10 @@ // @ts-nocheck -import { - isSchema, - isFileSchema, - inSameDomain, - getFileCssName, - isJSSlot, +import { + isSchema, + isFileSchema, + inSameDomain, + getFileCssName, + isJSSlot, getValue, getI18n, transformArrayToMap, @@ -15,9 +15,11 @@ import { isString, serializeParams, parseExpression, + parseThisRequiredExpression, parseI18n, parseData, } from '../../src/utils/common'; +import logger from '../../src/utils/logger'; describe('test isSchema', () => { it('should be false when empty value is passed', () => { @@ -335,19 +337,55 @@ describe('test parseExpression ', () => { const result = parseExpression(mockExpression, { scopeValue: 1 }); 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', () => { const mockExpression = { "type": "JSExpression", "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)); }); -}); + 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 ', () => { it('can handle normal parseI18n', () => {