react-router
とreact-router-redux
のサンプルを作成し、Webpack
の環境を構築する
react-router
が4.2で、react-router-redux
についてはα版の5.0がリリースされていた。
githubから入手して簡単な動作検証をしたかったが、なかなか動作させることができなかったので、簡単な動作確認ができる環境構築を備忘録として記載(今後随時更新予定)。
設定ファイルとindex.html
package.json
、webpack.config.js
、index.html
はreact-router
とreact-router-redux
のサンプルで共通で利用するようにした。
package.json
まずはpackage.json
について。
.babelrc
と.eslint
ファイルは用意せず、package.json
に記載した。必要に応じて、
// eslint-disable-line
(1行の場合)
や
/* eslint-disable */
(ファイル全体)
をJSファイルに指定する(参考)。
また、依存関係については、dependencies
とdevDependencies
のいずれかに記載があれば良い。
react-router-redux
を利用しない場合、redux
関連の依存を外す。
{
"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
で利用することを前提にしている。
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
ファイル。
<!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
のサンプルから。
ディレクトリ構成
ディレクトリ構成は下図。

サンプル
下記の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
を実行するとブラウザが起動し、下記画面が表示される。

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



react-router-redux
のサンプル
ディレクトリ構成
ディレクトリ構成は下図。

サンプル
サンプルは以下。
export const INCREASE = 'INCREASE'
export const DECREASE = 'DECREASE'
import { INCREASE, DECREASE } from '../states/constants'
export function increase(n) {
return {
type: INCREASE,
amount: n
}
}
export function decrease(n) {
return {
type: DECREASE,
amount: n
}
}
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
}
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)
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)
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)
export { App } from './App'
export { Count } from './Count'
export { Display } from './Display'
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
を実行するとブラウザが起動し、下記画面が表示される。

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

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

補足
Link
を利用しなくても、this.props.history.push('/count')
を実行することで、画面遷移ができる。
react-router-redux
のWebpack
開発環境を構築する
Rails 5.1
でReact
環境を構築した際、Webpack
の設定が非常に便利だったので、react-router-redux
の開発環境にも反映する。
構築方法にあたり、こちらに記載されている内容が参考になった。
ディレクトリ構成
「react-router-redux
のサンプル」のディレクトリ構成を下記のように変更する。
actions
、components
、reducers
、states
、app.js
をsrc
ディレクトリを作成し、その配下に移動。
また、dst
ディレクトリを作成。

実行ファイル
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
に相当する設定ファイルを作成する。
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]'
}
}]
}
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' } }
]
})
}
module.exports = {
test: /\.(js|jsx)?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react', 'stage-2']
}
}
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
}
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/
}
}
})
/* 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)$/
})
]
})
/* 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']
}
}
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.html
をindex.template.ejs
とリネームし、src
ディレクトリ配下に移動。
ハッシュ付きのscript
タグを挿入するため、index.html
に直接bundle.js
を記載せずhtml-webpack-plugin
により、index.html
をdst
に出力させることが目的。
<!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
のサンプルが表示される。

アプリケーションのファイル単位でブレークポイントを指定することができるので、デバッグや検証の利便性が向上する。
bin/webpack
によるビルド
bin/webpack
を実行すると、リリース(プロダクション)向けのビルドができる。
dst
配下にビルドされたハッシュ付きのjs
コードと、index.html
が作成されるので、nginx
等のWebサーバが起動している環境で、実行する。
その他
例えば、開発環境でcoffee
スクリプトを利用する場合、coffee-loader
のプラグインを追加でインストールし、下記ファイルを追加する。
module.exports = {
test: /\.coffee(\.erb)?$/,
loader: 'coffee-loader'
}
scss/sass
の場合は以下。必要に応じて、プラグインを追加でインストールする必要がある。
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 } }
]
})
}