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 changeCase from 'change-case';
import { Expression, MemberExpression } from '@babel/types';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
@ -25,7 +26,11 @@ import { generateExpression } from '../../../utils/jsExpression';
import { CustomHandlerSet, generateUnknownType } from '../../../utils/compositeType';
import { IScopeBindings, ScopeBindings } from '../../../utils/ScopeBindings';
import { parseExpressionConvertThis2Context, parseExpressionGetGlobalVariables } from '../../../utils/expressionParser';
import {
parseExpression,
parseExpressionConvertThis2Context,
parseExpressionGetGlobalVariables,
} from '../../../utils/expressionParser';
type PluginConfig = {
fileType: string;
@ -181,7 +186,62 @@ function transformLoopExpr(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(
@ -206,14 +266,14 @@ function isImportAliasDefineChunk(
*
*/
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
* @param expr
*/
function transformThis2Context(expr: string, customHandlers: CustomHandlerSet): string {
function transformThis2Context(expr: string | Expression, customHandlers: CustomHandlerSet): string {
// return expr
// .replace(/\bthis\.item\./g, () => 'item.')
// .replace(/\bthis\.index\./g, () => 'index.')

View File

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

View File

@ -79,7 +79,7 @@ class Home$$Page extends Component {
<View>
<Text>=== User Info: ===</Text>
</View>
{__$$eval(() => __$$context.state.user) && (
{__$$context.state.user && (
<View style={{ flexDirection: 'row' }}>
<Image
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>
<Text>操作提示</Text>

View File

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