feat: 🎸 优化 Rax 出码时对绑定的表达式的包裹逻辑, 对于一些简单的安全的表达式不做包裹

This commit is contained in:
牧毅 2020-08-21 10:44:38 +08:00
parent 475534f51f
commit facfa2afd6
4 changed files with 85 additions and 22 deletions

View File

@ -1,5 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import changeCase from 'change-case'; import changeCase from 'change-case';
import { Expression, MemberExpression } from '@babel/types';
import { import {
BuilderComponentPlugin, BuilderComponentPlugin,
BuilderComponentPluginFactory, BuilderComponentPluginFactory,
@ -25,7 +26,11 @@ import { generateExpression } from '../../../utils/jsExpression';
import { CustomHandlerSet, generateUnknownType } from '../../../utils/compositeType'; import { CustomHandlerSet, generateUnknownType } from '../../../utils/compositeType';
import { IScopeBindings, ScopeBindings } from '../../../utils/ScopeBindings'; import { IScopeBindings, ScopeBindings } from '../../../utils/ScopeBindings';
import { parseExpressionConvertThis2Context, parseExpressionGetGlobalVariables } from '../../../utils/expressionParser'; import {
parseExpression,
parseExpressionConvertThis2Context,
parseExpressionGetGlobalVariables,
} from '../../../utils/expressionParser';
type PluginConfig = { type PluginConfig = {
fileType: string; fileType: string;
@ -181,7 +186,62 @@ function transformLoopExpr(expr: string, handlers: CustomHandlerSet) {
} }
function transformJsExpr(expr: string, handlers: CustomHandlerSet) { function transformJsExpr(expr: string, handlers: CustomHandlerSet) {
return isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${transformThis2Context(expr, handlers)}))`; if (!expr) {
return 'undefined';
}
if (isLiteralAtomicExpr(expr)) {
return expr;
}
const exprAst = parseExpression(expr);
switch (exprAst.type) {
// 对于下面这些比较安全的字面值,可以直接返回对应的表达式,而非包一层
case 'BigIntLiteral':
case 'BooleanLiteral':
case 'DecimalLiteral':
case 'NullLiteral':
case 'NumericLiteral':
case 'RegExpLiteral':
case 'StringLiteral':
return expr;
// 对于直接写个函数的,则不用再包下,因为这样不会抛出异常的
case 'ArrowFunctionExpression':
case 'FunctionExpression':
return transformThis2Context(exprAst, handlers);
// 对于直接访问 this.xxx, this.utils.xxx, this.state.xxx 的也不用再包下
case 'MemberExpression':
if (isSimpleDirectlyAccessingThis(exprAst) || isSimpleDirectlyAccessingSafeProperties(exprAst)) {
return transformThis2Context(exprAst, handlers);
}
break;
default:
break;
}
// 其他的都需要包一层
return `__$$eval(() => (${transformThis2Context(exprAst, handlers)}))`;
}
/** this.xxx */
function isSimpleDirectlyAccessingThis(exprAst: MemberExpression) {
return !exprAst.computed && exprAst.object.type === 'ThisExpression';
}
/** this.state.xxx 和 this.utils.xxx 等安全的肯定应该存在的东东 */
function isSimpleDirectlyAccessingSafeProperties(exprAst: MemberExpression): boolean {
return (
!exprAst.computed &&
exprAst.object.type === 'MemberExpression' &&
exprAst.object.object.type === 'ThisExpression' &&
!exprAst.object.computed &&
exprAst.object.property.type === 'Identifier' &&
/^(state|utils|constants|i18n)$/.test(exprAst.object.property.name)
);
} }
function isImportAliasDefineChunk( function isImportAliasDefineChunk(
@ -206,14 +266,14 @@ function isImportAliasDefineChunk(
* *
*/ */
function isLiteralAtomicExpr(expr: string): boolean { function isLiteralAtomicExpr(expr: string): boolean {
return expr === 'null' || expr === 'undefined' || expr === 'true' || expr === 'false' || /^\d+$/.test(expr); return expr === 'null' || expr === 'undefined' || expr === 'true' || expr === 'false' || /^-?\d+(\.\d+)?$/.test(expr);
} }
/** /**
* this.xxx __$$context.xxx * this.xxx __$$context.xxx
* @param expr * @param expr
*/ */
function transformThis2Context(expr: string, customHandlers: CustomHandlerSet): string { function transformThis2Context(expr: string | Expression, customHandlers: CustomHandlerSet): string {
// return expr // return expr
// .replace(/\bthis\.item\./g, () => 'item.') // .replace(/\bthis\.item\./g, () => 'item.')
// .replace(/\bthis\.index\./g, () => 'index.') // .replace(/\bthis\.index\./g, () => 'index.')

View File

@ -7,8 +7,8 @@ import { isIdentifier, Node } from '@babel/types';
import { OrderedSet } from './OrderedSet'; import { OrderedSet } from './OrderedSet';
export class ParseError extends Error { export class ParseError extends Error {
constructor(public readonly expr: string, public readonly detail: unknown) { constructor(public readonly expr: string | t.Expression, public readonly detail: unknown) {
super(`Failed to parse expression "${expr}"`); super(`Failed to parse expression "${typeof expr === 'string' ? expr : generate(expr)}"`);
Object.setPrototypeOf(this, new.target.prototype); Object.setPrototypeOf(this, new.target.prototype);
} }
} }
@ -159,7 +159,7 @@ export function parseExpressionGetGlobalVariables(
} }
export function parseExpressionConvertThis2Context( export function parseExpressionConvertThis2Context(
expr: string, expr: string | t.Expression,
contextName: string = '__$$context', contextName: string = '__$$context',
localVariables: string[] = [], localVariables: string[] = [],
): string { ): string {
@ -168,7 +168,7 @@ export function parseExpressionConvertThis2Context(
} }
try { try {
const exprAst = parser.parseExpression(expr); const exprAst = typeof expr === 'string' ? parser.parseExpression(expr) : expr;
const exprWrapAst = t.expressionStatement(exprAst); const exprWrapAst = t.expressionStatement(exprAst);
const fileAst = t.file(t.program([exprWrapAst])); const fileAst = t.file(t.program([exprWrapAst]));
@ -229,11 +229,14 @@ export function parseExpressionConvertThis2Context(
const { code } = generate(exprWrapAst.expression, { sourceMaps: false }); const { code } = generate(exprWrapAst.expression, { sourceMaps: false });
return code; return code;
} catch (e) { } catch (e) {
// throw new ParseError(expr, e); throw new ParseError(expr, e);
throw e;
} }
} }
function indent(level: number) { export function parseExpression(expr: string) {
return ' '.repeat(level); try {
return parser.parseExpression(expr);
} catch (e) {
throw new ParseError(expr, e);
}
} }

View File

@ -79,7 +79,7 @@ class Home$$Page extends Component {
<View> <View>
<Text>=== User Info: ===</Text> <Text>=== User Info: ===</Text>
</View> </View>
{__$$eval(() => __$$context.state.user) && ( {__$$context.state.user && (
<View style={{ flexDirection: 'row' }}> <View style={{ flexDirection: 'row' }}>
<Image <Image
source={{ uri: __$$eval(() => __$$context.state.user.avatar) }} source={{ uri: __$$eval(() => __$$context.state.user.avatar) }}
@ -128,7 +128,7 @@ class Home$$Page extends Component {
}); });
}} }}
> >
<Text>点击次数{__$$eval(() => __$$context.state.clickCount)}(点击加 1)</Text> <Text>点击次数{__$$context.state.clickCount}(点击加 1)</Text>
</View> </View>
<View> <View>
<Text>操作提示</Text> <Text>操作提示</Text>

View File

@ -87,7 +87,7 @@
isSync: true, isSync: true,
}, },
dataHandler: { dataHandler: {
type: 'JSFunction', type: 'JSExpression',
value: 'function (response) {\nif (!response.success){\n throw new Error(response.message);\n }\n return response.data;\n}', value: 'function (response) {\nif (!response.success){\n throw new Error(response.message);\n }\n return response.data;\n}',
}, },
}, },
@ -104,13 +104,13 @@
isSync: true, isSync: true,
}, },
dataHandler: { dataHandler: {
type: 'JSFunction', type: 'JSExpression',
value: 'function (response) {\nif (!response.success){\n throw new Error(response.message);\n }\n return response.data.result;\n}', value: 'function (response) {\nif (!response.success){\n throw new Error(response.message);\n }\n return response.data.result;\n}',
}, },
}, },
], ],
dataHandler: { dataHandler: {
type: 'JSFunction', type: 'JSExpression',
value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}', value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}',
}, },
}, },
@ -164,7 +164,7 @@
componentName: 'View', componentName: 'View',
props: { props: {
onClick: { onClick: {
type: 'JSFunction', type: 'JSExpression',
value: 'this.hello', value: 'this.hello',
}, },
}, },
@ -210,7 +210,7 @@
props: { props: {
style: { flexDirection: 'row' }, style: { flexDirection: 'row' },
onClick: { onClick: {
type: 'JSFunction', type: 'JSExpression',
value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.order.title) }', value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.order.title) }',
}, },
}, },
@ -260,7 +260,7 @@
componentName: 'View', componentName: 'View',
props: { props: {
onClick: { onClick: {
type: 'JSFunction', type: 'JSExpression',
value: 'function (){ this.setState({ clickCount: this.state.clickCount + 1 }) }', value: 'function (){ this.setState({ clickCount: this.state.clickCount + 1 }) }',
}, },
}, },
@ -312,7 +312,7 @@
name: 'formatPrice', name: 'formatPrice',
type: 'function', type: 'function',
content: { content: {
type: 'JSFunction', type: 'JSExpression',
value: 'function formatPrice(price, unit) { return Number(price).toFixed(2) + unit; }', value: 'function formatPrice(price, unit) { return Number(price).toFixed(2) + unit; }',
}, },
}, },
@ -321,7 +321,7 @@
name: 'recordEvent', name: 'recordEvent',
type: 'function', type: 'function',
content: { content: {
type: 'JSFunction', type: 'JSExpression',
value: 'function recordEvent(eventName, eventDetail) { \n this.utils.Toast.show(`[EVENT]: ${eventName} ${eventDetail}`);\n console.log(`[EVENT]: ${eventName} (detail: %o) (user: %o)`, eventDetail, this.state.user); }', value: 'function recordEvent(eventName, eventDetail) { \n this.utils.Toast.show(`[EVENT]: ${eventName} ${eventDetail}`);\n console.log(`[EVENT]: ${eventName} (detail: %o) (user: %o)`, eventDetail, this.state.user); }',
}, },
}, },