Import gapi.auth2 in angular 2 typescript

泪湿孤枕 提交于 2019-11-28 18:25:59

To use gapi and gapi.auth with Angular2, install the type script definitions using NPM.

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

This will install two packages, @types/gapi and @types/gapi.auth2 to the node_modules folder and save the configuration in package.json.

Inspect your node_modules folder to check they install correctly. If your Angular2 app is called main-app, you should see:

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

Edit tsconfig.json to include new gapi and gapi.auth2 types (below is just a excerpt):

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

At this point I highly recommend grabbing a coffee and reading Typescript Module Resolution, you can skip straight to How Node.js resolves modules:

[...] resolution for a non-relative module name is performed differently. Node will look for your modules in special folders named node_modules. A node_modules folder can be on the same level as the current file, or higher up in the directory chain. Node will walk up the directory chain, looking through each node_modules until it finds the module you tried to load.

For this reason, you shouldn't need to add a reference to the type definitions in your Angular2 Service or Component (or wherever you're using gapi or gapi.auth2).

However, if you do add a reference to the gapi or gapi.auth2 TypeScript definitions, it must reference the .ts file installed using npm install (note, you must keep the /// oherwise you'll get an error):

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

The path is relative, so yours may differ depending on where your .ts file is relative to where you installed the TypeScript definitions.

Whether you added an explicit reference or used TypeScript's Node module resolution mechanism, you still need to declare your variables in your .ts file so Angular2 knows about the window gapi variable at compile time. Add declare var gapi: any; to your .ts file but do not place it within a class definition. I put mine just below any imports:

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

Looking at the definitions themselves (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi/index.d.ts), only functions are exported. Conversely, the interfaces are implementation details, so they are left un-exported and will not be visible to code outside the namespace.

Working with Other JavaScript Libraries in TypeScript documentation is worth reading to understand what we're getting with all this work.

Next, load the gapi client with your own function (possible in an Angular Service):

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

This function is non-trivial, and for good reason...

Firstly, note we're calling gapi.load with a configuration object and not just a callback. The GAPI reference states either can be used:

  • A callback function that is called when the libraries have finished loading.
  • An object encapsulating the various configuration parameters for this method. Only callback is required.

Using a configuration option allows us to reject the Promise when loading the library times-out, or just errors. In my experience, loading the library fails more often than initializing it - which is why configuration object is better than just a callback.

Secondly, we're wrapping gapi.load in

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

NgZone.run is documented and states

Running functions via zone.run allows you to reenter Angular zone from a task that was executed outside of the Angular zone [...]

This is exactly what we want since the call to gapi.load leaves the Angular zone. Omitting this can leave to very funky results that can be hard to debug.

Thirdly, loadClient() returns a promise that is resolved - allowing the caller to choose how they handle gapi.load. For example if our loadClient method belonged to an Angular service, apiLoaderServce, a component may use ngOnInit to load gapi:

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

Once gapi.load has been called, gapi.client will be ready and you should use it to initializes the JavaScript client with you API key, OAuth client ID, scope, and API discovery document(s):

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

Notice our friend NgZone.run is used once again to ensure the Angular Zone is re-entered.

In practice, I add loadClient() and initClient() to an Angular Service. In a high-level Angular component (usually just below the app-component) I load and initialize in 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;
    });
}

Lastly, you need to add the gapi script file to your file.

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

You must not use the async or defer attributes since either will cause Angular 2 world to enter before gapi has loaded.

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

I had previously suggested keeping page-load speeds fast by loading a local, minified copy of the gapi library in the /main-app/src/assests folder and importing:

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

However, I strongly recommend not doing this. Google may update https://apis.google.com/js/api.js and your client will break. I have been caught-out by this twice. In the end it was better just to import from //apis.google.com/js/ and keep it as a blocking call.

This is modified from @Jack's answer to use the RxJS library. While the original question asks for Angular 2, I'm using Angular 5 here in case anyone's working with an updated version.

  1. The first step is the same, downloading the gapi types with npm.

    npm install --save @types/gapi
    npm install --save @types/gapi.auth2
    
  2. You will need to update your tsconfig.json. If you're having issues, you may also need to update tsconfig.app.json and tsconfig.spec.json. They inherit from tsconfig.json, but if you specify types, I think they may overwrite the base. Snippet below:

    "typeRoots": [
      "node_modules/@types"
    ],
    "types": [
      "gapi",
      "gapi.auth2"
    ],
    "lib": [
      "es2017",
      "dom"
    ]
    
  3. Add a reference to Google's platform.js. I put mine in index.html. I left out async and defer as @Jack recommended.

    <script src="https://apis.google.com/js/platform.js"></script>
    
  4. Next create an authentication service. The complete code is here:

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

We have a lot happening here. Start by taking notice of the RxJS BehaviorSubjects. We'll be using these to notify our components of changes. Our loadAuth2 function uses Google's library to get a gapi.auth2.GoogleAuth object. If you need more information on Google's authentication library please check out their introduction or their documentation. Note we're using this.zone.run once we get our GoogleAuth object back. Running the entire function in an NgZone led to unexpected behavior for me. Next we take an RxJS BehaviorSubject isLoaded$ and set the value to true. You'll see similar behavior in the signIn() and signOut() functions- taking the results and running them in an NgZone and updating our appropriate BehaviorSubject.

  1. Now that we have our service, it's time to use it. We'll create a component for signing in and out. The code's below:

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

The most important part here is the ngOnInit implementation. This is where we'll subscribe to the AuthenticatorService's changes and update the view accordingly.

Hope these steps help someone out there to set up gapi.auth2 in their project.

The compiler will attempt to locate an ambient module declaration. So you should install:

https://github.com/retyped/gapi.auth2-tsd-ambient

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!