PAYATHON 2026

Angular SSR 在 cPanel 报 Unexpected token '??=' 怎么解决?

数据小陈

结论

优先检查处理网站请求的 Node.js 进程实际使用的版本。终端中的 node -v 只能证明当前 Shell 找到的版本,不能证明 cPanel/LiteSpeed 启动应用时使用的是同一个可执行文件。

Node.js 24 支持 ??=,无需通过降级解决这个语法错误。Node.js 15 的发布说明已列出逻辑赋值运算符 &&=、||= 和 ??=;Angular 21.2 的兼容范围也包含 Node.js ^24.0.0。题目提到的两个 Node.js 24 版本都不会因为缺少 ??= 语法支持而直接报错。Node.js 15 发布说明、Angular 版本兼容表

现有日志看起来像来自较旧的 Node.js 运行时,但其中没有实际进程版本和时间戳,暂时无法确定它对应哪次启动。先获取一次新请求产生的日志,再决定修改哪层配置。

为什么面板和终端都显示 24,应用仍可能使用旧版本?

本地构建进程、服务器终端和 Web 服务器启动的应用进程可以使用不同的 Node.js 路径。

日志中的路径表明,报错经过了 LiteSpeed 的启动脚本:

/usr/local/lsws/fcgi-bin/lsnodesm.js

如果主机使用 CloudLinux Node.js Selector,LiteSpeed 会读取相关应用配置,但其底层实现与 Apache 的 Passenger 不同。不能仅根据 cPanel 界面认定具体启动方式。LiteSpeed 的 CloudLinux 集成说明

503 Service Unavailable 表示应用当前无法正常提供服务,本身不能证明 Angular 编译失败。题目中的日志更直接地指向应用加载或启动阶段。

第一步:从 Web 应用进程记录真实版本

可以先在 cPanel 中使用相同的 Node.js 版本设置创建临时诊断应用。如果只能使用现有应用,先记录原启动文件,再短暂切换到诊断文件,完成后恢复 server.mjs。

创建 runtime-probe.cjs,内容如下:

console.error('[runtime-probe]', JSON.stringify({
  node: process.version,
  v8: process.versions.v8,
  execPath: process.execPath,
  pid: process.pid,
  cwd: process.cwd()
}));

try {
  var result = new Function('let value; value ??= 1; return value;')();
  console.error('[syntax-probe]', result);
} catch (error) {
  console.error('[syntax-probe]', error.stack);
}

var http = require('http');

http.createServer(function (req, res) {
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('runtime probe\n');
}).listen(process.env.PORT || 3000);

使用 .cjs 可以让诊断文件在项目声明 "type": "module" 时仍按 CommonJS 加载。将 ??= 放进字符串,可以先记录版本,再单独检测运行时是否支持该语法。

通过面板保存并重启诊断应用,访问一次绑定的网址,然后查看新产生的 stderr.log。重点对比 node、execPath 和 pid;不要只在终端执行这个文件。

支持该语法时,[syntax-probe] 应输出 1。在 LiteSpeed Selector 集成模式下,监听端口可能被替换为 Unix domain socket,因此不能仅凭无法直连 3000 端口判断诊断失败。LiteSpeed 的测试与监听说明

如果诊断标记没有出现,先确认启动文件路径和日志位置,再检查启动器是否在执行文件前就失败了。此时需要主机管理员协助查看对应进程及 LiteSpeed 日志。

第二步:根据诊断结果修复启动配置

Web 进程确实使用了旧版本

应修复应用进程的 Node.js 路径,继续调整终端的 PATH 无法保证改变应用进程使用的版本。

检查以下内容:

  1. 当前域名是否绑定到刚修改的应用,Application root 和 Application startup file 是否正确。
  2. 应用配置引用的 Node.js 可执行文件是否对应所选版本。
  3. 保存版本设置后,原应用进程是否真正停止并重新启动。

如果使用 CloudLinux Selector,可以检查其生成配置中的 PassengerNodejs 路径。LiteSpeed 官方列出了对该指令的支持,但实际配置位置取决于主机环境。应通过面板或由管理员修正,避免随意覆盖面板管理的配置。LiteSpeed 支持的配置指令

向主机商提供应用目录、新日志、process.version、process.execPath 和对应 PID,有助于定位具体启动链路。仅修改 .bashrc 或切换终端中的 Node.js,不足以保证 Web 应用进程随之改变。

Web 进程显示 Node.js 24,语法检测也通过

恢复 server.mjs,重新启动应用,再复现一次错误。如果仍出现相同的 ??= 报错,确认该记录是否来自另一个进程、另一份部署或历史日志。

也可以用已核实的 Node.js 绝对路径直接启动同一份 SSR 构建产物。下面两个路径都是占位符,需要替换:

/absolute/path/to/node24 --trace-warnings --unhandled-rejections=strict /absolute/path/to/server.mjs

结果可以帮助区分故障位置:

  • 直接启动正常、通过 cPanel 启动失败:继续检查启动器、工作目录、环境变量及 ESM 加载方式。
  • 直接启动也失败:根据新的错误堆栈检查构建产物或依赖,定位实际出错的文件。

LiteSpeed 官方也建议独立启动 Node.js 应用,辅助区分应用代码问题与服务器集成问题。LiteSpeed 应用故障排查

第三步:单独处理 Error: Not supported

不能直接将 Error: Not supported 和 Unexpected token '??=' 视为同一个错误。前者只显示了启动器中的调用位置,信息不足以确定具体原因。

确认运行时正确后,如果新日志仍报 Not supported,请让管理员核查当前 LiteSpeed 启动组件对 .mjs 和 ESM 的加载支持,并结合服务器实际安装的启动脚本定位失败位置。

项目使用了 "type": "module",启动文件也是 server.mjs。不要直接把它改名为 .cjs,或把 import 换成 require();这些修改可能改变模块解释方式,却无法修复启动器的问题。如果需要启动包装文件,还要确认 SSR 服务确实执行了监听逻辑,仅导入模块成功不代表网站已经启动。

哪些配置不必先改?

target: "ES2022" 与现代 Node.js 运行环境相符。降低 TypeScript 的 target 不能保证所有服务器端依赖都会被转换,也不会修复 Web 服务器调用了错误 Node.js 路径的问题。

@types/node 是类型声明包,packageManager 描述包管理器信息,它们都不会决定生产进程实际使用哪个 Node.js 可执行文件。

日志中的 UnhandledPromiseRejectionWarning 是加载失败之后的表现。增加 .catch() 或使用 --unhandled-rejections=strict 可以改善错误暴露方式,但不能让旧运行时识别 ??=。

修复后,通过网站请求再次核实实际进程版本,并确认新日志不再出现对应的启动错误。如果语法错误消失后仍有 503,再根据新的首个错误继续排查。现有材料不足以保证调整 Node.js 路径就能解决全部部署问题。

备注:内容仅供参考。