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 || {}; return self._dataSourceEngine.dataSourceMap || {};
}, },
async reloadDataSource() { async reloadDataSource() {
self._dataSourceEngine.reloadDataSource(); await self._dataSourceEngine.reloadDataSource();
}, },
get utils() { get utils() {
return self._utils; return self._utils;

View File

@ -1,23 +1,31 @@
import _ from 'lodash';
import changeCase from 'change-case';
import { import {
BuilderComponentPlugin, BuilderComponentPlugin,
BuilderComponentPluginFactory, BuilderComponentPluginFactory,
ChunkType, ChunkType,
CodePiece,
CompositeValue,
FileType, FileType,
HandlerSet,
ICodeChunk, ICodeChunk,
ICodeStruct, ICodeStruct,
IContainerInfo, IContainerInfo,
isJSExpression,
isJSFunction,
JSExpression, JSExpression,
JSFunction, NodeSchema,
NpmInfo, NpmInfo,
PIECE_TYPE,
} from '../../../types'; } from '../../../types';
import { RAX_CHUNK_NAME } from './const'; import { RAX_CHUNK_NAME } from './const';
import { COMMON_CHUNK_NAME } from '../../../const/generator'; 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 { generateExpression } from '../../../utils/jsExpression';
import { CustomHandlerSet, generateUnknownType } from '../../../utils/compositeType';
import { IScopeBindings, ScopeBindings } from '../../../utils/ScopeBindings';
import { parseExpressionConvertThis2Context, parseExpressionGetGlobalVariables } from '../../../utils/expressionParser';
type PluginConfig = { type PluginConfig = {
fileType: string; fileType: string;
@ -30,17 +38,22 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
...config, ...config,
}; };
const transformers = { // 什么都不做的的话,会有 3 个问题:
transformThis2Context, // 1. 小程序出码的时候,循环变量没法拿到
transformJsExpr: (expr: string) => // 2. 小程序出码的时候,很容易出现 Uncaught TypeError: Cannot read property 'avatar' of undefined 这样的异常(如下图的 50 行) -- 因为若直接出码Rax 构建到小程序的时候会立即计算所有在视图中用到的变量
isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${transformThis2Context(expr)}))`, // 3. 通过 this.xxx 能拿到的东西太多了,而且自定义的 methods 可能会无意间破坏 Rax 框架或小程序框架在页面 this 上的东东
transformLoopExpr: (expr: string) => `__$$evalArray(() => (${transformThis2Context(expr)}))`, // const transformers = {
}; // transformThis2Context: (expr: string) => expr,
// transformJsExpr: (expr: string) => expr,
// transformLoopExpr: (expr: string) => expr,
// };
const handlers = { // 不转换 this.xxx 到 __$$context.xxx 的话,依然会有上述的 1 和 3 的问题。
expression: (input: JSExpression) => transformers.transformJsExpr(generateExpression(input)), // const transformers = {
function: (input: JSFunction) => transformers.transformJsExpr(input.value || 'null'), // transformThis2Context: (expr: string) => expr,
}; // transformJsExpr: (expr: string) => (isLiteralAtomicExpr(expr) ? expr : `__$$eval(() => (${expr}))`),
// transformLoopExpr: (expr: string) => `__$$evalArray(() => (${expr}))`,
// };
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = { const next: ICodeStruct = {
@ -64,24 +77,57 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
// 然后过滤掉所有的别名 chunks // 然后过滤掉所有的别名 chunks
next.chunks = next.chunks.filter((chunk) => !isImportAliasDefineChunk(chunk)); next.chunks = next.chunks.filter((chunk) => !isImportAliasDefineChunk(chunk));
// 创建代码生成器 const customHandlers: CustomHandlerSet = {
const generator = createNodeGenerator( expression(this: CustomHandlerSet, input: JSExpression) {
{ return transformJsExpr(generateExpression(input), this);
string: generateString, },
expression: (input) => [handlers.expression(input)], function(input) {
function: (input) => [handlers.function(input)], return transformThis2Context(input.value || 'null', this);
},
loopDataExpr(input) {
return typeof input === 'string' ? transformLoopExpr(input, this) : '';
}, },
[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, tagName: mapComponentNameToAliasOrKeepIt,
}, nodeAttr: generateNodeAttrForRax,
); };
const generatorHandlers: HandlerSet<string> = {
string: generateString,
expression: (input) => [customHandlers.expression?.(input) || 'null'],
function: (input) => [customHandlers.function?.(input) || 'null'],
};
// 创建代码生成器
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 代码 // 生成 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({ next.chunks.push({
type: ChunkType.STRING, type: ChunkType.STRING,
@ -130,6 +176,14 @@ const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) =>
export default pluginFactory; 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( function isImportAliasDefineChunk(
chunk: ICodeChunk, chunk: ICodeChunk,
): chunk is ICodeChunk & { ): chunk is ICodeChunk & {
@ -159,11 +213,97 @@ function isLiteralAtomicExpr(expr: string): boolean {
* this.xxx __$$context.xxx * this.xxx __$$context.xxx
* @param expr * @param expr
*/ */
function transformThis2Context(expr: string): string { function transformThis2Context(expr: string, customHandlers: CustomHandlerSet): string {
// TODO: 应该根据语法分析来搞 // return expr
// TODO: 如何替换自定义名字的循环变量generateReactCtrlLine // .replace(/\bthis\.item\./g, () => 'item.')
return expr // .replace(/\bthis\.index\./g, () => 'index.')
.replace(/\bthis\.item\./, () => 'item.') // .replace(/\bthis\./g, () => '__$$context.');
.replace(/\bthis\.index\./, () => 'index.')
.replace(/\bthis\./, () => '__$$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: { dependencies: {
'@ali/lowcode-datasource-engine': '^0.1.0', '@ali/lowcode-datasource-engine': '^0.1.0',
'universal-env': '^3.2.0',
rax: '^1.1.0', rax: '^1.1.0',
'rax-app': '^2.0.0', 'rax-app': '^2.0.0',
'rax-document': '^0.1.0', 'rax-document': '^0.1.0',

View File

@ -60,8 +60,6 @@ export default function createIceJsProjectBuilder(): IProjectBuilder {
htmlEntry: [raxApp.plugins.entryDocument()], htmlEntry: [raxApp.plugins.entryDocument()],
packageJSON: [raxApp.plugins.packageJSON()], packageJSON: [raxApp.plugins.packageJSON()],
}, },
postProcessors: [ postProcessors: process.env.NODE_ENV !== 'test' ? [prettier()] : [],
// prettier() // 暂且禁用 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, JSSlot,
NodeSchema, NodeSchema,
NodeData, NodeData,
CodePiece,
} from '../types'; } from '../types';
import { generateExpression, generateFunction } from './jsExpression'; import { generateExpression, generateFunction } from './jsExpression';
import { IScopeBindings } from './ScopeBindings';
export interface CustomHandlerSet { export interface CustomHandlerSet {
boolean?: (bool: boolean) => string; boolean?(this: CustomHandlerSet, bool: boolean): string;
number?: (num: number) => string; number?(this: CustomHandlerSet, num: number): string;
string?: (str: string) => string; string?(this: CustomHandlerSet, str: string): string;
array?: (arr: JSONArray | CompositeArray) => string; array?(this: CustomHandlerSet, arr: JSONArray | CompositeArray): string;
object?: (obj: JSONObject | CompositeObject) => string; object?(this: CustomHandlerSet, obj: JSONObject | CompositeObject): string;
expression?: (jsExpr: JSExpression) => string; expression?(this: CustomHandlerSet, jsExpr: JSExpression): string;
function?: (jsFunc: JSFunction) => string; function?(this: CustomHandlerSet, jsFunc: JSFunction): string;
slot?: (jsSlot: JSSlot) => string; slot?(this: CustomHandlerSet, jsSlot: JSSlot): string;
node?: (node: NodeSchema) => string; node?(this: CustomHandlerSet, node: NodeSchema): string;
loopDataExpr?: (loopDataExpr: string) => string; nodeAttrs?(this: CustomHandlerSet, node: NodeSchema): CodePiece[];
conditionExpr?: (conditionExpr: string) => string; nodeAttr?(this: CustomHandlerSet, attrName: string, attrValue: CompositeValue): CodePiece[];
tagName?: (tagName: string) => string; 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 { 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') { if (attrName === 'initValue' || attrName === 'labelCol') {
return []; return [];
} }
const [isString, valueStr] = generateCompositeType(attrValue, handlers);
const [isString, valueStr] = generateCompositeType(attrValue, customHandlers);
return [ return [
{ {
value: `${attrName}=${isString ? `"${valueStr}"` : `{${valueStr}}`}`, 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; const { props } = nodeItem;
let pieces: CodePiece[] = []; let pieces: CodePiece[] = [];
@ -56,12 +70,12 @@ export function generateAttrs(nodeItem: NodeSchema, handlers: CustomHandlerSet):
if (props) { if (props) {
if (!Array.isArray(props)) { if (!Array.isArray(props)) {
Object.keys(props).forEach((propName: string) => { Object.keys(props).forEach((propName: string) => {
pieces = pieces.concat(generateAttr(propName, props[propName], handlers)); pieces = pieces.concat(generateAttr(propName, props[propName], customHandlers));
}); });
} else { } else {
props.forEach((prop) => { props.forEach((prop) => {
if (prop.name && !prop.spread) { 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)}/>) // TODO: 处理 spread 场景(<Xxx {...(something)}/>)
@ -97,10 +111,11 @@ export function generateReactCtrlLine(nodeItem: NodeSchema, handlers: CustomHand
const loopItemName = nodeItem.loopArgs?.[0] || 'item'; const loopItemName = nodeItem.loopArgs?.[0] || 'item';
const loopIndexName = nodeItem.loopArgs?.[1] || 'index'; const loopIndexName = nodeItem.loopArgs?.[1] || 'index';
// TODO: 静态的值可以抽离出来? const rawLoopDataExpr = isJSExpression(nodeItem.loop)
const loopDataExpr = (handlers.loopDataExpr || _.identity)( ? `(${nodeItem.loop.value})`
isJSExpression(nodeItem.loop) ? `(${nodeItem.loop.value})` : `(${JSON.stringify(nodeItem.loop)})`, : `(${JSON.stringify(nodeItem.loop)})`;
);
const loopDataExpr = handlers.loopDataExpr ? handlers.loopDataExpr(rawLoopDataExpr) : rawLoopDataExpr;
pieces.unshift({ pieces.unshift({
value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => (`, value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => (`,
@ -115,7 +130,8 @@ export function generateReactCtrlLine(nodeItem: NodeSchema, handlers: CustomHand
if (nodeItem.condition) { if (nodeItem.condition) {
const [isString, value] = generateCompositeType(nodeItem.condition, handlers); 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({ pieces.unshift({
value: `(${conditionExpr}) && (`, value: `(${conditionExpr}) && (`,
@ -149,7 +165,8 @@ export function linkPieces(pieces: CodePiece[], handlers: CustomHandlerSet): str
throw new CodeGeneratorError('One node only need one tag define'); 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 const beforeParts = pieces
.filter((p) => p.type === PIECE_TYPE.BEFORE) .filter((p) => p.type === PIECE_TYPE.BEFORE)

View File

@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@ali/lowcode-datasource-engine": "^0.1.0", "@ali/lowcode-datasource-engine": "^0.1.0",
"universal-env": "^3.2.0",
"rax": "^1.1.0", "rax": "^1.1.0",
"rax-app": "^2.0.0", "rax-app": "^2.0.0",
"rax-document": "^0.1.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 { create as __$$createDataSourceEngine } from '@ali/lowcode-datasource-engine';
import { isMiniApp as __$$isMiniApp } from 'universal-env';
import __$$projectUtils from '../../utils'; import __$$projectUtils from '../../utils';
import './index.css'; import './index.css';
@ -50,7 +52,7 @@ class Home$$Page extends Component {
return self._dataSourceEngine.dataSourceMap || {}; return self._dataSourceEngine.dataSourceMap || {};
}, },
async reloadDataSource() { async reloadDataSource() {
self._dataSourceEngine.reloadDataSource(); await self._dataSourceEngine.reloadDataSource();
}, },
get utils() { get utils() {
return self._utils; return self._utils;

View File

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

View File

@ -36,6 +36,7 @@
componentName: 'Page', componentName: 'Page',
fileName: 'home', fileName: 'home',
state: { state: {
clickCount: 0,
user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' }, user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' },
orders: [ orders: [
{ {
@ -176,11 +177,12 @@
type: 'JSExpression', type: 'JSExpression',
value: 'this.state.orders', value: 'this.state.orders',
}, },
loopArgs: ['order', 'index'],
props: { props: {
style: { flexDirection: 'row' }, style: { flexDirection: 'row' },
onClick: { onClick: {
type: 'JSFunction', type: 'JSFunction',
value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.item.title) }', value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.order.title) }',
}, },
}, },
children: [ children: [
@ -193,7 +195,7 @@
source: { source: {
uri: { uri: {
type: 'JSExpression', type: 'JSExpression',
value: 'this.item.coverUrl', value: 'this.order.coverUrl',
}, },
}, },
style: { style: {
@ -211,20 +213,42 @@
componentName: 'Text', componentName: 'Text',
children: { children: {
type: 'JSExpression', type: 'JSExpression',
value: 'this.item.title', value: 'this.order.title',
}, },
}, },
{ {
componentName: 'Text', componentName: 'Text',
children: { children: {
type: 'JSExpression', 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', componentName: 'View',
children: [ children: [
@ -243,6 +267,11 @@
props: {}, props: {},
children: '2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示', 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: 补充更多类型的测试用例