mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-03-13 07:16:04 +00:00
328 lines
10 KiB
TypeScript
328 lines
10 KiB
TypeScript
import {
|
||
IPublicTypeNodeSchema,
|
||
IPublicTypeJSExpression,
|
||
IPublicTypeNpmInfo,
|
||
IPublicTypeCompositeValue,
|
||
isJSExpression,
|
||
} from '@alilc/lowcode-types';
|
||
|
||
import _ from 'lodash';
|
||
import changeCase from 'change-case';
|
||
import {
|
||
BuilderComponentPlugin,
|
||
BuilderComponentPluginFactory,
|
||
ChunkType,
|
||
CodePiece,
|
||
FileType,
|
||
ICodeChunk,
|
||
ICodeStruct,
|
||
IContainerInfo,
|
||
PIECE_TYPE,
|
||
HandlerSet,
|
||
IScope,
|
||
NodeGeneratorConfig,
|
||
NodePlugin,
|
||
AttrPlugin,
|
||
} from '../../../types';
|
||
|
||
import { RAX_CHUNK_NAME } from './const';
|
||
import { COMMON_CHUNK_NAME } from '../../../const/generator';
|
||
|
||
import { generateExpression } from '../../../utils/jsExpression';
|
||
import {
|
||
createNodeGenerator,
|
||
generateConditionReactCtrl,
|
||
generateReactExprInJS,
|
||
} from '../../../utils/nodeToJSX';
|
||
import { generateCompositeType } from '../../../utils/compositeType';
|
||
import { Scope } from '../../../utils/Scope';
|
||
import { parseExpressionGetGlobalVariables } from '../../../utils/expressionParser';
|
||
import { transformThis2Context } from '../../../core/jsx/handlers/transformThis2Context';
|
||
import { transformJsExpr } from '../../../core/jsx/handlers/transformJsExpression';
|
||
|
||
export interface PluginConfig {
|
||
fileType: string;
|
||
|
||
/** 是否要忽略小程序 */
|
||
ignoreMiniApp?: boolean;
|
||
}
|
||
|
||
// TODO: componentName 若并非大写字符打头,甚至并非是一个有效的 JS 标识符怎么办??
|
||
// FIXME: 我想了下,这块应该放到解析阶段就去做掉,对所有 componentName 做 identifier validate,然后对不合法的做统一替换。
|
||
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?) => {
|
||
const cfg: PluginConfig = {
|
||
fileType: FileType.JSX,
|
||
...config,
|
||
};
|
||
|
||
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
|
||
const next: ICodeStruct = {
|
||
...pre,
|
||
};
|
||
|
||
const ir = next.ir as IContainerInfo;
|
||
const rootScope = Scope.createRootScope();
|
||
const { tolerateEvalErrors = true, evalErrorsHandler = '' } = next.contextData;
|
||
|
||
// Rax 构建到小程序的时候,不能给组件起起别名,得直接引用,故这里将所有的别名替换掉
|
||
// 先收集下所有的 alias 的映射
|
||
const componentsNameAliasMap = new Map<string, string>();
|
||
next.chunks.forEach((chunk) => {
|
||
if (isImportAliasDefineChunk(chunk)) {
|
||
componentsNameAliasMap.set(chunk.ext.aliasName, chunk.ext.originalName);
|
||
}
|
||
});
|
||
|
||
// 注意:这里其实隐含了一个假设:schema 中的 componentName 应该是一个有效的 JS 标识符,而且是大写字母打头的
|
||
// FIXME: 为了快速修复临时加的逻辑,需要用 pre-process 的方式替代处理。
|
||
const mapComponentNameToAliasOrKeepIt = (componentName: string) =>
|
||
componentsNameAliasMap.get(componentName) || componentName;
|
||
|
||
// 然后过滤掉所有的别名 chunks
|
||
next.chunks = next.chunks.filter((chunk) => !isImportAliasDefineChunk(chunk));
|
||
|
||
// 如果直接按目前的 React 的方式之间出码 JSX 的话,会有 3 个问题:
|
||
// 1. 小程序出码的时候,循环变量没法拿到
|
||
// 2. 小程序出码的时候,很容易出现 Uncaught TypeError: Cannot read property 'avatar' of undefined 这样的异常(如下图的 50 行) -- 因为若直接出码,Rax 构建到小程序的时候会立即计算所有在视图中用到的变量
|
||
// 3. 通过 this.xxx 能拿到的东西太多了,而且自定义的 methods 可能会无意间破坏 Rax 框架或小程序框架在页面 this 上的东东
|
||
const customHandlers: HandlerSet<string> = {
|
||
expression(input: IPublicTypeJSExpression, scope: IScope) {
|
||
return transformJsExpr(generateExpression(input, scope), scope, {
|
||
dontWrapEval: !tolerateEvalErrors,
|
||
});
|
||
},
|
||
function(input, scope: IScope) {
|
||
return transformThis2Context(input.value || 'null', scope);
|
||
},
|
||
};
|
||
|
||
// 创建代码生成器
|
||
const commonNodeGenerator = createNodeGenerator({
|
||
handlers: customHandlers,
|
||
tagMapping: mapComponentNameToAliasOrKeepIt,
|
||
nodePlugins: [generateReactExprInJS, generateConditionReactCtrl, generateRaxLoopCtrl],
|
||
attrPlugins: [generateNodeAttrForRax.bind({ cfg })],
|
||
});
|
||
|
||
// 生成 JSX 代码
|
||
const jsxContent = commonNodeGenerator(ir, rootScope);
|
||
|
||
if (!cfg.ignoreMiniApp) {
|
||
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,
|
||
fileType: cfg.fileType,
|
||
name: RAX_CHUNK_NAME.ClassRenderPre,
|
||
// TODO: setState, dataSourceMap, reloadDataSource, utils, i18n, i18nFormat, getLocale, setLocale 这些在 Rax 的编译模式下不能在视图中直接访问,需要转化成 this.xxx
|
||
content: `
|
||
const __$$context = this._context;
|
||
const { state, setState, dataSourceMap, reloadDataSource, utils, constants, i18n, i18nFormat, getLocale, setLocale } = __$$context;
|
||
`,
|
||
linkAfter: [RAX_CHUNK_NAME.ClassRenderBegin],
|
||
});
|
||
|
||
next.chunks.push({
|
||
type: ChunkType.STRING,
|
||
fileType: cfg.fileType,
|
||
name: RAX_CHUNK_NAME.ClassRenderJSX,
|
||
content: `return ${jsxContent};`,
|
||
linkAfter: [RAX_CHUNK_NAME.ClassRenderBegin, RAX_CHUNK_NAME.ClassRenderPre],
|
||
});
|
||
|
||
next.chunks.push({
|
||
type: ChunkType.STRING,
|
||
fileType: cfg.fileType,
|
||
name: COMMON_CHUNK_NAME.CustomContent,
|
||
content: [
|
||
tolerateEvalErrors &&
|
||
`
|
||
function __$$eval(expr) {
|
||
try {
|
||
return expr();
|
||
} catch (error) {
|
||
${evalErrorsHandler}
|
||
}
|
||
}
|
||
|
||
function __$$evalArray(expr) {
|
||
const res = __$$eval(expr);
|
||
return Array.isArray(res) ? res : [];
|
||
}
|
||
`,
|
||
`
|
||
function __$$createChildContext(oldContext, ext) {
|
||
return Object.assign({}, oldContext, ext);
|
||
}
|
||
`,
|
||
]
|
||
.filter(Boolean)
|
||
.join('\n'),
|
||
linkAfter: [COMMON_CHUNK_NAME.FileExport],
|
||
});
|
||
|
||
return next;
|
||
|
||
function generateRaxLoopCtrl(
|
||
nodeItem: IPublicTypeNodeSchema,
|
||
scope: IScope,
|
||
config?: NodeGeneratorConfig,
|
||
next?: NodePlugin,
|
||
): CodePiece[] {
|
||
if (nodeItem.loop) {
|
||
const loopItemName = nodeItem.loopArgs?.[0] || 'item';
|
||
const loopIndexName = nodeItem.loopArgs?.[1] || 'index';
|
||
const subScope = scope.createSubScope([loopItemName, loopIndexName]);
|
||
const pieces: CodePiece[] = next ? next(nodeItem, subScope, config) : [];
|
||
|
||
const loopDataExpr = tolerateEvalErrors
|
||
? `__$$evalArray(() => (${transformThis2Context(
|
||
generateCompositeType(nodeItem.loop, scope, { handlers: config?.handlers }),
|
||
scope,
|
||
)}))`
|
||
: `(${transformThis2Context(
|
||
generateCompositeType(nodeItem.loop, scope, { handlers: config?.handlers }),
|
||
scope,
|
||
)})`;
|
||
|
||
pieces.unshift({
|
||
value: `${loopDataExpr}.map((${loopItemName}, ${loopIndexName}) => ((__$$context) => (`,
|
||
type: PIECE_TYPE.BEFORE,
|
||
});
|
||
|
||
pieces.push({
|
||
value: `))(__$$createChildContext(__$$context, { ${loopItemName}, ${loopIndexName} })))`,
|
||
type: PIECE_TYPE.AFTER,
|
||
});
|
||
|
||
return pieces;
|
||
}
|
||
|
||
return next ? next(nodeItem, scope, config) : [];
|
||
}
|
||
};
|
||
|
||
return plugin;
|
||
};
|
||
|
||
export default pluginFactory;
|
||
|
||
function isImportAliasDefineChunk(chunk: ICodeChunk): chunk is ICodeChunk & {
|
||
ext: {
|
||
aliasName: string;
|
||
originalName: string;
|
||
dependency: IPublicTypeNpmInfo;
|
||
};
|
||
} {
|
||
return (
|
||
chunk.name === COMMON_CHUNK_NAME.ImportAliasDefine &&
|
||
!!chunk.ext &&
|
||
typeof chunk.ext.aliasName === 'string' &&
|
||
typeof chunk.ext.originalName === 'string' &&
|
||
!!(chunk.ext.dependency as IPublicTypeNpmInfo | null)?.componentName
|
||
);
|
||
}
|
||
|
||
function generateNodeAttrForRax(
|
||
this: { cfg: PluginConfig },
|
||
attrData: { attrName: string; attrValue: IPublicTypeCompositeValue },
|
||
scope: IScope,
|
||
config?: NodeGeneratorConfig,
|
||
next?: AttrPlugin,
|
||
): CodePiece[] {
|
||
if (!this.cfg.ignoreMiniApp && /^on/.test(attrData.attrName)) {
|
||
// else: onXxx 的都是事件处理函数需要特殊处理下
|
||
return generateEventHandlerAttrForRax(attrData.attrName, attrData.attrValue, scope, config);
|
||
}
|
||
|
||
if (attrData.attrName === 'ref') {
|
||
return [
|
||
{
|
||
name: attrData.attrName,
|
||
value: `__$$context._refsManager.linkRef('${attrData.attrValue}')`,
|
||
type: PIECE_TYPE.ATTR,
|
||
},
|
||
];
|
||
}
|
||
|
||
return next ? next(attrData, scope, config) : [];
|
||
}
|
||
|
||
function generateEventHandlerAttrForRax(
|
||
attrName: string,
|
||
attrValue: IPublicTypeCompositeValue,
|
||
scope: IScope,
|
||
config?: NodeGeneratorConfig,
|
||
): CodePiece[] {
|
||
// -- 事件处理函数中 JSExpression 转成 JSFunction 来处理,避免当 JSExpression 处理的时候多包一层 eval 而导致 Rax 转码成小程序的时候出问题
|
||
const valueExpr = generateCompositeType(
|
||
isJSExpression(attrValue) ? { type: 'JSFunction', value: attrValue.value } : attrValue,
|
||
scope,
|
||
{
|
||
handlers: config?.handlers,
|
||
},
|
||
);
|
||
|
||
// 查询当前作用域下的变量
|
||
const currentScopeVariables = scope.bindings?.getAllBindings() || [];
|
||
if (currentScopeVariables.length <= 0) {
|
||
return [
|
||
{
|
||
type: PIECE_TYPE.ATTR,
|
||
name: attrName,
|
||
value: valueExpr,
|
||
},
|
||
];
|
||
}
|
||
|
||
// 提取出所有的未定义的全局变量
|
||
const undeclaredVariablesInValueExpr = parseExpressionGetGlobalVariables(valueExpr);
|
||
const referencedLocalVariables = _.intersection(
|
||
undeclaredVariablesInValueExpr,
|
||
currentScopeVariables,
|
||
);
|
||
if (referencedLocalVariables.length <= 0) {
|
||
return [
|
||
{
|
||
type: PIECE_TYPE.ATTR,
|
||
name: attrName,
|
||
value: 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,
|
||
name: `data-${changeCase.snake(localVar)}`,
|
||
value: localVar,
|
||
})),
|
||
{
|
||
type: PIECE_TYPE.ATTR,
|
||
name: attrName,
|
||
value: wrappedAttrValueExpr,
|
||
},
|
||
];
|
||
}
|