Initial commit: 上传整个code目录

This commit is contained in:
董海洋
2026-05-23 14:05:22 +08:00
commit 34914088c6
4608 changed files with 573731 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
# lowcode-create 内存问题修复记录(2026-04
## 背景
反馈:`@lingshu/lowcode-create` 在长会话中存在"内存泄漏"现象。系统排查后判定**真泄漏 1 处 + 高频 GC 抖动 2 处**,本次三处都已修。`apps/lcdp` 通过 catalog 版本 `@lingshu/lowcode-create@1.0.8` 从内部 nexus 装包,三处修复的源码与本地 `dist/` 已同步落地,**正式生效需要 bump 版本并发包**。
---
## 修复清单
### #1 codeProtocol 不销毁导致的 Sandbox watch 泄漏(真泄漏)
**文件**`packages/lowcode-create/src/composables/FeFormCreate.tsx`
**症状**
`useLowCodeFormCreate` 在内部构造 `SandBox`(含 `effectScope`),通过 `codeProtocol.destroy()` 释放。但销毁责任在 caller,实际 4 个调用点中只有 `PreviewForm.vue` 显式 destroy,其余 3 个(`DesignerForm.vue``DesignerFormRenderMobile.vue``PreviewFormRenderMobile.vue``apps/lcdp/.../vant/index.vue`)都不销毁。每次 host 卸载,sandbox 内通过 `onFormDataChange`/`onFieldChange` 注册的 watch 闭包(持有旧 formApi、formState)都会留存;切页 / 弹窗反复打开时叠加。
**改动**
`codeProtocol` 创建后,自动挂到 caller 的 effect scope
```ts
import { getCurrentScope, onScopeDispose, unref } from "vue";
// ...创建 codeProtocol 之后
if (getCurrentScope()) {
onScopeDispose(() => codeProtocol.destroy());
}
```
**为什么安全**
- `effectScope.stop()` 是幂等的,`PreviewForm.vue` 现有的显式 destroy 不会出错。
- `getCurrentScope()` 只有在 Vue setup 上下文里才返回 scope,非 Vue 调用方不受影响。
- `useLowCodeFormCreate` 几乎都是在 `<script setup>` 中调用,自动挂上就够了。
**影响面**:所有调用 `useLowCodeFormCreate` 的入口现在都自动回收 sandbox watch,无需 host 自觉。
---
### #2 SandBox.onFormDataChange 在 watch source 里 cloneDeepGC 抖动)
**文件**`packages/lowcode-create/src/logics/SandBox.ts`
**症状**
原 source getter 是 `() => this.formApi.getFormValues()`,而 `getFormValues = () => cloneDeep(formState.value)`。每次表单字段变更,watcher 重跑 source → 整张表 cloneDeep。批量写入(如 `setFormValuesForce` 一次写 N 个字段)会 N 次连续 clone。
**改动**
直接以 Ref 为 source,让 Vue 自己做深度追踪:
```ts
public onFormDataChange(fn: UserChangeFnArgs, options: WatchOptions = { deep: true }) {
this.effectScope.run(() => {
watch(this.formApi.formState, (newVal, oldVal) => {
fn(newVal, oldVal);
}, options);
});
}
```
**行为变更**(已写在源码注释里):
- `newVal === oldVal`,都是同一个 reactive proxy 引用(Vue 在 reactive 源上的标准行为)。
- 用户脚本若需要"对比新旧",需自行 `cloneDeep(newVal)` 缓存。
- 验证过 `form-designer/src/components/user-script/Constants.tsx:58` 中提供的 demo 模板:`onFormDataChange(() => { console.log(...) })` 不消费 newVal/oldVal,公开契约未破。
**影响面**:注册了 `onFormDataChange` 的页面,每次字段变更省一次全表 cloneDeep;批量写入是 N 倍降本。
---
### #3 LowCodeFormItem.vue schemaRaw 无意义 cloneDeepGC 抖动,最大头)
**文件**`packages/lowcode-create/src/components/form/LowCodeFormItem.vue`
**症状**
原代码每次 `schemaRaw` computed 重算都先 `cloneDeep(schema)`,再叠加 `transformSchemaProperties` 返回的覆盖项:
```ts
const schemaRaw = computed(() => {
const transformSchemaProperties = apis?.runtimeApi?.transformSchemaProperties(
configs,
schema,
apis,
);
const raw = cloneDeep(schema);
if (transformSchemaProperties) {
Object.entries(transformSchemaProperties).forEach(([key, value]) =>
set(raw, key, value),
);
}
return raw;
});
```
`transformSchemaProperties``packages/form-designer/src/config/WidgetHelper.ts:322` + `widget-helper/utils/CommonUtils.ts:325`)只在 widget helper 注册了对应实现时才返回非空对象,**多数 widget 直接返回 undefined**。也就是说,绝大部分表单项每次 reactive 触发都跑了一次"clone 完啥也不改再返回"的废操作。100 字段表单 × 任一 reactive 变更 = 100 次大 cloneDeep。
**改动**:早出,无 overrides 时直接返回原 schema。
```ts
const schemaRaw = computed(() => {
const overrides = apis?.runtimeApi?.transformSchemaProperties(
configs,
schema,
apis,
);
if (!overrides || Object.keys(overrides).length === 0) {
return schema;
}
const raw = cloneDeep(schema);
Object.entries(overrides).forEach(([key, value]) => {
set(raw, key, value);
});
return raw;
});
```
**为什么安全**
- `set(raw, key, value)` 在 overrides 为空时本来就是空操作,原代码逻辑等价于"克隆完原样返回"。
- computed 自带缓存,schema 引用稳定时 schemaRaw 引用也稳定,下游模板 `:schema="schemaRaw"` 是只读消费,无 mutation 风险。
**影响面**:本次 3 处修复中收益最大。100 字段表单中假如只有 5 个字段需要 transform,从 100 次 cloneDeep 降到 5 次。
---
## 部署路径
`apps/lcdp` 走的是从 nexus 装包,本地 `dist/` 重 build 后**不会被自动消费**。两条路:
**临时验证**:把 `packages/lowcode-create/dist/` 覆盖到 `node_modules/@lingshu/lowcode-create/dist/`,重启 dev server。
**正式提测**
1.`packages/lowcode-create/package.json` bump 版本(当前 `1.0.8`)。
2. `pnpm --filter lowcode-create run release` 或既有发包流程发到内部 nexus。
3. 更新 `pnpm-workspace.yaml` catalog 中 `@lingshu/lowcode-create` 版本号。
4. `pnpm install`
---
## 已发现但未修的问题(备查)
排查过程中识别出的另外几处问题,本次未动,后续按优先级处理:
| 位置 | 问题 | 性质 |
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| `packages/lowcode-create/src/utils/CreateFormState.ts:78-91` | `getBindValue` 在 computed getter 里 `setFieldValue``const v = stateValue ?? stateValue` 是 typo(应为 `?? schemaValue`)。已两次提交修过响应式循环(commit `49558e2b9``3d16e41a8`),根因仍在。 | 反应式正确性 |
| `packages/lowcode-create/src/apis/FormApi.ts:120` | `setFieldValue` 触发 `FORM_FIELD_DATA_CHANGE_EVENT``oldValue: get(this.formState.value, field)` 在 set 之后读取,得到的是新值。 | 事件 payload 错误 |
| `packages/lowcode-create/src/logics/SandBox.ts:140-153` | `setFieldStyle` 的 3000ms `setTimeout` 没在 `stopEffect` 时 clear,组件卸载后仍会跑 `document.querySelectorAll``ElMessage.error`。 | 杂项 |
| `packages/form-designer/src/widget-mobile/config/widget/SerialNumber.vue:124` | `eventBus.on(FORM_FIELD_DATA_CHANGE_EVENT, ...)` 没有 `off`——每次挂载新增一个不释放的订阅者(PC 同名文件已注释掉)。 | 真泄漏(不在 lowcode-create,但用同一 event bus |
---
## 修复后建议的验证方法
1. Chrome DevTools → Performance → 录制一次"打开页面 → 编辑若干字段 → 关闭页面"循环 10 次。
2. Memory → Allocation timeline,对比修复前后:
- cloneDeep 调用堆栈应明显减少(#2#3 的效果)
- 多次循环后 detached DOM / 闭包数应稳定不增长(#1 的效果)
3. Heap snapshot 多次循环间对比,搜索 `SandBox``FormApi` 实例数应等于活跃页面数,不应累积。