@epxing/cli 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -2
- package/dist/THIRD_PARTY_NOTICES.txt +21 -0
- package/dist/cli.mjs +230 -0
- package/dist/project.mjs +178 -0
- package/dist/prompts.mjs +86 -0
- package/dist/skills/epxing-frontend-vue3-migrate/SKILL.md +129 -0
- package/dist/skills/epxing-frontend-vue3-migrate/pitfalls.md +381 -0
- package/dist/tools/epx-create/scaffold.mjs +248 -0
- package/dist/tools/epx-migrate/detect-vue3-risks.mjs +3 -0
- package/dist/tools/epx-migrate/migrate.mjs +80 -0
- package/dist/tools/epx-migrate/verify-components.mjs +10 -0
- package/package.json +27 -4
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: epxing-frontend-vue3-migrate
|
|
3
|
+
description: epx 产品/项目远程库 Vue2/webpack → Vue3/Vite 升级 + 按需打包(lazy/chunks 形态)的完整流程、判据与踩坑清单;也用于排查远程库「构建绿、控制台干净、组件却静默失效」——不渲染 / 注册名错 / 有组件没样式 / 登录拉整包 / 弹窗标题空 / 图表画不出 / 卡片 scoped 样式丢。含 epx-migrate CLI(migrate 迁移、detect-vue3-risks 发布前静态扫风险、verify-components 组件清单对齐)与三方对比环境搭法(本地 dist + 工装 + Web Worker 定时器垫片 + 运行时实证)。触发:产品升级 / Vue3 迁移 / 迁移包对比测试 / 升级后静默失效排查。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 升级产品远程库(Vue3 + Vite + 按需打包)
|
|
7
|
+
|
|
8
|
+
## 这是什么
|
|
9
|
+
|
|
10
|
+
epx 的产品/项目远程库(`window.library.<pkg>`,由 `FM_SYSTEM_WEBPACK` 配 url、运行时加载)
|
|
11
|
+
从 Vue2/webpack 迁到 Vue3/Vite,并可选切到「清单 + 组件按需加载」形态的完整流程与踩坑清单。
|
|
12
|
+
|
|
13
|
+
**核心判断:这条链上的失败几乎全是静默的。** 构建绿、控制台干净、组件就是不出来。
|
|
14
|
+
所以每一步都要有**判据**,别靠"看着没报错"。
|
|
15
|
+
|
|
16
|
+
## 什么时候用
|
|
17
|
+
|
|
18
|
+
- 要把某个产品库迁到 Vue3 / 从 webpack 切到 Vite
|
|
19
|
+
- 要给某个产品库开按需打包(库超过 ~1MB 时才值得,见「按需值不值」)
|
|
20
|
+
- 排查远程库的诡异现象:组件不渲染 / 注册名不对 / 有组件没样式 /
|
|
21
|
+
登录首屏拉了整个库 / `[loadComponent] 找不到 epx-xxx` / `[vuex] duplicate getter key`
|
|
22
|
+
|
|
23
|
+
**不适用**:平台自身(epx-frame)的构建问题;页面配置层面的组件用法(那看 `epx-frame-web`)。
|
|
24
|
+
|
|
25
|
+
## 顺序(别调换)
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
① 依赖 + codemod pnpm epx migrate --apply → 出 MIGRATION-REPORT.md
|
|
29
|
+
② .vue 转换 (同上,gogocode) → 逐个修构建报错
|
|
30
|
+
③ 切 Vite 换 vite.config.mjs → require.context → import.meta.glob
|
|
31
|
+
④ 跑起来 dev 壳验证 → 组件真渲染出来才算过
|
|
32
|
+
⑤ 开按需(可选) npm run build:lib:lazy → 验清单 + 真机验渲染
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**②→③ 之间必须先让 `build:lib` 绿**。没绿就切按需,等于同时改两个变量,出问题分不清是谁的。
|
|
36
|
+
|
|
37
|
+
## 每步的判据
|
|
38
|
+
|
|
39
|
+
| 步骤 | 判据(不是"没报错") |
|
|
40
|
+
|---|---|
|
|
41
|
+
| ① 依赖迁移 | `MIGRATION-REPORT.md` 的人工清单**逐条看过**——里面写着的坑会真的咬人 |
|
|
42
|
+
| ② .vue 转换 | `npx eslint src --ext .js,.jsx,.vue` 能跑(迁移会删掉 eslint 生态,先修工具链) |
|
|
43
|
+
| ③ 切 Vite | `npm run build:lib` exit 0 **且** `lib/` 里有 `index.js`/`index.css`/`index.deps.json` |
|
|
44
|
+
| ④ dev 壳 | 浏览器里 `#app.__vue_app__._context.components` 能数到你的组件个数;对自定义页、继承组件和多分支 render,核对实际根节点的样式命名空间及关键元素的 `getComputedStyle` |
|
|
45
|
+
| ⑤ 按需 | `index.manifest.json` 组件数 == `.vue` 文件数,**且路径兜底名 0 个** |
|
|
46
|
+
|
|
47
|
+
第⑤条那个"路径兜底名"是最容易漏的:注册名抽不到就退成 `hrcomponentsSearch` 这种,
|
|
48
|
+
**dev 壳看不出来**(dev 读运行时 `comp.name`),只在按需形态下炸。
|
|
49
|
+
|
|
50
|
+
## 快速排错
|
|
51
|
+
|
|
52
|
+
| 现象 | 多半是 |
|
|
53
|
+
|---|---|
|
|
54
|
+
| 组件不渲染,`[loadComponent] 找不到 epx-xxx` | 平台占位抢注 / 产品 install 没跑 |
|
|
55
|
+
| `__epxInstalledLibs__` 有记录但 app 里 0 个组件 | 记账写在 install 之前,产品幂等守卫空转 |
|
|
56
|
+
| 注册名变成 `xxxcomponentsYyy` | `extractName` 没抽到 `name`(按需形态特有) |
|
|
57
|
+
| 组件出来了但完全没样式 | 先查样式表是否加载、实际 DOM 能否匹配选择器;常见原因是 CSS 与 chunk 不同批(`data-v` hash 对不上),或根节点漏了 `.epx-xxx` 样式命名空间(F19) |
|
|
58
|
+
| 图片 404,路径是 `/assets/xxx.png` | 嵌套构建没继承 `assetsInlineLimit` |
|
|
59
|
+
| `Cannot read properties of undefined (reading 'i18n')` | recorder-core 执行序(模块图里没有那条边) |
|
|
60
|
+
| `[vuex] duplicate getter key` | 库被装了两次(本地 + 远程) |
|
|
61
|
+
| `[vite:vue] reading 'type'`(无行号,某 .vue) | gogocode 漏转元素上的 `slot="x"`(F1) |
|
|
62
|
+
| `组件注册名冲突:… copy.vue` | 垃圾副本文件撞名(F8,`find src -iname "*copy*"`) |
|
|
63
|
+
| `npm i` 显示成功但 vite/依赖没装上 | `node-sass`/`fibers` 原生编译挂、exit code 被管道吞(F7) |
|
|
64
|
+
| `[loadComponent] 找不到 epx-xxx`(本地新迁的库) | 源码版本落后线上部署、缺组件(`verify-components.mjs --online` 对齐) |
|
|
65
|
+
|
|
66
|
+
**详细根因、判据与修法见 [pitfalls.md](pitfalls.md)**:A 迁移工具 / B Vite 化 / C 按需 / D 联调 / E 主题 / **F gogocode 残留 & 换库(recruit 实证)**。
|
|
67
|
+
|
|
68
|
+
> **CLI 现已自动处理的机械项**(不用再手管,见 `tools/epx-migrate`):依赖表迁移、入口 `require.context`→`import.meta.glob`、
|
|
69
|
+
> 内联事件多语句补分号、`:prop.sync`→`v-model`、删 `node-sass`/`fibers`、`--to-vite` 生成 vite.config(含 resolve.extensions)、
|
|
70
|
+
> 第三方 Vue2-only 库 ⑥ 告警、`verify-components` 与线上组件清单对齐。**留给人工的就是 F 节那几类**:元素级 slot/slot-scope 结构转换、换库、编译残留定位。
|
|
71
|
+
|
|
72
|
+
> **本轮新增(postAppoint 实证)**:
|
|
73
|
+
> - `fix-jsx-vue2` 除自动改 4 类 JSX 属性(`domProps*`/`nativeOn*`/`scopedSlots=`),现在还**报告** `<script>` 区的 `slot="xxx"` 具名插槽(第 5 类,需人工改 `v-slots`——因要重组 JSX 结构故不自动改;甄别见 F14);
|
|
74
|
+
> - 新增 `detect-vue3-risks.mjs`(`node tools/epx-migrate/detect-vue3-risks.mjs <项目>`):发布前静态扫 scoped 整块失效等风险(R1 = extends+scoped+无 template+裸类选择器;已改 `:deep()` 的不误报;退出码 2 可 CI 拦),见 F15。
|
|
75
|
+
|
|
76
|
+
## 迁移完怎么验:迁移包 vs 线上 三方对比(postAppoint / recruit / hr 实证)
|
|
77
|
+
|
|
78
|
+
构建绿 ≠ 与线上等价。把「本地新迁的包」摆到线上同一平台、同一后端、同账号,逐页对比,是唯一可靠验收。
|
|
79
|
+
|
|
80
|
+
**三组环境**:① 线上(原样)② 本地平台 dist + 线上旧包(只换平台,作基线)③ 本地平台 dist + 本地新迁包(实验组)。①↔③ 看迁移包对不对,②↔③ 把「平台的锅 / 包的锅」分开。
|
|
81
|
+
|
|
82
|
+
**工装(只读、不改项目任何文件)**:
|
|
83
|
+
- 起个小服务(如 `:4183`)把 `/file-server/npm/test/web_<产品>/*` 路由到本地 `<产品>/lib/*`、其余 `/file-server/*` 302 回源线上(图片等照常,避免「反代没转发」式误报);
|
|
84
|
+
- 平台 `dist/assets/js/config.js` 末尾追加地址改写钩子,把 `web_<产品>` 的 script/link/fetch 指到该服务——平台 `util.js` 对含 `http` 的 id **原样返回**,所以改 `MINIO_IP` 无效,**必须在 script/link/fetch 层改写**;
|
|
85
|
+
- ⚠️ 线上同一产品可能有**两个包路径**(如 `web_post` 在用、`web_postAppoint` 废弃,二者组件同名):先 `performance` 看平台实际加载哪个,工装两个前缀都拦;
|
|
86
|
+
- ⚠️ 每次 `build:pre` 重建 dist 会**冲掉**钩子——重装并 `location.reload()`(同文档 hash 跳转不重载),再用 `performance` 确认 `web_<产品>` 的 chunk 确实来自本地服务,才开始采数据。
|
|
87
|
+
|
|
88
|
+
**后台标签页定时器冻结(采数据必踩,否则满屏假象)**:Chrome 后台/被遮挡标签页里页面自身的 `setTimeout`/`setInterval`/`requestAnimationFrame` 会被密集节流甚至冻结(几十秒零触发),表现为「列表空 / echarts 不画 / 数据延迟出现」——极易误判成缺陷。页内自动化脚本 sleep 一律走 **Web Worker 定时器**(`new Worker(URL.createObjectURL(new Blob(['self.onmessage=e=>setTimeout(()=>self.postMessage(1),e.data)'])))` + 页面侧回调);应用自己的定时器同样被冻,采数据前把该 tab 切前台、或注入 setTimeout/rAF 垫片,空页先复核再定论。截图 / resize 都**不解冻**。
|
|
89
|
+
|
|
90
|
+
**运行时实证判据(别靠肉眼 / 文本 / 静态 grep)**:
|
|
91
|
+
- 组件在不在:`#app.__vue_app__._context.components` 数量 / 组件实例的 `$slots` 键;
|
|
92
|
+
- 样式生不生效:`getComputedStyle(el)` 比对(容器高度是否塌陷 0、色值 / 尺寸是否对齐线上);
|
|
93
|
+
- 差异定性:`el-table` 固定列会复制一份 DOM(Vue2 Element UI 行数翻倍、Vue3 Element Plus 不翻)——比对「共 N 条 / 首行文本」而非 DOM 元素数,否则把「翻倍」误当差异;
|
|
94
|
+
- **静态 grep 到 Vue2 写法 ≠ 运行时一定失效**:失效与否取决于消费方 / 是否被平台全局样式兜住 / 组件运行时兼容(如 JSX `slot=` 在 el-popover reference 失效、在 el-dropdown 正常),必须运行时实证再判改不改。
|
|
95
|
+
|
|
96
|
+
## 按需值不值(实测,别想当然)
|
|
97
|
+
|
|
98
|
+
同一平台上只换 hr 的打包形态,Fast-3G 限速、每组 3 次取中位数:
|
|
99
|
+
|
|
100
|
+
| 形态 | 内容可见 | 关键路径 |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| 整体单体 | **11.1 s** | 1467 K / 49 请求 |
|
|
103
|
+
| 按需(含预取) | **13.5 s** | 1523 K / **107 请求** |
|
|
104
|
+
| 按需(关预取) | **11.8 s** | 1177 K / 49 请求 |
|
|
105
|
+
|
|
106
|
+
**按需没提速,甚至略慢**——省下的 290 KB 被多出来的往返吃掉了。
|
|
107
|
+
真正的大头是平台重建(旧平台 20–59 s → 新平台 11 s)。
|
|
108
|
+
|
|
109
|
+
所以按需的价值是**防止劣化**(库继续长大时首屏不跟着涨),不是当下提速。
|
|
110
|
+
库还不到 1MB 就别开。
|
|
111
|
+
|
|
112
|
+
## 部署前必查
|
|
113
|
+
|
|
114
|
+
1. **平台支不支持按需 ABI**:抓线上平台 bundle `grep registerLazyLibrary`。
|
|
115
|
+
没有就只能走降级②(壳自建 wrapper),没有集中预取、`deps.json` 也没人读——
|
|
116
|
+
**平台不先升级,产品这边开按需是白开**。
|
|
117
|
+
2. **按需形态必须传整个 `lib/` 目录**,不是 `index.js` + `index.css` 两个文件。
|
|
118
|
+
`FM_SYSTEM_WEBPACK` 里仍只配那两条 url,其余按相对路径取。
|
|
119
|
+
3. **`chunks/` 的 `import()` 需要文件服务给 CORS 头**,跨域没配会整库降级。
|
|
120
|
+
4. `index.deps.json` **必须与 index.js 同目录同名一并上传**,漏传不报错但精确 ensure 会静默退化。
|
|
121
|
+
|
|
122
|
+
## 常见错误
|
|
123
|
+
|
|
124
|
+
- **只看 dev 壳就以为好了** —— dev 走 `import.meta.glob` + 运行时 `comp.name`,
|
|
125
|
+
按需形态那几个 bug 一个都不走。要验按需必须用构建产物。
|
|
126
|
+
- **两个构建并发写同一个 `lib/`** —— 实测会丢文件。`build:component` 第 1 趟
|
|
127
|
+
`emptyOutDir:true` 会清空整个目录,跑一半断了就只剩残缺产物。
|
|
128
|
+
- **拿"构建 exit 0"当验证** —— 这条链上大部分故障构建期完全看不出来。
|
|
129
|
+
- **把窗口/阈值调大当修复** —— 比如 `extractName` 的字符窗口,调大只是挪门槛。
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# 踩坑清单(每条都在 product/hr 上实证过)
|
|
2
|
+
|
|
3
|
+
按"什么时候咬人"分组。每条给**判据**(怎么确认是它)和**修法**。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## A. 迁移工具阶段(`epx migrate`)
|
|
8
|
+
|
|
9
|
+
### A1. gogocode 会改坏二进制文件
|
|
10
|
+
|
|
11
|
+
**现象**:字体/图片体积莫名变化,构建仍然绿。实测 `FontAwesome.otf` 134808→258854 B、
|
|
12
|
+
`fontawesome-webfont.eot` 165742→212524 B。
|
|
13
|
+
|
|
14
|
+
**根因**:整目录 `cpSync` 把 gogocode 处理过的文件原样拷回,二进制被按文本处理过。
|
|
15
|
+
|
|
16
|
+
**判据**:`git status` 看 `src/assets/` 有没有莫名其妙的改动;`git diff --stat` 看体积。
|
|
17
|
+
|
|
18
|
+
**修法**:拷回只走扩展名白名单(`.vue/.js/.jsx/.ts/.tsx`)。已修在
|
|
19
|
+
`tools/epx-migrate/copy-back-sources.mjs`。踩到就 `git checkout -- src/assets/`。
|
|
20
|
+
|
|
21
|
+
### A2. `install(Vue, options)` 的正则漏匹配
|
|
22
|
+
|
|
23
|
+
**现象**:运行时 install 报 TypeError,构建期完全正常。
|
|
24
|
+
|
|
25
|
+
**根因**:老正则只认 `install = function (Vue)`,产品写成 `(Vue, options)` 就不匹配 →
|
|
26
|
+
gogocode 按它自己的约定把函数体改成 `window.$vueApp.*`,而**平台从不设置这个全局**。
|
|
27
|
+
|
|
28
|
+
**判据**:`grep -n 'window.\$vueApp' src/` 有命中就是它。
|
|
29
|
+
|
|
30
|
+
**修法**:`tools/epx-migrate/codemod-lib-entry.mjs` 覆盖 fn/method 两种形态与额外形参,
|
|
31
|
+
且必须排在 gogocode(stage ③)**之前**。
|
|
32
|
+
|
|
33
|
+
### A3. 具名导入收敛丢掉正文还在用的名字
|
|
34
|
+
|
|
35
|
+
**现象**:运行时 `ReferenceError: store is not defined`,webpack 构建是绿的。
|
|
36
|
+
|
|
37
|
+
**修法**:import 收敛必须放在**所有正文改写之后**,且保留仍被引用的名字。
|
|
38
|
+
|
|
39
|
+
### A4. eslint 全家被删,工具链先坏
|
|
40
|
+
|
|
41
|
+
**现象**:`npx eslint --fix <file>` 报 `couldn't find the config "@vue/standard"`。
|
|
42
|
+
|
|
43
|
+
**根因**:依赖迁移删 vue-cli 生态时连带删了它。
|
|
44
|
+
|
|
45
|
+
**修法**(eslint 8 的正确组合):`@vue/eslint-config-standard@8` +
|
|
46
|
+
`eslint-config-standard@17`(16 要 eslint@7)+ `@babel/eslint-parser`;
|
|
47
|
+
`.eslintrc.js` 切 `plugin:vue/vue3-essential`、`parser: '@babel/eslint-parser'`。
|
|
48
|
+
注意 standard 17 起 `eslint-plugin-node` 改名 `eslint-plugin-n`。
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## B. Vite 化阶段
|
|
53
|
+
|
|
54
|
+
### B1. `recorder-core` 引擎与核心之间**没有模块图边**
|
|
55
|
+
|
|
56
|
+
**现象**:dev 壳白屏,`TypeError: Cannot read properties of undefined (reading 'i18n')`
|
|
57
|
+
指向 `.vite/deps/chunk-XXXX.js`。
|
|
58
|
+
|
|
59
|
+
**根因**:引擎(`src/engine/wav.js`)对核心是**零 import**,执行时从 `window.Recorder` 取,
|
|
60
|
+
第一行就是 `var rec = win.Recorder, ni = rec.i18n`。模块图里没有 wav→core 这条边,
|
|
61
|
+
**没有任何打包器有义务先执行核心**。源码里两条相邻 import 只是"声明序",
|
|
62
|
+
esbuild 依赖预打包一拆 chunk 就没了。
|
|
63
|
+
|
|
64
|
+
**注意**:炸的是 **epx-frame 自己**(`lib/epx-frame.esm.mjs` 第 1、2 行),产品侧收口模块管不着。
|
|
65
|
+
|
|
66
|
+
**修法**:补边 shim + **dev 专用** alias(正则 `$` 锚定,shim 内部引带 `.js` 后缀的说明符
|
|
67
|
+
避免 alias 回自己)。
|
|
68
|
+
|
|
69
|
+
**别用 `optimizeDeps.exclude`**:recorder-core 只有 CJS 入口,排除预打包就没有 interop,
|
|
70
|
+
换来 `SyntaxError: ... does not provide an export named 'default'`。
|
|
71
|
+
|
|
72
|
+
### B2. `import * as ns` 挡住 shim 生成
|
|
73
|
+
|
|
74
|
+
**现象**:按需构建报死,说某个 external 无法生成 shim。
|
|
75
|
+
|
|
76
|
+
**根因**:shim 无法静态枚举命名空间导入会访问哪些名字。
|
|
77
|
+
|
|
78
|
+
**修法**:改具名导入。这是**构建期报死**的设计(不静默),改掉即可。
|
|
79
|
+
|
|
80
|
+
### B3. 未映射的 external 全局名
|
|
81
|
+
|
|
82
|
+
**现象**:构建期报 `external 'xxx' 没有对应的运行时全局映射`。
|
|
83
|
+
|
|
84
|
+
**修法**:产品自己知道全局名的(如 `recorder-core` → `Recorder`),
|
|
85
|
+
在 `vite.config.mjs` 的 `output.globals` 里配;平台提供的去 `build/lazy-lib/gen-shims.mjs` 补。
|
|
86
|
+
|
|
87
|
+
### B4. `url(~@)` 在 Vite 下静默 404
|
|
88
|
+
|
|
89
|
+
webpack 遗留写法,Vite 不认 `~`。改成 `@/` 或相对路径。
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## C. 按需打包特有(**dev 壳一个都验不出来**)
|
|
94
|
+
|
|
95
|
+
### C1. `extractName` 抽不到 `name` → 注册名退成路径兜底
|
|
96
|
+
|
|
97
|
+
**现象**:页面按 `epx-hr-search` 找组件找不到,控制台一声不吭。
|
|
98
|
+
|
|
99
|
+
**根因**(已修):老实现用 `export default` 之后 **400 字符的窗口**找 `name`。
|
|
100
|
+
`Search.vue` 里 `export default {` 在 264 行、`name` 在 303 行,隔了 39 行远超窗口 →
|
|
101
|
+
退到路径兜底名 `hrcomponentsSearch`。而 dev 壳读**运行时** `comp.name`,两边不一致。
|
|
102
|
+
|
|
103
|
+
**判据**:`index.manifest.json` 里有不含 `-` 的键,就是路径兜底名。
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
node -e "const m=require('./lib/index.manifest.json');console.log(Object.keys(m.components).filter(k=>!k.includes('-')))"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**修法**:babelParse 真解析、只认顶层属性。**别把窗口调大**——`name` 写在 methods
|
|
110
|
+
后面又会漏。
|
|
111
|
+
|
|
112
|
+
### C2. 平台占位抢注
|
|
113
|
+
|
|
114
|
+
**现象**:`[loadComponent] system.component 里没有 epx-xxx`。
|
|
115
|
+
|
|
116
|
+
**根因**:平台 `loadComponent` 与产品 `install` 都是「没人占我才占」
|
|
117
|
+
(都是 `if (!app.component(name))`),谁先跑谁赢。页面配置引用组件时
|
|
118
|
+
`Page.vue` 先按名字调平台工厂抢注了占位,产品 install 随后跳过。
|
|
119
|
+
而占位的解析链里**只有平台组件 + dashboard + designer,没有产品库**。
|
|
120
|
+
|
|
121
|
+
**判据**:控制台查槽位里是谁——产品的懒加载 wrapper 有 `.name`,平台占位没有。
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
const c = document.querySelector('#app').__vue_app__._context.components['epx-hr-search']
|
|
125
|
+
c?.name ? 'product wrapper' : 'platform stub'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**修法**(已修):解析链在平台组件之后、可选能力组之前查 `window.componentLibrary`。
|
|
129
|
+
排在可选能力组之前很关键,否则一个产品组件会白拉 dashboard + 体积很大的 designer。
|
|
130
|
+
|
|
131
|
+
### C3. 记账写在 install 之前 → 产品幂等守卫空转
|
|
132
|
+
|
|
133
|
+
**现象**:`window.__epxInstalledLibs__` 里有 `hr@3.0.1`,但 app 里 **0 个**产品组件。完全静默。
|
|
134
|
+
|
|
135
|
+
**根因**:平台 `util.loadJs` 先记账再调 install,而产品 install 开头是
|
|
136
|
+
`if (marks[sig]) return`(PROJECT-GUIDE 约定的幂等写法)——**一进门就返回**。
|
|
137
|
+
两侧去重分两次提交时各自都对,撞一起才炸。
|
|
138
|
+
|
|
139
|
+
**判据**:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
JSON.stringify({
|
|
143
|
+
libs: Object.keys(window.__epxInstalledLibs__ || {}),
|
|
144
|
+
count: Object.keys(document.querySelector('#app').__vue_app__._context.components)
|
|
145
|
+
.filter(n => /^epx-/.test(n)).length
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
表里有记录、count 却是 0,就是它。
|
|
149
|
+
|
|
150
|
+
**修法**(已修):记账挪到 `useVuePlugin` **之后**。
|
|
151
|
+
|
|
152
|
+
### C4. 嵌套构建没继承 `assetsInlineLimit`
|
|
153
|
+
|
|
154
|
+
**现象**:图片变成 `assets/*.png` 文件,产物里引用为**绝对路径** `/assets/xxx.png`。
|
|
155
|
+
远程库从文件服务子路径加载,这个 URL 解析到平台页面的 origin 根目录 → **运行时 404,构建期无声**。
|
|
156
|
+
|
|
157
|
+
**判据**:整体单体形态**不产生** `assets/` 目录(全内联),按需形态却有 → 就是没继承。
|
|
158
|
+
|
|
159
|
+
**修法**(已修):`inheritedBuildOptions()` 把 `assetsInlineLimit`/`assetsDir`/`target`/
|
|
160
|
+
`cssTarget`/`sourcemap`/`minify` 一并转发。`target` 同样要紧:壳由外层构建、chunk 由嵌套
|
|
161
|
+
构建,两边不一致会产出语法级别不同的代码。
|
|
162
|
+
|
|
163
|
+
### C5. 没有读者的 `.deps.json`
|
|
164
|
+
|
|
165
|
+
第①趟给每个 chunk、第③趟给 `index.full.js` 都会产出一份,**都没人读**
|
|
166
|
+
(chunk 走 `import()` 不过 ensure;`index.full.js` 只经降级裸 script 加载)。
|
|
167
|
+
实测 hr:49 个文件 49 KB 死重量。
|
|
168
|
+
|
|
169
|
+
**修法**(已修):`rmStrayDepsJson` 事后删,**不能**把 `epx-frame-deps` 插件从嵌套构建滤掉——
|
|
170
|
+
组件源码只在第①趟被 transform,插件靠同一实例跨趟累积,滤掉会让壳的清单变成空数组。
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## D. 本地联调环境
|
|
175
|
+
|
|
176
|
+
### D1. junction 链接平台必须配 `resolve.dedupe`
|
|
177
|
+
|
|
178
|
+
Vite 默认 `preserveSymlinks:false`,按真实路径解析 → vue 会有两份
|
|
179
|
+
(实测 frame 3.5.39 / 产品 3.5.42),provide/inject 断链、组件注册表分裂。
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
resolve: { dedupe: ['vue', 'vuex', 'vue-router'] }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### D2. Git Bash 不跟随 junction
|
|
186
|
+
|
|
187
|
+
`ls/stat/grep` 会报文件不存在。用 PowerShell 或 node 验。Vite/Node 走 Windows 原生 API 正常。
|
|
188
|
+
|
|
189
|
+
### D3. 本地顶替远程库时,CSS 会走线上旧构建
|
|
190
|
+
|
|
191
|
+
后端 `listWebpackUrl` 返回的**一相对一绝对**:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{"code":"0","data":{"entitys":[{"path":[
|
|
195
|
+
"/file-server/npm/test/web_hr/index.js", // 相对 → 拼 window.IP → 本地
|
|
196
|
+
"https://dev.epxing.com:88/file-server/npm/test/web_hr/index.css" // 绝对 → 原样用 → 线上
|
|
197
|
+
]}]}}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`loadLibrary` 只给相对路径拼 `window.IP`。于是组件是本地新 chunk、样式是线上旧 CSS,
|
|
201
|
+
scoped 的 `data-v-<hash>` 两边对不上 → **组件渲染出来但完全没样式**。
|
|
202
|
+
|
|
203
|
+
**判据**:`[...document.styleSheets].map(s=>s.href).filter(h=>h&&h.includes('<pkg>'))`
|
|
204
|
+
看 host 是不是本地。
|
|
205
|
+
|
|
206
|
+
**修法**:本地反代里拦 `listWebpackUrl` 响应,把绝对地址改写成相对。
|
|
207
|
+
注意响应外面**包了一层 `data`**(`{code,msg,data:{entitys},...}`),
|
|
208
|
+
直接取 `json.entitys` 会拿到 undefined 并静默改写 0 条。
|
|
209
|
+
|
|
210
|
+
### D4. `epx-frame` 未发布时 npm 装不了任何东西
|
|
211
|
+
|
|
212
|
+
产品 package.json 声明 `epx-frame@^5.3.0`,未发布前 npm **解析根清单就失败**(ETARGET),
|
|
213
|
+
连加一个 devDep 都装不了。绕法:别处建空目录装好,再逐包拷进产品的 `node_modules`
|
|
214
|
+
(所有 `@scope` 要下沉到子包粒度判存在,按整个 scope 目录判会漏);
|
|
215
|
+
版本冲突的包放**嵌套** `node_modules`。
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## E. 主题/样式
|
|
220
|
+
|
|
221
|
+
### E1. 换肤 mixin 按主题展开,既臃肿又漏主题
|
|
222
|
+
|
|
223
|
+
**现象**:`index.css` 里同一条规则有 N 份 `[data-theme=x]` 拷贝;
|
|
224
|
+
用户选了某些主题色,产品组件不跟着变。
|
|
225
|
+
|
|
226
|
+
**根因**:产品自带的 `mixins.scss` 用 `@each` 遍历**写死的主题列表**。
|
|
227
|
+
实测 hr 写死 8 套,而平台有 **11 套预设 + custom**(运行时注入)——**4 套完全不生效**。
|
|
228
|
+
|
|
229
|
+
**判据**:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
node -e "const c=require('fs').readFileSync('lib/index.css','utf8');console.log(c.split('}').filter(r=>/\[data-theme=/.test(r.split('{')[0])).length)"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**修法**:统一走 `rgba(var(--color-primary), $opacity)`。平台在 `[data-theme=x]` 上设
|
|
236
|
+
`--color-primary: r,g,b`,custom 由运行时 JS 注入,所有主题自动生效。
|
|
237
|
+
`$color` 参数改作 `var()` 的兜底值。实测 hr:130.4 → 85.0 KB(−35%),
|
|
238
|
+
`[data-theme=*]` 规则 600 → 0。
|
|
239
|
+
|
|
240
|
+
**注意 gzip 收益很小**(21.3 → 17.4 KB)——重复内容压得好,**省的是解析与选择器匹配成本**。
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## F. gogocode 转换残留 & 第三方库(product/recruit 145 组件实证)
|
|
245
|
+
|
|
246
|
+
> 这一组是「gogocode 没转干净 + 项目自带 Vue2 依赖」,多数在 `build:lib:lazy` 才暴露。
|
|
247
|
+
> **能机械化的已进 CLI 自动做**(下面标「已 CLI」),**结构性/换库的靠本节手动**。
|
|
248
|
+
|
|
249
|
+
### F1. gogocode 漏转「元素上的」旧具名插槽 `slot="x"`
|
|
250
|
+
|
|
251
|
+
**现象**:build 报 `[vite:vue] Cannot read properties of undefined (reading 'type')`(**无行号**),
|
|
252
|
+
或 `slot="x" is deprecated`。gogocode-plugin-vue 转了 `<template slot>`,却漏了**直接写在元素上**的。
|
|
253
|
+
|
|
254
|
+
**判据**:`grep -rn 'slot="' src --include=*.vue | grep -v v-slot`(排掉注释行)。
|
|
255
|
+
|
|
256
|
+
**修法**(结构变化,逐个手改;CLI 不做——配对闭合标签的正则不可靠):
|
|
257
|
+
```
|
|
258
|
+
<el-dropdown-menu slot="dropdown">…</el-dropdown-menu>
|
|
259
|
+
→ <template #dropdown><el-dropdown-menu>…</el-dropdown-menu></template>
|
|
260
|
+
|
|
261
|
+
<span slot="footer" class="dialog-footer">…</span> (el-dialog 的 footer)
|
|
262
|
+
→ <template #footer><span class="dialog-footer">…</span></template>
|
|
263
|
+
```
|
|
264
|
+
recruit 实测 7 处需改(另 3 处在注释里,忽略)。定位无行号的 `reading 'type'`:从报错文件里逐个 `slot="` 查起。
|
|
265
|
+
|
|
266
|
+
### F2. gogocode 漏转 `slot-scope`
|
|
267
|
+
`slot-scope="scope"` → `v-slot="scope"`(或 `#default="scope"`)。recruit 1 处,手改。
|
|
268
|
+
|
|
269
|
+
### F3.(已 CLI)`:prop.sync` → `v-model:prop`
|
|
270
|
+
Vue3 移除 `.sync`。gogocode 漏,现由 `fix-inline-handlers.mjs` 的 `fixSyncModifier` 在 migrate ③ 自动转,不用手改。
|
|
271
|
+
|
|
272
|
+
### F4. epxing 字段名里的 `|` 把 gogocode filters 规则搞崩(良性,忽略)
|
|
273
|
+
**现象**:③ 转换刷 `文件转换异常,规则:filters … expressionStatementAst.attr is not a function`。
|
|
274
|
+
**根因**:epxing 字段名带竖线(`info['SCHEME|NOTICE_FILE#']`),gogocode 过滤器规则把它当 Vue2 过滤器管道解析 AST,崩。
|
|
275
|
+
**良性**:不是真过滤器,文件其它转换正常、内容没坏。dry-run 报的「过滤器 N 文件」也因此**虚高**(把字段名 `|` 误算成过滤器)。
|
|
276
|
+
**判据**:`grep -rnE "\{\{[^}]*\|\s*[a-z_]+\s*\}\}" src` 把 `|大写字段名` 排掉后基本为 0 → 确认无真过滤器,忽略刷屏即可。
|
|
277
|
+
|
|
278
|
+
### F5.(已 CLI)内联事件多语句缺分号
|
|
279
|
+
Vue2 容忍换行分隔、Vue3 编译器报 `Unexpected token, expected ","`。现由 `fixInlineHandlers` 在 migrate ③ 自动补分号。
|
|
280
|
+
|
|
281
|
+
### F6. 换掉没有 Vue3 版的第三方库(⑥ 会告警,换库是人工决策)
|
|
282
|
+
|
|
283
|
+
| 库 | Vue3 替代 | 改动 |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `vue-quill-editor@3` | `@vueup/vue-quill` | 换 import + 组件用法(v-model/options 略变) |
|
|
286
|
+
| `vuedraggable@2` | `vuedraggable@4`(同名升 major) | 用法基本兼容,重装升版本 |
|
|
287
|
+
| `vue-orgchart-direction` | 无——改 `bpmn-js` 直用 / SVG 自实现 | 组织架构图重写(hr T2 同款) |
|
|
288
|
+
| `element-ui` | 平台运行时已提供 element-plus | **直接删 import**(el-* 全局可用),不装 element-plus |
|
|
289
|
+
| `vant@2` | `vant@4`(移动端) | 用法有差异,逐个核 |
|
|
290
|
+
|
|
291
|
+
**先查有几个文件真 import 再决定工作量**:`grep -rl <lib> src --include=*.vue --include=*.js`。
|
|
292
|
+
recruit 实测:element-ui 1 / vue-quill-editor 1 / vuedraggable 2 / vant 2 —— 量小,逐个换即可。
|
|
293
|
+
(依赖在 package.json 但源码 0 引用的是**遗留依赖**,如 recruit 的 bpmn-js/vue-bpmn,直接从 deps 删。)
|
|
294
|
+
|
|
295
|
+
### F7.(已 CLI)`node-sass` / `fibers` 残留
|
|
296
|
+
老 scss 工具链(node-gyp 原生编译),新 node 上 `npm i` 必挂、还骗过 exit code(管道吞 code)。
|
|
297
|
+
已加进 to-vite 的 `WEBPACK_ONLY_DEPS` 自动删,Vite 用 dart-sass(`sass`)。**判 npm i 真成败别信 exit code**——
|
|
298
|
+
`grep -iE "gyp ERR|Build failed" 安装日志`。
|
|
299
|
+
|
|
300
|
+
### F8. 垃圾副本文件(`Xxx copy.vue`)在按需形态撞名
|
|
301
|
+
**现象**:build 报 `[lazy-lib] 组件注册名冲突:epx-xxx 同时来自 A copy.vue / A.vue`。
|
|
302
|
+
单体形态静默覆盖(线上不暴露)、按需形态 `assertUniqueKeys` 拦下(替项目揪出垃圾副本)。
|
|
303
|
+
**判据**:`find src -iname "*copy*"`。**修法**:删副本(或改其 name)。
|
|
304
|
+
|
|
305
|
+
### F9.(已 CLI)无扩展名 import .vue
|
|
306
|
+
老项目 webpack `resolve.extensions` 含 `.vue`,`import X from './X'` 遍地;Vite 默认不含。
|
|
307
|
+
create 模板的 vite.config 已带 `resolve.extensions: ['.mjs','.js','.json','.vue']`,`--to-vite` 生成的配置自带,不用手加。
|
|
308
|
+
|
|
309
|
+
### F10.(已 CLI)`import * as Vue/Vuex/VueRouter` 命名空间导入 → 具名
|
|
310
|
+
gogocode 把 Vue2 默认导入转 Vue3 时,对 vue/vuex/vue-router 常产**命名空间导入**:
|
|
311
|
+
`import * as Vue from 'vue'` + `Vue.h(...)`、`import * as Vuex from 'vuex'` + `Vuex.createStore(...)`、
|
|
312
|
+
`import * as VueRouter from 'vue-router'` + `VueRouter.createRouter/createWebHashHistory(...)`(router/store 是 .js,gogocode `-s ./src` 也转了它们)。
|
|
313
|
+
`lazyLib` 为 external `'vue'` 生成 shim 时**不支持 `import * as ns`**(见 B2,recruit 实测 build 挂:`无法为 external 'vue' 生成 shim`)。
|
|
314
|
+
CLI ③ 修补⑥(`fix-namespace-import.mjs`)收敛为具名:收集 `NS.member` 用到的成员改 `import { m1, m2 } from 'lib'`,
|
|
315
|
+
`Vue.h`→`h` 等;整文件没用到就删这行。只认这三个框架库(白名单),业务库的命名空间导入不碰。
|
|
316
|
+
|
|
317
|
+
### F11.(已 CLI)`require('@/assets/*.png')` → 静态 import + `assetsInlineLimit`
|
|
318
|
+
webpack 支持 `require('图片')` 当 URL 用(三元/字符串拼接里),Vite 不支持 → 运行时 `require is not defined`。
|
|
319
|
+
CLI V 阶段(`fix-vite-source.mjs`)提为顶部静态 import(别名 `@` 支持)、`require(...)` 换成变量。
|
|
320
|
+
**只在 `<script>`/JS 里改**:模板表达式里的 `require` 换成模块顶层变量后 Options API 的 this 取不到,那种报人工(手工挂 data/computed,见 F11 配套的 recruit Center.vue/Guide.vue 改法)。
|
|
321
|
+
配套:create 模板的 `build.assetsInlineLimit` 设 `50*1024`,让这些小图标内联成 data URI——否则超 4096 的(recruit 的 `fileHover.png` 4115B,只超一点点)会出独立文件、远程库形态引用 `/assets/xxx` 绝对路径运行时 404(构建期看不出;C4 管 lazy 嵌套构建继承这个值,前提是主 build 设了)。
|
|
322
|
+
|
|
323
|
+
### F12.(已 CLI)`require.context` 非入口(store 收集 vuex modules)→ `import.meta.glob`
|
|
324
|
+
入口的 `require.context('@/X', true, ...)` `--to-vite` 一直会转;**漏网的是 store/index.js** 的
|
|
325
|
+
`require.context('./modules', false, ...)`(收集 vuex modules)。CLI V 阶段现遍历全 src 通用改写:
|
|
326
|
+
recursive 决定 `**/` 还是单层、正则决定扩展名(`.vue`/`.js`)、dir 前缀(`@/` 或 `./`)原样保留;
|
|
327
|
+
配套 `VAR.keys()`→`Object.keys(VAR)`、`VAR(k)`→`VAR[k]`(变量名不一定叫 files,store 里叫 context,故按声明抓变量名)。
|
|
328
|
+
|
|
329
|
+
### F13. `::v-deep` 弃用警告(**不进 CLI;容器块形态手工 / postcss**)
|
|
330
|
+
Vue3 弃用 `::v-deep` 作 combinator,建议 `:deep(<inner>)`。recruit 实测 **141 处是容器块 `::v-deep{ ... }`**(combinator 仅 10、`>>>` 仅 5)。
|
|
331
|
+
容器块转 `:deep()` 要拆 SCSS 嵌套块(花括号配对、多层嵌套),141 处正则拆分**极易改坏 style,风险远大于收益**——
|
|
332
|
+
而它只是**弃用警告:build 通过、运行正常**。故**不进 CLI**(同 gogocode 的 style 快照保护:不用正则动 style 块)。
|
|
333
|
+
真要消警告:写 postcss 插件按 AST 转、或按文件手工,别正则批量。
|
|
334
|
+
|
|
335
|
+
### F14.(部分已 CLI)`<script>` 里的 Vue2 JSX 残留 → Vue3(gogocode 只转 template,不碰 JSX)
|
|
336
|
+
gogocode 只转 `<template>`,**不碰 `<script>` 里的 JSX / 手写 render**——Vue2 JSX 前缀原样留到 Vue3,
|
|
337
|
+
`@vue/babel-plugin-jsx` 不识别、当普通属性丢弃 → **build 过、console 不报错、运行时静默失效**(最隐蔽的一类)。
|
|
338
|
+
recruit 实测(145 组件):表单 note 不显示、折叠操作点击无效、子表格自定义列/操作列不渲染。
|
|
339
|
+
③ 本地简历审核详情「流程图」tab 空白也是同款——平台 `ProcessChart.vue` 用 `domPropsInnerHTML` 贴 saveSVG 的图,
|
|
340
|
+
实证显示容器 `innerHTML.length=0`(图在隐藏容器画好了、没贴出来);平台侧 Vue2 JSX 残留共 **11 处 / 8 文件**(`domPropsInnerHTML` 9 + `nativeOnClick` 2:`ProcessChart` / `EntityList`×2 / `Mobile`×2 / `Field` / `AddFormItem` / `NoInstallSystemList` / `AppNav` / `Login`),本会话已用**同一个 CLI `fix-jsx-vue2`** 批量修完(平台组件同样适用)、`build:pre` 通过,`ProcessChart` 流程图肉眼验证 `innerHTML 0→30753`。
|
|
341
|
+
- **已 CLI**(③ 修补⑦ `fix-jsx-vue2.mjs`,只换 JSX 属性名、不动值/结构,安全):
|
|
342
|
+
`domPropsInnerHTML=`→`innerHTML=`、`domPropsValue=`→`value=`、`nativeOn<E>=`→`on<E>=`(nativeOnClick→onClick,Vue3 组件事件默认透传根元素)、`scopedSlots={{`→`v-slots={{`。
|
|
343
|
+
- **人工**(结构性重排,CLI 不碰):手写 render 的 data 对象 `return { scopedSlots:{...} }` / `h(tag,{on,props,attrs})`——
|
|
344
|
+
Vue2 createElement data 到 Vue3 差异大,正则不保险。recruit 实测 InnerEntitySubList 3 处(方法返回 `{scopedSlots:{default}}` +
|
|
345
|
+
调用点 `<el-table-column {...this.renderX()}>` spread),最小改法:调用点 `{...this.renderX()}` → `v-slots={this.renderX().scopedSlots}`(方法内数据属性名不动)。改后实测子表格字段/操作列/彩色标签正常渲染。
|
|
346
|
+
- 排查:`grep -rE "domProps|nativeOn[A-Z]|scopedSlots" src`;`scopedSlots=`(等号)=JSX 属性可自动,`scopedSlots:`(冒号)=render 对象需人工。
|
|
347
|
+
- **第 5 类 `slot="xxx"`(postAppoint 补)**:`<script>`/JSX 区的具名插槽(`slot="reference"`/`slot="footer"`/`slot="title"`)同样被 Vue3 JSX 丢弃 → el-popover 触发钮不渲染(「更多」按钮消失)/ el-dialog 底部按钮·标题不显示。`fix-jsx-vue2` 现**报告**(列人工清单,不自动改——改 `v-slots` 要重组 JSX 结构,正则做不了)。⚠ **不能一律判失效**:实测 el-dropdown 的 `slot="dropdown"` 靠组件运行时兼容仍正常(`$slots` 含 dropdown),el-popover `reference` / el-dialog `footer`·`title` 才真失效——逐个 `$slots` 或 DOM 实证。postAppoint `MainEntityList..vue` 的 `slot="reference"`(卡片「更多」)实证失效,改 `v-slots={{ reference: () => (...) }}`(commit `d6386c3`)。
|
|
348
|
+
|
|
349
|
+
### F15. JSX/render 组件的 `<style scoped>` 在 Vue3 深层失效(样式问题系统根因)
|
|
350
|
+
Vue3 的 scoped `data-v` 属性只在 `<template>` 编译时**逐元素**注入;**JSX/render 函数只有根元素运行时带 scopeId、深层元素不带** → 「无 `<template>` + `<style scoped>`」的组件,scoped 深层样式在 Vue3 **整体失效**(Vue2 的 vue-jsx 给所有元素加 scopeId,所以旧版正常、迁移后才塌)。
|
|
351
|
+
recruit 实测:简历审核详情左侧头像**撑满整栏**(平台 `CardFormItem`)——实证元素 `.awatar-container` 只有 class、**无 data-v**,而 scoped 规则是 `.awatar-container[data-v-1a175043]` → 不匹配、86×120 约束失效、图片按原图 728×1024 撑开;博士后基本资料照片则空白。**特征:build 过、console 不报错、样式默默塌**——功能层排查抓不到,必须 ①③ 样式对照才发现。
|
|
352
|
+
- **CLI(已加)**:⑦ 扫描告警(`migrate.mjs` 扫「无 `<template>` + `<style scoped>`」组件、标注含几处 ::v-deep,报人工清单),**不自动改**(根 class 命名 / extends 根 override / 作用域判断都是设计决策)。
|
|
353
|
+
- **修法**(dom-pitfalls 第7条,逐个手工):① `<style scoped>`→`<style>`;② 组件根加**唯一 class**——按组件形态四选一:(a) **自有 render** 根元素 class 数组追加 `class={['原class','epx-xxx']}`;(b) **extends/mixins 无自有根**(继承父类 render)用 `mounted(){ this.$el && this.$el.classList && this.$el.classList.add('epx-xxx') }`(比 override render/`data.className` 简单可靠,recruit 9 个 extends/mixins 组件均用此);(c) **多分支 render**(如 `Login`)各分支实际 DOM 都须位于命名空间内(如 `Login`,默认分支和 `renderPage` 自定义页面分支都要检查,见 F19);(d) **纯 BEM 组件**(class 已全局唯一,如 `.m-task-*` / `@include e()`)**仅去 scoped、不必包命名空间**(class 本身即隔离);③ 样式全包进该根 class(scss 嵌套 `.epx-xxx{…}`)防全局污染;④ 深层选择器去掉改普通后代选择器(非 scoped 本就穿透)——`::v-deep`/`/deep/`/`>>>`(旧)**与 `:deep(<inner>)`(新语法,平台组件常用,如 CardFormItem)都要去**:`:deep()` 在非 scoped `<style>` 里编译器不转换、会原样输出成无效 CSS 选择器被浏览器丢弃,须把 `:deep(.foo)` 改成 `.foo`;⑤ **对照①线上样式改**(extends 子类 scoped 作用域 Vue2 下本就微妙,别盲目让全部生效、反而多出样式)。
|
|
354
|
+
- **已验证**:recruit `ConditionFormItem`(自有 render、1 条 `.addCondition`)改后产物 css 由 `.addCondition[data-v-x]`(失效)变 `.epx-recruit-admin-condition-form-item .addCondition`(非 scoped、生效)。
|
|
355
|
+
- **排查**:`grep -rL "<template" --include=*.vue src`(无 template 的 .vue)∩ 有 `<style scoped>`。recruit 实测 9 个:EntitySubList / InnerEntitySubList / AddForm / ConditionFormItem / WriteFormItem / ExpertEntityList / ExpertSubList / ResumePage×2;平台侧 CardFormItem 等同病(平台仓修)。
|
|
356
|
+
- **机制精化(2026-09-14 实证)**:不是「scoped 深层样式整体失效」——精确说是**普通深层选择器失效、顶层 `:deep()` 仍生效**。scoped 下 `.foo{}` 编译成 `.foo[data-v]`(元素自身要带 data-v,深层元素没有→失效);而顶层 `:deep(.foo)` 编译成 `[data-v] .foo`(靠**根元素**的 data-v,根后代 .foo 命中→生效)。所以同是「头像」:**自定义图片框**(`.awatar-container`/`.logo`/`img` 用普通选择器写死尺寸)→ 失效撑满(真 bug);**用顶层 `:deep(.el-avatar)` 写的 EP 头像** → 靠根 data-v 生效、正常(不是 bug)。甄别「真坏」优先查「普通选择器 + 固定尺寸/图片框/绝对定位」,颜色字体类失效视觉退化轻。
|
|
357
|
+
- **平台侧同病是系统性的**:平台 `frontend/frame/packages` 实测 73 个 render+scoped 组件(结构风险)。已按本修法修的撑满型:`CardFormItem`(简历头像)/`NavAddForm`(卡片头像,均自有 render 根加 class)/`Login`(登录页 logo,默认分支根 `.container` 加 class)/`TaskList` 移动端头像块(全 BEM 唯一 class、仅去 scoped 即隔离)。**平台团队另在用「整簇搬到无 scoped 块 + BEM 命名(`@include e()`)」方式渐进迁移**(见 `TaskList.vue` 块注释)——改平台组件优先随此 BEM 方式、不要混用 `.epx-xxx` 包裹;extends 父类的组件(如 `HallNav` extends `AppNav`)改前先核实自身 style 的 class 是否还对应实际 DOM(父类 BEM 化后可能已是历史死样式)。
|
|
358
|
+
- **移动端 F15 验证技巧**:受控浏览器 `resize_window` 常无效(视口固定 1920)、UA 是桌面时,用 `Object.defineProperty(window,'innerWidth',{configurable:true,get:()=>390}); window.dispatchEvent(new Event('resize'))` 让 epx-frame 重算成 mobileViews(**仅骗 JS 判断、不改真实渲染视口**,截图仍宽屏;真移动端视觉需本机 DevTools 设备模式 / 真机窄视口——用户手动切设备模式后 `innerWidth/outerWidth` 才真变 390)。移动端页 F15 用 JS 量尺寸排查:`[...document.querySelectorAll('img,[class*=avatar],[class*=photo]')].map(el=>el.getBoundingClientRect())`,宽/高远超设计值/视口即撑满。recruit 移动端 site(`zpwzx`)实测首页 banner 293×165、缩略图 80×80、我的空状态图 150×130、职位列表——图片全正常无撑满。
|
|
359
|
+
- **CLI 静态检测(postAppoint 加)**:`detect-vue3-risks.mjs` R1 = extends+scoped+无 template+**裸类选择器**(`.foo{`),退出码 2 可 CI 拦;**已改 `:deep()` 的按裸选择器判定不误报**。postAppoint `MainEntityList..vue`(extends EntityList、卡片透明+状态点尺寸 0)实证:28 个裸类选择器 `.foo{`→`:deep(.foo){` 后卡片背景 `rgb(255,245,234)`/状态点 8×8/字号字重与线上一致(commit `d6386c3`)。⚠ scoped 块里若含平台类(`.epx-entity-list__*`)**别一律去 scoped**(会泄漏成全局污染其它页面),此时 `:deep()` 更稳。
|
|
360
|
+
|
|
361
|
+
### F16. 数组 `splice` 同引用 + `deep:1` watcher 不触发(弹窗标题恒空,postAppoint 平台侧实证)
|
|
362
|
+
Vue3 起浅层 watcher 不响应 push/splice;`deep:1` 只监听「数组元素引用」这一层。若 `route.name=title; routes.splice(i,1,route)`——splice 放回**同一对象引用**、只改元素内部属性 `name`,`deep:1` 监听不到 → watcher 不触发 → `$emit('change')` 不重发 → 消费方(`Popup.routerChange`)拿到的 route.name 恒空 → **弹窗标题不显示**(build 过、无报错)。
|
|
363
|
+
- 修法:splice 放**浅拷贝新对象** `routes.splice(i,1,{...route,name:title})`(改元素引用才触发 `deep:1`);消费方再加空值兜底(异步 `$refs.component` 未 resolve 时用 `route.name`)。平台 `RouterView.pageChange` + `Popup.routerChange` 双改、build:pre 实测标题空→「新增公示」(commit `0061cb7c63`)。修前先运行时验证:手动 `rv.routes.splice(0,1,{...rv.routes[0]})` 看消费方值是否变,成立再改、免白 build。
|
|
364
|
+
- 通用:Vue3 靠 push/splice 增删的数组,watcher 要么 `deep`、要么 splice 换新对象引用;**改元素内部属性不触发浅层 watcher**(同类历史 commit `df29393`/`2161c04` 数组 watcher 补 deep)。
|
|
365
|
+
|
|
366
|
+
### F17. CSS code-split 下组件借用别组件的全局样式(图表容器塌陷,postAppoint 平台侧实证)
|
|
367
|
+
Vue2 webpack 把全部 CSS 合进主 bundle,任何组件的非 scoped 全局样式总能加载;Vue3 vite **CSS code-split** 按组件拆 CSS chunk。若「实际渲染的组件」自身无 style、借用**别组件**里定义的非 scoped 全局样式(如 `.epx-chart` 布局散在 `chart/Table.vue`、`report-chart/Chart.vue` 的 `@include b(chart)`,而实际渲染的 `Common.vue`(epx-chart-common) 无 style),只加载渲染组件 chunk 时**不带那些 CSS chunk** → 样式缺失。实证:`.epx-chart__main` 高度塌陷 0、echarts 画不出(JS chunk 已加载、`getReportData` 数据正常)。
|
|
368
|
+
- 修法:让渲染组件**自带**渲染必需的样式(Common.vue 补 scoped `<style>` 定义 `.epx-chart` 根 flex+min-height / `__main` flex / `__echarts` 铺满,随本组件 chunk 加载、有 template 故 scoped 生效、零污染,commit `0061cb7c63`);别关 `cssCodeSplit`(牺牲首屏)。
|
|
369
|
+
- 排查判据:`getComputedStyle(容器).height==='0px'` + `performance` 里该组件 CSS chunk **不在**已加载列表(`document.styleSheets` 里也搜不到那几条规则)。
|
|
370
|
+
|
|
371
|
+
### F18. `$emit` 不读实例 `__epxEvents`(程序化事件断链、内容区空白,postAppoint 平台侧实证)
|
|
372
|
+
Vue3 移除实例 `$on/$off`,平台改用实例级轻量事件 `$epxOn/$epxEmit`(handler 存实例 `__epxEvents`,见 core `mixins/methods-to-prototype.js`)。子组件(如 RouterView)用 `$parent.$epxOn('createPage',...)` 注册;而发射方仍是裸 `this.$emit('createPage')`——Vue3 `$emit` 只触发**父模板上**的监听器、**不读 `__epxEvents`** → 注册方永远收不到(emit 没人听的事件是合法 no-op、无报错)→ 内容区整块空白、点击无效。实证 `EntityTreeManage.vue`(机构管理右侧空白)。
|
|
373
|
+
- 修法:发射走「`$epxEmit` + `$emit`」**双发射**闸口(保留 `$emit` 以防模板上有 `@xxx`)。commit `543f85daf3`。
|
|
374
|
+
- 排查:全仓 grep `\.\$emit\('程序化事件名'`(createPage / backModule / push 等),逐个确认消费方是 `$epxOn` 注册还是模板绑定;全仓「没有任何模板 `@createPage`」即强信号。
|
|
375
|
+
|
|
376
|
+
### F19. 非 scoped 样式改为根 class 命名空间后,其他渲染入口漏加 class(服务大厅登录页实证)
|
|
377
|
+
|
|
378
|
+
迁移中把 `<style scoped>` 改成 `<style>`,并将规则包进 `.epx-xxx { ... }` 时,所有实际渲染入口都必须在该命名空间内。检查 `extends` 子组件覆盖的 `render()`、父组件的自定义页面分支、动态 `<component>` 和多分支根节点;只给默认分支加 class 不够。此问题与 `data-v` hash 失配不同:CSS 已加载、DOM 也有原来的内部类,但根节点缺少 `.epx-xxx`,整组选择器匹配不到,构建和控制台通常正常。
|
|
379
|
+
|
|
380
|
+
- **实证**:`Login.vue` 样式改为 `.epx-login` 包裹后,默认登录分支有该类;`ClientLogin.vue` 覆盖的根节点及配置 `LOGIN_PAGE` 走的 `renderPage()` 没有。`#/hall/login/frame` 渲染出 `.PCcontainer` 和 `.loginCon`,却没有 `.epx-login` 祖先,登录页失去布局。给 `ClientLogin` 根和 `renderPage()` 的页面容器补 class 后,浏览器实测 `.loginCon` 恢复绝对定位、宽 357px,`.PCcontainer` 铺满 1440×900 视口。
|
|
381
|
+
- **升级检查**:凡将 scoped 样式改为根 class 命名空间,逐个打开使用该组件的实际页面/分支,检查 `el.closest('.epx-xxx')` 是否存在,并用 `getComputedStyle(el)` 验证宽高、定位等关键规则。若 CSS 已加载但命名空间不存在,在相应渲染根或共同祖先补 class;不要靠失效的 `__scopeId` 转写补丁。静态扫描只能提示风险,页面验收仍需运行时确认。
|