Angular SSR应用中如何安全使用window、document、location等浏览器专属对象
我完全懂你现在的困扰——用Angular 18.2.12搭了SSR应用,构建和启动都没问题,但一用到window、localStorage这些浏览器独有的对象就报错,试了Domino、JSDOM也没搞定,而且项目里这些对象用得还挺多,改起来头大对吧?
下面给你几个实用的解决方案,都是Angular SSR场景下的最佳实践,不用依赖第三方库或者复杂配置:
1. 用Angular官方的平台检测(最推荐)
Angular本身提供了PLATFORM_ID令牌和isPlatformBrowser工具函数,可以帮你在代码里判断当前是运行在浏览器还是服务器端,只有在浏览器环境下才调用这些专属对象。
组件里直接使用的例子:
import { Component, Inject, PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Component({ selector: 'app-example', templateUrl: './example.component.html' }) export class ExampleComponent { constructor(@Inject(PLATFORM_ID) private platformId: Object) {} ngOnInit() { // 只有在浏览器环境下才访问localStorage if (isPlatformBrowser(this.platformId)) { const userData = localStorage.getItem('user'); console.log(userData); // 操作window对象 window.addEventListener('resize', () => { console.log('窗口大小变化了'); }); } } }
封装成全局服务(更适合大量使用的场景)
如果很多组件都要用到,可以封装一个服务来统一处理,避免重复代码:
import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class BrowserService { private isBrowser: boolean; constructor(@Inject(PLATFORM_ID) private platformId: Object) { this.isBrowser = isPlatformBrowser(this.platformId); } // 封装localStorage操作 getLocalStorageItem(key: string): string | null { return this.isBrowser ? localStorage.getItem(key) : null; } setLocalStorageItem(key: string, value: string): void { if (this.isBrowser) { localStorage.setItem(key, value); } } // 封装window对象的访问 getWindow(): Window | undefined { return this.isBrowser ? window : undefined; } // 同理可以封装document、location等 getDocument(): Document | undefined { return this.isBrowser ? document : undefined; } }
之后在组件里注入这个服务就行,不用每次都写平台检测的判断。
2. 服务器端模拟浏览器对象(兜底方案)
如果你的项目里这些对象用得太零散,改起来成本太高,可以尝试在server.ts里正确配置JSDOM来模拟浏览器环境——你之前的代码注释了这部分,可能是配置不对导致没生效。
修改你的server.ts,把JSDOM的配置放在最前面,确保Angular模块加载前就模拟好全局对象:
import 'zone.js/node'; import { renderModule } from '@angular/platform-server'; import express from 'express'; import { join } from 'path'; import AppServerModule from './src/main.server'; import { readFileSync } from 'fs'; import { JSDOM } from 'jsdom'; // 第一步:先创建JSDOM实例,模拟浏览器环境 const DIST_FOLDER = join(process.cwd(), 'dist/angular-ssr-project/browser'); const template = readFileSync(join(DIST_FOLDER, 'index.html')).toString(); const dom = new JSDOM(template); // 把模拟的window、document挂载到全局 global['window'] = dom.window; global['document'] = dom.window.document; global['navigator'] = dom.window.navigator; global['localStorage'] = dom.window.localStorage; global['sessionStorage'] = dom.window.sessionStorage; global['location'] = dom.window.location; const app = express(); const PORT = process.env['PORT'] || 4000; // Serve static files app.use(express.static(DIST_FOLDER, { maxAge: '1y', index: false })); // Serve SSR page app.get('*', (req, res) => { // 注意这里要用读取的template作为document,而不是硬写的'<app-root></app-root>' renderModule(AppServerModule, { document: template, url: req.url }).then(html => { res.send(html); }).catch(err => { console.error('SSR Rendering Error:', err); res.status(500).send('Error during SSR rendering'); }); }); // Start the server app.listen(PORT, () => { console.log(`Node Express server listening on http://localhost:${PORT}`); });
注意两点:一是要先加载JSDOM再导入Angular的模块,二是renderModule里的document要使用读取的index.html模板,而不是硬编码的字符串,这样模拟的DOM才和浏览器端一致。
3. 避免在服务器端执行的生命周期钩子中调用浏览器对象
Angular SSR会在服务器端执行组件的constructor、ngOnInit等钩子,所以不要在这些地方直接调用浏览器对象。如果必须要初始化一些依赖浏览器的逻辑,可以放在ngAfterViewInit(这个钩子只在浏览器端执行)里,或者用平台检测包裹。
比如:
ngAfterViewInit() { // 这里可以安全访问浏览器对象 const height = window.innerHeight; console.log('窗口高度:', height); }
额外提醒:处理localStorage的持久化问题
服务器端模拟的localStorage是内存级别的,每次重启服务器就会清空。如果你的业务依赖localStorage的持久化,最好在浏览器端再同步一次数据,或者在服务器端用数据库来存储对应的信息。
备注:内容来源于stack exchange,提问作者Kishan Bhola

