Posted at

Reactのためのwebpack環境整備

More than 1 year has passed since last update.

勉強のためにPython django + Reactの構成で実験アプリを作っていて、その時に知ったwebpackの設定などをまとめておきたいと思います。

ちなみに、もしPython django + Reactの構成でWebアプリを作成するのなら、非公式ですが以下にプロジェクトのボイラープレートを公開してくださっているものがあるのでこれを使うのが楽そうです。

今回は、同じような構成を作ろうとすると何が必要なのかを勉強するため、上記を参考にさせてもらいつつ、自分で整備してみました。


create-react-app

まず、そもそもreactを今から始めるのであれば、webpackやbabelを自分でいろいろ整備するより、create-react-appを使うのが簡単です。

これは、npmでインストールできるreactの初期状態作成用のコマンドです。

これで作成されたReactアプリはreact-scriptsというコマンドでアプリの起動、ビルドなどを行うようになっています。

このreact-scriptsコマンドが、内部的にwebpackやbabelを使って開発用サーバー起動や本番用のビルドなどをうまい具合に行ってくれます。

ただ、ちょっと困る点として、webpackに独自に設定を追加しようと思ったときにそのままでは対応方法がありません。

対応するには、react-scriptsを使うのをやめて独自でwebpackなどの設定をメンテすることになります。

独自メンテにするにあたっての救済措置として、npm ejectを実行すると元々react-scriptsで管理していたwebpack.config.jsenvファイル群などの各種設定ファイルを生成することができます。

react-scriptsをそのまま使いつつ、独自のwebpack設定をすることについては、以下のissueで提案があったようですが対応不可としてクローズされています。

なお、本記事の以降の部分は、このejectを使わず自前で書こうとした上での記述になります。


webpack設定

webpack.config.jsの記述方法については、公式の文書をまずは見ておくのがいいと思います。

というのも、webpackのバージョンがv2に上がって設定ファイルの書き方が変わっていっているようです。

ネット上で調べものをしたときに、同じ設定なのに記事ごとに書き方が違ったりする場合もあるので、旧式の表記と最新の表記を把握しておく必要がありそうです。

私が見つけた範囲では、module.loadersmodule.loaders.queryなどのパラメータは非推奨になっています。

v1 旧表記


webpack.config.js

// パターンA: loaderのオプション設定をGETクエリ表記で書く方法

module: {
loaders: [
{
test: /\.png$/,
loader: "url-loader?mimetype=image/png"
}
]
}


webpack.config.js

// パタンーンB: loaderのオプション設定を query で指定する方法

module: {
loaders: [
{
test: /\.png$/,
loader: "url-loader",
query: { mimetype: "image/png" }
}
]
}

v2 新表記


webpack.config.js

module: {

rules: [

{
test: /\.png$/,
use: [
{
loader: "url-loader",
options: {
mimetype: "image/png"
}
}
]
}

]
}


対象のファイル条件をtestで指定した上で、複数のloaderの定義をuseの中で列挙する方式のようです。

また、オプション設定はoptionsという項目で指定します。


babel

Reactを使うにあたってのbabelの設定をシンプルに書くと以下のようになるようです。


webpack.config.js

module.exports = {

context: __dirname,

entry: [
'babel-polyfill',
'./src/index.js',
],

module: {
rules: [

{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
// react用表記と、es2015表記を有効化
presets: ['react', 'es2015'],

// 開発時に変換結果をキャッシュする
// 本番用のビルドにはこの設定は不要
cacheDirectory: true
}
}
]
}

]
}
}


なお、上記の設定を実行するには以下のnpmのインストールが必要です。

npm install --save react babel-polyfill

npm install --save-dev babel-loader babel-preset-es2015

上記で基本的には問題ないですが、最新のjavascriptの記述方法のいくつかを利用したい場合は個別にpluginsとして導入します。


babel追加設定: class内の変数定義

classの定義内に直接変数を定義したい場合はtransform-class-propertiesを使います。

class SomeClass extends Component {

// class変数
static classVar = "class var";

// インスタンス変数
instanceVar = 10;
}

例えば、React Componentの作成時にpropTypesを以下のように書くことができます。

class Some extends Component {

// class変数
static propTypes = {
tweets: PropTypes.array
}
}


babel追加設定: 配列やハッシュの展開表記

transform-object-rest-spreadを使うと、配列やハッシュを簡単に複製するための表記が使えるようになります。

以下のような stateに新規パラメータをマージした新しいハッシュを生成するような処理があるとします。

var state = { a: 10, b: 20 };

console.log(Object.assign({}, state, { b: 2000, c: 30 }));
//=> { a: 10, b: 2000, c: 30 }

これを、以下のように表記できるようになります。

var state = { a: 10, b: 20 };

console.log({...state, b: 2000, c: 30 });
//=> { a: 10, b: 2000, c: 30 }

上記、2つのpluginを加えたbabelの設定は以下のようになります。


webpack.config.js

// rules内の記述

{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: ['react', 'es2015']
plugins: [
'transform-class-properties', // class 変数定義を可能にする
'transform-object-rest-spread', // {...rest} 表記を可能にする
]
}
}
]
}

npm install --save-dev transform-class-properties transform-object-rest-spread


.babelrcファイル

babel-loaderで指定している optionsの内容はプロジェクトのトップディレクトリの.babelrcファイルに記述することもできます。

これまでの例を.babelrcを使うようにすると以下のような記述になります。


webpack.config.js

// rules内の記述

{
test: /\.js$/,
exclude: /node_modules/
use: [
{ loader: 'babel-loader' }
]
}


.bashrc

{

"cacheDirectory": true,
"presets": ["react", "es2015"]
"plugins": [
"transform-class-properties",
"transform-object-rest-spread"
]
}

Web上のデモなどを参考にするときはwebpack.config.js以外のこういったファイルも確認しておく必要があります。

個人的には、暗黙で利用されるファイルに設定を分散させると後から見落として理解しにくくなるような気がするのと、.bashrcはjsonファイルであるためコメントを書いたりすることができないので、webpack.config.jsに書く方がいいかなと思っています。


開発用サーバー

次に、開発用サーバー(ファイルを修正したら即時ブラウザを自動リロードしてくれる)を起動できるようにする設定を追加します。


webpack-config.js

+// pluginsで使用する

+var webpack = require('webpack')

module.exports = {
// 略
entry: [
+ 'webpack-dev-server/client?http://localhost:3000',
+ 'webpack/hot/only-dev-server',
+ 'react-hot-loader/patch',

'babel-polyfill',
'./assets/src/index.js',
],

// 略
// module.rules
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: ['react', 'es2015'],
plugins: [
+ 'react-hot-loader/babel',
'transform-class-properties', // class 変数定義を可能にする
'transform-object-rest-spread', // {...rest} 表記を可能にする
]
}

// 略
+ plugins: [
+ // webpack moduleに含まれる
+ new webpack.HotModuleReplacementPlugin(),
+ new webpack.NoEmitOnErrorsPlugin(), // don't reload if there is an error
+ ],

+ // webpack-dev-server向け設定
+ devServer: {
+ hot: true,
+ inline: true,
+ historyApiFallback: true,
+ port: 3000,
+ headers: { 'Access-Control-Allow-Origin': '*' } // 必要があれば設定する
+ },

+ // jsのsource mappingファイルを作成する
+ // https://webpack.js.org/configuration/devtool/#devtool
+ devtool: 'cheap-module-eval-source-map'
}


追加で以下のnpmモジュールが必要です。

$ npm install --save-dev react-hot-loader webpack-dev-server

上記の設定を追加した上で、以下のコマンドで開発用のサーバーが起動できます。

$ webpack-dev-server

これを、package.jsonscriptsに定義しておくのが良いと思います。


package.json

"scripts": {

"start": "webpack-dev-server"
}


暗黙で全てのjsファイルにモジュールをimportする

webpack.ProvidePluginを使うと、すべてのjsファイルでグローバルな定義として暗黙でモジュールを定義することができます。


webpack.config.js

var webpack = require('webpack')

module.exports = {
// 略
plugins: [
// 全js内にimportを暗黙定義
new webpack.ProvidePlugin({
Paper: ['material-ui/Paper', 'default'],
RaisedButton: ['material-ui/RaisedButton', 'default'],
TextField: ['material-ui/TextField', 'default'],
List: ['material-ui/List', 'List'],
ListItem: ['material-ui/List', 'ListItem'],
Colors: ['material-ui/styles', 'colors'],
})
]
// 略
}


上記は、material-uiというUI Componentライブラリを使った例です。

これで、importで明示せずともUI用のコンポートネントを普通のHTMLタグと同レベルで使えるようになります。

定義の書き方は以下のような書き換えになります。モジュール側でのexport defaultに対応して, 'default']の部分が必要になるのに注意です。

import Paper from 'material-ui/Paper';


Paper: ['material-ui/Paper', 'default']

import { ListItem } from 'material-ui/List';

ListItem: ['material-ui/List', 'ListItem']


まとめ

今回の内容は、最初に紹介した django + react boilerplateや、react-scripts ejectで生成されたものなどを参考にさせていただいて、気になるところを調べていったものです。

Javascriptの開発環境がwebpack + babelにおおよそ収束している感じですが、いざそれらについて調べ物をすると、記載されている内容が断片的だったり、バージョンによって書き方が違ったり、そもそも複数の表記方法があるため書き方が違ったり、色々と違いがあって辛いところです。