tyoshikawa1106のブログ

- Force.com Developer Blog -

React:React Router Tutorialを試してみました

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

デモアプリが動きました。
f:id:tyoshikawa1106:20160520133831p:plain

コードはこんな感じ
f:id:tyoshikawa1106:20160520134053p:plain

f:id:tyoshikawa1106:20160520134105p:plain

Make Some Changes

module/App.jsのメッセージを変更するとBrowserが自動でリロードされます。
f:id:tyoshikawa1106:20160520134448p:plain

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ページに移動できるようになります。
f:id:tyoshikawa1106:20160520142028p:plain

Lesson 4

Nested Routes

ルーティングのネストについてです。こういった対応に便利なEmber.jsが紹介されています。

Nested UI and Nested URLs

ネスト時のURLの考え方。
f:id:tyoshikawa1106:20160520151228p:plain

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}』がでてきました。子ルートのコンポーネントとしてレンダリングさせたいときに使う感じだと思います。
f:id:tyoshikawa1106:20160520152444p:plain

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(選択時)のスタイルを簡単に指定できます。
f:id:tyoshikawa1106:20160520152728p:plain

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』を表現できました。
f:id:tyoshikawa1106:20160520155429p:plain

Lesson 7

More Nesting

ネストのネストになっているページにアクセスするとリンク部分が非表示になりますが、これは表示したままにすることも可能です。

f:id:tyoshikawa1106:20160520163412p:plain


こういう書き方になっていますが・・・

<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に変更すると/#/の#がなくなる
f:id:tyoshikawa1106:20160520192955p:plain

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までのコードです。