前回に引き続きGoogle Sign-In をReactなSPAで使ってみた時の日記、2日目です。
今回はReactRouterを使って、ログイン前と後で画面を変えてみます。
ReactRouterを導入
ReactRouterを使う。バージョンは 4.2.2
。
$ npm install --save-prod react-router-dom + react-router-dom@4.2.2
とりあえず、以下のようにルーティングする。
/
にアクセスするとトップページ/login
にアクセスするとログインページ
前回のコードを元に書き換えたのが以下。これを元に書いていく。
// src/App.js import React, { Component } from 'react'; import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; class App extends Component { render() { return ( <Router> <Switch> <Route exact path="/" component={Top}/> <Route path="/login" component={Login}/> </Switch> </Router> ) } } export default App; class Login extends Component { componentDidMount() { this.downloadGoogleScript(this.initSignInButton) } onSignIn = (googleUser) => { console.log('ID: ' + googleUser.getBasicProfile().getId()); } downloadGoogleScript = (callback) => { const element = document.getElementsByTagName('script')[0]; const js = document.createElement('script'); js.id = 'google-platform'; js.src = '//apis.google.com/js/platform.js'; js.async = true; js.defer = true; element.parentNode.insertBefore(js, element); js.onload = () => callback(window.gapi); } initSignInButton = (gapi) => { gapi.load('auth2', () => { gapi.auth2.init({client_id: <CLIENT_ID>}) .then( (result) => { gapi.signin2.render('google-signin-button', { 'onsuccess': this.onSignIn, 'onfailure': (err) => console.error(err) }); }, (err) => console.error(err) ); }) } render() { return ( <div> <h1>ようこそ!</h1> <div id="google-signin-button"></div> <Link to='/'>Topへ</Link> </div> ) } } class Top extends Component { render() { return ( <div> <h1>Topページ</h1> <Link to='/login'>サインイン</Link> </div> ) } }
ルーティングの設計
/
と login
へのアクセスをログイン前か後かで挙動を変える。それからログイン後でないとアクセスできないページとして /private
を用意することにする。
/
- ログイン前状態: ログイン前トップページを表示
- ログイン後状態: ログイン後トップページを表示
/login
- ログイン前状態: ログインページを表示 => ログインに成功すると
/
へ遷移 - ログイン後状態:
/
へ遷移
- ログイン前状態: ログインページを表示 => ログインに成功すると
/private
- ログイン前状態: ログインページへ遷移 => ログインに成功するとアクセスしたパス(
/private
)へ遷移 - ログイン後状態: プライベート画面を表示
- ログイン前状態: ログインページへ遷移 => ログインに成功するとアクセスしたパス(
トップページ
まずは、 /
をやってみる。
/
- ログイン前: ログイン前トップページを表示
- ログイン後: ログイン後トップページを表示
ログイン済かどうかを保持するフラグを state
に持たせる。ログイン後のcallbackである onSignIn
を Login
コンポーネントから App
へ移動して state
を書き換えるようにする。
--- a/src/App.js +++ b/src/App.js @@ -2,13 +2,16 @@ import React, { Component } from 'react'; import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; class App extends Component { + state = { + authenticated: false + } + onSignIn = (googleUser) => { + console.log('ID: ' + googleUser.getBasicProfile().getId()); + this.setState({authenticated: true}) + } render() { return ( <Router> <Switch> <Route exact path="/" component={Top}/> - <Route path="/login" component={Login}/> + <Route path="/login" render={props => + <Login onSignIn={this.onSignIn} /> + }/> </Switch> </Router> @@ -22,9 +25,6 @@ class Login extends Component { componentDidMount() { this.downloadGoogleScript(this.initSignInButton) } - onSignIn = (googleUser) => { - console.log('ID: ' + googleUser.getBasicProfile().getId()); - } downloadGoogleScript = (callback) => { const element = document.getElementsByTagName('script')[0]; const js = document.createElement('script'); @@ -41,7 +41,7 @@ class Login extends Component { .then( (result) => { gapi.signin2.render('google-signin-button', { - 'onsuccess': this.onSignIn, + 'onsuccess': this.props.onSignIn, 'onfailure': (err) => console.error(err) }); },
認証済みかどうかを Top
コンポーネントに渡して見出しの文字列を変える。
--- a/src/App.js +++ b/src/App.js @@ -13,7 +13,9 @@ class App extends Component { return ( <Router> <Switch> - <Route exact path="/" component={Top}/> + <Route exact path="/" render={props => + <Top authenticated={this.state.authenticated} /> + }/> <Route path="/login" render={props => <Login onSignIn={this.onSignIn} /> }/> @@ -66,9 +68,10 @@ class Login extends Component { class Top extends Component { render() { + const header = this.props.authenticated ? 'ログイン済み:Topページ' : 'ログイン前:Topページ'; return ( <div> - <h1>Topページ</h1> + <h1>{header}</h1> <Link to='/login'>サインイン</Link> </div> )
これで以下の流れができた。
localhost:3000/
にアクセスすると「ログイン前:Topページ」と表示- 「サインイン」リンクからログイン画面(
/login
)へ遷移 - Googleサインインでログイン
- 「Topへ」リンクからトップページ(
/
)へ遷移 - 「ログイン済み:Topページ」と表示
再アクセス時の挙動
ここで2つ程考えないといけないことが。
1つ目は、上のステップ5の後にブラウザをリロードしたりアドレスバーから直接アクセスした場合にログイン前状態になる、という点。これはログイン状態を state
に保存しているだけだから。この挙動をどうするか。
2つ目は、リロードでログイン前状態になってしまった後、上のステップ3をやらなくてもログイン状態となる、という点。これはCookieが残るのでログイン画面( /login
)に遷移しGoogleサインインボタンをレンダリングするだけで、以下の onsuccess
コールバックが実行される。この挙動をどうするか。
gapi.signin2.render('google-signin-button', { 'onsuccess': this.props.onSignIn, 'onfailure': (err) => console.error(err) });
セッション管理のCookieがあるから、Googleサインインがログイン成功のコールバックを実行するのは良い。だけど、ログインボタンをレンダリングする前にアプリケーション側で把握したい。それができれば上の2つとも対応できる。
アプリケーション側でログイン中かどうかを判定するには、ログイン状態を state
だけでなくsessionStorageに保存すれば良いかなぁと最初は思った。有効期限にログイン時成功時に取得できる gapi.auth2.AuthResponse の有効期限を使って。
でも、Cookieの有効期限を使えば良いんじゃないかなぁと思いつつ、Googleのドキュメントを眺めてると、GoogleAuth.isSignedIn.get()というのを発見。これを使おう。
Googleサインインのgapi.auth2の初期化を Login
コンポーネントでやってるけど、 App
コンポーネントに移す。 Login
コンポーネントではSignInボタンをレンダリングするときに gapi
オブジェクトが必要なんだよな・・・。 props
で渡すか。。
--- a/src/App.js +++ b/src/App.js @@ -3,7 +3,36 @@ import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; class App extends Component { state = { - authenticated: false + authenticated: false, + gapi: null + } + componentDidMount() { + this.downloadGoogleScript(this.initSignInButton) + } + downloadGoogleScript = (callback) => { + const element = document.getElementsByTagName('script')[0]; + const js = document.createElement('script'); + js.id = 'google-platform'; + js.src = '//apis.google.com/js/platform.js'; + js.async = true; + js.defer = true; + element.parentNode.insertBefore(js, element); + js.onload = () => callback(window.gapi); + } + initSignInButton = (gapi) => { + gapi.load('auth2', () => { + gapi.auth2.init({client_id: <CLIENT_ID>}) + .then( + (result) => { + if (result.isSignedIn.get()) { + this.setState({authenticated: true, gapi}) + } else { + this.setState({authenticated: false, gapi}) + } + }, + (err) => console.error(err) + ); + }) } onSignIn = (googleUser) => { console.log('ID: ' + googleUser.getBasicProfile().getId()); @@ -17,7 +46,7 @@ class App extends Component { <Top authenticated={this.state.authenticated} /> }/> <Route path="/login" render={props => - <Login onSignIn={this.onSignIn} /> + <Login onSignIn={this.onSignIn} gapi={this.state.gapi}/> }/> </Switch> </Router> @@ -29,31 +58,12 @@ export default App; class Login extends Component { componentDidMount() { - this.downloadGoogleScript(this.initSignInButton) - } - downloadGoogleScript = (callback) => { - const element = document.getElementsByTagName('script')[0]; - const js = document.createElement('script'); - js.id = 'google-platform'; - js.src = '//apis.google.com/js/platform.js'; - js.async = true; - js.defer = true; - element.parentNode.insertBefore(js, element); - js.onload = () => callback(window.gapi); - } - initSignInButton = (gapi) => { - gapi.load('auth2', () => { - gapi.auth2.init({client_id: <CLIENT_ID>}) - .then( - (result) => { - gapi.signin2.render('google-signin-button', { - 'onsuccess': this.props.onSignIn, - 'onfailure': (err) => console.error(err) - }); - }, - (err) => console.error(err) - ); - }) + if (this.props.gapi) { + this.props.gapi.signin2.render('google-signin-button', { + 'onsuccess': this.props.onSignIn, + 'onfailure': (err) => console.error(err) + }); + } } render() { return (
これで、ログイン後再アクセスした場合でも、「ログイン前:Topページ」ではなく「ログイン済み:Topページ」と表示されるようになった!
ローディング中表示
だけど、 gapi.auth2.init
の処理が終わるまでの間「ログイン前:Topページ」と表示されてしまう。ローディング中の表示をしよう。
--- a/src/App.js +++ b/src/App.js @@ -39,6 +39,9 @@ class App extends Component { this.setState({authenticated: true}) } render() { + if (!this.state.gapi) { + return ('Loading...') + } return ( <Router> <Switch>
ログアウト
続いてログアウトボタンを追加。
--- a/src/App.js +++ b/src/App.js @@ -38,6 +38,11 @@ class App extends Component { console.log('ID: ' + googleUser.getBasicProfile().getId()); this.setState({authenticated: true}) } + onSignOut = () => { + const auth2 = this.state.gapi.auth2.getAuthInstance(); + auth2.signOut().then(() => console.log('sign out')); + this.setState({authenticated: false}) + } render() { if (!this.state.gapi) { return ('Loading...') @@ -46,7 +51,7 @@ class App extends Component { <Router> <Switch> <Route exact path="/" render={props => - <Top authenticated={this.state.authenticated} /> + <Top authenticated={this.state.authenticated} onSignOut={this.onSignOut} /> }/> <Route path="/login" render={props => <Login onSignIn={this.onSignIn} gapi={this.state.gapi}/> @@ -81,12 +86,20 @@ class Login extends Component { class Top extends Component { render() { - const header = this.props.authenticated ? 'ログイン済み:Topページ' : 'Topページ'; - return ( - <div> - <h1>{header}</h1> - <Link to='/login'>サインイン</Link> - </div> - ) + if (this.props.authenticated) { + return ( + <div> + <h1>ログイン済み:Topページ</h1> + <button onClick={this.props.onSignOut}>ログアウト</button> + </div> + ) + } else { + return ( + <div> + <h1>ログイン前:Topページ</h1> + <Link to='/login'>サインイン</Link> + </div> + ) + } } }
おしまい
今日はここまで。次回は、ログインページ( /login
) とログイン後じゃないとアクセスできないページ( /private
)をやっていく。
ここまでの結果。