网站的Google登录和使用TypeScript的Angular 2

35

我正在构建一个具有标准RESTful Web服务的网站,用于处理持久性和复杂业务逻辑。我正在使用Angular 2编写组件来构建消费此服务的用户界面。

我希望能够依赖Google网站登录来实现身份验证,而不是构建自己的身份验证系统。这样用户就可以通过提供的框架登录到该网站,然后发送生成的ID令牌,由托管RESTful服务的服务器进行验证。

在Google Sign-In文档中,有JavaScript创建登录按钮的说明,这是必须完成的操作,因为登录按钮在Angular模板中是动态呈现的。模板的相关部分:

<div class="login-wrapper">
  <p>You need to log in.</p>
  <div id="{{googleLoginButtonId}}"></div>
</div>
<div class="main-application">
  <p>Hello, {{userDisplayName}}!</p>
</div>

并且在Typescript中定义的Angular 2组件:

import {Component} from "angular2/core";

// Google's login API namespace
declare var gapi:any;

@Component({
    selector: "sous-app",
    templateUrl: "templates/sous-app-template.html"
})
export class SousAppComponent {
  googleLoginButtonId = "google-login-button";
  userAuthToken = null;
  userDisplayName = "empty";

  constructor() {
    console.log(this);
  }

  // Angular hook that allows for interaction with elements inserted by the
  // rendering of a view.
  ngAfterViewInit() {
    // Converts the Google login button stub to an actual button.
    api.signin2.render(
      this.googleLoginButtonId,
      {
        "onSuccess": this.onGoogleLoginSuccess,
        "scope": "profile",
        "theme": "dark"
      });
  }

  // Triggered after a user successfully logs in using the Google external
  // login provider.
  onGoogleLoginSuccess(loggedInUser) {
    this.userAuthToken = loggedInUser.getAuthResponse().id_token;
    this.userDisplayName = loggedInUser.getBasicProfile().getName();
    console.log(this);
  }
}

基本流程如下:

  1. Angular 渲染模板并显示消息 "Hello, empty!"。
  2. ngAfterViewInit 钩子被触发,并调用 gapi.signin2.render(...) 方法,将空的 div 转换为 Google 登录按钮。这部分功能正常工作,点击该按钮将触发登录过程。
  3. 这也会将组件的 onGoogleLoginSuccess 方法附加到实际处理用户登录后返回的令牌上。
  4. Angular 检测到 userDisplayName 属性已更改,并更新页面以显示 "Hello, Craig(或您的名字)!"。

首先出现的问题是在 onGoogleLoginSuccess 方法中。请注意 constructor 中和那个方法中的 console.log(...) 调用。如预期的那样,在 constructor 中返回 Angular 组件。然而,在 onGoogleLoginSuccess 方法中返回 JavaScript 的 window 对象。

因此,看起来在跳转到 Google 登录逻辑时丢失了上下文,接下来我尝试使用 jQuery 的 $.proxy 调用来保持正确的上下文。因此,我通过在组件顶部添加 declare var $: any; 来导入 jQuery 命名空间,然后将 ngAfterViewInit 方法的内容转换为:

// Angular hook that allows for interaction with elements inserted by the
// rendering of a view.
ngAfterViewInit() {
    var loginProxy = $.proxy(this.onGoogleLoginSuccess, this);

    // Converts the Google login button stub to an actual button.
    gapi.signin2.render(
      this.googleLoginButtonId,
      {
        "onSuccess": loginProxy,
        "scope": "profile",
        "theme": "dark"
      });
}

添加了这个之后,两个console.log调用返回相同的对象,因此属性值现在正确更新。第二个日志消息显示具有预期更新的属性值的对象。

遗憾的是,当这种情况发生时,Angular模板不会被更新。在调试时,我偶然发现了一些能够解释这种情况的东西。我在ngAfterViewInit钩子的末尾添加了以下行:

setTimeout(function() {
  this.googleLoginButtonId = this.googleLoginButtonId },
  5000);

这段代码实际上没有做任何事情。它只是在钩子结束后等待五秒钟,然后将属性值设置为它本身。但是,如果将代码行放置其中,则页面加载后约五秒钟后,“Hello,empty!”消息会变成“Hello,Craig!”。这启示我Angular并没有注意到属性值在onGoogleLoginSuccess方法中的更改。因此,当其他一些事情发生以通知Angular属性值已更改(例如上面无用的自赋值),Angular就会被唤醒并更新所有内容。

显然,这不是我想留下的黑客技巧,所以我想知道是否有Angular专家可以帮助我?我应该调用哪个函数来强制Angular注意到某些属性已更改吗?

更新于2016-02-21,提供解决问题的具体答案的清晰度

最终,我需要使用所选答案中提供的两个建议。首先,正如建议的那样,我需要将onGoogleLoginSuccess方法转换为使用箭头函数。其次,我需要利用一个NgZone对象,以确保属性更新发生在Angular知道的上下文中。因此,最终的方法看起来像:

onGoogleLoginSuccess = (loggedInUser) => {
    this._zone.run(() => {
        this.userAuthToken = loggedInUser.getAuthResponse().id_token;
        this.userDisplayName = loggedInUser.getBasicProfile().getName();
    });
}

我确实需要导入_zone对象:import {Component, NgZone} from "angular2/core";

如答案中建议的那样,我还需要通过类的构造函数注入它:constructor(private _zone: NgZone) { }


刚接触 TypeScript,gapi 是从哪里来的?我也在使用你的示例来实现 Google 登录。我声明了变量 gapi,但它是一个空变量,对吧?所以当我尝试调用它的方法时,会出现很多错误。gapi 是从哪里来的?我漏掉了什么? - NDevox
1
gapi变量实际上是在Google API的JavaScript文件中定义的对象(https://apis.google.com/js/platform.js?onload=renderButton)。您需要在运行脚本的页面上包含该脚本。declare语句只是使其对TypeScript代码可见,而不是创建它。如果该变量对您来说只是一个空对象,则我的猜测是需要包含脚本。 - Craig Phillips
谢谢,现在它可以工作了。我还需要使用我的客户端ID添加google-signin-client-id头文件。 - NDevox
但是我遇到了以下问题。一旦我在Google认证页面上点击允许,它就会跳转到一个链接“https://accounts.google.com/o/noscript”。这个页面说:“您已经到达了这个页面,因为我们检测到您的浏览器禁用了Javascript。如果脚本被禁用,您尝试加载的页面无法正确显示。请启用脚本并重试操作或返回到您的浏览器。”但是我已经在我的浏览器中启用了javascript。我的问题是为什么它没有渲染回原始URL? - Jyotirmay
它第一次运行是可以的。但每当我在注销后尝试再次登录时,就会出现以下错误。 “Uncaught TypeError: Cannot read property 'style' of null”由于这个回调函数'onGoogleLoginSuccess'没有被调用。 - Mubashir
我有两个组件A和B。登录按钮在A中。我进入A并没有登录就返回到B,然后再次进入组件A并尝试登录。在这种情况下,“onGoogleLoginSuccess”函数不起作用,并出现以下错误-Uncaught TypeError: Cannot read property 'style' of null at G_ (cb=gapi.loaded_0:271) at H_.<anonymous> (cb=gapi.loaded_0:274) at Function.<anonymous> (cb=gapi.loaded_0:151) at MessagePort.c.port1.onmessage (cb=gapi.loaded_0:73)我需要你的帮助@CraigPhillips - Mubashir
5个回答

22

针对您的第一个问题,解决方案是使用箭头函数,它可以保留this的上下文:

  onGoogleLoginSuccess = (loggedInUser) => {
    this.userAuthToken = loggedInUser.getAuthResponse().id_token;
    this.userDisplayName = loggedInUser.getBasicProfile().getName();
    console.log(this);
  }

第二个问题的发生是因为第三方脚本在 Angular 的上下文之外运行。Angular 使用 zones,当您运行某些东西时,例如被修改过的 setTimeout(),以便在该 zone 中运行,Angular 将得到通知。您可以像这样在 zone 中运行 jQuery:

  constructor(private zone: NgZone) {
    this.zone.run(() => {
      $.proxy(this.onGoogleLoginSuccess, this);
    });
  }

有许多关于该区域的问题/答案有比我更好的解释,如果你想了解更多,但如果你使用箭头函数,这对你的例子不应该是一个问题。


我正在使用相同的代码,但是即使在登录后,userDisplayName 仍为空。 - Deepak
在这里提供client_id吗?我收到了“client_id missing”的错误。 - Jyotirmay
1
@Jyotirmay,请将其添加到index.html的<head>标签中。像这样:<meta name="google-signin-client_id" content="xxx.apps.googleusercontent.com"> - Insane Rose
@InsaneRose.. 嗯,谢谢。我看到了并添加了。现在已呈现出用Google登录的按钮,但是当在 Google 认证页面上点击 "允许" 时,它会将我渲染到 https://accounts.google.com/o/noscript 页面,显示 **您已达到此页面,因为我们检测到您的浏览器中已禁用 Javascript。如果禁用脚本,则无法正确显示您尝试加载的页面。请启用脚本并重试操作,或在浏览器中返回。** 但是我的浏览器已启用了 Javascript。为什么它没有返回原始 URL 呢? - Jyotirmay
1
在console.developers.google.com中的“授权重定向URI”中添加“http://localhost/callback/”。 - Insane Rose
我有两个组件A和B。登录按钮在A中。我进入A并没有登录就返回B,然后再次进入组件A并尝试登录。在这种情况下,“onGoogleLoginSuccess”函数不起作用,并出现以下错误-Uncaught TypeError: Cannot read property 'style' of null at G_ (cb=gapi.loaded_0:271) at H_.<anonymous> (cb=gapi.loaded_0:274) at Function.<anonymous> (cb=gapi.loaded_0:151) at MessagePort.c.port1.onmessage (cb=gapi.loaded_0:73) - Mubashir

8
我制作了一个谷歌登录组件,如果你需要一个示例。
  ngOnInit()
  {
    this.initAPI = new Promise(
        (resolve) => {
          window['onLoadGoogleAPI'] =
              () => {
                  resolve(window.gapi);
          };
          this.init();
        }
    )
  }

  init(){
    let meta = document.createElement('meta');
    meta.name = 'google-signin-client_id';
    meta.content = 'xxxxx-xxxxxx.apps.googleusercontent.com';
    document.getElementsByTagName('head')[0].appendChild(meta);
    let node = document.createElement('script');
    node.src = 'https://apis.google.com/js/platform.js?onload=onLoadGoogleAPI';
    node.type = 'text/javascript';
    document.getElementsByTagName('body')[0].appendChild(node);
  }

  ngAfterViewInit() {
    this.initAPI.then(
      (gapi) => {
        gapi.load('auth2', () =>
        {
          var auth2 = gapi.auth2.init({
            client_id: 'xxxxx-xxxxxx.apps.googleusercontent.com',
            cookiepolicy: 'single_host_origin',
            scope: 'profile email'
          });
          auth2.attachClickHandler(document.getElementById('googleSignInButton'), {},
              this.onSuccess,
              this.onFailure
          );
        });
      }
    )
  }

  onSuccess = (user) => {
      this._ngZone.run(
          () => {
              if(user.getAuthResponse().scope ) {
                  //Store the token in the db
                  this.socialService.googleLogIn(user.getAuthResponse().id_token)
              } else {
                this.loadingService.displayLoadingSpinner(false);
              }
          }
      );
  };

  onFailure = (error) => {
    this.loadingService.displayLoadingSpinner(false);
    this.messageService.setDisplayAlert("error", error);
    this._ngZone.run(() => {
        //display spinner
        this.loadingService.displayLoadingSpinner(false);
    });
  }

虽然有点晚了,但我想给大家举个例子,如果有人想在ng2中使用Google登录API。


3
将下面的文件包含在您的index.html中:
<script src="https://apis.google.com/js/platform.js" async defer></script>

login.html

<button id="glogin">google login</button>

login.ts

declare const gapi: any;
public auth2:any
ngAfterViewInit() {
     gapi.load('auth2',  () => {
      this.auth2 = gapi.auth2.init({
        client_id: '788548936361-h264uq1v36c5ddj0hf5fpmh7obks94vh.apps.googleusercontent.com',
        cookiepolicy: 'single_host_origin',
        scope: 'profile email'
      });
      this.attachSignin(document.getElementById('glogin'));
    });
}

public attachSignin(element) {
    this.auth2.attachClickHandler(element, {},
      (loggedInUser) => {  
      console.log( loggedInUser);

      }, function (error) {
        // alert(JSON.stringify(error, undefined, 2));
      });

 }

“https://apis.google.com/js/platform.js”被我的浏览器阻止了,这真的很奇怪。我在控制台中收到了这条消息:“<script>加载源“https://apis.google.com/js/platform.js”的失败。” - DFSFOT

1

尝试使用这个包 - npm install angular2-google-login

Github - https://github.com/rudrakshpathak/angular2-google-login

我已经在Angular2中实现了Google登录。只需导入该软件包,您就可以开始使用。

步骤 -

import { AuthService, AppGlobals } from 'angular2-google-login';

提供者 -providers: [AuthService];

构造函数 -constructor(private _googleAuth: AuthService){}

设置Google客户端ID -AppGlobals.GOOGLE_CLIENT_ID = 'SECRET_CLIENT_ID';

使用此调用服务 -

this._googleAuth.authenticateUser(()=>{
  //YOUR_CODE_HERE 
});

退出登录 -

this._googleAuth.userLogout(()=>{
  //YOUR_CODE_HERE 
});

我遇到了“gapi未定义”的错误。尝试通过查看https://www.npmjs.com/package/angular2-google-login进行修复,但没有成功。如何在此组件中使用AfterViewInit?您能否解释一下? - Santosh Prasad Sah

0

Sasxa所选的答案也帮了我,但我发现我可以使用.bind(this)将其绑定到onSuccess函数上,这样我就不必创建一个带有fat arrow的函数了。

ngAfterViewInit() {
  var loginProxy = $.proxy(this.onGoogleLoginSuccess, this);

  // Converts the Google login button stub to an actual button.
  gapi.signin2.render(
    this.googleLoginButtonId,
    {
      "onSuccess": loginProxy.bind(this),
      "scope": "profile",
      "theme": "dark"
    });
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接