从 Google API 加载 auth2 时,更改检测不会 运行

Change detection doesn't run when loading auth2 from Google API

我正在尝试从 Angular 2 服务中加载脚本 https://apis.google.com/js/api.js?onload=initGapi 以便调用 Google API,但每当我尝试return 脚本完成加载和初始化后的值,async 管道不想将值呈现给模板。

我正在使用 EventEmitter,它在 gapi.load()gapi.client.init 完成时触发,并且在组件中订阅了可观察对象。异步管道似乎不想读取该值,直到我单击同一组件内的一个按钮(并且可能触发更改检测)。当我使用 ApplicationRef.

中的 tick() 时,在组件内强制更改检测不起作用

正在加载 Google API 的服务如下:

google-api.service.ts

import { Injectable, EventEmitter } from '@angular/core';
import { Observable } from 'rxjs/Observable';

import { User, Client } from '../../+home/models';

declare var gapi: any;

@Injectable()
export class GoogleApiService {

    CLIENT_ID: string = '....apps.googleusercontent.com';
    DISCOVERY_DOCS: string[] = ["https://www.googleapis.com/discovery/v1/apis/admin/directory_v1/rest"];
    SCOPES = 'https://www.googleapis.com/auth/admin.directory.user.readonly';

    authEmitter: EventEmitter<boolean> = new EventEmitter();

    constructor() {
        window['initGapi'] = (ev) => {
            this.handleClientLoad();
        };
        this.loadScript();
    }

    loadScript() {
        let script = document.createElement('script');
        script.src = 'https://apis.google.com/js/api.js?onload=initGapi';
        script.type = 'text/javascript';
        document.getElementsByTagName('head')[0].appendChild(script);
    }

    handleClientLoad() {
        gapi.load('client:auth2', this.initClient.bind(this));
    }

    initClient() {
        gapi.client.init({
            discoveryDocs: this.DISCOVERY_DOCS,
            clientId: this.CLIENT_ID,
            scope: this.SCOPES
        }).then(() => {
            this.authEmitter.emit(true);
        });
    }

    get gapi(): Observable<any> {
        return this.authEmitter.map(() => 'hello world');
    }
}

以及从服务中读取的组件

app.component.ts

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';

import { GoogleApiService } from '../../+core/services';

@Component({
    template: `
    What does the service say?
    {{ api$ | async }}
    <button (click)="click()">Button</button>`
})
export class InviteEmployeesContainer implements OnInit {
    api$: Observable<string>;

    constructor(private gApiService: GoogleApiService) {}

    ngOnInit() {
        this.api$ = this.gApiService.gapi;

        this.gApiService.gapi.subscribe((result) => {
            console.log(result);
        });
    }

    click() {
        console.log('clicked');
    }
}

生成的页面打印出字符串 What does the service say? 并有一个按钮,但在我单击按钮之前不会打印文本 hello world,这不是所需的行为,它应该立即在页面上可见。

此外,当我使用 this.gApiService.gapi.subscribe 订阅并登录到控制台时,它会在我期望的时候记录 hello world

有谁知道我如何使用异步管道让 hello worldauthEmitter 触发事件时打印到页面。

我最终找到了这个问题的解决方案,这个问题有几个问题。

首先,EventEmitter 应该只用于发射,不应该被订阅,所以我用 Subject 换掉了它。通常我会尝试使用可观察对象,但在这种情况下,我发现一个主题更合适。

下面是一个使用 Subject 而不是 EventEmitter 的修改后的 plunker。我还注释掉 gapi.client.init... 并用不同的承诺替换它,因为它实际上不是问题的一部分。请注意,在点击按钮之前,plunker 更改检测仍然不会 运行,这不是预期的。

https://plnkr.co/edit/YaFP07N6A4CQ3Zz1SbUi?p=preview

我遇到的问题是因为 gapi.load('client:auth2', this.initClient.bind(this)); 运行s initClient 在 Angular 2 区域之外,这将它排除在变化检测之外。

为了在变化检测中捕获主体,我们必须 运行 主体的 nextcomplete 在 Angular 2 区域内调用。这是通过导入 NgZone 然后修改行来完成的:

    new Promise((res) => res()).then(() => {
      this.zone.run(() => {
        this.authSubject.next(true);
        this.authSubject.complete(true);
      });
    });

See the final (resolved) plunker here.

对于其他为此苦苦挣扎的人,您还需要包装任何其他 gapi 承诺结果。

与上述解决方案类似,这是我一直在使用的解决方案:

export function nonZoneAwarePromiseToObservable(promise: Promise<T>, zone: NgZone): Observable<T> {
  return new Observable<T>(subscriber => {
    promise
      .then((v) => {
        zone.run(() => {
          subscriber.next(v)
          subscriber.complete()
        })
      })
      .catch((error) => {
        zone.run(() => {
          subscriber.error(error)
        })
      })
  })
}

我是如何使用它的:

const calendar: typeof  gapi.client.calendar = _gapi.client.calendar
        const $timezone = calendar.settings.get({setting: 'timezone'})
        return nonZoneAwarePromiseToObservable($timezone, this.zone)
          .pipe(map((timezoneResponse) => timezoneResponse?.result?.value))