You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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}`);
});

问题原因

  1. 服务端无原生localStorage:Node.js环境不存在原生localStorage,你用domino模拟了window对象,但domino创建的window不包含localStorage属性,导致服务端注入LOCAL_STORAGE时返回undefined,调用getItem时触发报错。
  2. 注入逻辑未区分环境:当前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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 19:24:09