前端页面运行期冒烟(verify-page-runtime.mjs)¶
位置:
deploy/nfos/tools/verify-page-runtime.mjs| 语言:node(ESM,.mjs) | 是否需要 root:否
工具性质:离线冒烟(最小 DOM 桩 + 真实共享层;不写任何文件)
运行前提:目标前端目录在场(<frontendDir>/<site>/index.html 与 <frontendDir>/shared/js/);路径全部来自命令行,不自动定位仓库根
部署状态:随仓库检出运行,不部署到 /opt/Cy/services/system.d/nfos/
用途¶
在没有浏览器(也没有无头 Chrome)的环境里,证明前端页面模块能真正执行并完成一次渲染,而不只是“文件部署正确 + 语法通过”。
源码头部说明的动因(逐字引用):
* 为什么需要:服务器上没有浏览器,也没有无头 Chrome,所以此前只能证明
* “文件被正确部署、语法通过”,**不能**证明页面模块在浏览器里能真正执行。
* 而历史上真实发生过的问题恰恰是运行期类型错误(例如 UI.table 的
* rowActions 不存在、UI.modal 的 fields 不受支持)—— 语法检查发现不了。
做法(源码头部逐字):
* 做法:用极小 DOM/浏览器桩加载真实共享层(config/auth/api/store/ui)
* 与目标页面模块,让它完成自注册与一次渲染,捕获任何运行期异常。
* 这不是完整浏览器,但足以抓出“函数不存在 / 参数形状不对”这类错误。
关键行为:
| 环节 | 源码行为 |
|---|---|
| 脚本加载顺序 | 解析 <site>/index.html 里 <script src="..."> 的真实顺序(跳过 http(s): 与 // 开头的,去掉 ?v= 参数);解析不到时回落到内置顺序并打印 !! index.html 未解析到脚本顺序,回落到内置顺序 |
| 共享层解析 | js/core/* 映射到 <frontendDir>/shared/js/*,其余按 <site>/ 下相对路径;已部署文件优先,缺失则回落到 shared/js |
| 单页模式 | 传入 [pageFile] 时,/pages/ 下的其它页被跳过;未在 index.html 声明的页面文件会补扫 js/pages/ 目录 |
| 渲染 | 对注册的每个页面调用 def.render(host, ctxStub),thenable 结果会 await;异常被收集为 页面 <key> 渲染异常:<msg> |
| 空站保护 | 页面注册数为 0 直接判错:页面注册数为 0(整站无可见页面):检查 index.html 脚本顺序 / 页面模块报错(对应 2026-09-15 整站空白事故) |
| 登录组件 | NF.loginGate 缺失、install() 抛错、prompt 不是函数 → 记入错误 |
| 网络 | fetch 是桩:返回 {ok:true,status:200} 且 data: [],不联网;只统计调用次数并打印 fetch 调用记录:<N> 次 |
典型场景¶
- 前端改动(页面模块、
js/core/*共享层、index.html脚本顺序)之后的冒烟:部署到服务器后、无浏览器环境下的“能不能跑起来”验证; - 事故复现:
index.html把app.js排在pages/*.js之后导致整站空白(2026-09-15)这类顺序问题,本工具按真实声明顺序加载,因此能复现; - 单页排查:只指定一个 Page 文件,缩小到某个页面的渲染异常。
用法与参数¶
源码给出的用法(逐字引用,即无参数时打印到 stderr 的那一行):
参数取 const [, , FE, SITE = 'online', PAGE] = process.argv;:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
<frontendDir> |
字符串(目录路径) | 必填(缺失即报错退出) | 前端根目录,其下有 <site>/ 与 shared/js/(如仓库的 apps/web) |
<site> |
字符串(目录名) | online |
站点子目录名;同时写入 window.NF_PLATFORM |
[pageFile] |
字符串(文件名) | 无(不传则跑全部页面) | 只测 js/pages/ 下的这一个文件,跳过其它页;与 index.html 里 /pages/ 声明匹配时会保留声明加载 |
无参数时的默认行为:没有默认行为 —— 缺 <frontendDir> 时向 stderr 打印上述用法行并立即 process.exit(2)(实测确认)。
示例¶
从仓库根目录执行(站点取默认值时也必须显式给出 <frontendDir>):
只冒烟单个页面:
期望输出形态(末尾为结论):
按 index.html 顺序加载 12 个脚本
已加载共享层;注册页面数=...
待渲染页面:... 个
fetch 调用记录:... 次
── 运行期错误 ──
(无)
PAGE_RUNTIME_SMOKE_OK
| 情形 | 期望退出码 |
|---|---|
无运行期错误(末行 PAGE_RUNTIME_SMOKE_OK) |
0 |
有运行期错误(末行 PAGE_RUNTIME_SMOKE_FAILED (<N> 项),最多列 25 条) |
1 |
缺 <frontendDir>(打印用法行) |
2(实测确认) |
用法行是打印到 stderr 的;判定门禁时应同时看 stderr 与退出码。
依赖与权限¶
- 语言/版本:Node.js(ESM,读取
node:fs/node:path/node:vm)。 - 第三方依赖:无(不使用 jsdom、不使用无头浏览器)。
- 仓库/部署依赖:需要一份前端目录树(仓库的
apps/web/,或已部署到服务器的同名结构):至少要有<site>/index.html,以及shared/js/或<site>/js/core/下的共享层文件;缺文件会被记为错误缺少文件:<path>。不要求从仓库根执行(路径全由参数给出)。 - 权限:不需要 root、不需要写权限、不需要网络(纯只读)。
风险与注意事项¶
- 纯离线只读:不写文件、不起服务、不联网(
fetch为桩,返回空data);沙箱里注册的setTimeout/requestAnimationFrame均可用,但setInterval被替换为返回0的空实现。 - 不是浏览器:DOM 是极简桩,
querySelector返回新元素、querySelectorAll返回空数组、getBoundingClientRect返回固定值。因此它抓不到 CSS 布局/视觉/交互问题,只能抓“函数不存在 / 参数形状不对 / 渲染抛异常”。 - 脚本确实会被执行:被加载的页面模块与共享层代码会在
vm沙箱内真实运行,所以任何模块级副作用(例如在加载期发起的fetch)都会发生 —— 好在fetch被替换为桩,不会真的出网。 index.html顺序是权威:改动index.html的<script>顺序会直接改变本工具的加载顺序;若解析不到脚本,会回落内置顺序并打印提醒 —— 此时“通过”不代表线上顺序正确。- 注册数为 0 必 FAIL:这是有意设计的“不许掩盖事故”断言;如果某个站点确实没有页面模块,需要调整该断言的适用范围,而不是把错误忽略。
- 页面
render异常只记录前 25 条(errors.slice(0, 25)),错误很多时需分批排查。
回滚¶
本工具不部署到线上、不改线上系统,且不产出任何文件:
- 无输出文件、无临时目录、无状态文件、无缓存;
- 不改动被加载的前端文件(只读打开);
- 无需清理。若只是运行过本冒烟,工作区比对结果应为空。