Reactで開発するときビューのルーティング部分はreact-routerが便利ということでした。
どうやって使えばいいのかなとGithubを確認したところチュートリアルアプリが用意されていたので、ちょっと試してみました。
Lesson 1 - Setting Up
Clone the Tutorial
$ git clone https://github.com/reactjs/react-router-tutorial $ cd react-router-tutorial $ cd lessons/01-setting-up $ npm install $ npm start
デモアプリが動きました。
コードはこんな感じ
Make Some Changes
module/App.jsのメッセージを変更するとBrowserが自動でリロードされます。
Lesson 2
Rendering a Route
index.jsを修正
// ... import { Router, Route, hashHistory } from 'react-router' render(( <Router history={hashHistory}> <Route path="/" component={App}/> </Router> ), document.getElementById('app'))
localhost:8080を確認するとさっきと同じように表示できる
Adding More Screens
次の2つのファイルを追加
- modules/About.js
- modules/Repos.js
modules/About.js
import React from 'react' export default React.createClass({ render() { return <div>About</div> } })
modules/Repos.js
import React from 'react' export default React.createClass({ render() { return <div>Repos</div> } })
ファイル追加後にindex.jsを修正
import React from 'react' import { render } from 'react-dom' import App from './modules/App' import { Router, Route, hashHistory } from 'react-router' import About from './modules/About' import Repos from './modules/Repos' render(( <Router history={hashHistory}> <Route path="/" component={App}/> {/* add the routes here */} <Route path="/repos" component={Repos}/> <Route path="/about" component={About}/> </Router> ), document.getElementById('app'))
次のURL指定で画面にアクセスできるようになります。
Lesson 3
Navigating with Link
画面から移動できるようにリンクを追加
modules/App.js
// modules/App.js import React from 'react' import { Link } from 'react-router' export default React.createClass({ render() { return ( <div> <h1>React Router Tutorial</h1> <ul role="nav"> <li><Link to="/about">About</Link></li> <li><Link to="/repos">Repos</Link></li> </ul> </div> ) } })
TOPページからAboutとReposページに移動できるようになります。
Lesson 4
Nested Routes
ルーティングのネストについてです。こういった対応に便利なEmber.jsが紹介されています。
Nested UI and Nested URLs
ネスト時のURLの考え方。
Sharing Our Navigation
index.js
// index.js // ... render(( <Router history={hashHistory}> <Route path="/" component={App}> {/* make them children of `App` */} <Route path="/repos" component={Repos}/> <Route path="/about" component={About}/> </Route> </Router> ), document.getElementById('app'))
modules/App.js
// modules/App.js // ... render() { return ( <div> <h1>Ghettohub Issues</h1> <ul role="nav"> <li><Link to="/about">About</Link></li> <li><Link to="/repos">Repos</Link></li> </ul> {/* add this */} {this.props.children} </div> ) } // ...
『{this.props.children}』がでてきました。子ルートのコンポーネントとしてレンダリングさせたいときに使う感じだと思います。
By Small and Simple Things are Great Things Brought to Pass
すべてのルートは独立したアプリとして動かせるという考え方についての紹介だと思います。
Lesson 5
Active Links
リンクのスタイル設定の話
Active Styles
modules/App.js
<li><Link to="/about" activeStyle={{ color: 'red' }}>About</Link></li> <li><Link to="/repos" activeStyle={{ color: 'red' }}>Repos</Link></li>
active(選択時)のスタイルを簡単に指定できます。
Active Class Name
CSSを指定する場合はこっち。
modules/App.js
<li><Link to="/about" activeClassName="active">About</Link></li> <li><Link to="/repos" activeClassName="active">Repos</Link></li>
index.htmlに追加
<link rel="stylesheet" href="index.css" />
index.css追加
.active { color: green; }
Nav Link Wrappers
active cssはReactコンポーネントにも指定できます。Linkコンポーネントの場合はこんな感じ。
<Link {...this.props} activeClassName="active"/>
これをつかったサンプルを作成します。
modules/NavLink.js
import React from 'react' import { Link } from 'react-router' export default React.createClass({ render() { return <Link {...this.props} activeClassName="active"/> } })
App.jsに追加
App.js
// App.js import NavLink from './NavLink' // ... <li><NavLink to="/about">About</NavLink></li> <li><NavLink to="/repos">Repos</NavLink></li>
Lesson 6
URL Params
以下の様なURLがあるとします。
/repos/reactjs/react-router
/repos/facebook/react
ルートパスで指定するときはこうなります。
/repos/:userName/:repoName
これはthis.props.params[name]で指定することができます。
Adding a Route with Parameters
Repoページを追加します。
// modules/Repo.js import React from 'react' export default React.createClass({ render() { return ( <div> <h2>{this.props.params.repoName}</h2> </div> ) } })
新しいルートを追加するときはindex.jsを修正します。
// ... // import Repo import Repo from './modules/Repo' render(( <Router history={hashHistory}> <Route path="/" component={App}> <Route path="/repos" component={Repos}/> {/* add the new route */} <Route path="/repos/:userName/:repoName" component={Repo}/> <Route path="/about" component={About}/> </Route> </Router> ), document.getElementById('app'))
Repos.jsを修正してリンクを追加します。
// Repos.js import { Link } from 'react-router' // ... export default React.createClass({ render() { return ( <div> <h2>Repos</h2> {/* add some links */} <ul> <li><Link to="/repos/reactjs/react-router">React Router</Link></li> <li><Link to="/repos/facebook/react">React</Link></li> </ul> </div> ) } })
これで『/repos/:userName/:repoName』を表現できました。
Lesson 7
More Nesting
ネストのネストになっているページにアクセスするとリンク部分が非表示になりますが、これは表示したままにすることも可能です。
こういう書き方になっていますが・・・
<Route path="/repos/:userName/:repoName" component={Repo}/>
こうしてあげます。
<Route path="/repos" component={Repos}> <Route path="/repos/:userName/:repoName" component={Repo}/> </Route>
index.js
<Route path="/repos" component={Repos}> <Route path="/repos/:userName/:repoName" component={Repo}/> </Route>
Repos.js
// Repos.js // ... <div> <h2>Repos</h2> <ul> <li><Link to="/repos/reactjs/react-router">React Router</Link></li> <li><Link to="/repos/facebook/react">React</Link></li> </ul> {/* will render `Repo.js` when at /repos/:userName/:repoName */} {this.props.children} </div>
Active Links
modules/Repos.js
// modules/Repos.js // import it import NavLink from './NavLink' // ... <li><NavLink to="/repos/reactjs/react-router">React Router</NavLink></li> <li><NavLink to="/repos/facebook/react">React</NavLink></li> // ...
Lesson 8
Index Routes
『/』にアクセスしたときのページ表示です。
modules/Home.js
import React from 'react' export default React.createClass({ render() { return <div>Home</div> } })
App.js
// App.js import Home from './Home' // ... <div> {/* ... */} {this.props.children || <Home/>} </div> //...
ここまででも動作はしますが、index.jsにルーティング設定も行います。
index.js
// index.js // new imports: // add `IndexRoute` to 'react-router' imports import { Router, Route, hashHistory, IndexRoute } from 'react-router' // and the Home component import Home from './modules/Home' // ... render(( <Router history={hashHistory}> <Route path="/" component={App}> {/* add it here, as a child of `/` */} <IndexRoute component={Home}/> <Route path="/repos" component={Repos}> <Route path="/repos/:userName/:repoName" component={Repo}/> </Route> <Route path="/about" component={About}/> </Route> </Router> ), document.getElementById('app'))
Lesson 9
Indexへのリンクについてです。
Index Links
まずこんな書き方ができます。
<li><NavLink to="/">Home</NavLink></li>
IndexLink
IndexLinkをImportすることで別の指定方法が利用できます。
import { IndexLink, Link } from 'react-router' // ... <li><IndexLink to="/" activeClassName="active">Home</IndexLink></li>
onlyActiveOnIndex Property
あとはこんな感じ
<li><Link to="/" activeClassName="active" onlyActiveOnIndex={true}>Home</Link></li>
<li><NavLink to="/" onlyActiveOnIndex={true}>Home</NavLink></li>
Lesson 10
Clean URLs with Browser History
URLの(#)の話
Configuring Browser History
browserHistoryについて
index.js
// bring in `browserHistory` instead of `hashHistory` import { Router, Route, browserHistory, IndexRoute } from 'react-router' render(( <Router history={browserHistory}> {/* ... */} </Router> ), document.getElementById('app'))
hashHistoryをbrowserHistoryに変更すると/#/の#がなくなる
Configuring Your Server
サーバー側でURLを正しく扱ってくれる設定
package.json
"start": "webpack-dev-server --inline --content-base . --history-api-fallback"
パスの指定を見直す必要があります。
<!-- index.html --> <!-- index.css -> /index.css --> <link rel=stylesheet href=/index.css> <!-- bundle.js -> /bundle.js --> <script src="/bundle.js"></script>
Lesson 11
Production-ish Server
Production環境とDev環境で実行時のオプションを切り替える方法
$ npm install express if-env compression --save
// package.json "scripts": { "start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev", "start:dev": "webpack-dev-server --inline --content-base . --history-api-fallback", "start:prod": "webpack && node server.js" },
server.jsの作成
var express = require('express') var path = require('path') var compression = require('compression') var app = express() // serve our static stuff like index.css app.use(express.static(__dirname)) // send all requests to index.html so browserHistory in React Router works app.get('*', function (req, res) { res.sendFile(path.join(__dirname, 'index.html')) }) var PORT = process.env.PORT || 8080 app.listen(PORT, function() { console.log('Production Express server running at localhost:' + PORT) })
Productionモードで実行する場合
NODE_ENV=production npm start
publicディレクトリを用意してindex.htmlとcssを移動します。
server.jsを編集
// server.js // ... // add path.join here app.use(express.static(path.join(__dirname, 'public'))) // ... app.get('*', function (req, res) { // and drop 'public' in the middle of here res.sendFile(path.join(__dirname, 'public', 'index.html')) })
webpackのディレクトリ設定をpublicに設定
// webpack.config.js // ... output: { path: 'public', // ... }
最後にnpm startのオプションを設定
"start:dev": "webpack-dev-server --inline --content-base public --history-api-fallback",
webpackのroot設定
// webpack.config.js // make sure to import this var webpack = require('webpack') module.exports = { // ... // add this handful of plugins that optimize the build // when we're in production plugins: process.env.NODE_ENV === 'production' ? [ new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin() ] : [], // ... }
server.jsの修正
// server.js // ... var compression = require('compression') var app = express() // must be first! app.use(compression())
Productionモードで動作すれば正しく設定できています。
NODE_ENV=production npm start
Lesson 12
Navigating Programatically
クリックイベントなどの処理からリンク処理を実行する方法
modules/Repos.js
// modules/Repos.js import React from 'react' import NavLink from './NavLink' export default React.createClass({ // add this method handleSubmit(event) { event.preventDefault() const userName = event.target.elements[0].value const repo = event.target.elements[1].value const path = `/repos/${userName}/${repo}` console.log(path) }, render() { return ( <div> <h2>Repos</h2> <ul> <li><NavLink to="/repos/reactjs/react-router">React Router</NavLink></li> <li><NavLink to="/repos/facebook/react">React</NavLink></li> {/* add this form */} <li> <form onSubmit={this.handleSubmit}> <input type="text" placeholder="userName"/> / {' '} <input type="text" placeholder="repo"/>{' '} <button type="submit">Go</button> </form> </li> </ul> {this.props.children} </div> ) } })
browserHistoryにpushすると新しい履歴を作成できます。
// Repos.js import { browserHistory } from 'react-router' // ... handleSubmit(event) { // ... const path = `/repos/${userName}/${repo}` browserHistory.push(path) }, // ...
Lesson 13
Server Rendering
Reactのサーバーサイドレンダリング。こういう書き方がそうみたいです。
render(<App/>, domNode) // can be rendered on the server as const markup = renderToString(<App/>)
webpack.server.config.jsファイルを新しく用意します。
var fs = require('fs') var path = require('path') module.exports = { entry: path.resolve(__dirname, 'server.js'), output: { filename: 'server.bundle.js' }, target: 'node', // keep node_module paths out of the bundle externals: fs.readdirSync(path.resolve(__dirname, 'node_modules')).concat([ 'react-dom/server', 'react/addons', ]).reduce(function (ext, mod) { ext[mod] = 'commonjs ' + mod return ext }, {}), node: { __filename: true, __dirname: true }, module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader?presets[]=es2015&presets[]=react' } ] } }
package.jsonにbuildの処理を追加
"scripts": { "start": "if-env NODE_ENV=production && npm run start:prod || npm run start:dev", "start:dev": "webpack-dev-server --inline --content-base public/ --history-api-fallback", "start:prod": "npm run build && node server.bundle.js", "build:client": "webpack", "build:server": "webpack --config webpack.server.config.js", "build": "npm run build:client && npm run build:server" },
routes.jsを用意します。ここまではindex.jsでルーティングの設定をしていましたが、この設定はroutes側で対応します。
modules/routes.js
import React from 'react' import { Route, IndexRoute } from 'react-router' import App from './App' import About from './About' import Repos from './Repos' import Repo from './Repo' import Home from './Home' module.exports = ( <Route path="/" component={App}> <IndexRoute component={Home}/> <Route path="/repos" component={Repos}> <Route path="/repos/:userName/:repoName" component={Repo}/> </Route> <Route path="/about" component={About}/> </Route> )
index.js側を修正します。
import React from 'react' import { render } from 'react-dom' import { Router, browserHistory } from 'react-router' // import routes and pass them into <Router/> import routes from './modules/routes' render( <Router routes={routes} history={browserHistory}/>, document.getElementById('app') )
server.jsを修正します。
import express from 'express' import path from 'path' import compression from 'compression' import React from 'react' import { renderToString } from 'react-dom/server' import { match, RouterContext } from 'react-router' import routes from './modules/routes' var app = express() app.use(compression()) // serve our static stuff like index.css app.use(express.static(path.join(__dirname, 'public'), {index: false})) // send all requests to index.html so browserHistory works app.get('/', (req, res) => { match({ routes, location: req.url }, (err, redirect, props) => { if (err) { res.status(500).send(err.message) } else if (redirect) { res.redirect(redirect.pathname + redirect.search) } else if (props) { // hey we made it! const appHtml = renderToString(<RouterContext {...props}/>) res.send(renderPage(appHtml)) } else { res.status(404).send('Not Found') } }) }) function renderPage(appHtml) { return ` <!doctype html public="storage"> <html> <meta charset=utf-8/> <title>My First React Router App</title> <link rel=stylesheet href=/index.css> <div id=app>${appHtml}</div> <script src="/bundle.js"></script> ` } var PORT = process.env.PORT || 8080 app.listen(PORT, function() { console.log('Production Express server running at localhost:' + PORT) })
Lesson13ですが、実装後に画面を表示すると『Cannot GET /』となりました。ここに関してはちょっとよくわかりませんでした。
Lesson14は参考サイトリンクが記載してあるだけなので、チュートリアルはひとまずここまで。順番どおりに説明があったので分かりやすかったです。
サンプルコード
公式サイトのものと同じものですが、実際に動かしたLesson12までのコードです。