如何将窗口注入到服务中?

我正在用 TypeScript 编写一个 Angular 2服务,它将利用 localstorage。我想注入一个对浏览器 window对象的引用到我的服务中,因为我不想引用任何像 Angular 1.x $window这样的全局变量。

我该怎么做?

156093 次浏览

您可以在设置提供程序之后直接注入:

import {provide} from 'angular2/core';
bootstrap(..., [provide(Window, {useValue: window})]);


constructor(private window: Window) {
// this.window
}

截止到今天(2016年4月) ,上一个解决方案中的代码已经不能工作了,我认为可以直接将窗口注入 App.ts,然后收集你需要的值到应用程序中的全局访问服务中,但是如果你更喜欢创建和注入你自己的服务,一个更简单的解决方案是这样的。

Https://gist.github.com/willdelavega777/9afcbd6cc661f4107c2b74dd6090cebf

//--------------------------------------------------------------------------------------------------
// Imports Section:
//--------------------------------------------------------------------------------------------------
import {Injectable} from 'angular2/core'
import {window} from 'angular2/src/facade/browser';


//--------------------------------------------------------------------------------------------------
// Service Class:
//--------------------------------------------------------------------------------------------------
@Injectable()
export class WindowService
{
//----------------------------------------------------------------------------------------------
// Constructor Method Section:
//----------------------------------------------------------------------------------------------
constructor(){}


//----------------------------------------------------------------------------------------------
// Public Properties Section:
//----------------------------------------------------------------------------------------------
get nativeWindow() : Window
{
return window;
}
}

我知道问题是如何将 window 对象注入到组件中,但是您这样做似乎只是为了访问 localStorage。如果您真的只想要 localStorage,那么为什么不使用公开这些内容的服务,比如 H5webStorage。然后您的组件将描述其真正的依赖关系,这使您的代码更具可读性。

这是我目前的工作(2018-03,角度5.2与 AoT,测试在角-cli 和自定义 webpack 构建) :

首先,创建一个提供窗口引用的可注入服务:

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


// This interface is optional, showing how you can add strong typings for custom globals.
// Just use "Window" as the type if you don't have custom global stuff
export interface ICustomWindow extends Window {
__custom_global_stuff: string;
}


function getWindow (): any {
return window;
}


@Injectable()
export class WindowRefService {
get nativeWindow (): ICustomWindow {
return getWindow();
}
}

现在,在您的根 AppModule 中注册该服务,这样它就可以被注入到任何地方:

import { WindowRefService } from './window-ref.service';


@NgModule({
providers: [
WindowRefService
],
...
})
export class AppModule {}

然后再注射 window:

import { Component} from '@angular/core';
import { WindowRefService, ICustomWindow } from './window-ref.service';


@Component({ ... })
export default class MyCoolComponent {
private _window: ICustomWindow;


constructor (
windowRef: WindowRefService
) {
this._window = windowRef.nativeWindow;
}


public doThing (): void {
let foo = this._window.XMLHttpRequest;
let bar = this._window.__custom_global_stuff;
}
...

如果在应用程序中使用 nativeDocument和其他全局变量,您也可能希望以类似的方式向该服务添加这些全局变量。


编辑: 根据 Truchainz 的建议更新。 编者2: 更新为角度2.1.2 编者: 增加了 AoT 注释 编者4: 添加 any类型的解决方案说明 Ed5: 更新了使用 WindowRefService 的解决方案,该解决方案修复了在使用不同版本的上一个解决方案时出现的错误 编辑6: 添加示例自定义 Window 类型

随着角度2.0.0-rc 的释放,引入了5 NgModule。以前的解决方案对我不起作用了。我就是这么解决的:

应用程序模块:

@NgModule({
providers: [
{ provide: 'Window',  useValue: window }
],
declarations: [...],
imports: [...]
})
export class AppModule {}

在某些方面:

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


@Component({...})
export class MyComponent {
constructor (@Inject('Window') window: Window) {}
}

您也可以使用 OpaqueToken代替字符串‘ Window’

编辑:

AppModule 用于在 main.ts 中引导应用程序,如下所示:

import { platformBrowserDynamic  } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';


platformBrowserDynamic().bootstrapModule(AppModule)

有关 NgModule 的更多信息,请阅读 Angular 2文档: https://angular.io/docs/ts/latest/guide/ngmodule.html

我用 OpaqueToken表示“窗口”字符串:

import {unimplemented} from '@angular/core/src/facade/exceptions';
import {OpaqueToken, Provider} from '@angular/core/index';


function _window(): any {
return window;
}


export const WINDOW: OpaqueToken = new OpaqueToken('WindowToken');


export abstract class WindowRef {
get nativeWindow(): any {
return unimplemented();
}
}


export class BrowserWindowRef extends WindowRef {
constructor() {
super();
}
get nativeWindow(): any {
return _window();
}
}




export const WINDOW_PROVIDERS = [
new Provider(WindowRef, { useClass: BrowserWindowRef }),
new Provider(WINDOW, { useFactory: _window, deps: [] }),
];

并用于导入 WINDOW_PROVIDERS在引导角度2.0。0-rc-4。

但随着 Angular 2.0.0-rc.5的发布,我需要创建一个单独的模块:

import { NgModule } from '@angular/core';
import { WINDOW_PROVIDERS } from './window';


@NgModule({
providers: [WINDOW_PROVIDERS]
})
export class WindowModule { }

并且刚刚在我的主 app.module.ts的导入属性中定义

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';


import { WindowModule } from './other/window.module';


import { AppComponent } from './app.component';


@NgModule({
imports: [ BrowserModule, WindowModule ],
declarations: [ ... ],
providers: [ ... ],
bootstrap: [ AppComponent ]
})
export class AppModule {}

在角度 RC4下面的作品是上面的一些答案的组合,在你的根应用程序中添加它的提供者:

@Component({
templateUrl: 'build/app.html',
providers: [
anotherProvider,
{ provide: Window, useValue: window }
]
})

然后在您的服务等注入到构造函数

constructor(
@Inject(Window) private _window: Window,
)

为了让它在 Angular 2.1.1上工作,我必须使用字符串 @Inject窗口

  constructor( @Inject('Window') private window: Window) { }

然后像这样嘲笑它

beforeEach(() => {
let windowMock: Window = <any>{ };
TestBed.configureTestingModule({
providers: [
ApiUriService,
{ provide: 'Window', useFactory: (() => { return windowMock; }) }
]
});

在普通的 @NgModule中,我是这样提供的

{ provide: 'Window', useValue: window }

Angular 4引入了 InjectToken,它们还为文档创建了一个名为 文件的令牌。我认为这是官方的解决方案,它在 AoT 中起作用。

我使用相同的逻辑来创建一个名为 窗口令牌的小库,以防止重复这样做。

我在其他项目中使用过它,在 AoT 中构建时也没有出现问题。

下面是我在 另一个包裹中使用它的方法

这是 混蛋

在你的模块里

导入: [ BrowserModule,WindowTokenModule ] 在你的组件里

构造函数(@Inject (WINDOW) _ WINDOW){}

这是我在使用角4 AOT 时找到的最短/最干净的答案

来源: Https://github.com/angular/angular/issues/12631#issuecomment-274260009

@Injectable()
export class WindowWrapper extends Window {}


export function getWindow() { return window; }


@NgModule({
...
providers: [
{provide: WindowWrapper, useFactory: getWindow}
]
...
})
export class AppModule {
constructor(w: WindowWrapper) {
console.log(w);
}
}

在@Component 声明之前,您也可以这样做,

declare var window: any;

现在编译器实际上将允许您访问全局窗口变量,因为您将其声明为假定的全局变量,类型为 any。

我不建议在你的应用程序中访问任何地方的窗口,但是,你应该创建访问/修改所需的窗口属性的服务(并将这些服务注入到你的组件中) ,以确定你可以用窗口做什么,而不让它们修改整个窗口对象。

@ maxisam 感谢 窗口令牌。我也在做类似的事情,但是换成了你的。这是我监听窗口大小调整事件并通知订阅者的服务。

import { Inject, Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import { WINDOW } from 'ngx-window-token';




export interface WindowSize {
readonly width: number;
readonly height: number;
}


@Injectable()
export class WindowSizeService {


constructor( @Inject(WINDOW) private _window: any ) {
Observable.fromEvent(_window, 'resize')
.auditTime(100)
.map(event => <WindowSize>{width: event['currentTarget'].innerWidth, height: event['currentTarget'].innerHeight})
.subscribe((windowSize) => {
this.windowSizeChanged$.next(windowSize);
});
}


readonly windowSizeChanged$ = new BehaviorSubject<WindowSize>(<WindowSize>{width: this._window.innerWidth, height: this._window.innerHeight});
}

又短又甜,效果很好。

这已经足够了

export class AppWindow extends Window {}

也确实如此

{ provide: 'AppWindow', useValue: window }

让 AOT 高兴

你可以在角度4上使用 NgZone:

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


constructor(private zone: NgZone) {}


print() {
this.zone.runOutsideAngular(() => window.print());
}

有一个机会直接访问窗口的对象通过文档

document.defaultView == window

当全局变量在整个应用程序中都可以访问时,通过 DI (依赖注入)获取窗口对象并不是一个好主意。

但是如果你不想使用窗口对象,那么你也可以使用 self关键字,也指向窗口对象。

简单点,伙计们!

export class HeroesComponent implements OnInit {
heroes: Hero[];
window = window;
}


<div>\{\{window.Object.entries({ foo: 1 }) | json}}</div>

可以从注入的文档中获取窗口。

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


export class MyClass {


private window: Window;


constructor(@Inject(DOCUMENT) private document: Document) {
this.window = this.document.defaultView;
}


check() {
console.log(this.document);
console.log(this.window);
}


}

实际上访问窗口对象非常简单 这是我的基本组件,我测试了它的工作原理

import { Component, OnInit,Inject } from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';


@Component({
selector: 'app-verticalbanners',
templateUrl: './verticalbanners.component.html',
styleUrls: ['./verticalbanners.component.css']
})
export class VerticalbannersComponent implements OnInit {


constructor(){ }


ngOnInit() {
console.log(window.innerHeight );
}


}

DOCUMENT标记为可选项也是一个好主意:

当应用程序和呈现上下文不相同时(例如当将应用程序运行到 Web Worker 中时) ,应用程序上下文中的文档可能不可用。

下面是一个使用 DOCUMENT查看浏览器是否支持 SVG 的例子:

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


...


constructor(@Optional() @Inject(DOCUMENT) document: Document) {
this.supportsSvg = !!(
document &&
document.createElementNS &&
document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect
);

这是我最近想到的另一个解决方案,因为我厌倦了从 DOCUMENT内置标记获取 defaultView并检查它是否为 null:

import {DOCUMENT} from '@angular/common';
import {inject, InjectionToken} from '@angular/core';


export const WINDOW = new InjectionToken<Window>(
'An abstraction over global window object',
{
factory: () => {
const {defaultView} = inject(DOCUMENT);


if (!defaultView) {
throw new Error('Window is not available');
}


return defaultView;
}
});

注意 : 我已经和许多其他全局对象访问器一起发布了这个令牌,作为一个微型库: Https://github.com/ng-web-apis/common

查看组织的其他部分,你会发现很多 Angular 的原生 API 库: Https://github.com/ng-web-apis

如果您需要注入窗口,因为您确实需要属于“窗口”的属性,只需按照以下方式创建一个服务

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


@Injectable({
providedIn: 'root',
})
export class WindowService {
public getInstance(): Window {
return window;
}
}

下面是一个为了测试目的而模拟 WindowService变得很容易的例子:

export class AnotherService {
constructor(private windowService: WindowService) {}


public hasPreviousPage(): boolean {
return this.windowService.getInstance().history.length > 2;
}
}


然而,如果你使用窗口,然后得到一个全局定义的变量,我建议首先使用 GlobalThis代替。然后,输入全局是一个 打字稿模组的问题,简而言之: 做这样的事情:

将此环境上下文声明为 this

declare global {
function hello():string;
}

那么它就不会抱怨下面的代码了:

globalThis.hello(); // best way
window.hello(); // okay but window could be undefined in tests, workers or headless nodejs


注意,您仍然需要在全局的某个地方添加 hello()的实现。

当然你也可以(但是 我没有推荐)使用下面这个肮脏的技巧:

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


interface ExtraParams {
hello:() => string;
}


@Injectable({
providedIn: 'root',
})
export class WindowService {
public getInstance(): Window & ExtraParams {
return window as unknown as Window & ExtraParams;
}
}

在模块中声明提供程序:

  providers: [
{ provide: Window, useValue: window }
]

注入组件或服务:

constructor(private window: Window)

你可进入以下物业:

console.log(this.window.document);

或者创造新的价值:

(<any>this.window).MyNewValue = 'John Doe' ;