feat: 🎸 解决通过 Rax 出码到小程序的时候循环里面没法用循环变量的问题

This commit is contained in:
牧毅 2020-08-13 17:31:11 +08:00
parent 6cd07524b6
commit 779ea7c718
16 changed files with 812 additions and 80 deletions

View File

@ -53,7 +53,7 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
return self._dataSourceEngine.dataSourceMap || {};
},
async reloadDataSource() {
self._dataSourceEngine.reloadDataSource();
await self._dataSourceEngine.reloadDataSource();
},
get utils() {
return self._utils;

View File

@ -1,23 +1,31 @@
import _ from 'lodash';
import changeCase from 'change-case';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
CodePiece,
CompositeValue,
FileType,
HandlerSet,
ICodeChunk,
ICodeStruct,
IContainerInfo,
isJSExpression,
isJSFunction,
JSExpression,
JSFunction,
NodeSchema,
NpmInfo,
PIECE_TYPE,
} from '../../../types';
import { RAX_CHUNK_NAME } from './const';
import { COMMON_CHUNK_NAME } from '../../../const/generator';
import { createNodeGenerator, generateReactCtrlLine, generateString } from '../../../utils/nodeToJSX';
import { createNodeGenerator, generateReactCtrlLine, generateString, generateAttr } from '../../../utils/nodeToJSX';
import { generateExpression } from '../../../utils/jsExpression';
import { CustomHandlerSet, generateUnknownType } from '../../../utils/compositeType';
import { IScopeBindings, ScopeBindings } from '../../../utils/ScopeBindings';
import { parseExpressionConvertThis2Context, parseExpressionGetGlobalVariables } from '../../../utils/expressionParser';
type PluginConfig = {
fileType: string;
@ -30,17 +38,22 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
...config,
};
const transformers = {
transformThis2Context,
transformJsExpr: (expr: string) =>
isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${transformThis2Context(expr)}))`,
transformLoopExpr: (expr: string) => `__$$evalArray(() => (${transformThis2Context(expr)}))`,
};
// 什么都不做的的话,会有 3 个问题:
// 1. 小程序出码的时候,循环变量没法拿到
// 2. 小程序出码的时候,很容易出现 Uncaught TypeError: Cannot read property 'avatar' of undefined 这样的异常(如下图的 50 行) -- 因为若直接出码Rax 构建到小程序的时候会立即计算所有在视图中用到的变量
// 3. 通过 this.xxx 能拿到的东西太多了,而且自定义的 methods 可能会无意间破坏 Rax 框架或小程序框架在页面 this 上的东东
// const transformers = {
// transformThis2Context: (expr: string) => expr,
// transformJsExpr: (expr: string) => expr,
// transformLoopExpr: (expr: string) => expr,
// };
const handlers = {
expression: (input: JSExpression) => transformers.transformJsExpr(generateExpression(input)),
function: (input: JSFunction) => transformers.transformJsExpr(input.value || 'null'),
};
// 不转换 this.xxx 到 __$$context.xxx 的话,依然会有上述的 1 和 3 的问题。
// const transformers = {
// transformThis2Context: (expr: string) => expr,
// transformJsExpr: (expr: string) => (isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${expr}))`),
// transformLoopExpr: (expr: string) => `__$$evalArray(() => (${expr}))`,
// };
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
@ -64,24 +77,57 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
// 然后过滤掉所有的别名 chunks
next.chunks = next.chunks.filter((chunk) => !isImportAliasDefineChunk(chunk));
const customHandlers: CustomHandlerSet = {
expression(this: CustomHandlerSet, input: JSExpression) {
return transformJsExpr(generateExpression(input), this);
},
function(input) {
return transformThis2Context(input.value || 'null', this);
},
loopDataExpr(input) {
return typeof input === 'string' ? transformLoopExpr(input, this) : '';
},
tagName: mapComponentNameToAliasOrKeepIt,
nodeAttr: generateNodeAttrForRax,
};
const generatorHandlers: HandlerSet<string> = {
string: generateString,
expression: (input) => [customHandlers.expression?.(input) || 'null'],
function: (input) => [customHandlers.function?.(input) || 'null'],
};
// 创建代码生成器
const generator = createNodeGenerator(
{
string: generateString,
expression: (input) => [handlers.expression(input)],
function: (input) => [handlers.function(input)],
},
[generateReactCtrlLine],
{
expression: (input) => (isJSExpression(input) ? handlers.expression(input) : ''),
function: (input) => (isJSFunction(input) ? handlers.function(input) : ''),
loopDataExpr: (input) => (typeof input === 'string' ? transformers.transformLoopExpr(input) : ''),
tagName: mapComponentNameToAliasOrKeepIt,
},
);
const commonNodeGenerator = createNodeGenerator(generatorHandlers, [generateReactCtrlLine], customHandlers);
const raxCodeGenerator = (node: NodeSchema): string => {
if (node.loop) {
const loopItemName = node.loopArgs?.[0] || 'item';
const loopIndexName = node.loopArgs?.[1] || 'index';
return runInNewScope({
scopeHost: customHandlers,
newScopeOwnVariables: [loopItemName, loopIndexName],
run: () => commonNodeGenerator(node),
});
}
return commonNodeGenerator(node);
};
generatorHandlers.node = (node) => [raxCodeGenerator(node)];
customHandlers.node = raxCodeGenerator;
// 生成 JSX 代码
const jsxContent = generator(ir);
const jsxContent = raxCodeGenerator(ir);
next.chunks.push({
type: ChunkType.STRING,
fileType: cfg.fileType,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: `import { isMiniApp as __$$isMiniApp } from 'universal-env';`,
linkAfter: [],
});
next.chunks.push({
type: ChunkType.STRING,
@ -130,6 +176,14 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
export default pluginFactory;
function transformLoopExpr(expr: string, handlers: CustomHandlerSet) {
return `__$$evalArray(() => (${transformThis2Context(expr, handlers)}))`;
}
function transformJsExpr(expr: string, handlers: CustomHandlerSet) {
return isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${transformThis2Context(expr, handlers)}))`;
}
function isImportAliasDefineChunk(
chunk: ICodeChunk,
): chunk is ICodeChunk & {
@ -159,11 +213,97 @@ function isLiteralAtomicExpr(expr: string): boolean {
* this.xxx __$$context.xxx
* @param expr
*/
function transformThis2Context(expr: string): string {
// TODO: 应该根据语法分析来搞
// TODO: 如何替换自定义名字的循环变量generateReactCtrlLine
return expr
.replace(/\bthis\.item\./, () => 'item.')
.replace(/\bthis\.index\./, () => 'index.')
.replace(/\bthis\./, () => '__$$context.');
function transformThis2Context(expr: string, customHandlers: CustomHandlerSet): string {
// return expr
// .replace(/\bthis\.item\./g, () => 'item.')
// .replace(/\bthis\.index\./g, () => 'index.')
// .replace(/\bthis\./g, () => '__$$context.');
return parseExpressionConvertThis2Context(expr, '__$$context', customHandlers.scopeBindings?.getAllBindings() || []);
}
function generateNodeAttrForRax(this: CustomHandlerSet, attrName: string, attrValue: CompositeValue): CodePiece[] {
if (!/^on/.test(attrName)) {
return generateAttr(attrName, attrValue, {
...this,
nodeAttr: undefined,
});
}
// 先出个码
const valueExpr = generateUnknownType(attrValue, this);
// 查询当前作用域下的变量
const currentScopeVariables = this.scopeBindings?.getAllBindings() || [];
if (currentScopeVariables.length <= 0) {
return [
{
type: PIECE_TYPE.ATTR,
value: `${attrName}={${valueExpr}}`,
},
];
}
// 提取出所有的未定义的全局变量
const undeclaredVariablesInValueExpr = parseExpressionGetGlobalVariables(valueExpr);
const referencedLocalVariables = _.intersection(undeclaredVariablesInValueExpr, currentScopeVariables);
if (referencedLocalVariables.length <= 0) {
return [
{
type: PIECE_TYPE.ATTR,
value: `${attrName}={${valueExpr}}`,
},
];
}
const wrappedAttrValueExpr = [
`(...__$$args) => {`,
` if (__$$isMiniApp) {`,
` const __$$event = __$$args[0];`,
...referencedLocalVariables.map((localVar) => `const ${localVar} = __$$event.target.dataset.${localVar};`),
` return (${valueExpr}).apply(this, __$$args);`,
` } else {`,
` return (${valueExpr}).apply(this, __$$args);`,
` }`,
`}`,
].join('\n');
return [
...referencedLocalVariables.map((localVar) => ({
type: PIECE_TYPE.ATTR,
value: `data-${changeCase.snake(localVar)}={${localVar}}`,
})),
{
type: PIECE_TYPE.ATTR,
value: `${attrName}={${wrappedAttrValueExpr}}`,
},
];
}
function runInNewScope<T>({
scopeHost,
newScopeOwnVariables,
run,
}: {
scopeHost: {
scopeBindings?: IScopeBindings;
};
newScopeOwnVariables: string[];
run: () => T;
}): T {
const originalScopeBindings = scopeHost.scopeBindings;
try {
const newScope = new ScopeBindings(originalScopeBindings);
newScopeOwnVariables.forEach((varName) => {
newScope.addBinding(varName);
});
scopeHost.scopeBindings = newScope;
return run();
} finally {
scopeHost.scopeBindings = originalScopeBindings;
}
}

View File

@ -36,6 +36,7 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => {
},
dependencies: {
'@ali/lowcode-datasource-engine': '^0.1.0',
'universal-env': '^3.2.0',
rax: '^1.1.0',
'rax-app': '^2.0.0',
'rax-document': '^0.1.0',

View File

@ -60,8 +60,6 @@ export default function createIceJsProjectBuilder(): IProjectBuilder {
htmlEntry: [raxApp.plugins.entryDocument()],
packageJSON: [raxApp.plugins.packageJSON()],
},
postProcessors: [
// prettier() // 暂且禁用 prettier
],
postProcessors: process.env.NODE_ENV !== 'test' ? [prettier()] : [],
});
}

View File

@ -0,0 +1,33 @@
export class OrderedSet<T> {
private _set = new Set<T>();
private _arr: T[] = [];
constructor(items?: T[]) {
if (items) {
this._set = new Set(items);
this._arr = items.slice(0);
}
}
add(item: T) {
if (!this._set.has(item)) {
this._set.add(item);
this._arr.push(item);
}
}
delete(item: T) {
if (this._set.has(item)) {
this._set.delete(item);
this._arr.splice(this._arr.indexOf(item), 1);
}
}
has(item: T) {
return this._set.has(item);
}
toArray() {
return this._arr.slice(0);
}
}

View File

@ -0,0 +1,52 @@
import { OrderedSet } from './OrderedSet';
export interface IScopeBindings {
readonly parent: IScopeBindings | null;
hasBinding(varName: string): boolean;
hasOwnBinding(varName: string): boolean;
addBinding(varName: string): void;
removeBinding(varName: string): void;
getAllBindings(): string[];
getAllOwnedBindings(): string[];
}
export class ScopeBindings implements IScopeBindings {
private _bindings = new OrderedSet<string>();
constructor(public readonly parent: IScopeBindings | null = null) {}
hasBinding(varName: string): boolean {
return this._bindings.has(varName) || !!this.parent?.hasBinding(varName);
}
hasOwnBinding(varName: string): boolean {
return this._bindings.has(varName);
}
addBinding(varName: string): void {
this._bindings.add(varName);
}
removeBinding(varName: string): void {
this._bindings.delete(varName);
}
getAllBindings(): string[] {
const allBindings = new OrderedSet(this._bindings.toArray());
for (let parent = this.parent; parent; parent = parent?.parent) {
parent.getAllOwnedBindings().forEach((varName) => {
allBindings.add(varName);
});
}
return allBindings.toArray();
}
getAllOwnedBindings(): string[] {
return this._bindings.toArray();
}
}

View File

@ -12,22 +12,27 @@ import {
JSSlot,
NodeSchema,
NodeData,
CodePiece,
} from '../types';
import { generateExpression, generateFunction } from './jsExpression';
import { IScopeBindings } from './ScopeBindings';
export interface CustomHandlerSet {
boolean?: (bool: boolean) => string;
number?: (num: number) => string;
string?: (str: string) => string;
array?: (arr: JSONArray | CompositeArray) => string;
object?: (obj: JSONObject | CompositeObject) => string;
expression?: (jsExpr: JSExpression) => string;
function?: (jsFunc: JSFunction) => string;
slot?: (jsSlot: JSSlot) => string;
node?: (node: NodeSchema) => string;
loopDataExpr?: (loopDataExpr: string) => string;
conditionExpr?: (conditionExpr: string) => string;
tagName?: (tagName: string) => string;
boolean?(this: CustomHandlerSet, bool: boolean): string;
number?(this: CustomHandlerSet, num: number): string;
string?(this: CustomHandlerSet, str: string): string;
array?(this: CustomHandlerSet, arr: JSONArray | CompositeArray): string;
object?(this: CustomHandlerSet, obj: JSONObject | CompositeObject): string;
expression?(this: CustomHandlerSet, jsExpr: JSExpression): string;
function?(this: CustomHandlerSet, jsFunc: JSFunction): string;
slot?(this: CustomHandlerSet, jsSlot: JSSlot): string;
node?(this: CustomHandlerSet, node: NodeSchema): string;
nodeAttrs?(this: CustomHandlerSet, node: NodeSchema): CodePiece[];
nodeAttr?(this: CustomHandlerSet, attrName: string, attrValue: CompositeValue): CodePiece[];
loopDataExpr?(this: CustomHandlerSet, loopDataExpr: string): string;
conditionExpr?(this: CustomHandlerSet, conditionExpr: string): string;
tagName?(this: CustomHandlerSet, tagName: string): string;
scopeBindings?: IScopeBindings;
}
function generateArray(value: CompositeArray, handlers: CustomHandlerSet): string {

View File

@ -0,0 +1,239 @@
import * as parser from '@babel/parser';
import generate from '@babel/generator';
import traverse, { NodePath } from '@babel/traverse';
import * as t from '@babel/types';
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}"`);
Object.setPrototypeOf(this, new.target.prototype);
}
}
const MAYBE_EXPRESSIONS: {
[k in Node['type']]?: {
// fields: Array<keyof (Node & { type: k })>
fields: string[] | ((node: Node) => string[]);
};
} = {
ArrayExpression: { fields: ['elements'] },
AssignmentExpression: { fields: ['left', 'right'] },
BinaryExpression: { fields: ['left', 'right'] },
CallExpression: { fields: ['arguments', 'callee'] },
ConditionalExpression: { fields: ['test', 'consequent', 'alternate'] },
DoWhileStatement: { fields: ['test'] },
ExpressionStatement: { fields: ['expression'] },
ForInStatement: { fields: ['right'] },
ForStatement: { fields: ['init', 'test', 'update'] },
IfStatement: { fields: ['test'] },
LogicalExpression: { fields: ['left', 'right'] },
MemberExpression: {
fields: (node) => (node.type === 'MemberExpression' && node.computed ? ['object', 'property'] : ['object']),
},
NewExpression: { fields: ['callee', 'arguments'] },
ObjectMethod: {
fields: (node) => (node.type === 'ObjectMethod' && node.computed ? ['key'] : []),
},
ObjectProperty: {
fields: (node) => (node.type === 'ObjectProperty' && node.computed ? ['key', 'value'] : ['value']),
},
ReturnStatement: { fields: ['argument'] },
SequenceExpression: { fields: ['expressions'] },
ParenthesizedExpression: { fields: ['expression'] },
SwitchCase: { fields: ['test'] },
SwitchStatement: { fields: ['discriminant'] },
ThrowStatement: { fields: ['argument'] },
UnaryExpression: { fields: ['argument'] },
UpdateExpression: { fields: ['argument'] },
VariableDeclarator: { fields: ['init'] },
WhileStatement: { fields: ['test'] },
WithStatement: { fields: ['object'] },
AssignmentPattern: { fields: ['right'] },
ArrowFunctionExpression: { fields: ['body'] },
ClassExpression: { fields: ['superClass'] },
ClassDeclaration: { fields: ['superClass'] },
ExportDefaultDeclaration: { fields: ['declaration'] },
ForOfStatement: { fields: ['right'] },
ClassMethod: { fields: (node) => (node.type === 'ClassMethod' && node.computed ? ['key'] : []) },
SpreadElement: { fields: ['argument'] },
TaggedTemplateExpression: { fields: ['tag'] },
TemplateLiteral: { fields: ['expressions'] },
YieldExpression: { fields: ['argument'] },
AwaitExpression: { fields: ['argument'] },
OptionalMemberExpression: {
fields: (node) => (node.type === 'OptionalMemberExpression' && node.computed ? ['object', 'property'] : ['object']),
},
OptionalCallExpression: { fields: ['callee', 'arguments'] },
JSXSpreadAttribute: { fields: ['argument'] },
BindExpression: { fields: ['object', 'callee'] },
ClassProperty: { fields: (node) => (node.type === 'ClassProperty' && node.computed ? ['key', 'value'] : ['value']) },
PipelineTopicExpression: { fields: ['expression'] },
PipelineBareFunction: { fields: ['callee'] },
ClassPrivateProperty: { fields: ['value'] },
Decorator: { fields: ['expression'] },
TupleExpression: { fields: ['elements'] },
TSDeclareMethod: { fields: (node) => (node.type === 'TSDeclareMethod' && node.computed ? ['key'] : []) },
TSPropertySignature: {
fields: (node) => (node.type === 'TSPropertySignature' && node.computed ? ['key', 'initializer'] : ['initializer']),
},
TSMethodSignature: {
fields: (node) => (node.type === 'TSMethodSignature' && node.computed ? ['key'] : []),
},
TSAsExpression: { fields: ['expression'] },
TSTypeAssertion: { fields: ['expression'] },
TSEnumDeclaration: { fields: ['initializer'] },
TSEnumMember: { fields: ['initializer'] },
TSNonNullExpression: { fields: ['expression'] },
TSExportAssignment: { fields: ['expression'] },
};
export type ParseExpressionGetGlobalVariablesOptions = { filter?: (varName: string) => boolean };
const CROSS_THIS_SCOPE_TYPE_NODE: {
[k in Node['type']]?: boolean;
} = {
ArrowFunctionExpression: false, // 箭头函数不跨越 this 的 scope
FunctionExpression: true,
FunctionDeclaration: true,
// FunctionTypeAnnotation: false, // 这是 TS 定义
// FunctionTypeParam: false, // 这是 TS 定义
ClassDeclaration: true,
ClassExpression: true,
ClassBody: true,
ClassImplements: true,
ClassMethod: true,
ClassPrivateMethod: true,
ClassProperty: true,
ClassPrivateProperty: true,
DeclareClass: true,
};
export function parseExpressionGetGlobalVariables(
expr: string | null | undefined,
{ filter = (x) => true }: ParseExpressionGetGlobalVariablesOptions = {},
): string[] {
if (!expr) {
return [];
}
try {
const undeclaredVars = new OrderedSet<string>();
const ast = parser.parse(`!(${expr});`);
const addUndeclaredIdentifierIfNeeded = (x: object | null | undefined, path: NodePath<Node>) => {
if (isIdentifier(x) && !path.scope.hasBinding(x.name)) {
undeclaredVars.add(x.name);
}
};
traverse(ast, {
enter(path) {
const node = path.node;
const expressionFields = MAYBE_EXPRESSIONS[node.type]?.fields;
if (expressionFields) {
(typeof expressionFields === 'function' ? expressionFields(node) : expressionFields).forEach((fieldName) => {
const fieldValue = node[fieldName as keyof typeof node];
if (typeof fieldValue === 'object') {
if (Array.isArray(fieldValue)) {
fieldValue.forEach((item) => {
addUndeclaredIdentifierIfNeeded(item, path);
});
} else {
addUndeclaredIdentifierIfNeeded(fieldValue, path);
}
}
});
}
},
});
return undeclaredVars.toArray().filter(filter);
} catch (e) {
throw new ParseError(expr, e);
}
}
export function parseExpressionConvertThis2Context(
expr: string,
contextName: string = '__$$context',
localVariables: string[] = [],
): string {
if (!expr) {
return expr;
}
try {
const exprAst = parser.parseExpression(expr);
const exprWrapAst = t.expressionStatement(exprAst);
const fileAst = t.file(t.program([exprWrapAst]));
const localVariablesSet = new Set(localVariables);
let thisScopeLevel = CROSS_THIS_SCOPE_TYPE_NODE[exprAst.type] ? -1 : 0;
traverse(fileAst, {
enter(path) {
if (CROSS_THIS_SCOPE_TYPE_NODE[path.node.type]) {
thisScopeLevel++;
}
},
exit(path) {
if (CROSS_THIS_SCOPE_TYPE_NODE[path.node.type]) {
thisScopeLevel--;
}
},
MemberExpression(path) {
if (!path.isMemberExpression()) {
return;
}
const obj = path.get('object');
if (!obj.isThisExpression()) {
return;
}
// 处理局部变量
if (!path.node.computed) {
const prop = path.get('property');
if (prop.isIdentifier() && localVariablesSet.has(prop.node.name)) {
path.replaceWith(t.identifier(prop.node.name));
return;
}
}
// 替换 this (只在顶层替换)
if (thisScopeLevel <= 0) {
obj.replaceWith(t.identifier(contextName));
}
},
ThisExpression(path) {
if (!path.isThisExpression()) {
return;
}
// MemberExpression 中的 this.xxx 已经处理过了
if (path.parent.type === 'MemberExpression') {
return;
}
if (thisScopeLevel <= 0) {
path.replaceWith(t.identifier(contextName));
}
},
});
const { code } = generate(exprWrapAst.expression, { sourceMaps: false });
return code;
} catch (e) {
// throw new ParseError(expr, e);
throw e;
}
}
function indent(level: number) {
return ' '.repeat(level);
}

View File

@ -35,11 +35,21 @@ export function handleChildren(children: NodeData | NodeData[], handlers: Handle
}
}
export function generateAttr(attrName: string, attrValue: CompositeValue, handlers: CustomHandlerSet): CodePiece[] {
export function generateAttr(
attrName: string,
attrValue: CompositeValue,
customHandlers: CustomHandlerSet,
): CodePiece[] {
if (customHandlers.nodeAttr) {
return customHandlers.nodeAttr(attrName, attrValue);
}
// TODO: 这两个为啥要特殊处理??
if (attrName === 'initValue' || attrName === 'labelCol') {
return [];
}
const [isString, valueStr] = generateCompositeType(attrValue, handlers);
const [isString, valueStr] = generateCompositeType(attrValue, customHandlers);
return [
{
value: `${attrName}=${isString ? `"${valueStr}"` : `{${valueStr}}`}`,
@ -48,7 +58,11 @@ export function generateAttr(attrName: string, attrValue: CompositeValue, handle
];
}
export function generateAttrs(nodeItem: NodeSchema, handlers: CustomHandlerSet): CodePiece[] {
export function generateAttrs(nodeItem: NodeSchema, customHandlers: CustomHandlerSet): CodePiece[] {
if (customHandlers.nodeAttrs) {
return customHandlers.nodeAttrs(nodeItem);
}
const { props } = nodeItem;
let pieces: CodePiece[] = [];
@ -56,12 +70,12 @@ export function generateAttrs(nodeItem: NodeSchema, handlers: CustomHandlerSet):
if (props) {
if (!Array.isArray(props)) {
Object.keys(props).forEach((propName: string) => {
pieces = pieces.concat(generateAttr(propName, props[propName], handlers));
pieces = pieces.concat(generateAttr(propName, props[propName], customHandlers));
});
} else {
props.forEach((prop) => {
if (prop.name && !prop.spread) {
pieces = pieces.concat(generateAttr(prop.name, prop.value, handlers));
pieces = pieces.concat(generateAttr(prop.name, prop.value, customHandlers));
}
// TODO: 处理 spread 场景(<Xxx {...(something)}/>)
@ -97,10 +111,11 @@ export function generateReactCtrlLine(nodeItem: NodeSchema, handlers: CustomHand
const loopItemName = nodeItem.loopArgs?.[0] || 'item';
const loopIndexName = nodeItem.loopArgs?.[1] || 'index';
// TODO: 静态的值可以抽离出来?
const loopDataExpr = (handlers.loopDataExpr || _.identity)(
isJSExpression(nodeItem.loop) ? `(${nodeItem.loop.value})` : `(${JSON.stringify(nodeItem.loop)})`,
);
const rawLoopDataExpr = isJSExpression(nodeItem.loop)
? `(${nodeItem.loop.value})`
: `(${JSON.stringify(nodeItem.loop)})`;
const loopDataExpr = handlers.loopDataExpr ? handlers.loopDataExpr(rawLoopDataExpr) : rawLoopDataExpr;
pieces.unshift({
value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => (`,
@ -115,7 +130,8 @@ export function generateReactCtrlLine(nodeItem: NodeSchema, handlers: CustomHand
if (nodeItem.condition) {
const [isString, value] = generateCompositeType(nodeItem.condition, handlers);
const conditionExpr = (handlers.conditionExpr || _.identity)(isString ? `'${value}'` : value);
const rawConditionExpr = isString ? `'${value}'` : value;
const conditionExpr = handlers.conditionExpr ? handlers.conditionExpr(rawConditionExpr) : rawConditionExpr;
pieces.unshift({
value: `(${conditionExpr}) && (`,
@ -149,7 +165,8 @@ export function linkPieces(pieces: CodePiece[], handlers: CustomHandlerSet): str
throw new CodeGeneratorError('One node only need one tag define');
}
const tagName = (handlers.tagName || _.identity)(tagsPieces[0].value);
const rawTagName = tagsPieces[0].value;
const tagName = handlers.tagName ? handlers.tagName(rawTagName) : rawTagName;
const beforeParts = pieces
.filter((p) => p.type === PIECE_TYPE.BEFORE)

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@ali/lowcode-datasource-engine": "^0.1.0",
"universal-env": "^3.2.0",
"rax": "^1.1.0",
"rax-app": "^2.0.0",
"rax-document": "^0.1.0",

View File

@ -8,6 +8,8 @@ import Text from 'rax-text';
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
import { isMiniApp as __$$isMiniApp } from 'universal-env';
import __$$projectUtils from '../../utils';
import './index.css';
@ -50,7 +52,7 @@ class Home$$Page extends Component {
return self._dataSourceEngine.dataSourceMap || {};
},
async reloadDataSource() {
self._dataSourceEngine.reloadDataSource();
await self._dataSourceEngine.reloadDataSource();
},
get utils() {
return self._utils;

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@ali/lowcode-datasource-engine": "^0.1.0",
"universal-env": "^3.2.0",
"rax": "^1.1.0",
"rax-app": "^2.0.0",
"rax-document": "^0.1.0",

View File

@ -10,12 +10,15 @@ import Image from 'rax-image';
import { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
import { isMiniApp as __$$isMiniApp } from 'universal-env';
import __$$projectUtils from '../../utils';
import './index.css';
class Home$$Page extends Component {
state = {
clickCount: 0,
user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' },
orders: [
{
@ -61,7 +64,7 @@ class Home$$Page extends Component {
source={{ uri: __$$eval(() => __$$context.state.user.avatar) }}
style={{ width: '32px', height: '32px' }}
/>
<View onClick={__$$eval(() => __$$context.hello)}>
<View onClick={__$$context.hello}>
<Text>{__$$eval(() => __$$context.state.user.name)}</Text>
<Text>{__$$eval(() => __$$context.state.user.age)}</Text>
</View>
@ -70,29 +73,47 @@ class Home$$Page extends Component {
<View>
<Text>=== Orders: ===</Text>
</View>
{__$$evalArray(() => __$$context.state.orders).map((item, index) => (
{__$$evalArray(() => __$$context.state.orders).map((order, index) => (
<View
style={{ flexDirection: 'row' }}
onClick={__$$eval(
() =>
function () {
__$$context.utils.recordEvent(`CLICK_ORDER`, item.title);
},
)}
data-order={order}
onClick={(...__$$args) => {
if (__$$isMiniApp) {
const __$$event = __$$args[0];
const order = __$$event.target.dataset.order;
return function () {
__$$context.utils.recordEvent(`CLICK_ORDER`, order.title);
}.apply(this, __$$args);
} else {
return function () {
__$$context.utils.recordEvent(`CLICK_ORDER`, order.title);
}.apply(this, __$$args);
}
}}
>
<View>
<Image source={{ uri: __$$eval(() => item.coverUrl) }} style={{ width: '80px', height: '60px' }} />
<Image source={{ uri: __$$eval(() => order.coverUrl) }} style={{ width: '80px', height: '60px' }} />
</View>
<View>
<Text>{__$$eval(() => item.title)}</Text>
<Text>{__$$eval(() => __$$context.utils.formatPrice(item.price, '元'))}</Text>
<Text>{__$$eval(() => order.title)}</Text>
<Text>{__$$eval(() => __$$context.utils.formatPrice(order.price, '元'))}</Text>
</View>
</View>
))}
<View
onClick={function () {
__$$context.setState({
clickCount: __$$context.state.clickCount + 1,
});
}}
>
<Text>点击次数{__$$eval(() => __$$context.state.clickCount)}(点击加 1)</Text>
</View>
<View>
<Text>操作提示</Text>
<Text>1. 点击会员名可以弹出 Toast "Hello xxx!"</Text>
<Text>2. 点击订单会记录点击的订单信息并弹出 Toast 提示</Text>
<Text>3. 最下面的点击次数点一次应该加 1</Text>
</View>
</View>
);
@ -112,7 +133,7 @@ class Home$$Page extends Component {
return self._dataSourceEngine.dataSourceMap || {};
},
async reloadDataSource() {
self._dataSourceEngine.reloadDataSource();
await self._dataSourceEngine.reloadDataSource();
},
get utils() {
return self._utils;

View File

@ -36,6 +36,7 @@
componentName: 'Page',
fileName: 'home',
state: {
clickCount: 0,
user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' },
orders: [
{
@ -176,11 +177,12 @@
type: 'JSExpression',
value: 'this.state.orders',
},
loopArgs: ['order', 'index'],
props: {
style: { flexDirection: 'row' },
onClick: {
type: 'JSFunction',
value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.item.title) }',
value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.order.title) }',
},
},
children: [
@ -193,7 +195,7 @@
source: {
uri: {
type: 'JSExpression',
value: 'this.item.coverUrl',
value: 'this.order.coverUrl',
},
},
style: {
@ -211,20 +213,42 @@
componentName: 'Text',
children: {
type: 'JSExpression',
value: 'this.item.title',
value: 'this.order.title',
},
},
{
componentName: 'Text',
children: {
type: 'JSExpression',
value: 'this.utils.formatPrice(this.item.price, "元")',
value: 'this.utils.formatPrice(this.order.price, "元")',
},
},
],
},
],
},
{
componentName: 'View',
props: {
onClick: {
type: 'JSFunction',
value: 'function (){ this.setState({ clickCount: this.state.clickCount + 1 }) }',
},
},
children: [
{
componentName: 'Text',
children: [
'点击次数:',
{
type: 'JSExpression',
value: 'this.state.clickCount',
},
'(点击加 1)',
],
},
],
},
{
componentName: 'View',
children: [
@ -243,6 +267,11 @@
props: {},
children: '2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示',
},
{
componentName: 'Text',
props: {},
children: '3. 最下面的【点击次数】,点一次应该加 1',
},
],
},
],

View File

@ -0,0 +1,79 @@
import test from 'ava';
import type { ExecutionContext, Macro } from 'ava';
import { parseExpressionConvertThis2Context } from '../../../src/utils/expressionParser';
const macro: Macro<any[]> = (
t: ExecutionContext<{}>,
input: [string, string, string[]],
expected: string,
error?: { message: RegExp },
) => {
if (!error) {
t.deepEqual(parseExpressionConvertThis2Context(input[0], input[1], input[2]), expected);
} else {
t.throws(() => {
t.deepEqual(parseExpressionConvertThis2Context(input[0], input[1], input[2]), expected);
}, error.message);
}
};
macro.title = (providedTitle: string | undefined, ...args: any[]): string => {
const [input, expected] = args;
return providedTitle || `after convert this to context "${input[0]}" should be "${expected}"`.replace(/\n|\s+/g, ' ');
};
test(macro, ['this.hello', '__$$context', []], '__$$context.hello');
test(macro, ['this.utils.recordEvent', '__$$context', []], '__$$context.utils.recordEvent');
test(
macro,
['this.utils.recordEvent.bind(this)', '__$$context', []],
'__$$context.utils.recordEvent.bind(__$$context)',
);
test(macro, ['this.item', '__$$context', ['item']], 'item');
test(macro, ['this.user.name', '__$$context', ['user']], 'user.name');
test(macro, ['function (){}', '__$$context', []], 'function () {}');
test(
macro,
['function (){ this.utils.Toast.show("Hello world!") }', '__$$context'],
'function () {\n __$$context.utils.Toast.show("Hello world!");\n}',
);
// 变量能被替换掉
test(
macro,
['function (){ this.utils.recordEvent("click", this.item) }', '__$$context', ['item']],
'function () {\n __$$context.utils.recordEvent("click", item);\n}',
);
// 只替换顶层的,不替换内层
test(
macro,
['function (){ return function (){ this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']],
'function () {\n return function () {\n this.utils.recordEvent("click", item);\n };\n}',
);
// 只替换顶层的,不替换内层
test(
macro,
['function onClick(){ return function (){ this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']],
'function onClick() {\n return function () {\n this.utils.recordEvent("click", item);\n };\n}',
);
// 只替换顶层的,不替换内层
test(
macro,
['() => { return function (){ this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']],
'() => {\n return function () {\n this.utils.recordEvent("click", item);\n };\n}',
);
// 但是若内层有用箭头函数定义的则还是要替换下
test(
macro,
['() => { return () => { this.utils.recordEvent("click", this.item) } }', '__$$context', ['item']],
'() => {\n return () => {\n __$$context.utils.recordEvent("click", item);\n };\n}',
);

View File

@ -0,0 +1,114 @@
import test from 'ava';
import type { ExecutionContext, Macro } from 'ava';
import {
parseExpressionGetGlobalVariables,
ParseExpressionGetGlobalVariablesOptions,
} from '../../../src/utils/expressionParser';
const macro: Macro<any[]> = (
t: ExecutionContext<{}>,
input: [string | null | undefined, ParseExpressionGetGlobalVariablesOptions],
expected: string[],
error?: { message: RegExp },
) => {
if (!error) {
t.deepEqual(parseExpressionGetGlobalVariables(input[0], input[1]), expected);
} else {
t.throws(() => {
t.deepEqual(parseExpressionGetGlobalVariables(input[0], input[1]), expected);
}, error.message);
}
};
macro.title = (providedTitle: string | undefined, ...args: any[]): string => {
const [input, expected] = args;
return providedTitle || `global variables of "${input[0]}" should be "${expected.join(', ')}"`;
};
test(macro, ['function (){ }', {}], []);
test(macro, ['function (){ __$$context.utils.Toast.show("Hello world!") }', {}], ['__$$context']);
test(macro, ['function (){ __$$context.utils.formatPrice(item.price1, "元") }', {}], ['__$$context', 'item']);
test(
macro,
[
'function (){ __$$context.utils.formatPrice(item2, "元"); }',
{ filter: (varName: string) => !/^__\$\$/.test(varName) },
],
['item2'],
);
test(
macro,
[
'function (){ __$$context.utils.log(item3, [item4, item5]); }',
{ filter: (varName: string) => !/^__\$\$/.test(varName) },
],
['item3', 'item4', 'item5'],
);
test(
macro,
['function (){ item3[item4]("Hello"); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3', 'item4'],
);
test(macro, ['function (){ item3("Hello"); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }], ['item3']);
test(
macro,
['function foo(){ foo[item3]("Hello"); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3'],
);
// isAssignmentExpression/right
test(
macro,
['function (){ let foo; foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3'],
);
// isAssignmentExpression/left
test(
macro,
['function (){ foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['foo', 'item3'],
);
// isVariableDeclarator
test(
macro,
['function (){ const foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3'],
);
// isVariableDeclarator
test(
macro,
['function (){ let foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3'],
);
// isVariableDeclarator
test(
macro,
['function (){ var foo = item3; foo(); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['item3'],
);
// isTemplateLiteral
test(
macro,
['function (){ console.log(`Hello ${item3};`); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['console', 'item3'],
);
// isBinaryExpression
test(
macro,
['function (){ console.log(item2 | item3); }', { filter: (varName: string) => !/^__\$\$/.test(varName) }],
['console', 'item2', 'item3'],
);
// TODO: 补充更多类型的测试用例