Skip to content

Angular SSR 环境下使用 localStorage 的解决方案

问题描述

在 Angular 17 中使用 SSR(服务器端渲染)时,访问 localStorage 会出现 "localStorage is not defined" 错误。这是因为:

  • 🌐 localStorage 是浏览器特有的 Web API
  • ⚙️ SSR 在服务端执行时无法访问浏览器环境
  • 🚫 直接在组件构造函数或生命周期钩子中调用 localStorage 会导致服务端报错

此问题常见于从 Angular 16 升级到 17 的项目中,尤其是启用了 SSR 的应用。虽然禁用 SSR 可以临时解决,但会牺牲服务器渲染的优势。

推荐解决方案

✅ 方案1:使用 afterNextRender(Angular 17+, 最佳实践)

官方推荐

Angular 17+ 原生提供了 afterNextRender 方法,专门解决浏览器专属 API 的访问问题。

typescript
import { Component, afterNextRender } from '@angular/core';

@Component({...})
export class UserComponent {
  loggedInUser: any;

  constructor() {
    afterNextRender(() => {
      // 此处代码仅在浏览器环境执行
      const storedData = localStorage.getItem('auth');
      if (storedData) {
        this.loggedInUser = JSON.parse(storedData);
      }
    });
  }
}

优势

  • 无需注入额外依赖
  • 精准把控执行时机
  • 代码简洁易维护

✅ 方案2:使用 isPlatformBrowser(兼容旧版)

通用检测平台类型的服务方案,支持 Angular 16+:

typescript
import { Component, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Component({...})
export class SessionService {
  constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

  saveUserSession(user: User): void {
    if (isPlatformBrowser(this.platformId)) {
      // 安全访问浏览器 API
      sessionStorage.setItem('userInfo', JSON.stringify(user));
    }
  }
}

使用场景

  • 在服务(Service)中存储数据
  • 需要支持 Angular 16 及以下版本
  • 跨组件复用的业务逻辑

🛠️ 方案3:通过 DOCUMENT 令牌注入(替代方案)

适用于直接 DOM 操作场景:

typescript
import { Component, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Component({...})
export class CounterComponent {
  constructor(@Inject(DOCUMENT) private document: Document) {
    const localStorage = this.document.defaultView?.localStorage;
    
    if (localStorage) {
      const counter = localStorage.getItem('counter') || '0';
      localStorage.setItem('counter', (+counter + 1).toString());
    }
  }
}

注意事项

此方案需要访问 defaultView 属性,需确保在正确的上下文中执行(避免过早调用)。

⚠️ 不推荐方案:全局环境检测

以下方式虽然可行但存在隐患:

typescript
// 风险:仍可能在初始化时访问
private isLocalStorageAvailable = typeof localStorage !== 'undefined';

// 风险:服务端可能执行 window 检测
if (typeof window !== 'undefined') {
  localStorage.setItem('key', 'value');
}

潜在问题

  • 条件检测可能被意外触发
  • 无法与 Angular SSR 生命周期完美集成
  • 增加调试复杂度

核心原理

🌍 为什么需要特殊处理?

环境可用 API执行时机
服务器环境Node.js API页面初始渲染阶段
浏览器环境Web API(localStorage)交互和后续操作阶段

🔧 解决方案运作机制

  • afterNextRender:延迟执行至 Angular 完成首次渲染
  • isPlatformBrowser:利用依赖注入标记运行环境
  • DOCUMENT 注入:通过 Angular 的 DOM 抽象层访问浏览器对象

总结与最佳实践

  1. 优先选择 afterNextRender(Angular 17+)
    • 语法简洁,执行时机明确
  2. 服务中使用 isPlatformBrowser
    • 适用于跨组件逻辑复用
  3. 避免在构造函数中访问浏览器 API
    • 数据初始化建议在 ngOnInit 中处理
  4. 不要禁用 SSR 来解决
    diff
    // 禁⽌ SSR 是下策(angular.json)
    "prerender": false,
    - "ssr": { "entry": "server.ts" }
    + "ssr": false

:::success 迁移策略 现有项目改造步骤:

  1. 定位所有 localStorage/sessionStorage 调用
  2. 根据使用位置选择包装方案(组件用 afterNextRender,服务用 isPlatformBrowser
  3. 对服务端逻辑添加降级处理(如返回默认值) :::

通过正确使用浏览器环境检测机制,可保持 SSR 优势的同时无缝集成 localStorage 功能。