Meteor and Reactによるリアクティブシステム「ラーメン野郎を追いかけろ! @ Twitter」を作ってみた
Falcor+Reactフルスタック(開発環境)
Falcor+Reactフルスタック(react-router)
Falcor+Reactフルスタック(views層とcomponents層)
Falcor+Reactフルスタック(formsy-react)
Falcor+Reactフルスタック(エラー処理)
Falcor+Reactフルスタック(Material-UI)
前回はフルスタックの開発環境について述べました。今回はreact-routerに終点を当てたいと思います。個々の要素を詳細に述べるよりは、フルスタックのどちらかと言えば全体像を説明していきたいと思います。
アプリは以下のコマンドで起動します(前回のpackage.confを参照)
npm start
このコマンドで、まずサーバ側でexpressサーバが起動します。次にブラウザから、以下のURLにアクセスします。
http://www.mypress.jp:3019/
ホームページが表示され「私のホームページへようこそ !!!」というメッセージを目にするでしょう。このメカニズムは以下のようになっています。
①ブラウザからサーバにindex.htmlをリクエストします。
②サーバからはdist/index.htmlをブラウザに返します。
これはexpressで静的コンテンツディレクトリを以下のように指定しているからです。
app.use(express.static('dist'));
③index.htmlがブラウザに展開されると、次にapp.jsをリクエストします
<script src="app.js"></script>
④サーバからはdist/app.jsをブラウザに返します。
これは②と同じメカニズムです。
⑤app.jsのReactプログラムがブラウザに展開されます。(終わり)
以下にサーバのプログラム(server/server.js)を掲載します。falcor-routerは別ファイルにしてありますのでスッキリしたものになっています。
import express from 'express';
import bodyParser from 'body-parser';
import falcor from 'falcor';
import falcorExpress from 'falcor-express';
import falcorRouter from 'falcor-router';
import sessionRoutes from './routesSession.js';
const app = express();
/* username/passwordのPOST入力のために必要 */
app.use(bodyParser.urlencoded({extended: false}));
app.use('/model.json', falcorExpress.dataSourceRoute((req, res) => {
return new falcorRouter(sessionRoutes);
}));
app.use(express.static('dist'));
app.listen(3019, function () {
console.log('Example app listening on port 3019!')
})
export default app;
以下がブラウザが最初に読み込む dist/index.html です。
<!doctype html>
<html lang="jp">
<head>
<title>My App</title>
<meta charset="utf-8">
</head>
<body>
<div id="appRoot"></div>
<script src="app.js"></script>
</body>
</html>
このプログラムはシングルページアプリケーションとなっています。react-routerの出番です。react-routerは、v4になって以前の古いバージョンのソースコートは動作しなくなりました。ここでの説明は最新版のものとなりますのでご注意ください。
アプリのトップはwebpack.config.jsで指定したようにsrc/routes.jsとなります。
entry: ['babel-polyfill', './src/routes.js'],
routes.jsはこのプロジェクト・アプリのトップcomponentになります。ここでRouterの定義を行います。今回の場合は、Material-UIを使いますので、少し紛らわしいのですがMaterial-UIのテーマでRouterをラッピングすることになります。
import React from 'react';
import {render} from 'react-dom'
//import {BrowserRouter as Router, Route, Switch, Link, Redirect} from 'react-router-dom'
import {BrowserRouter} from 'react-router-dom'
import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme';
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(darkBaseTheme)}>
<BrowserRouter>
<App />
</BrowserRouter>
</MuiThemeProvider>
);
render(node, target);
MuiThemeProviderがMaterial-UIのテーマの定義となります。Material-UIを使う場合、これは必要です。Material-UIを使わない場合は不要で、Routerがトップになります。
react-routeは動的ルーティング(Dynamic Routing)を採用し、ルーティングはレンダリング時に実行されます。そのためRouterをトップに置く必要があるのです。全てのcomponentはRouterの中で動作します。
https://reacttraining.com/react-router/web/guides/philosophy
Appの役割は、アプリ全体のレイアウトとそれに伴うルーティングの定義となります。通常Navigationの定義はここで行うことになるでしょう。またアプリ全体のエラー処理ハンドラの定義もここが良いでしょう。動的ルーティングの必然的な結果として、レイアウトとルーティング定義は表裏の関係となり、同じ場所で行うことになります。以下にAppの全体を表示します。
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';
export default class App extends React.Component {
static propTypes = {
}
render () {
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>
<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>
);
}
}
基本的にここではルーティングの定義とNavigationを構築しています。今回はエラー処理については省いています。NavigationにはMaterial-UIのAppBarを使っています。AppBarの左にはhomeLinksJSXを、右側側にはmenuLinksJSXを置きます。menuLinksJSXはログイン/非ログインの状態によって表示内容を変えています。ログイン状態の判断は以下の変数で行っています。
let userIsLoggedIn = typeof localStorage !== 'undefined' && localStorage.token && location.pathname !== '/logout' ;
ソースコードは、詳細より全体的な概略を掴むことが大事だと思われます。詳細は、react-routerとMaterial-UIの公式サイトで確認してください。react-routerについての重要事項として、Routeから呼ばれたcomponentにはpropsとしてmatch、location、historyが渡されることを指摘しておきます。例えばLoginViewの中では、ログイン成功時に以下のようにしてリダイレクトすることが可能となります。
this.props.history.push({pathname: '/dashboard'});
以上でreact-routerの使い方の概略を述べました。詳細は省きましたが、大筋の意図は十分理解できると思います。
次回はreactアプリのReactのプログラム構成について述べたいと思います。