LoginSignup
0
0

More than 5 years have passed since last update.

Falcor+Reactフルスタック(エラー処理)

Last updated at Posted at 2017-08-16

Meteor and Reactによるリアクティブシステム「ラーメン野郎を追いかけろ! @ Twitter」を作ってみた

Falcor+Reactフルスタック(開発環境)
Falcor+Reactフルスタック(react-router)
Falcor+Reactフルスタック(views層とcomponents層)
Falcor+Reactフルスタック(formsy-react)
Falcor+Reactフルスタック(エラー処理)
Falcor+Reactフルスタック(Material-UI)

 今回はエラー処理についてです。ログイン画面で未登録のユーザ名でログインしようとするとブラウザの画面下にエラーが表示されます。以下のサイトで確認できますが、その仕組みについてです。
http://www.mypress.jp:3020/

 サーバ側で起きたエラーをクライアント側で表示させるわけですが、Falcorを使って、フルスタック全体で、どのようにエラー処理をするのかを示すのが目的です。

 まずはエラー表示にはMaterial-UIを使いますが、エラーの見た目をわかりやすくするためにテーマをdarkBaseThemeからlightBaseThemeに変えます。src/routes.jsを2カ所変更します。

src/routes.js
import React  from 'react';
import {render} from 'react-dom'
import {BrowserRouter} from 'react-router-dom'

//import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme';
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme'; <==これを追加 
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import AppBar from 'material-ui/AppBar';

import App from './App';
import PageNotFound from './components/PageNotFound'

const target = document.getElementById('appRoot');

const node =(
  <MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme)}> <==これを変更
      <BrowserRouter>
        <App />
      </BrowserRouter>
  </MuiThemeProvider>
);

render(node, target);

 次に、クライアント側ですが、エラーが生じたときに呼ばれるcallback関数を設定します。これはFalcorのmodel作成時に指定します。またcallback関数の定義自体は、エラー表示上Appで実行させたいので、Appにおいて定義します。

 まずはmodel作成ですが、src/falcorModel.jsという独立したファイルで行います。modelを使いたいcomponentはこれをimportします。

src/falcorModel.js
import falcor from 'falcor';
import FalcorDataSource from 'falcor-http-datasource';
import { errorFunc } from './App';

const model = new falcor.Model({
  source: new FalcorDataSource('/model.json'),
  errorSelector: function(path, error) {
    errorFunc(error.value, path);
    /* この時間の間はログインリクエストはエラーとなり続ける。
     * 正しいリクエストもエラーとなる。リクエスト自体がキャッシュで実行されない。エラー表示も抑制される。*/
    error.$expires = -1000 * 60 * 0.5; //0.5分
    return error;
  }
});

export default model;

 model作成時にerrorSelectorプロパティを設定します。これがcallback関数でエラー発生時に呼ばれます。中身はerrFunc()でAppからimportしたものです。error.$expires はこのエラーオブジェクトの期限で、この時間の間はキャッシュに残り、同じリクエストをしても実行されません。例えばログインに失敗したら0.5分の間は再ログインを試みてもキャッシュの値を返すのみで実行されません。

 errFunc()はAppで以下のように定義されます。

src/App.js
import React  from 'react';
import {Route, Switch, Link, Redirect} from 'react-router-dom'

import HomeApp from './views/HomeApp';
import LoginView from './views/LoginView';
import LogoutView from './views/LogoutView';
import DashboardView from './views/DashboardView';
import RegisterView from './views/RegisterView';
import PageNotFound from './components/PageNotFound'

import AppBar from 'material-ui/AppBar';
import RaisedButton from 'material-ui/RaisedButton';
import IconButton from 'material-ui/IconButton';
import ActionHome from 'material-ui/svg-icons/action/home';
import Snackbar from 'material-ui/Snackbar';  <==エラー表示に使う

export default class App extends React.Component {
  static propTypes = {
  }

  constructor(props) {
    super(props);
    this.state = {
      errorValue: null
    }
    errorFuncUtil = this.handleFalcorErrors.bind(this); <==エラーハンドラにthisをbind
  }

  handleFalcorErrors(errMsg, errPath) { <==エラーハンドラの実体を定義
    let errorValue = `Error: ${errMsg} (path ${JSON.stringify(errPath)})`
    this.setState({errorValue});
  }

  render () {

    let errorSnackbarJSX = null; <==エラーが起きた時に Snackbarでエラーを表示する
    if(this.state.errorValue) {
      errorSnackbarJSX = <Snackbar
        open={true}
        message={this.state.errorValue}
        autoHideDuration={8000}
        onRequestClose={ () => {console.log('onCloseイベントのコードを書く');this.setState({errorValue:null});} } />;
    }

    let homeLinksJSX = (<IconButton tooltip="go to home">
        <Link to='/'><ActionHome /></Link>
    </IconButton>);

    let menuLinksJSX;
    let userIsLoggedIn = typeof localStorage !== 'undefined' && localStorage.token && location.pathname !== '/logout' ;

    if(userIsLoggedIn) {
        menuLinksJSX = (<span>
          <Link to='/dashboard'><RaisedButton label="管理画面" /></Link> 
          <Link to='/logout'><RaisedButton label="ログアウト"  /></Link> 
        </span>);
    } else {
        menuLinksJSX = (<span>
          <Link to='/register'><RaisedButton label="登録" /></Link> 
          <Link to='/login'><RaisedButton label="ログイン"  /></Link> 
        </span>);
    }

    return (
      <div>
        {errorSnackbarJSX} <==SnackbarをJSXに入れる
        <AppBar
            title='私のホームページ'
            iconElementLeft={homeLinksJSX}
            iconElementRight={menuLinksJSX} />

        <div>
          <Switch>
            <Route exact path='/' component={HomeApp} name='home' />
            <Route path='/login' component={LoginView} name='login' />
            <Route path='/logout' component={LogoutView} name='logout' />
            <Route path='/dashboard' component={DashboardView} name='dashboard' />
            <Route path='/register' component={RegisterView} name='register' />
            <Route component={PageNotFound}/>
          </Switch>
        </div>
      </div>
    );
  }
}

let errorFuncUtil =  (errMsg, errPath) => {
}

export { errorFuncUtil as errorFunc }; <==エラーハンドラーをexportしfalcorModel.jsでimportする

 Appではエラーハンドラの定義とエラー表示のJSXを定義します。表示にはSnackbarを使っているので、例えば未登録のユーザでログインしようとすると、ブラウザの画面下にエラーが表示されます。以下のサイトで試すことができます。
http://www.mypress.jp:3020/

 次にサーバ側に移ります。サーバ側のFalcorの処理は全てserver/routesSession.jsで行っています。サーバ側で注意すべきは1点で$errorの利用のみです。その他のFalcorのプログラムに関しては以下のページを参照してください。
Netflix Falcorについて(1)

server/routesSession.js
import configMongoose from './configMongoose';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import jwtSecret from './configSecret';

import jsonGraph from 'falcor-json-graph';
let $error = jsonGraph.error; <==JSONGraphのerrorの定義

const User = configMongoose.User;

export default [
  {
    route: ['login'] , /* ①password認証 ②返信のjwt tokenの作成 */
    call: (callPath, args) =>
    {
      const {username, password} = args[0];
      const saltedPassword = password + 'pubApp'; // pubApp is our salt string
      const saltedPassHash = crypto.
                             createHash('sha256').   /* cryptのHash instancesの作成 */
                             update(saltedPassword). /* hashする内容をupdateする。updateは複数回呼ぶことが可能 */
                             digest('hex');          /* 複数回updateされた内容のdigestを作成する */
      const userStatementQuery = {
        $and: [
          { 'username': username },
          { 'password': saltedPassHash }
        ]
      }

      return User.find(userStatementQuery, (err, user) => { if (err) throw err; }).then((result) => {
        if (result.length) {
          const role = result[0].role;
          const userDetailsToHash = username+role;
          const token = jwt.sign(userDetailsToHash, jwtSecret.secret); /* jwt tokenの作成 */

          return [
            {
              path: ['login', 'token'],
              value: token
            },
            {
              path: ['login', 'username'],
              value: username
            },
            {
              path: ['login', 'role'],
              value: role
            },
            {
              path: ['login', 'error'],
              value: false
            }
          ]; 
        } else {
          return {
              path: ['login'],
              value: $error('このユーザ名の登録がありません!') <==エラーを返す
          }
        }

        return result;
      });
    }
  },
  {
    route: ['register'],
    call: (callPath, args) => {
      const newUserObj = args[0];

      newUserObj.password = newUserObj.password + 'pubApp';
      newUserObj.password = crypto
        .createHash('sha256')
        .update(newUserObj.password)
        .digest('hex');

      const newUser = new User(newUserObj);

      return newUser.save((err, data) => { if (err) {throw err;}})
        .then((newRes) => {
          const newUserDetail = newRes.toObject();

          if (newUserDetail._id) {
            const newUserId = newUserDetail._id.toString();

            return [
              {
                path: ['register', 'newUserId'],
                value: newUserId
              },
              {
                path: ['register', 'error'],
                value: false
              }
            ];

          } else {
            return [
              {
                path: ['register', 'newUserId'],
                value: 'INVALID'
              },
              {
                path: ['register', 'error'],
                value: 'Registration failed - no id has been created'
              }
            ];
          }
        }).catch((reason) => console.error(reason));
    }
  }

];

 JSON Graphは3つのsentinelを持っています。$typeで指定するmetadataのことです。

atom: 配列のインデックス幅やプロパティの制限なくオブジェクト丸ごと返すときに使われる
ref: JSONをGraphとして扱うときに使われる、シンボリックリンクのようなもの
error: エラーを返すときに使われる

 今回はerrorを使います。サーバがerrorを返すと(ソースの②)、クライアント側のFalcorのmodelで定義したエラーのcallback関数が呼ばれます。これによってAppのSnackbarにエラーが表示されるのです。

 Falcorの$errorとMaterial-UIを使うと効果的なエラー処理が行えると思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0