web-dev-qa-db-fra.com

localStorage n'est pas défini (Angular Universal)

J'utilise universal-starter comme colonne vertébrale.

Lorsque mon client démarre, il lit un jeton sur les informations de l'utilisateur à partir de localStorage.

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

  bar() {}

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

    // do other things
  };
}

Tout fonctionne bien, mais je l’ai eu dans côté serveur (terminal) à cause du rendu du serveur:

EXCEPTION: ReferenceError: le stockage local n'est pas défini

J'ai eu l'idée de ng-conf-2016-universal-patterns que l'utilisation de l'injection de dépendance pour résoudre ce problème. Mais cette démo est vraiment ancienne.

Disons que j'ai ces deux fichiers maintenant:

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

À l'heure actuelle, ils utilisent les mêmes UserService. Quelqu'un peut-il donner des codes pour expliquer comment utiliser différentes injections de dépendance pour résoudre ce problème?

S'il existe un autre meilleur moyen que l'injection de dépendance, ce sera cool aussi.


UPDATE 1 J'utilise Angular 2 RC4, j'ai essayé de manière @ Martin. Mais même si je l'importe, cela me donne toujours une erreur dans le terminal ci-dessous:

Terminal (npm start)

/my-project/node_modules/@angular/core/src/di/reflective_provider.js:240 jette de nouvelles reflective_exceptions_1.NoAnnotationError (typeOrFunc, params); ^ Erreur: Impossible de résoudre tous les paramètres pour 'UserService' (Http,?). Assurez-vous que tous les paramètres sont décorés avec Inject ou avoir des annotations de type valides et que 'UserService' est décoré avec Injectable.

Terminal (npm run watch)

erreur TS2304: Impossible de trouver le nom 'LocalStorage'.

Je suppose qu’il est en quelque sorte dupliqué avec le LocalStorage de angular2-universal (bien que je n’utilise pas import { LocalStorage } from 'angular2-universal';), mais même j’ai essayé de changer le mien en LocalStorage2, cela ne fonctionne toujours pas.

Et pendant ce temps, mon IDE WebStorm apparaît également en rouge:

 enter image description here

BTW, j'ai trouvé un import { LocalStorage } from 'angular2-universal';, mais je ne sais pas comment l'utiliser.


UPDATE 2, je suis passé à (ne sais pas s'il existe un meilleur moyen):

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`

    // …
  };
}

Cela résout le problème dans UPADAT 1, mais maintenant, une erreur s'est produite dans le terminal:

EXCEPTION: TypeError: this.localStorage.getItem n'est pas une fonction

18
Hongbo Miao

Mise à jour pour les nouvelles versions de Angular

OpaqueToken a été remplacé par InjectionToken qui fonctionne pratiquement de la même manière, à la différence qu’il possède une interface générique InjectionToken<T> qui permet une meilleure vérification de type et une meilleure déduction. 

Réponse originale

Deux choses:

  1. Vous n'injectez aucun objet contenant l'objet localStorage, vous essayez d'y accéder directement en tant que global. Tout accès global devrait être le premier indice que quelque chose ne va pas. 
  2. Il n'y a pas de window.localStorage dans nodejs. 

Ce que vous devez faire, c'est injecter un adaptateur pour localStorage qui fonctionnera à la fois pour le navigateur et pour NodeJS. Cela vous donnera également du code testable.

dans local-storage.ts: 

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

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

Dans votre main.browser.ts, nous injecterons l’objet localStorage à partir de votre navigateur:

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

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

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

Et ensuite, dans main.node.ts, nous utiliserons un objet vide:

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

Ensuite, votre service injecte ceci:

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

export class UserService {

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

    loadCurrentUser() {

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

Dans Angular 4 (et 5), vous pouvez facilement traiter ce problème avec une fonction simple de la manière suivante:

app.module.ts

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

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

Si vous avez un fichier fractionné serveur/client AppModule, placez-le dans le fichier app.module.shared.ts - la fonction ne cassera pas votre code - sauf si vous devez imposer des comportements complètement différents pour les versions serveur et client. Si tel est le cas, il serait peut-être plus sage de mettre en place une fabrique de classes personnalisée, tout comme cela a été montré dans d’autres réponses.

Quoi qu'il en soit, une fois que vous avez terminé avec l'implémentation du fournisseur, vous pouvez injecter le générique LOCALSTORAGE dans n'importe quel composant angulaire et vérifier le type de plate-forme avec la fonction angulaire-native isPlatformBrowser avant de l'utiliser:

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.
        }
    }
}

Il convient de noter que, puisque la fonction getLocalStorage() renverra null si l’objet window n’est pas disponible, vous pouvez simplement vérifier la nullabilité de this.localStorage et ignorer entièrement la vérification du type de plate-forme. Cependant, je recommande fortement l’approche ci-dessus car l’implémentation de la fonction (et la valeur de retour) pourrait être modifiée à l’avenir; à l'inverse, les valeurs de retour isPlatformBrowser/isPlatformServer peuvent être approuvées par conception.

Pour plus d'informations, consultez cet article de blog que j'ai écrit sur le sujet.

6
Darkseal

Je ne pense pas que ce soit une bonne solution, mais le générateur d'aspnetcore-spa m'a posé le même problème et je l'ai résolu de cette façon:

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

  bar() {}

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

    // do other things
  };
}

Cette condition empêche le code client de s'exécuter sur le serveur où l'objet 'window' n'existe pas. 

4
Gregory Khakov

J'ai un problème similaire avec Angular 4 + Universal en suivant les étapes ici pour configurer un SPA qui peut être rendu côté client ou côté serveur.

J'utilise oidc-client parce que j'ai besoin de mon SPA pour agir en tant que client OpenId Connect/Oauth2 pour mon serveur d'identité.

Le problème, c’est que j’avais le problème typique où localStorage ou sessionStorage ne sont pas définis côté serveur (ils n’existent que s’il ya un objet window, donc il n’a pas de sens que nodeJs ait ces objets).

J'ai essayé sans succès l'approche de mocker le stockage localStorage ou sessionStorage et d'utiliser le réel lorsque vous êtes dans le navigateur et un serveur vide sur le serveur.

Mais je suis arrivé à la conclusion que pour mes besoins, je n'ai pas vraiment besoin de localStorage ou de sessionStorage pour tout faire côté serveur. S'il est exécuté dans NodeJs, ignorez simplement la partie où sessionStorage ou localStorage est utilisé. L'exécution aura alors lieu côté client.

Cela suffirait:

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

Dans le rendu côté client, il affiche: Window is: object

Dans nodeJs, il affiche: Window is: undefined

La beauté de cela est qu'Annular Universal ignorera simplement l'exécution/le rendu côté serveur lorsqu'il n'y a pas d'objet window, MAIS cette exécution fonctionnera correctement si Angular Universal envoie la page avec le javascript au navigateur, donc même si je suis en cours d'exécution mon application dans NodeJs mon navigateur affiche finalement les informations suivantes: Window is: object

Je sais que ce n'est pas une bonne réponse pour ceux qui ont vraiment besoin d'accéder à localStorage ou sessionStorage côté serveur, mais dans la plupart des cas, nous utilisons Angular Universal simplement pour restituer tout ce qu'il est possible de rendre côté serveur, et pour l'envoi des choses qui ne peut pas être rendu au navigateur pour fonctionner normalement.

3
iberodev

Merci pour la grande aide de Martin. Mais il y a plusieurs endroits ci-dessous qui doivent être mis à jour pour que cela fonctionne:

  • constructor dans user.service.ts
  • useValue dans main.node.ts , main.browser.ts

Voici à quoi mes codes ressemblent maintenant. 

J'adorerais accepter la réponse de @ Martin lors de la mise à jour.

BTW, j'ai trouvé un import { LocalStorage } from 'angular2-universal';, mais Je ne sais pas comment l'utiliser.

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

local-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);
}
2
Hongbo Miao

C’est ainsi que nous l’avons dans notre projet, à partir de ce 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')
        }
    }    
}
1
AJT_82

LocalStorage non implémenté par le côté serveur. Nous devons choisir d’écrire en fonction du code de la plate-forme ou d’utiliser un cookie pour enregistrer et lire les données .ngx-cookie est la meilleure solution (que je connaisse) pour utiliser les cookies sur le serveur et le navigateur . Vous pouvez écrire une extension de classe de stockage. avec cookie work: 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);
  }
}
0
gorniv

Je n'ai pas une connaissance suffisante de la préparation d'applications angulaires pour exécuter des serveurs. Mais dans le même scénario pour react & nodejs, il faut que le serveur sache ce que localStorage est. Par exemple:

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

J'espère que cela peut vous aider.

0
Amid