読者です 読者をやめる 読者になる 読者になる

ゆとり世代休学生、インドに散る

ゆとり世代私立文系ノースキルマンが、休学、海外インターンによって意識が急上昇し、蒸発するブログ。

React(Redux)アプリで、FriendlyForwarding機能(ページ遷移戻し)を実装する。

Web開発

お気軽に、宜しくお願い致します。やる気、でます。

読者になる

f:id:OnomimonO:20161130002637j:plain こんにちは、どりんく(@_OnomimonO) です。

Reduxのプロジェクトでフレンドリーフォワーディング機能を実装する機会があったので、 備忘録として記録しておきます。 今回はcookieを使用した方法になります。

FriendlyForwordingとは

フレンドリーフォワーディングとか、かっこいい横文字並べがちなweb開発ですが、簡単に言えば、
ユーザーがログインした後、ログイン直前に閲覧していたページヘとリダイレクトさせる機能 ですね。

具体的な事例

例えば、ログインしていないユーザーが
root/my_page/
というプロフィールページに行こうとした時に、当然ログインしてないわけですから、my_pageは表示できません。その際に一度ログインページに飛ばす必要があります。
root/sign_in
このページからログインした後に、
root/home
ではなくて
root/my_page
に飛ばしてあげましょうというものです。(わかりにくい)

ログイン後に一括的にユーザーをホームに飛ばすのではなく、各々の行きたいところに飛ばしてあげましょうっていう
心優しい機能です。 昨今web開発に関しては、UI、UXのウェイトは高まってきているので、とても大切な機能の一つとなります。

ディレクトリ構成

※この記事内ではわかりやすいように直接関係ないものは全て削ってあります。

redux  
 |     
 | -- container  
 |  |  
 |  | -- sign_in.js  
 |  |  
 |  | -- my_page.js  
 |  |  
 |  | -- home.js  
 |  
 | -- reducers  
 |  |  
 |  | -- root_reducer 
 |   
 |_ root.js
root_reducerについて

これはreduxのルーティングプラグイン、「react-router-redux」です。今回は省略します。以下を参照してください。
GitHub : react-router-redux

ルーティング

以下 root.js

'use strict';

import React, {Component} from 'react';
import {Provider} from 'react-redux';
import {Router, Route, browserHistory} from 'react-router';
import {syncHistoryWithStore} from 'react-router-redux';
import rootReducer from './reducers/root_reducer';
import {SignIn, MyPage, Home } from './containers';

export default class Root extends Component {
  handleUpdate() {
    let {action} = this.state.location;

    if (action === 'PUSH') {
      window.scrollTo(0, 0);
    }
  }

  render() {
    const store = createStore(rootReducer);
    const history = syncHistoryWithStore(browserHistory, store);

    return (
        <Provider store={store}>
          <Router history={history} onUpdate={this.handleUpdate}>
            <Route path='/' component={App}>
              <Route path='home' component={Home}/>
              <Route path='sign_in' component={SignIn}/>
              <Route path='my_page' component={MyPage}/>
              <Route path='*' component={NoMatch}/>
            </Route>
          </Router>
        </Provider>
    );
  }
}

実装したいこと

  • 通常ログイン後は /home に遷移させる。
  • ログインせずに /my_page にアクセスしようとしたユーザーを /sign_in に遷移させる。
  • ログイン後に /my_page に遷移させる。

実装方法

今回はcookieに直前のページURLを保存しておき、ログイン後にそのURLにリダイレクトさせます。
はじめてcookieを触ることになったので、その辺も含めていい勉強になりました。

ソースコード

my_page.js
'use strict';

import React, {Component} from 'react';
import {browserHistory} from 'react-router';
import {UserAction} from '../actions';

class MyPage extends Component {
  constructor(props) {
    super(props);
    this.state = {currentUser: null}
    if (!this.props.currentUser) {return}
    UserAction.getUser((user) => {
      if (this.state.isMounted) {
        this.setState({currentUser: user});
      }
    });
  }

  componentWillMount() {
    if (!this.props.currentUser) {
  //①クッキーに現在のルート(/my_page)を、変数forwarding_urlをキーとして格納。
      document.cookie = `forwarding_url=${location.pathname}; path=/;`;
      //②sign_inに遷移。
      return browserHistory.push('sign_in');
    }
  }

  render() {
    return (
      <div>
        マイページだよ。
      </div>
    );
  }
}

export default MyPage;
sign_in.js
'use strict';

import React, {Component} from 'react';
import {reduxForm} from 'redux-form';
import {browserHistory} from 'react-router';

class SignIn extends Component {
  //③ログインボタンを押した瞬間にcookieを読み込み、値があれば遷移させる。
  goRoot() {
    const searchForwardingUrl = new RegExp('forwarding_url=(.*?)(?:;|$)');
    if (document.cookie.match(searchForwardingUrl)) {
      return browserHistory.push(RegExp.$1);
    }
    //④もしなければ通常通りホーム画面に
    browserHistory.push('/home');
  }
  //⑤sign_in を離れる際にクッキーを削除(誤作動の防止)
  componentWillUnmount() {
    const pastDate = new Date(SignInConstants.PAST_DATE);
    document.cookie = 'forwarding_url=;path=/;expires=' + pastDate.toGMTString();
  }

  render() {
    return (
      <form>
        <div>
          <button onClick={this.goRoot}>ログイン</button>
        </div>
      </form>
    );
  }
}

export default SignIn;
home.js
'use strict';

import React, {Component} from 'react';
import {reduxForm} from 'redux-form';

class Home extends Component {
  render() {
    return (
        <div>Home</div>
    );
  }
}

export default Home;

実装手順

おおきな処理は5つです

0、ルーティングをする

今回は、react-router-reduxを使っているので仕様通りにルーティング。
条件としてはmy_page.jsに遷移した際にcurrentUserの値を

 UserAction.getUser((user) => {
      if (this.state.isMounted) {
        this.setState({currentUser: user});
      }

という部分でデータベースから引っ張ってきます。 現在ログインをしているのであれば、stateにuserが入ります。

その後、componentWillMount() 関数で、stateにcurrentUserが入っているかどうかを判断します。 componentWillMount()は、そのコンポーネントレンダリングされる前、つまりviewの表示前に、自動的してくれる処理になります。

その中で

  componentWillMount() {
    if (!this.props.currentUser) {
      //①クッキーに現在のルート(/my_page)を、変数forwarding_urlをキーとして格納。
      document.cookie = `forwarding_url=${location.pathname}; path=/;`;
      //②sign_inに遷移。
      return browserHistory.push('/sign_in');
    }
  }

②の処理にて、currentUserが入っているかどうかで、もし入っていなければ(未ログインであれば)homeに遷移させます。

①クッキーをセットする

cookieとは webを支える技術を最近読んだので、タイムリーでした。

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

みんなが聞いたことがある、けどなんだかよくわからんあれです。笑

cookieは、連想配列の形式になっていて、キーと値をセットで格納しています。 ちなみに確認したければ、コンソールから確認できますよ。 chromeにおけるcookieの確認

上の①をもう一度。
document.cookie = `forwarding_url=${location.pathname}; path=/;`;
今回はforwarding_urlというキーに現在いるpath ( my_page )を格納しています。
別のキーでpathというのは、後で説明します。

兎にも角にも、これでcookieにはforwarding_url、つまり見ていたページのurl(path)が保存されました。

②sign_inへの遷移

ソースコードを参照ください

cookieの読み込み

ログインボタンをおすとgoRoot()が起動します。

const searchForwardingUrl = new RegExp('forwarding_url=(.*?)(?:;|$)');
ここではsearchForwardingUrlという変数をconstで定義しています。
constとはES6から出た新しい宣言方法です。
おそらくこれから大切になってくるので、let,varとの違いは押さえておいたほうがいいでしょう。
ES6の新機能: 「let」「const」宣言を調べてみた

後半のnew以降は、正規表現を使って新しい検索ワードを生成しています。
正規表現の生成
難しく感じるかもしれませんが、これは要するに、cookieにforwarding_urlという
値を持ったプロパティが設定されているか、という意味の検索になります。
forwarding_urlがない → 他のページからは来ていない → homeに遷移
というロジックです。

   if (document.cookie.match(searchForwardingUrl)) {
      return browserHistory.push(RegExp.$1);
    }

続いてはcookieというオブジェクトにmatchというメソッドを適用します。
なんだか難しそうですが、
"searchForwardingUrl'という検索ワードで、cookieの中を検索しているだけ です。
検索した値があるのであれば、(friendly_forwardingが存在すれば)
return以下の処理をします。
RegExp.$1は、検索した値の中での、1番目の値が入っています。
このようにする理由は、cookieの中には様々なデータが記憶されており、
その中での順序を操作できないからです。

④もしなければ、/home に遷移

もしfriendly_forwardingが存在しなければ、
/homeに遷移します。

⑤sign_in を離れる際にクッキーを削除(誤作動の防止)

こちらがポイントかなと思うのですが、
cookieというのは意図的に削除しない限りブラウザにずっと記憶されてしまいます。
今回の例で言うと、もしずっと/my_pageというように記憶されてしまっていては、
普通にログインしたのに毎回/my_pageに飛ばされてしまう、、、
ということが起きてしまいます。
あくまでforwarding_url つまり、直前のurlであって、
一時的である必要があります。
そのため、/sign_inを離れる瞬間に、必ず消すようにしたいです。

消し方は、少し変な感じがしますが、
cookieの有効期限を現在より前にする。
という方法で消します。

  componentWillUnmount() {
    const pastDate = new Date('1970/01/01');
    document.cookie = 'forwarding_url=;path=/;expires=' + pastDate.toGMTString();
  }

componentWillUnmount()は、そのコンポーネントレンダリングされた後、
つまりviewの表示終了直前に、自動的してくれる処理になります。

pastDateという変数に過去の日時
(今回は1970年1/1から、この時間は計測が開始される基準になっています。)
を入れます。

※1970/01/01などは、特に意味を持っていないので、可読性が低いです。
 このようなケースではPAST_DATE:'1970/01/01'などと設定し、
その変数名を使うのが一般的です。

  このように変数を使うことでコードの可読性と再利用性を高めるために使うものを
  マジックナンバーと言います。

cookieの中のexpiresとは、その値の有効期限を表します。
これを過去に設定することで、その値を削除するのです。
う〜ん不便。
正直バットプラクティスな気がしてなりません。

まとめ

兎にも角にも、cookieでの実装はそこまで難しくはないと感じました。
しかし反面で、cookieはユーザー側でいじることができたり、
あまり機能しないことが多い(らしい)です。
加えて、セキュリティ面でも悪いことがあるそうです。(ざっくり)

もっといい実装方法があれば、更新したいと思います。