Импортировать gapi.auth2 в angular 2 typescript

Я попытался импортировать некоторые классы или функции из Google gapi.auth2 в typescript. Но ниже код никогда не работает, даже я правильно добавил типы gapi.auth2 в каталог типирования.

import { GoogleAuth } from 'gapi.auth2';

У меня всегда была ошибка:

Error TS2307: Cannot find module 'gapi.auth2'

Должен ли я использовать некоторый поиск относительного каталога, например "../../typings/gapi.auth2"?

Или, может быть, способ, которым я пользуюсь, не соответствует действительности?

Спасибо!

Ответ 1

Чтобы использовать gapi и gapi.auth с помощью Angular2, установите определения типа script, используя NPM.

npm install --save @types/gapi
npm install --save @types/gapi.auth2

Это установит два пакета: @types/gapi и @types/gapi.auth2 в папку node_modules и сохраните конфигурацию в package.json.

Осмотрите папку node_modules, чтобы проверить правильность установки. Если ваше приложение Angular2 называется основным приложением, вы должны увидеть:

main-app/
  node_modules/
    @types/
      gapi/
      gapi.auth2/

Изменить tsconfig.json, чтобы включить новые типы gapi и gapi.auth2 (ниже всего лишь выдержка):

{
  "compileOnSave": false,
  "compilerOptions": {
    "types": ["gapi", "gapi.auth2"]
  }
}

В этот момент я настоятельно рекомендую захватить кофе и читать Typescript Разрешение модуля, вы можете перейти прямо к Как Node.js разрешает модули:

Выполнено

[...] разрешение для имени необязательного модуля иначе. Node будет искать ваши модули в специальных папках с именем node_modules. Папка node_modules может находиться на том же уровне, что и текущий файл или выше в цепочке каталогов. Node поднимется цепочку каталогов, просматривая каждую node_modules, пока не найдет модуль, который вы пытались загрузить.

По этой причине вам не нужно добавлять ссылку на определения типов в Angular2 службе или компоненте (или везде, где вы используете gapi или gapi.auth2).

Однако, если вы добавите ссылку на определения gapi или gapi.auth2 TypeScript, она должна ссылаться на файл .ts, установленный с помощью npm install (обратите внимание, вы должны оставить /// oherwise you 'получится ошибка):

/// <reference path="../../node_modules/@types/gapi/index.d.ts" />

Путь относительный, поэтому ваш может отличаться в зависимости от того, где находится ваш файл .ts относительно того, где вы установили определения TypeScript.

Если вы добавили явную ссылку или использовали механизм разрешения модуля TypeScript Node, вам все равно нужно объявить свои переменные в файле .ts, чтобы Angular2 знал о переменной окна gapi во время компиляции. Добавьте declare var gapi: any; в ваш .ts файл, но не помещайте его в определение класса. Я поставил мой чуть ниже любого импорта:

// You may not have this explicit reference.
/// <reference path="../../node_modules/@types/gapi/index.d.ts" />
import { NgZone, Injectable, Optional } from '@angular/core';
declare var gapi: any;

Рассматривая сами определения (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi/index.d.ts), экспортируются только функции. И наоборот, интерфейсы представляют собой детали реализации, поэтому они остаются не экспортированными и не будут отображаться для кода вне пространства имен.

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

Затем загрузите клиент gapi своей собственной функцией (возможно в службе Angular):

 loadClient(): Promise<any> {
     return new Promise((resolve, reject) => {
         this.zone.run(() => {
                gapi.load('client', {
                    callback: resolve,
                    onerror: reject,
                    timeout: 1000, // 5 seconds.
                    ontimeout: reject
                });
         });
    });
}

Эта функция является нетривиальной и по уважительной причине...

Во-первых, обратите внимание, что мы вызываем gapi.load с помощью объекта конфигурации, а не только обратного вызова. Можно указать ссылку GAPI:

  • Функция обратного вызова, которая вызывается, когда библиотеки закончены погрузка.
  • Объект, инкапсулирующий различные параметры конфигурации для этого метода. Требуется только обратный вызов.

Использование параметра конфигурации позволяет отклонять обещание при загрузке библиотечного тайм-аута или просто ошибок. По моему опыту, загрузка библиотеки происходит чаще, чем ее инициализация, поэтому объект конфигурации лучше, чем просто обратный вызов.

Во-вторых, мы обертываем gapi.load в

this.zone.run(() => {
  // gapi.load
});

NgZone.run задокументирован и заявляет

Выполнение функций через zone.run позволяет повторно войти в зону Angular из задача, выполненная вне зоны Angular [...]

Это именно то, что мы хотим, так как вызов gapi.load покидает зону Angular. Опуская это, можно оставить очень забавные результаты, которые трудно отлаживать.

В-третьих, loadClient() возвращает обещание, которое разрешено, позволяя вызывающему выбрать, как они обрабатывают gapi.load. Например, если наш метод loadClient принадлежал службе Angular, apiLoaderServce, компонент может использовать ngOnInit для загрузки gapi:

ngOnInit(): void {
    this.apiLoaderService.loadClient().then(
        result => this.apiLoaded = true,
        err => this.apiLoaded = false
    );
}

Как только gapi.load будет вызван, gapi.client будет готов, и вы должны использовать его для инициализации клиента JavaScript с помощью ключа API, идентификатора клиента OAuth, области действия и документов (API) по обнаружению API:

initClient(): Promise<any> {
    var API_KEY = // Your API key.
    var DISCOVERY_DOC = // Your discovery doc URL.
    var initObj = {
        'apiKey': API_KEY,
        'discoveryDocs': [DISCOVERY_DOC],
    };

    return new Promise((resolve, reject) => {
        this.zone.run(() => {
            gapi.client.init(initObj).then(resolve, reject);
        });
    });
}

Обратите внимание, что наш друг NgZone.run снова используется для обеспечения повторного ввода зоны Angular.

На практике я добавляю loadClient() и initClient() в службу Angular. В высокоуровневом компоненте Angular (обычно чуть ниже компонента приложения) я загружаю и инициализирую в ngOnInit:

ngOnInit(): void {
    this.apiLoaderService.loadClient().then(
        result => {
            this.apiLoaded = true;
            return this.apiLoaderService.initClient()
        },
        err => {
            this.apiFailed = true;
        }
    ).then(result => {
        this.apiReady = true;
    }, err => {
        this.apiFailed = true;
    });
}

Наконец, вам нужно добавить файл gapi script в ваш файл.

<html>
  <head>
    <script src="https://apis.google.com/js/api.js"></script>

Вы не должны использовать атрибуты async или defer, поскольку либо приведет к тому, что мир Angular 2 будет введен до того, как будет загружен глюки.

<!-- This will not work. -->
<html>
  <head>
    <script async defer src="https://apis.google.com/js/api.js"></script>

Ранее я предлагал быстро поддерживать скорость загрузки страницы, загружая локальную, уменьшенную копию библиотеки gapi в папке /main-app/src/assests и импортирование:

    <html>
      <head>
        <script src="assets/api.js"></script>

Однако я сильно рекомендую не делать. Google может обновить https://apis.google.com/js/api.js, и ваш клиент сломается. Я был дважды увлечен этим. В итоге было лучше просто импортировать из //apis.google.com/js/ и сохранить его как блокирующий вызов.

Ответ 2

Это изменение из ответа @Jack для использования библиотеки RxJS. Хотя оригинальный вопрос касается Angular 2, я использую здесь Angular 5 на тот случай, если кто-то работает с обновленной версией.

  1. Первый шаг такой же, загрузка типов зазоров с помощью npm.

    npm install --save @types/gapi
    npm install --save @types/gapi.auth2
    
  2. Вам нужно будет обновить ваш tsconfig.json. Если у вас возникли проблемы, вам также может потребоваться обновить tsconfig.app.json и tsconfig.spec.json. Они наследуются от tsconfig.json, но если вы укажете типы, я думаю, они могут перезаписать базу. Фрагмент ниже:

    "typeRoots": [
      "node_modules/@types"
    ],
    "types": [
      "gapi",
      "gapi.auth2"
    ],
    "lib": [
      "es2017",
      "dom"
    ]
    
  3. Добавьте ссылку на Google platform.js. Я положил свой в index.html. Я пропустил async и defer, как рекомендовано @Jack.

    <script src="https://apis.google.com/js/platform.js"></script>
    
  4. Далее создайте сервис аутентификации. Полный код здесь:

    import { Injectable, NgZone, Output } from '@angular/core';
    import { Observable } from 'rxjs/Observable';
    import { BehaviorSubject } from 'rxjs';
    import { HttpClient } from '@angular/common/http';
    import { User } from './User';
    
    @Injectable()
    export class AuthenticatorService {
        public auth2: any;
        public user$: BehaviorSubject<User> = new BehaviorSubject<User>(null);
        public isLoggedIn$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
        public isLoaded$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
    
        constructor(private zone: NgZone, private http: HttpClient) { }
    
        validateToken(token: string): Observable<User> {
            return this.http.get<User>('http://yourServer:3000/validationApi/${token}');
        }
    
        signIn(): void {
            this.auth2.signIn().then(user => {
                this.validateToken(user.getAuthResponse().id_token).subscribe(user => {
                    this.zone.run(() => {
                        this.user$.next(user);
                        this.isLoggedIn$.next(true);
                    });
                },
                    (err) => {
                        console.error(err);
                    });
            });
        };
    
        signOut(): void {
            this.auth2.signOut().then(() => {
                this.zone.run(() => {
                    this.isLoggedIn$.next(false);
                    this.user$.next(null);
                });
            },
                (err) => {
                    console.error(err);
                });
        }
    
        loadAuth2(): void {
            gapi.load('auth2', () => {
                gapi.auth2.init({
                    client_id: 'yourClientId',
                    fetch_basic_profile: true
                }).then((auth) => {
                    this.zone.run(() => {
                        this.auth2 = auth;
                        this.isLoaded$.next(true);
                    });
                },
                );
            });
        }
    }
    

У нас здесь много чего происходит. Начните с рассмотрения RxJS BehaviorSubjects. Мы будем использовать их для уведомления наших компонентов об изменениях. Наша функция loadAuth2 использует библиотеку Google для получения объекта gapi.auth2.GoogleAuth. Если вам нужна дополнительная информация о библиотеке аутентификации Google, просмотрите их введение или их документацию. Обратите внимание, что мы используем this.zone.run, как только мы возвращаем наш объект GoogleAuth. Запуск всей функции в NgZone привел к неожиданному поведению для меня. Далее мы берем RxJS BehaviorSubject isLoaded$ и устанавливаем значение в true. Вы увидите похожее поведение в signIn() и signOut() functions-, получающих результаты и запускающих их в NgZone и обновляющих наш соответствующий BehaviorSubject.

  1. Теперь, когда у нас есть наш сервис, пришло время его использовать. Мы создадим компонент для входа и выхода. Код ниже:

    import { Component, OnInit } from '@angular/core';
    import { AuthenticatorService } from  '../authenticator.service'
    import { User } from '../User';
    
    
    @Component({
    selector: 'sign-in',
    template: '
        <ng-container *ngIf="authIsLoaded">
             <button *ngIf="!isLoggedIn" (click)="signIn()">Sign In With Google</button>
            <button *ngIf="isLoggedIn" (click)="signOut()">Sign Out</button>
        </ng-container>
        <h2 *ngIf="authIsLoaded && isLoggedIn"> Signed in as {{user.name}} </h2>'
    })
    export class GoogleAuthenticatorComponent implements OnInit {
    
    public authIsLoaded: boolean = false;
    public isLoggedIn: boolean = false;
    public user: User;
    
    constructor(private authenticatorService: AuthenticatorService) { }
    
        signIn(): void {
        this.authenticatorService.signIn();
        };
    
        signOut(): void {
        this.authenticatorService.signOut();
        }
    
        ngOnInit() {
        this.authenticatorService.isLoaded$.subscribe( value => {
            this.authIsLoaded = value;
        });
    
        this.authenticatorService.isLoggedIn$.subscribe( value => {
            this.isLoggedIn = value;
        });
    
        this.authenticatorService.user$.subscribe( value => {
            this.user = value;
        });
    
        this.authenticatorService.loadAuth2();
        }
    }
    

Наиболее важной частью здесь является реализация ngOnInit. Здесь мы подпишемся на изменения AuthenticatorService и соответствующим образом обновим представление.

Надеюсь, что эти шаги помогут кому-то настроить gapi.auth2 в своем проекте.