28
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

react-routerとreact-router-reduxのサンプルと環境構築

Last updated at Posted at 2017-11-01

react-routerreact-router-reduxのサンプルを作成し、Webpackの環境を構築する

react-routerが4.2で、react-router-reduxについてはα版の5.0がリリースされていた。
githubから入手して簡単な動作検証をしたかったが、なかなか動作させることができなかったので、簡単な動作確認ができる環境構築を備忘録として記載(今後随時更新予定)。

設定ファイルとindex.html

package.jsonwebpack.config.jsindex.htmlreact-routerreact-router-reduxのサンプルで共通で利用するようにした。

package.json

まずはpackage.jsonについて。
.babelrc.eslintファイルは用意せず、package.jsonに記載した。必要に応じて、
// eslint-disable-line(1行の場合)

/* eslint-disable */(ファイル全体)
をJSファイルに指定する(参考)。
また、依存関係については、dependenciesdevDependenciesのいずれかに記載があれば良い。
react-router-reduxを利用しない場合、redux関連の依存を外す。

package.json
{
  "name": "react-router-sample",
  "version": "0.1.0",
  "description": "sample",
  "repository": "",
  "license": "MIT",
  "authors": [
    "Author Name"
  ],
  "babel": {
    "presets": [
      "es2015",
      "es2016",
      "es2017",
      "react",
      "stage-2"
    ]
  },
  "eslintConfig": {
    "parser": "babel-eslint",
    "env": {
      "node": true,
      "es6": true,
      "jest": true
    },
    "plugins": [
      "import",
      "react"
    ],
    "extends": [
      "eslint:recommended",
      "plugin:import/errors",
      "plugin:react/recommended"
    ],
    "rules": {
      "prefer-arrow-callback": 2,
      "react/display-name": 0,
      "semi": [
        2,
        "never"
      ]
    }
  },
  "dependencies": {
    "webpack": "^3.8.1",
    "webpack-dev-server": "^2.9.4"
  },
  "devDependencies": {
    "history": "^4.7.2",
    "prop-types": "^15.6.0",
    "react-json-tree": "^0.11.0",
    "react-router": "^4.2.0",
    "react-router-redux": "^5.0.0-alpha.8",
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.0",
    "babel-eslint": "^8.0.1",
    "babel-jest": "^21.2.0",
    "babel-loader": "^7.1.2",
    "babel-plugin-transform-react-remove-prop-types": "^0.4.10",
    "babel-preset-env": "^1.6.1",
    "babel-preset-es2015": "^6.14.0",
    "babel-preset-es2016": "^6.24.1",
    "babel-preset-es2017": "^6.24.1",
    "babel-preset-react": "^6.5.0",
    "babel-preset-stage-0": "^6.24.1",
    "babel-preset-stage-1": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "babel-preset-stage-3": "^6.24.1",
    "create-react-class": "^15.6.2",
    "css-loader": "^0.28.7",
    "eslint": "^4.9.0",
    "eslint-config-rackt": "^1.1.1",
    "eslint-plugin-import": "^2.2.0",
    "eslint-plugin-react": "^7.4.0",
    "gzip-size": "^4.0.0",
    "jest": "^21.2.1",
    "pretty-bytes": "^4.0.2",
    "react": "^16.0.0",
    "react-dom": "^16.0.0",
    "react-redux": "^5.0.6",
    "react-router-dom": "^4.2.2",
    "react-test-renderer": "^16.0.0",
    "redux": "^3.6.0",
    "rollup": "^0.50.0",
    "rollup-plugin-babel": "^3.0.2",
    "rollup-plugin-uglify": "^2.0.1",
    "style-loader": "^0.19.0",
    "webpack-manifest-plugin": "^1.3.2",
    "webpack-merge": "^4.1.1",
    "html-webpack-plugin": "^2.30.1",
    "chunk-manifest-webpack-plugin": "^1.1.2",
    "extract-text-webpack-plugin": "^3.0.2",
    "js-yaml": "^3.10.0",
    "path-complete-extname": "^0.1.0",
    "compression-webpack-plugin": "^1.0.1"
  },
  "scripts": {
    "start": "webpack-dev-server --history-api-fallback --no-info --open"
  }
}

package.jsonのあるディレクトリでnpm installまたは、yarn installを実行し、インストールする。

webpack.config.js

webpack-dev-serverで利用することを前提にしている。

webpack.config.js
const path = require('path');

module.exports = {
  entry: './app.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
    loaders: [{
      test: /\.js$/,
      loader: 'babel',
      exclude: /node_modules/,
      include: __dirname,
      options: {
        presets: [
          ['env', {'modules': false}]
        ]
      }
    }]
  }
}

var src = path.join(__dirname, '..', '..', 'src')
var fs = require('fs')
if (fs.existsSync(src)) {
  // Use the latest src
  module.exports.resolve = { alias: { 'react-router-redux': src } }
  module.exports.module.loaders.push({
    test: /\.js$/,
    loaders: ['babel'],
    include: src
  });
}

index.html

起動元となるhtmlファイル。

index.html
<!DOCTYPE html>
<html>
  <head>
    <title>react-router-redux basic example</title>
    <meta charset="utf8"/>
  </head>
  <body>
    <div id="mount"></div>
    <script src="/bundle.js"></script>
  </body>
</html>

react-routerのサンプル

まずはreact-routerのサンプルから。

ディレクトリ構成

ディレクトリ構成は下図。

スクリーンショット 2017-11-02 0.33.32.png

サンプル

下記のapp.jsを作成する。

app.js
import React from 'react'
import ReactDOM from 'react-dom'
import { render } from 'react-dom'
import { Router, Route } from 'react-router'
import { Link } from 'react-router-dom'
import createHistory from 'history/createBrowserHistory'

const history = createHistory()

export class App extends React.Component {
  render() {
    return (
      <div>
        <h1>App</h1>
        <ul>
          <li><Link to="/about">About</Link></li>
          <li><Link to="/inbox">Inbox</Link></li>
          <li><Link to="/messages/10">Message</Link></li>
        </ul>
      </div>
    )
  }
}

export class About extends React.Component {
  render() {
    return <h3>About</h3>
  }
}

export class Inbox extends React.Component {
  render() {
    return (
      <div>
        <h2>Inbox</h2>
        {this.props.children || "Welcome to your Inbox"}
      </div>
    )
  }
}

export class Message extends React.Component {
  render() {
    return <h3>Message {this.props.match.params.id}</h3>
  }
}

ReactDOM.render(
  <Router history={history}>
    <div>
      <Route exact path="/" component={ App }/>
      <Route path="/about" component={About} />
      <Route path="/inbox" component={Inbox} />
      <Route path="/messages/:id" component={Message} />
    </div>
  </Router>,
  document.getElementById('mount')
)

実行とデモ

package.jsonのあるディレクトリでyarn run startまたはnpm run startを実行するとブラウザが起動し、下記画面が表示される。

スクリーンショット 2017-11-02 0.59.54.png

それぞれのリンクをクリックすると下記のように表示される。

スクリーンショット 2017-11-02 1.00.05.png スクリーンショット 2017-11-02 1.00.13.png スクリーンショット 2017-11-02 1.00.22.png

react-router-reduxのサンプル

ディレクトリ構成

ディレクトリ構成は下図。

スクリーンショット 2017-11-02 1.19.34.png

サンプル

サンプルは以下。

states/constants.js
export const INCREASE = 'INCREASE'
export const DECREASE = 'DECREASE'
actions/count.js
import { INCREASE, DECREASE } from '../states/constants'

export function increase(n) {
  return {
    type: INCREASE,
    amount: n
  }
}

export function decrease(n) {
  return {
    type: DECREASE,
    amount: n
  }
}
reducers/count.js
import { INCREASE, DECREASE } from '../states/constants'

const initialState = {
  number: 1
}

export function countReducer(state = initialState, action) {
  if(action.type === INCREASE) {
    return { number: state.number + action.amount }
  }
  else if(action.type === DECREASE) {
    return { number: state.number - action.amount }
  }
  return state
}
components/App.js
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as myActions from '../actions/count'

class AppComponent extends Component {
  render() {
    return (
      <div>
        <header>
          Links:
          <ul>
            <li><Link to="/count">count</Link></li>
            <li><Link to="/display">display</Link></li>
          </ul>
        </header>
      </div>
    )
  }
}

const mapState = (state) => ({
  count: state.count
})

const mapDispatch = (dispatch) => ({
  actions: bindActionCreators(myActions, dispatch)
})

export const App = connect(mapState, mapDispatch)(AppComponent)
components/Count.js
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as myActions from '../actions/count'

class CountComponent extends React.Component {
  render() {
    const { count, actions } = this.props
    return (
      <div>
        Some state changes:
        { count.number }
        <button onClick={() => actions.increase(1)}>Increase</button>
        <button onClick={() => actions.decrease(1)}>Decrease</button>
      </div>
    )
  }
}

const mapState = (state) => ({
  count: state.count
})

const mapDispatch = (dispatch) => ({
  actions: bindActionCreators(myActions, dispatch)
})

export const Count = connect(mapState, mapDispatch)(CountComponent)
components/Display.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as myActions from '../actions/count'

class DisplayComponent extends Component {
  render() {
    const { count, actions } = this.props

    return (
      <div style={{ width: "300px", marginLeft: "200px" }} >
      { count.number }
      </div>
    )
  }
}

const mapState = (state) => ({
  count: state.count
})

const mapDispatch = (dispatch) => ({
  actions: bindActionCreators(myActions, dispatch)
})

export const Display = connect(mapState, mapDispatch)(DisplayComponent)
components/index.js
export { App } from './App'
export { Count } from './Count'
export { Display } from './Display'
app.js
import React from 'react'
import ReactDOM from 'react-dom'

import { createStore, combineReducers, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'

import createHistory from 'history/createBrowserHistory'
import { Route } from 'react-router'

import { ConnectedRouter, routerReducer, routerMiddleware } from 'react-router-redux'

import { App, Count, Display } from './components'
import { countReducer } from './reducers/count'

const history = createHistory()
const middleware = routerMiddleware(history)


const store = createStore(
  combineReducers({
    count: countReducer,
    routing: routerReducer
  }),
  applyMiddleware(middleware)
)

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={ history }>
      <div>
        <Route exact path="/" component={ App }/>
        <Route path="/count" component={ Count }/>
        <Route path="/display" component={ Display }/>
      </div>
    </ConnectedRouter>
  </Provider>,
  document.getElementById('mount')
)

実行とデモ

package.jsonのあるディレクトリでyarn run startまたはnpm run startを実行するとブラウザが起動し、下記画面が表示される。

スクリーンショット 2017-11-02 1.25.50.png

"count"をクリックして、ボタン操作で数値を変更する。

スクリーンショット 2017-11-02 1.26.07.png

バックキー等で戻って、"display"をクリックすると、変更された数値が表示される。

スクリーンショット 2017-11-02 1.26.19.png

補足

Linkを利用しなくても、this.props.history.push('/count')を実行することで、画面遷移ができる。

react-router-reduxWebpack開発環境を構築する

Rails 5.1React環境を構築した際、Webpackの設定が非常に便利だったので、react-router-reduxの開発環境にも反映する。
構築方法にあたり、こちらに記載されている内容が参考になった。

ディレクトリ構成

react-router-reduxのサンプル」のディレクトリ構成を下記のように変更する。
actionscomponentsreducersstatesapp.jssrcディレクトリを作成し、その配下に移動。
また、dstディレクトリを作成。

スクリーンショット 2017-11-04 11.35.20.png

実行ファイル

binディレクトリを作成し、ruby2.xで実行ファイルを作成する。

```rb:bin/webpack`

!/usr/bin/env ruby

$stdout.sync = true

require "shellwords"
require "yaml"

ENV["NODE_ENV"] = "production"
NODE_ENV = "production"

APP_PATH = File.expand_path("../", dir)
NODE_MODULES_PATH = File.join(APP_PATH, "node_modules")
WEBPACK_CONFIG = File.join(APP_PATH, "config/webpack/#{NODE_ENV}.js")

unless File.exist?(WEBPACK_CONFIG)
puts "Webpack configuration not found."
puts "Please run bundle exec rails webpacker:install to install webpacker"
exit!
end

newenv = { "NODE_PATH" => NODE_MODULES_PATH.shellescape }
cmdline = ["yarn", "run", "webpack", "--", "--config", WEBPACK_CONFIG] + ARGV

Dir.chdir(APP_PATH) do
exec newenv, *cmdline
end


```rb:bin/webpack-dev-server
# !/usr/bin/env ruby
$stdout.sync = true

require "shellwords"
require "yaml"

ENV["NODE_ENV"] = "development"

APP_PATH          = File.expand_path("../", __dir__)
CONFIG_FILE       = File.join(APP_PATH, "config/webpacker.yml")
NODE_MODULES_PATH = File.join(APP_PATH, "node_modules")
WEBPACK_CONFIG    = File.join(APP_PATH, "config/webpack/development.js")

def args(key)
  index = ARGV.index(key)
  index ? ARGV[index + 1] : nil
end

begin
  dev_server = YAML.load_file(CONFIG_FILE)["development"]["dev_server"]

  DEV_SERVER_HOST = "http#{"s" if args('--https') || dev_server["https"]}://#{args('--host') || dev_server["host"]}:#{args('--port') || dev_server["port"]}"

rescue Errno::ENOENT, NoMethodError
  puts "Webpack dev_server configuration not found in #{CONFIG_FILE}."
  exit!
end

newenv = {
  "NODE_PATH" => NODE_MODULES_PATH.shellescape,
  "ASSET_HOST" => DEV_SERVER_HOST.shellescape
}.freeze

cmdline = ["yarn", "run", "webpack-dev-server", "--", "--progress", "--color", "--config", WEBPACK_CONFIG] + ARGV

Dir.chdir(APP_PATH) do
  exec newenv, *cmdline
end

設定ファイル

configディレクトリを作成、その配下にwebpack.config.jsに相当する設定ファイルを作成する。

config/webpack/loaders/assets.js
const { env, publicPath } = require('../configuration.js')

module.exports = {
  test: /\.(jpg|jpeg|png|gif|svg|eot|ttf|woff|woff2)$/i,
  use: [{
    loader: 'file-loader',
    options: {
      publicPath,
      name: env.NODE_ENV === 'production' ? '[name]-[hash].[ext]' : '[name].[ext]'
    }
  }]
}
config/webpack/loaders/css.js
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const { env } = require('../configuration.js')

module.exports = {
    test: /\.(css)$/i,
    use: ExtractTextPlugin.extract({
        fallback: 'style-loader',
        use: [
            { loader: 'css-loader', options: { minimize: env.NODE_ENV === 'production' } }
        ]
    })
}
config/webpack/loaders/react.js
module.exports = {
  test: /\.(js|jsx)?$/,
  exclude: /node_modules/,
  loader: 'babel-loader',
  query: {
    presets: ['es2015', 'react', 'stage-2']
  }
}
config/webpack/configuration.js
const { join, resolve } = require('path')
const { env } = require('process')
const { safeLoad } = require('js-yaml')
const { readFileSync } = require('fs')

const configPath = resolve('config', 'webpacker.yml')
const loadersDir = join(__dirname, 'loaders')
const settings = safeLoad(readFileSync(configPath), 'utf8')[env.NODE_ENV]

const output = {
  path: resolve('./dst', settings.public_output_path),
  publicPath: `http://${settings.dev_server.host}:${settings.dev_server.port}/${settings.public_output_path}`
}

module.exports = {
  settings,
  env,
  loadersDir,
  output
}
config/webpack/development.js
const merge = require('webpack-merge')
const sharedConfig = require('./shared.js')
const { settings, output } = require('./configuration.js')

module.exports = merge(sharedConfig, {
  devtool: 'cheap-eval-source-map',

  stats: {
    errorDetails: true
  },

  output: {
    pathinfo: true
  },

  devServer: {
    clientLogLevel: 'none',
    https: settings.dev_server.https,
    host: settings.dev_server.host,
    port: settings.dev_server.port,
    contentBase: output.path,
    publicPath: output.publicPath,
    compress: true,
    headers: { 'Access-Control-Allow-Origin': '*' },
    historyApiFallback: true,
    watchOptions: {
      ignored: /node_modules/
    }
  }
})
config/webpack/production.js
/* eslint global-require: 0 */

const webpack = require('webpack')
const merge = require('webpack-merge')
const CompressionPlugin = require('compression-webpack-plugin')
const sharedConfig = require('./shared.js')

module.exports = merge(sharedConfig, {
  output: { filename: '[name]-[chunkhash].js' },
  devtool: 'source-map',
  stats: 'normal',

  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      minimize: true,
      sourceMap: true,

      compress: {
        warnings: false
      },

      output: {
        comments: false
      }
    }),

    new CompressionPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: /\.(js|css|html|json|ico|svg|eot|otf|ttf)$/
    })
  ]
})
config/webpack/shared.js
/* eslint global-require: 0 */
/* eslint import/no-dynamic-require: 0 */

const webpack = require('webpack')
const ManifestPlugin = require('webpack-manifest-plugin')
const ChunkManifestPlugin = require('chunk-manifest-webpack-plugin')
const { basename, dirname, join, relative, resolve } = require('path')
const { sync } = require('glob')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const extname = require('path-complete-extname')
const { env, settings, output, loadersDir } = require('./configuration.js')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const extensionGlob = `**/*{${settings.extensions.join(',')}}*`
const entryPath = join(settings.source_path, settings.source_entry_path)
const packPaths = sync(join(entryPath, extensionGlob))

module.exports = {
  entry: packPaths.reduce(
    (map, entry) => {
      const localMap = map
      const namespace = relative(join(entryPath), dirname(entry))
      localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry)
      return localMap
    }, {}
  ),

  output: {
    filename: '[name].js',
    path: output.path,
    publicPath: output.publicPath
  },

  module: {
    rules: sync(join(loadersDir, '*.js')).map(loader => require(loader))
  },

  plugins: [
    new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
    new ExtractTextPlugin(env.NODE_ENV === 'production' ? '[name]-[hash].css' : '[name].css'),
    new ManifestPlugin(),
    new ChunkManifestPlugin({
      filename: 'chunk-manifest.json',
      manifestVariable: 'webpackManifest',
    }),
    new HtmlWebpackPlugin({
      hash: true,
      chunks: ['app'],
      filename: 'index.html',
      template: join(entryPath, '/index.template.ejs'),
      inject: 'body',
    })
  ],

  resolve: {
    extensions: settings.extensions,
    modules: [
      resolve(settings.source_path),
      'node_modules'
    ]
  },

  resolveLoader: {
    modules: ['node_modules']
  }
}
config/webpacker.yml
default: &default
  source_path: src
  source_entry_path: ""
  public_output_path: ""

  extensions:
    - .js
    - .jsx
    - .ts
    - .vue
    - .scss
    - .css
    - .png
    - .svg
    - .gif
    - .jpeg
    - .jpg

  server:
    host: 0.0.0.0
    port: 3000
    https: false

development:
  <<: *default

  dev_server:
    host: 0.0.0.0
    port: 3000
    https: false


production:
  <<: *default

  dev_server:
    host: 0.0.0.0
    port: 80
    https: false

index.htmlのテンプレート作成

index.htmlindex.template.ejsとリネームし、srcディレクトリ配下に移動。
ハッシュ付きのscriptタグを挿入するため、index.htmlに直接bundle.jsを記載せずhtml-webpack-pluginにより、index.htmldstに出力させることが目的。

src/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>react-router-redux basic example</title>
    <meta charset="utf8"/>
  </head>
  <body>
    <div id="mount"></div>
  </body>
</html>

bin/webpack-dev-serverによるビルド

bin/webpack-dev-serverを実行するとビルドされて、http://localhost:3000/にアクセスするとreact-router-reduxのサンプルが表示される。

スクリーンショット 2017-11-04 11.58.22.png

アプリケーションのファイル単位でブレークポイントを指定することができるので、デバッグや検証の利便性が向上する。

bin/webpackによるビルド

bin/webpackを実行すると、リリース(プロダクション)向けのビルドができる。
dst配下にビルドされたハッシュ付きのjsコードと、index.htmlが作成されるので、nginx等のWebサーバが起動している環境で、実行する。

その他

例えば、開発環境でcoffeeスクリプトを利用する場合、coffee-loaderのプラグインを追加でインストールし、下記ファイルを追加する。

config/webpack/loaders/react.js
module.exports = {
  test: /\.coffee(\.erb)?$/,
  loader: 'coffee-loader'
}

scss/sassの場合は以下。必要に応じて、プラグインを追加でインストールする必要がある。

config/webpack/loaders/react.js
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const { env } = require('../configuration.js')

module.exports = {
  test: /\.(scss|sass|css)$/i,
  use: ExtractTextPlugin.extract({
    fallback: 'style-loader',
    use: [
      { loader: 'css-loader', options: { minimize: env.NODE_ENV === 'production' } },
      { loader: 'postcss-loader', options: { sourceMap: true } },
      'resolve-url-loader',
      { loader: 'sass-loader', options: { sourceMap: true } }
    ]
  })
}
28
17
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
28
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?