React+Redux アプリに Google Sign-In を組み込んでみた
React+Redux アプリに Google Sign-In の機能を組み込んでみました。
はじめに
Google Sign-In とは、Googleが提供している認証系ツールの総称である Google Identity Platform のツールの1つで、スマホアプリやWebアプリの認証をGoogleアカウントでできるようにするものです。
参考:
React, Redux, Google Sign-In という組み合わせだと、さすがに情報が少なく、結構試行錯誤しましたが、最終的にはなんとか方法が見つかりました。
準備
まずは、Google API Console でクレデンシャルを登録する必要があります。
以下のページで説明されています:
Google Platform Library の組み込み
一般的なWebクライアントでの設定方法は以下のページで説明されています:
Integrating Google Sign-In into your web app | Google Sign-In for Websites | Google Developers
ですが、このやり方だと React+Redux の環境で使いづらいことがわかったので、Google Platform Library だけを組み込みます:
<script src="https://apis.google.com/js/platform.js" async defer></script>
React Component の中に入れるとタイミングが遅れるようなので、僕は index.html に入れました。
追記(2017-07-24)
async defer
を指定すると、タイミングが遅れることがあるようです。
なので、僕は外すことにしました。
追記終了
上のGoogleの手順では、この後、metaタグにクライアントIDを入れ、ボタンを設置することになっていますが、そうするとカスタマイズの余地が少なくなってしまうので、これはやらずに、自分で処理を書くことにしました。
サインインの実装
Reduxの構成では、Component でサインインボタンを設置し、Container の mapDispatchToProps でボタンの onClick ハンドラーを定義し、その中で Google Sign-In の認証処理を行うことになります。
// Container const GOOGLE_SIGN_IN_PARAMS = { client_id: 'xxxxx', ux_mode: 'redirect', // popupはブラウザーでブロックされる }; const mapDispatchToProps = (dispatch: Dispatch<Action>) => { return { onSignInClick: () => { signIn(onInitFailure, onSignInSuccess, onSignInFailure, onAlreadySignedIn); }, }; }; const signIn = (onInitFailure: IFunc, onSignInSuccess: IFunc, onSignInFailure: IFunc, onAlreadySignedIn: IFunc) => { const gapi = (<any>window).gapi; gapi.load('auth2', () => { gapi.auth2.init(GOOGLE_SIGN_IN_PARAMS).then( // 初期化成功 (gAuth: any) => { if (gAuth.isSignedIn.get()) { // サインイン済み onAlreadySignedIn(gAuth); } else { // 未サインイン gAuth.signIn() .then( (gUser: any) => onSignInSuccess(gUser), (err: any) => onSignInFailure(err), ); } }, // 初期化失敗 (err: any) => onInitFailure(err), ); }); };
処理内容はすぐにわかると思いますが、ポイントは、load() → init() → isSignedIn.get() → signIn() という順番で行う必要があるということです。
必要に応じて、各ハンドラーでアクションをディスパッチします。
各APIやパラメーターについては、以下のページに詳しく書かれています:
Google Sign-In JavaScript client reference | Google Sign-In for Websites | Google Developers
追記 2017-07-31
GOOGLE_SIGN_IN_PARAMS
の ux_mode
についてですが、コメントでは、popupはブラウザーでブロックされると書きましたが、popupにする方法が見つかりました。
ボタンのハンドラーでできるだけすぐに signIn()
を実行するようにするとうまくいきました:
const signIn = (onSignInSuccess: IFunc, onSignInFailure: IFunc, onAlreadySignedIn: IFunc) => { // 前提:すでに gapi.auth2.init() を実行済み const gAuth = (<any>window).gapi.auth2.getAuthInstance(); if (gAuth.isSignedIn.get()) { // サインイン済み onAlreadySignedIn(gAuth); } else { // 未サインイン gAuth.signIn() .then( (gUser: any) => onSignInSuccess(gUser), (err: any) => onSignInFailure(err), ); } };
Ref. Avoiding Popup Blocking when Authenticating with Google
追記終了
サインアウトの実装
サインアウトは、サインインと同様のコードになります:
// Container const mapDispatchToProps = (dispatch: Dispatch<Action>) => { return { ..... onSignOutClick: () => { signOut(onInitFailure, onSignOutSuccess, onSignOutFailure); }, }; }; const signOut = (onInitFailure: IFunc, onSignOutSuccess: IFunc, onSignOutFailure: IFunc) => { const gapi = (<any>window).gapi; gapi.load('auth2', () => { gapi.auth2.init(GOOGLE_SIGN_IN_PARAMS).then( // 初期化成功 (gAuth: any) => { gAuth.signOut() .then( () => onSignOutSuccess(), (err: any) => onSignOutFailure(), ); }, // 初期化失敗 (err: any) => onInitFailure(err), ); }); };
サインインの方と重複している処理があるので、クラス化した方が見通しが良くなると思います。
サインイン済みかどうかをチェックする
サインイン済みかどうかは、サインインの処理で使った gAuth.isSignedIn.get()
で取得できますが、どこでそれを実行するかが問題です。
通常、ユーザー認証のあるWebアプリの場合、どのページでも最初に認証チェックを行う必要があるので、Component の componentDidMount() でチェックすることになります。
追記(2017-07-24)
componentDidMount() は render() の後で実行されるため、render() で認証状態を確認したい場合は componentWillMount() でチェックする必要があります。
追記終了
それをどこに入れるかですが、トップレベルのComponent/Container でやるか、認証チェック用の Component を作ってトップレベルの Component に入れることになると思います。
僕はトップレベルの App Component でやることにし、そのために App に Container の構造を追加しました。
// Component class App extends React.Component<IProps> { render() { ..... } componentDidMount() { this.props.onDidMount(); } }
// Container const mapDispatchToProps = (dispatch: Dispatch<Action>) => { return { onDidMount: () => { checkAuth(dispatch); }, }; }; const checkAuth = (dispatch: Dispatch<Action>) => { const gapi = (window as any).gapi; gapi.load('auth2', () => { gapi.auth2.init(GOOGLE_SIGN_IN_PARAMS).then( // 初期化成功 (gAuth: any) => { let isSignedIn = gAuth.isSignedIn.get(); // dispatch action }, // 初期化失敗 (err: any) => { // dispatch action }, ); }); }; const AppContainer = connect( mapStateToProps, mapDispatchToProps )(App); export default AppContainer;
あとがき
React+Redux のアプリに Google Sign-In の機能を組み込む方法は以上になります。
もしかすると、Reduxの非同期処理のパターンで、もう少しきれいに書けるかもしれませんが、まだ勉強していないので、現時点ではこのレベルになります。
あと、実は、react-google-login というライブラリーがあり、それでもGoogle Sign-In の機能を使用することができます。
ただし、react-google-login では、Component ですべてを実装しているため、Reduxの構造に合わせるのは難しいです。
また、サインアウトや状態チェックの機能がなかったりします。
ということで、今のところは自分で実装するしかなさそうです。
追記:Reduxの非同期処理での実装(2017-07-31)
その後、Reduxの非同期処理での実装についても調べてみました。
公式サイトの情報によると、Redux Thunk を使用するのが標準的な方法のようですが、Actionに非同期処理のロジックが入るのでちょっと違和感を感じました。
そこで、他の方法を調べた結果、redux-saga が良さそうに思えました。
redux-saga を使うと、Reduxの各要素とは別のものとして実装できるので、見通しが良いです。
Google Sign-In については、上記のコードではコールバックでざっと書きましたが、Promiseを使うとさらに読みやすくなります。
なお、Googleのサインイン処理を redux-saga で実装すると、ポップアップがブロックされてしまいます。
なので、ポップアップにする場合は、そこだけはUI要素のハンドラーで実装する必要があります。
Ref. redux-sagaで非同期処理と戦う - Qiita
(redux-saga については、上の記事が非常にわかりやすかったです。)