LocalStorage не определен (Angular Universal)

Я использую universal-starter в качестве основы.

Когда мой клиент запускается, он читает токен о пользовательской информации из localStorage.

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

  bar() {}

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

    // do other things
  };
}

Все работает хорошо, однако я получил это на серверной стороне (терминал) из-за рендеринга сервера:

ИСКЛЮЧЕНИЕ: ReferenceError: localStorage не определен

У меня появилась идея от ng-conf-2016-universal-patterns, которая использует Dependency Injection для решения этой проблемы. Но эта демонстрация действительно старая.

Скажем, у меня есть эти два файла:

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)

/мой-проект/node_modules/@angular/core/src/di/reflective_provider.js:240         throw new reflective_exceptions_1.NoAnnotationError(typeOrFunc, params);         ^ Ошибка: не удается разрешить все параметры для "UserService" (Http,?). Убедитесь, что все параметры украшены вводом или имеют действительные аннотации типов и что "UserService" украшен Вводимый.

Терминал (просмотр в режиме npm)

ошибка TS2304: Не удается найти имя "LocalStorage".

Я думаю, что он каким-то образом дублируется с LocalStorage от angular2-universal (хотя я не использую import { LocalStorage } from 'angular2-universal';), но даже я пытался изменить мой на LocalStorage2, все еще не работает.

И тем временем мой IDE WebStorm также показывает красный цвет:

введите описание изображения здесь

Кстати, я нашел import { LocalStorage } from 'angular2-universal';, но не знаю, как это использовать.


ОБНОВЛЕНИЕ 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, но теперь я получил ошибку в терминале:

ИСКЛЮЧЕНИЕ: TypeError: this.localStorage.getItem не является функцией

Ответ 1

Обновление для более новых версий Angular

OpaqueToken был заменен InjectionToken который работает практически таким же образом, за исключением того, что имеет универсальный интерфейс InjectionToken<T> который обеспечивает лучшую проверку типов и вывод.

Оригинальный ответ

Две вещи:

  1. Вы не внедряете какой-либо объект, который содержит объект localStorage, вы пытаетесь получить к нему прямой доступ как глобальный. Любой глобальный доступ должен быть первым признаком того, что что-то не так.
  2. В nodejs нет window.localStorage.

Вам нужно добавить адаптер для localStorage, который будет работать как для браузера, так и для NodeJS. Это также даст вам тестируемый код.

в local-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');
        ...
    };
}

Ответ 2

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

Если у вас есть файл AppModule для сервера/клиента, поместите его в файл app.module.shared.ts - функция не нарушит ваш код - если вам не нужно применять совершенно разные app.module.shared.ts поведения для сборок сервера и клиента; если это так, то было бы разумнее вместо этого реализовать собственную фабрику классов, как это было показано в других ответах.

В любом случае, как только вы закончите реализацию провайдера, вы можете внедрить универсальный LOCALSTORAGE в любой компонент Angular и проверить тип платформы с помощью функции 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() функция возвращает null, если объект окна разве доступен, вы можете просто проверить this.localStorage допустимости пустого и полностью пропустить проверку типа платформы. Тем не менее, я настоятельно рекомендую вышеупомянутый подход, так как реализация функции (и возвращаемое значение) может быть изменена в будущем; и наоборот, возвращаемые значения isPlatformBrowser/isPlatformServer - это то, чему можно доверять при разработке.

Для получения дополнительной информации, проверьте этот пост в блоге, который я написал на эту тему.

Ответ 3

Я не думаю, что это хорошее решение, но я столкнулся с той же проблемой, используя генератор aspnetcore-spa и решил его следующим образом:

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

  bar() {}

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

    // do other things
  };
}

Это условие предотвращает запуск клиентского кода на стороне сервера, где объект "окна" не существует.

Ответ 4

У меня есть аналогичная проблема с Angular 4 + Universal после шаги здесь, чтобы настроить SPA, который может отображать на стороне клиента или сервер.

Я использую oidc-client, потому что мне нужен мой SPA для работы в качестве клиента OpenId Connect/Oauth2 для моего сервера удостоверений.

Дело в том, что у меня была типичная проблема, когда localStorage или sessionStorage не определены на стороне сервера (они существуют только там, где есть объект окна, поэтому для узлов не было смысла иметь эти объекты).

Я безуспешно пытался подходить к подделке 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

Красота этого заключается в том, что Angular Universal просто игнорирует выполнение/рендеринг на стороне сервера, когда нет объекта окна, НО это выполнение будет работать нормально, когда Angular Universal отправит страницу с javascript в браузер, поэтому, даже если я запускаю свое приложение в NodeJ, в конечном итоге мой браузер печатает следующее: Window is: object

Я знаю, что это не правильный ответ тем, кто действительно нуждается в доступе к localStorage или sessionStorage на стороне сервера, но для большинства случаев мы используем Angular Universal просто для рендеринга, который можно отобразить на стороне сервера, и для отправки вещей, которые не могут быть обработаны браузером для нормальной работы.

Ответ 5

Спасибо за @Martin отличную помощь. Но есть несколько мест ниже, которые нужно обновить, чтобы заставить их работать:

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

Вот как выглядят мои коды сейчас.

Я бы с удовольствием принял @Martin ответ, когда он обновил.

Кстати, я нашел a import { LocalStorage } from 'angular2-universal';, но не знаете, как это использовать.

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

Ответ 6

Так мы имеем это в нашем проекте, как из этого 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')
        }
    }    
}

Ответ 7

У меня нет достаточных знаний о подготовке приложений angular для запуска серверов. Но в подобном сценарии для реакции и nodejs, что нужно сделать, это сообщить серверу, что localStorage. Например:

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

Надеюсь, это может помочь вам.

Ответ 8

Каким бы странным ни казался этот подход, он работает, и мне не пришлось ничего делать из того, что предлагают другие ответы.

Шаг 1

Установите localstorage-polyfill: https://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

Ответ 9

LocalStorage не реализован на стороне сервера. Нам нужно выбрать запись в зависимости от кода платформы или использовать куки для сохранения и чтения данных. ngx-cookie - лучшее решение (ngx-cookie мне известно) для работы с cookie на сервере и в браузере. Вы можете написать расширенный класс Storage с помощью cookie cookie: 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);
  }
}

Ответ 10

Эти шаги решили мою проблему:

Шаг 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)

Ответ 11

Решена проблема "getItem undefined" в следующих строках:

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

Ответ 12

Я также столкнулся с той же проблемой. Вы можете писать внутри "isBrowser", импортируя инструкцию ниже.

import { isBrowser } from 'angular2-universal';