Angular Universal集成@ng-toolkit/universal LOCAL_STORAGE时SSR报错求助
Angular Universal SSR 中 localStorage 注入报错排查
问题现象
Angular应用已配置Angular Universal,但SSR未生效,排查后发现是直接使用window和localStorage导致。改用@ng-toolkit/universal提供的LOCAL_STORAGE令牌,实现了LocalService并在AppModule中配置注入逻辑,客户端通过ng serve/npm start运行正常,但执行npm run build:ssr构建并启动服务后,出现以下错误:
ERROR TypeError: Cannot read properties of undefined (reading 'getItem')
相关代码
LocalService 实现
import { Injectable, Inject } from '@angular/core'; import { LOCAL_STORAGE } from '@ng-toolkit/universal'; @Injectable({ providedIn: 'root' }) export class LocalService implements Storage { constructor(@Inject(LOCAL_STORAGE) private localStorage: any) {} get length(): number { return this.localStorage.length; } clear(): void { this.localStorage.clear(); } getItem(key: string): any { return this.localStorage.getItem(key); } key(index: number): string | null { return this.localStorage.key(index); } removeItem(key: string): void { this.localStorage.removeItem(key); } setItem(key: string, data: string): void { this.localStorage.setItem(key, data); } [key: string]: any; [index: number]: string; }
AppModule 配置
providers: [ { provide: LOCAL_STORAGE, useFactory: () => window.localStorage } ]
组件使用示例
import { LocalService } from '../services/local.service'; constructor(private localStorage: LocalService) this.localStorage.setItem('lang', 'en'); this.localStorage.getItem('lang');
server.ts 文件
import 'zone.js/dist/zone-node'; import * as express from 'express'; import { join } from 'path'; // Fix for non SSR modulеs const domino = require('domino'); const fs = require('fs'); const path = require('path'); const compression = require('compression'); const template = fs.readFileSync(path.join('.', 'dist/browser', 'index.html')).toString(); const win = domino.createWindow(template); // tslint:disable-next-line:no-string-literal global['window'] = win; // tslint:disable-next-line:no-string-literal global['document'] = win.document; // tslint:disable-next-line:no-string-literal global['DOMTokenList'] = win.DOMTokenList; // tslint:disable-next-line:no-string-literal global['Node'] = win.Node; // tslint:disable-next-line:no-string-literal global['Text'] = win.Text; // tslint:disable-next-line:no-string-literal global['HTMLElement'] = win.HTMLElement; // tslint:disable-next-line:no-string-literal global['navigator'] = win.navigator; // tslint:disable-next-line:no-string-literal global['MutationObserver'] = getMockMutationObserver(); function getMockMutationObserver() { return class { observe(node, options) { } disconnect() { } takeRecords() { return []; } }; } // End of Fix for non SSR modulеs // Express server const app = express(); const PORT = process.env.PORT || 4000; const DIST_FOLDER = join(process.cwd(), 'dist/browser'); // * NOTE :: leave this as require() since this file is built Dynamically from webpack const { AppServerModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap } = require('./dist/server/main'); // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP) ] })); app.set('view engine', 'html'); app.set('views', DIST_FOLDER); // Example Express Rest API endpoints // app.get('/api/**', (req, res) => { }); // Serve static files from /browser app.get('*.*', express.static(DIST_FOLDER, { maxAge: '1y' })); // All regular routes use the Universal engine app.get('*', (req, res) => { res.render('index', { req }); }); // Start up the Node server app.listen(PORT, () => { console.log(`Node Express server listening on http://localhost:${PORT}`); });
问题原因
- 服务端无原生localStorage:Node.js环境不存在原生
localStorage,你用domino模拟了window对象,但domino创建的window不包含localStorage属性,导致服务端注入LOCAL_STORAGE时返回undefined,调用getItem时触发报错。 - 注入逻辑未区分环境:当前
AppModule中的LOCAL_STORAGE工厂函数直接返回window.localStorage,服务端执行时window是模拟对象但无localStorage,因此注入值为undefined。
解决方案
步骤1:在服务端添加localStorage模拟实现
在server.ts中,设置global['window'] = win;之后添加模拟的localStorage:
// 模拟localStorage实现 const mockLocalStorage = (function() { let store = {}; return { getItem: function(key) { return store[key] || null; }, setItem: function(key, value) { store[key] = value.toString(); }, removeItem: function(key) { delete store[key]; }, clear: function() { store = {}; }, key: function(index) { const keys = Object.keys(store); return keys[index] || null; }, get length() { return Object.keys(store).length; } }; })(); // 挂载到模拟window和global对象 win.localStorage = mockLocalStorage; global['localStorage'] = mockLocalStorage;
步骤2:优化LOCAL_STORAGE注入逻辑,区分客户端/服务端
修改AppModule中的LOCAL_STORAGE提供者,使用PLATFORM_ID判断运行环境:
import { PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common'; // ... providers: [ { provide: LOCAL_STORAGE, useFactory: (platformId: Object) => { if (isPlatformBrowser(platformId)) { return window.localStorage; } else if (isPlatformServer(platformId)) { // 服务端返回模拟实现或空兜底 return { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {}, key: () => null, length: 0 }; } return null; }, deps: [PLATFORM_ID] } ]
步骤3:为LocalService添加空值兜底(可选)
在LocalService中添加判断,避免注入值为undefined时出错:
@Injectable({ providedIn: 'root' }) export class LocalService implements Storage { private readonly storage: Storage; constructor(@Inject(LOCAL_STORAGE) localStorage: any) { // 注入值不存在时使用空实现兜底 this.storage = localStorage || { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {}, key: () => null, length: 0 } as Storage; } get length(): number { return this.storage.length; } clear(): void { this.storage.clear(); } getItem(key: string): any { return this.storage.getItem(key); } // 其他方法同理,均使用this.storage代替原this.localStorage }
内容的提问来源于stack exchange,提问作者networkdavit
相关产品推荐
相关产品推荐

