localStorage 未定义(Angular 通用)

localStorage is not defined (Angular Universal)

我正在使用 universal-starter 作为 backbone。

当我的客户端启动时,它从 localStorage 中读取了一个关于用户信息的令牌。

@Injectable()
export class UserService {
  foo() {}

  bar() {}

  loadCurrentUser() {
    const token = localStorage.getItem('token');

    // do other things
  };
}

一切正常,但是由于服务器渲染,我在服务器端(终端)得到了这个:

EXCEPTION: ReferenceError: localStorage is not defined

我从 ng-conf-2016-universal-patterns 那里得到了使用依赖注入来解决这个问题的想法。不过那个demo真的很老

假设我现在有这两个文件:

main.broswer.ts

export function ngApp() {
  return bootstrap(App, [
    // ...

    UserService
  ]);
}

main.node.ts

export function ngApp(req, res) {
  const config: ExpressEngineConfig = {
    // ...
    providers: [
      // ...
      UserService
    ]
  };

  res.render('index', config);
}

现在他们使用相同的 UserService。有人可以提供一些代码来解释如何使用不同的依赖注入来解决这个问题吗?

如果有其他比依赖注入更好的方法,那也很酷。


更新 1 我正在使用 Angular 2 RC4,我尝试了@Martin 的方式。但即使我导入它,它仍然在下面的终端中给我错误:

终端(npm 启动)

/my-project/node_modules/@angular/core/src/di/reflective_provider.js:240 throw new reflective_exceptions_1.NoAnnotationError(typeOrFunc, params); ^ Error: Cannot resolve all parameters for 'UserService'(Http, ?). Make sure that all the parameters are decorated with Inject or have valid type annotations and that 'UserService' is decorated with Injectable.

终端(npm 运行手表)

error TS2304: Cannot find name 'LocalStorage'.

我猜它与 angular2-universal 中的 LocalStorage 有某种重复(虽然我没有使用 import { LocalStorage } from 'angular2-universal';),但即使我试图将我的更改为 LocalStorage2 , 还是不行。

与此同时,我的 IDE WebStorm 也显示为红色:

顺便说一句,我找到了 import { LocalStorage } from 'angular2-universal';,但不确定如何使用它。


UPDATE 2,我改成了(不知道有没有更好的办法):

import { Injectable, Inject } from '@angular/core';
import { Http } from '@angular/http';
import { LocalStorage } from '../../local-storage';

@Injectable()
export class UserService {
  constructor (
    private _http: Http,
    @Inject(LocalStorage) private localStorage) {}  // <- this line is new

  loadCurrentUser() {
    const token = this.localStorage.getItem('token'); // here I change from `localStorage` to `this.localStorage`

    // …
  };
}

这解决了 UPADAT 1 中的问题,但现在我在终端中遇到错误:

EXCEPTION: TypeError: this.localStorage.getItem is not a function

我没有足够的知识准备 angular 应用到 运行 服务器端。但是在react & nodejs 的类似场景中,需要做的是让服务器知道localStorage 是什么。例如:

//Stub for localStorage
(global as any).localStorage = {
  getItem: function (key) {
    return this[key];
  },
  setItem: function (key, value) {
    this[key] = value;
  }
};

希望对您有所帮助。

Angular

更新版本更新

OpaqueTokenInjectionToken 取代,后者的工作方式大致相同——除了它有一个通用接口 InjectionToken<T>,这有助于更好的类型检查和推理。

原答案

两件事:

  1. 您没有注入任何包含 localStorage 对象的对象,您正试图将其作为全局对象直接访问。任何全局访问都应该是出现问题的第一个线索。
  2. nodejs中没有window.localStorage。

您需要做的是为 localStorage 注入一个适用于浏览器和 NodeJS 的适配器。这也将为您提供可测试的代码。

本地-storage.ts:

import { OpaqueToken } from '@angular/core';

export const LocalStorage = new OpaqueToken('localStorage');

在您的 main.browser.ts 中,我们将从您的浏览器中注入实际的 localStorage 对象:

import {LocalStorage} from './local-storage.ts';

export function ngApp() {
  return bootstrap(App, [
    // ...

    UserService,
    { provide: LocalStorage, useValue: window.localStorage}
  ]);

然后在main.node.ts中我们将使用一个空对象:

... 
providers: [
    // ...
    UserService,
    {provide: LocalStorage, useValue: {getItem() {} }}
]
...

然后你的服务注入这个:

import { LocalStorage } from '../local-storage';

export class UserService {

    constructor(@Inject(LocalStorage) private localStorage: LocalStorage) {}

    loadCurrentUser() {

        const token = this.localStorage.getItem('token');
        ...
    };
}

感谢@Martin 的大力帮助。但是下面有几个地方需要更新才能让它工作:

  • constructoruser.service.ts
  • useValuemain.node.ts, main.browser.ts

这就是我的代码现在的样子。

@Martin 更新后我很乐意接受他的回答。

BTW, I found a import { LocalStorage } from 'angular2-universal';, but not sure how to use that.

user.service.ts

import { Injectable, Inject } from '@angular/core';

import { LocalStorage } from '../local-storage';

@Injectable()
export class UserService {
  constructor (
    @Inject(LocalStorage) private localStorage) {}

  loadCurrentUser() {
    const token = localStorage.getItem('token');

    // do other things
  };
}

本地-storage.ts

import { OpaqueToken } from '@angular/core';

export const LocalStorage = new OpaqueToken('localStorage');

main.broswer.ts

import { LocalStorage } from './local-storage';

export function ngApp() {
  return bootstrap(App, [
    // ...

    { provide: LocalStorage, useValue: window.localStorage},
    UserService
  ]);
}

main.node.ts

import { LocalStorage } from './local-storage';

export function ngApp(req, res) {
  const config: ExpressEngineConfig = {
    // ...
    providers: [
      // ...

      { provide: LocalStorage, useValue: { getItem() {} }},
      UserService
    ]
  };

  res.render('index', config);
}

我认为这不是一个好的解决方案,但我在使用 aspnetcore-spa 生成器时遇到了同样的问题,并以这种方式解决了它:

@Injectable()
export class UserService {
  foo() {}

  bar() {}

  loadCurrentUser() {
    if (typeof window !== 'undefined') {
       const token = localStorage.getItem('token');
    }

    // do other things
  };
}

此条件阻止客户端代码从 运行 在 'window' 对象不存在的服务器端。

我也 运行 遇到了同样的问题。 您可以通过导入以下语句在 'isBrowser' 内部写入检查。

import { isBrowser } from 'angular2-universal';

我遇到了与 Angular 4 + Universal 类似的问题 steps here 配置可以在客户端或服务器端呈现的 SPA。

我正在使用 oidc-client 因为我需要我的 SPA 充当我的身份服务器的 OpenId Connect/Oauth2 客户端。

问题是我遇到了典型的问题,其中 localStorage 或 sessionStorage 未在服务器端定义(它们仅在存在 window 对象时存在,因此 nodeJs 没有意义这些对象)。

我没有成功尝试模拟 localStorage 或 sessionStorage 的方法,并在浏览器中使用真实的,在服务器中使用空的。

但我得出的结论是,根据我的需要,我真的不需要 localStorage 或 sessionStorage 在服务器端执行任何操作。如果在NodeJs中执行,直接跳过使用sessionStorage或localStorage的部分,在客户端执行。

这就足够了:

console.log('Window is: ' + typeof window);
    this.userManager = typeof window !== 'undefined'? new oidc.UserManager(config) : null; //just don't do anything unless there is a window object

在客户端渲染中打印: Window is: object

在 nodeJs 中打印: Window is: undefined

这样做的美妙之处在于,当没有 window 对象时,Angular Universal 将简单地忽略服务器端的 execution/rendering,但是当 Angular Universal 将带有 javascript 的页面发送到浏览器,因此即使我是 运行 我在 NodeJs 中的应用程序最终我的浏览器会打印以下内容: Window is: object

我知道对于那些真正需要在服务器端访问 localStorage 或 sessionStorage 的人来说,这不是一个正确的答案,但对于大多数情况,我们使用 Angular Universal 只是为了呈现可能呈现的内容服务器端,以及将无法渲染的东西发送到浏览器正常工作。

这就是我们项目中的方式,根据这个 github comment:

import { OnInit, PLATFORM_ID, Inject } from '@angular/core';
import { isPlatformServer, isPlatformBrowser } from '@angular/common';

export class SomeClass implements OnInit {

    constructor(@Inject(PLATFORM_ID) private platformId: Object) { }

    ngOnInit() {
        if (isPlatformServer(this.platformId)) {
            // do server side stuff
        }

        if (isPlatformBrowser(this.platformId)) {
           localStorage.setItem('myCats', 'Lissie & Lucky')
        }
    }    
}

在 Angular 4(和 5)中,您可以通过以下方式使用简单的函数轻松处理此类问题:

app.module.ts

@NgModule({
    providers: [
        { provide: 'LOCALSTORAGE', useFactory: getLocalStorage }
    ]
})
export class AppModule {
}

export function getLocalStorage() {
    return (typeof window !== "undefined") ? window.localStorage : null;
}

如果您有一个 server/client 拆分文件 AppModule,请将它放在 app.module.shared.ts 文件中 - 该函数不会破坏您的代码 - 除非您需要为服务器和客户端构建实施完全不同的行为;如果是这种情况,那么实施自定义 class 工厂可能更明智,就像其他答案中显示的那样。

无论如何,一旦你完成了提供者的实现,你就可以在任何 Angular 组件中注入 LOCALSTORAGE 泛型,并使用 Angular-native 检查平台类型isPlatformBrowser 使用前的功能:

import { PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';

@Injectable()
export class SomeComponent {
    constructor(
        @Inject(PLATFORM_ID) private platformId: any,
        @Inject('LOCALSTORAGE') private localStorage: any) {

        // do something

    }

    NgOnInit() {
        if (isPlatformBrowser(this.platformId)) {
            // localStorage will be available: we can use it.
        }
        if (isPlatformServer(this.platformId)) {
            // localStorage will be null.
        }
    }
}

值得注意的是,由于 getLocalStorage() 函数将 return null 如果 window 对象不可用,您可以检查 this.localStorage 可空性并完全跳过平台类型检查。但是,我强烈推荐上述方法,因为该函数实现(和 return 值)将来可能会发生变化;相反,isPlatformBrowser / isPlatformServer return 值在设计上是可以信任的。

有关详细信息,请查看我就该主题撰写的 this blog post

尽管这种方法看起来很奇怪,但它确实有效,而且我必须对其他答案所建议的管道进行 none。

步骤 1

安装localstorage-polyfillhttps://github.com/capaj/localstorage-polyfill

步骤 2

假设您遵循了以下步骤:https://github.com/angular/angular-cli/wiki/stories-universal-rendering,您的项目根文件夹中应该有一个名为 server.js 的文件。

在此server.js中添加:

import 'localstorage-polyfill'

global['localStorage'] = localStorage;

步骤 3

重建您的项目,npm run build:ssr,现在一切正常。


以上方法行得通吗?是的,据我所知。

这是最好的吗?也许不是

有任何性能问题吗?从来没听说过。开导我。

然而,就目前而言,这是让我的 localStorage 通过

的最愚蠢、最干净的方法

服务器端未实现 LocalStorage。我们需要选择写依赖于平台的代码还是使用cookie来保存和读取数据。 ngx-cookie 是在服务器和浏览器上使用 cookie 的最佳解决方案(据我所知)。 您可以使用 cookie work 编写 extend Storage class : universal.storage.ts

import { Injectable } from '@angular/core';
import { CookieService } from 'ngx-cookie';

@Injectable()
export class UniversalStorage implements Storage {
  [index: number]: string;
  [key: string]: any;
  length: number;
  cookies: any;

  constructor(private cookieService: CookieService) {}

  public clear(): void {
    this.cookieService.removeAll();
  }

  public getItem(key: string): string {
    return this.cookieService.get(key);
  }

  public key(index: number): string {
    return this.cookieService.getAll().propertyIsEnumerable[index];
  }

  public removeItem(key: string): void {
    this.cookieService.remove(key);
  }

  public setItem(key: string, data: string): void {
    this.cookieService.put(key, data);
  }
}

这些步骤解决了我的问题:

步骤 1: 运行 此命令:

npm i localstorage-polyfill --save

步骤 2:server.ts 文件中添加这两行:

import 'localstorage-polyfill'

global['localStorage'] = localStorage;

完成后,运行构建命令(例如:npm run build:serverless

一切就绪。再次启动服务器,可以看到问题已经解决

注:使用localStorage不window.localStorage,例如:localStorage.setItem(keyname, value)

通过以下几行解决了 "getItem undefined" 问题:

if(window.localStorage){
      return window.localStorage.getItem('user');
}

您可以使用PLATFORM_ID令牌来检查当前平台是浏览器还是服务器。

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

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html"
})
export class AppComponent implements OnInit {

   constructor(@Inject(PLATFORM_ID) private platformId: Object) {  }

   ngOnInit() {

     // Client only code.
     if (isPlatformBrowser(this.platformId)) {
        let item = {key1: 'value1', key2: 'valu2' };
        localStorage.setItem( itemIndex, JSON.stringify(item) );
     }

   }
 }