Angular14升级Firebase9后SSR报id未定义错误崩溃
问题概述
- 完成Angular v13到v14、Firebase v8到v9模块化版本升级后,本地测试全流程正常,服务器部署后服务运行约6小时触发
id未定义报错,直接导致SSR服务崩溃,必须执行npm run serve:ssr才能重启恢复。 - 同类报错在Angular v13阶段曾出现但从未引发崩溃,升级v14后崩溃频率大幅提升。

当前server.ts配置
(global as any).WebSocket = require('ws'); (global as any).XMLHttpRequest = require('xhr2'); import 'zone.js/dist/zone-node'; const domino = require('domino'); const fs = require('fs'); const path = require('path'); import { ngExpressEngine } from '@nguniversal/express-engine'; import * as express from 'express'; import { join } from 'path'; import { existsSync } from 'fs'; const distFolder = join(process.cwd(), 'dist/projectfolder/browser'); const template = fs.readFileSync(path.join(distFolder, 'index.html')).toString(); const win = domino.createWindow(template.toString()); global['window'] = win; global['document'] = win.document; global['DOMTokenList'] = win.DOMTokenList; global['Node'] = win.Node; global['Text'] = win.Text; global['HTMLElement'] = win.HTMLElement; global['navigator'] = win.navigator; global['getComputedStyle'] = win.getComputedStyle; (global as any).self = {fetch: require('node-fetch')}; import { AppServerModule } from './src/main.server'; import { APP_BASE_HREF } from '@angular/common'; import { environment } from './src/environments/environment'; // Express实例可导出供serverless函数使用 export function app(): express.Express { const server = express(); const distFolder = join(process.cwd(), 'dist/projectfolder/browser'); const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index'; // Universal express-engine 配置 server.engine('html', ngExpressEngine({ bootstrap: AppServerModule, })); server.set('view engine', 'html'); server.set('views', distFolder); // 静态资源服务配置 server.get('*.*', express.static(distFolder, { maxAge: '1y' })); const routesPath = ['/invite', '/invite/**','/lm', '/dashboard', '/dashboard/**', '/public/**', '/public', '/pre']; server.get(routesPath, (req, res) => { res.sendFile(distFolder + '/index.html'); }); // 其余路由走SSR渲染 server.get('*', (req, res) => { res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] }); }); return server; } function run(): void { const port = process.env.PORT || environment.PORT; const server = app(); server.listen(port, () => { console.log(`Node Express server listening on https://localhost:${port}`); }); } // 适配webpack打包逻辑,确保直接运行时启动服务 declare const __non_webpack_require__: NodeRequire; const mainModule = __non_webpack_require__.main; const moduleFilename = mainModule && mainModule.filename || ''; if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { run(); } export * from './src/main.server';
故障原因
三个问题叠加导致该故障:
- 全局单例DOM污染:当前用domino创建的window、document等对象直接挂在global上是所有请求共享的单例,Angular 14对渲染上下文的校验比v13更严格,长时间运行多请求并发时,异步回调(尤其是Firebase的网络请求回调)会拿到已被前序请求回收的DOM节点引用,访问id属性时直接抛出未定义错误;Angular v13会自动降级吞掉这类渲染异常,v14会将异常抛到进程顶层直接触发退出,因此崩溃频率明显上升。
- Firebase v9 polyfill缺失:Firebase v9模块化版本运行在SSR环境时,会从全局self对象上读取crypto、structuredClone等API,当前配置把self覆盖为仅含fetch的对象,Firebase内部鉴权、Firestore模块的缓存上下文长时间运行失效后,访问内部实例id属性就会报错。
- 无异常兜底逻辑:当前服务既没有进程级的未捕获异常监听,也没有Express渲染层的错误回调,偶发异常会直接导致Node进程退出。
修复步骤
按以下顺序调整配置即可解决问题:
- 补全全局polyfill,修改文件顶部的polyfill逻辑,替换原有self赋值的代码:
(global as any).WebSocket = require('ws'); (global as any).XMLHttpRequest = require('xhr2'); (global as any).crypto = require('crypto').webcrypto; (global as any).structuredClone = (obj: unknown) => JSON.parse(JSON.stringify(obj)); (global as any).fetch = require('node-fetch'); (global as any).self = global; import 'zone.js/dist/zone-node';
- 移除顶部直接给global赋值window、document等dom属性的逻辑,改为在ngExpressEngine中为每个请求创建独立的dom上下文,修改engine配置:
const distFolder = join(process.cwd(), 'dist/projectfolder/browser'); const template = fs.readFileSync(path.join(distFolder, 'index.html')).toString(); server.engine('html', (filePath, options, callback) => { // 每个请求创建独立的dom上下文,避免状态污染 const win = domino.createWindow(template); const providers = [ { provide: APP_BASE_HREF, useValue: options.req.baseUrl }, { provide: 'WINDOW', useValue: win }, { provide: 'DOCUMENT', useValue: win.document } ]; ngExpressEngine({ bootstrap: AppServerModule, providers })(filePath, options, callback); });
- 添加进程级异常捕获,在run()函数调用前添加以下代码,避免偶发异常直接退出进程:
process.on('uncaughtException', (err) => { console.error('未捕获异常:', err.message, err.stack); }); process.on('unhandledRejection', (reason) => { console.error('未处理Promise异常:', reason); });
- 给SSR渲染逻辑加错误兜底,修改通用路由配置,渲染失败时直接返回静态CSR版本的index.html:
server.get('*', (req, res) => { res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] }, (err, html) => { if (err) { console.error('SSR渲染异常:', err.message); res.sendFile(join(distFolder, 'index.html')); return; } res.send(html); }); });
- 可选高可用配置:使用pm2托管SSR进程,配置异常自动重启,启动命令替换为
pm2 start npm --name "ssr-service" -- run serve:ssr,避免极端场景下服务不可用。
内容的提问来源于stack exchange,提问作者Santosh
相关产品推荐
相关产品推荐

