Webpack4でReactの環境構築。Create react appを使わずにReactアプリを作る。- 1 -Babel, React Hot Loader-

【所要時間】

トータル20時間くらい(2019年~2月10,11日)

【概要】

  • create-react-appを使わずにReactアプリを作る。
  • 第一弾はとりあえずReactを開発環境と本番環境でデプロイ出来るようにする方法にフォーカス
  • Webpack 4, Babel 7, HMR(Hot Module Replacement)を設定
  • 理解できてるかまだわからないので、間違ってたらご指摘いただきたいです。

参考記事一覧

【要約・学んだこと】

Webpack、Babelとは?

とても簡単に説明すると

  • Webpack :
    いろんなJavaScriptを1つのファイルにまとめて、htmlに読み込める状態に変換してくれる。オプションでcss、画像とかもまとめられる。
  • Babel:
    ES2015など新しいJavaScriptの機能を使って書いたコードなどを、対応していないブラウザでも表示できるように変換してくれる。オプションでJSXで書いたコードなども変換できる。

こんなイメージらしい。いろんな人が解説してくれている。

ハマりどころ

Reactの開発、本番環境を構築する上で個別にインストールしなければいけないWebpackやBabelの関連dependenciesが複数ある(と見せかけて@babel/preset-reactは複数のdependenciesのパッケージであったりする)。さらに2018年に書かれた記事でもバージョン違いでうまくいかないことがある。(Webpack 4からはwebpack-cliのダウンロードが必要。Babel-coreじゃなくて@babel/coreが必要みたいな。)
結果、それぞれのdependenciesが何を行なっているか、設定の意味を理解しなければ、プロジェクト毎に必要なdependenciesや設定がわからなくてきつい。

おまけにwebpack公式ページの解説内で、同じタイトルで違うページがあったりで非常にしんどかった。

Reactをインストール

create-react-appで作った場合存在しない手順をとりあえず書いていく。まずはReactのインストール

yarn add react react-dom

とりあえずルートとなるindex.jsを作成。Babelの力を試すためにclassコンポーネントを作る。

/src/index.jsimport React from 'react';
import ReactDOM from 'react-dom';
class Index extends React.Component {
render() {
return (
<div>
<h2>This is Root</h2>
</div>
);
}
}
ReactDOM.render(
<Index />,
document.getElementById('root')
);

エントリーポイントとなるHTMLファイルを作成する。

//public/index.html<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>この下にReactが表示される</p>
<div id="root"></div>
</body>
</html>

そもそもstartというコマンドが存在しないので、yarn run startを実行してもうまくいかない。

yarn run start
yarn run v1.9.4
error Command "start" not found.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

次から本番のWebpackとBabelの設定に入る。

Webpackのインストールと設定

  • Webpackのインストール
yarn add -D webpack webpack-cli

それぞれのdependenciesを確認。

  • webpack : webpack本体
  • webpack-cli: Comand line Interfaceの略。これに送られたparameterが、設定ファイル(通常webpack.config.js)と一致するparameterにマッピングしてくれる。

これらはプロジェクト毎にアップグレードできるようにローカルにインストールした方が良い。

  • webpackの設定

package.jsonにコマンドラインからwebpackを実行出来るようにするためのscriptを書く。

package.json
...
“scripts”: {
“start”: “webpack — config webpack.config.js”
},
...

これによりyarn run startとコマンドラインで入力すると、“webpack — config webpack.config.js”が実行される。

  • webpack.config.jsを書く。

下記を参照に最低限必要そうな部分を書く。

// webpack.config.jsconst path = require('path');module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public')
}
};
  • const path: これがないとコマンドラインから実行できないようだ。定義しないとエラーになる。
  • mode: development、production, noneのいずれかを定義しないと警告が出る。指定しないとproductionになる。
  • entry: エントリーポイントとなるファイルの指定(Reactだと一番上の親となるファイルのこと)。何も指定しないと./src/index.jsになる。複数指定することも可能とのこと。指定しないと忘れた時にどうやってエントリーポイントを指定しているかわからなくなりそうなので、指定した方がいいと思う。
  • output: webpackが実行された後に出力されるファイルの場所。何も指定しないと./dist/main.jsとなる。
    filename: 出力されるファイル名。今回はbundle.js
    path: 絶対パスで指定しなければいけない。今回は/public

これで実行すると

Tats-MacBA-9:webpackori AsamiTasuya$ yarn run start
yarn run v1.9.4
$ webpack --mode development
/Users/AsamiTasuya/my-app/webpackori/node_modules/webpack-cli/bin/cli.js:231
throw err;
^
Error: custom keyword definition is invalid: data/errors should be boolean
at Ajv.addKeyword (/Users/AsamiTasuya/my-app/webpackori/node_modules/ajv/lib/keyword.js:65:13)

先日はつまらなかったところでエラーが出た。ググるとわずか3時間前にでたイシューで、その解決案がソッコーで出てる。

というわけでpackage.jsonに下記を追記

"resolutions": {
"ajv": "6.8.1"
}

そして下記のコマンドを実行。

yarn remove ajv
yarn install

改めて実行すると

Tats-MacBA-9:webpackori AsamiTasuya$ yarn start
yarn run v1.9.4
$ webpack --config webpack.config.js
Hash: 4486d62221769549a278
Version: webpack 4.29.3
Time: 102ms
Built at: 02/10/2019 12:38:38 PM
Asset Size Chunks Chunk Names
bundle.js 3.98 KiB main [emitted] main
Entrypoint main = bundle.js
[./src/index.js] 206 bytes {main} [built] [failed] [1 error]
ERROR in ./src/index.js 5:2
Module parse failed: Unexpected token (5:2)
You may need an appropriate loader to handle this file type.
|
| const Index = () => (
> <div>
| <h2>This is Root</h2>
| </div>

モジュールの解析に失敗しましたとエラーが出るが、public/bundle.jsが作成されていることがわかる。つまりwebpackは機能している!

babelのインストールと設定

  • babelのインストール

まずは必要なdependenciesをインストール。ここら辺が古いチュートリアルだと@がついてないのをインストールしている、(インストールは出来る)が、後ほどうまくいかなくなるので注意。

yarn add -D @babel/core @babel/preset-env @babel/preset-react babel-loader

それぞれのdependenciesを確認する。

以上4つがReactとWebpackの環境ではミニマムで必要となりそう。

  • babelの設定

公式ドキュメントでReactの推奨設定のようなものがある。ただ今回Webpackの推奨設定のようなものもあるので、理解と工夫が必要になりそう。

まずは.babelrcを作成。これはbabelの設定ファイルである。

// .babelrc{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}

これはpresetの設定。これによりプラグインが使用できる。ここでいうプラグインは@babel/preset-envと@babel/preset-reactのことだと思われる。

オプションも設定できるようだが、とりあえずここでは指定しない。

次にwebpackの設定を修正する。

const path = require('path');module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public')
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
}

};
  • module: モジュールに関する設定。
  • rules: モジュールのルール。
  • test: このモジュールを適用するファイルの拡張子を指定する。ここでは.jsと.jsx
  • exclude: ここで指定されたディレクトリ、ファイルはこのモジュールの設定から除外される。ここでは/node_modules/ディレクトリの中身は無視するという意味。
  • loader: どのローダーを使用するか。ここでは先ほどインストールしたbabel-loaderを使用する。

そしてhtmlファイルにこのWebpackで作成される/public/bundle.jsを組み込む。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>この下にReactが表示される</p>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>

これで設定ができた。yarn startを実行すると

Tats-MacBA-9:webpackori AsamiTasuya$ yarn start
yarn run v1.9.4
$ webpack --config webpack.config.js
Hash: 591014c59cd91e97cc9f
Version: webpack 4.29.3
Time: 3313ms
Built at: 02/10/2019 2:25:43 PM
Asset Size Chunks Chunk Names
bundle.js 118 KiB 0 [emitted] main
Entrypoint main = bundle.js
[7] (webpack)/buildin/global.js 472 bytes {0} [built]
[8] ./src/index.js + 2 modules 3.6 KiB {0} [built]
| ./src/index.js 182 bytes [built]
| + 2 hidden modules
+ 7 hidden modules
✨ Done in 5.01s.webpackは上手く実行されたようだ。

public/index.htmlのページをブラウザ上で見てみると

上手く表示された。これでとりあえずReactアプリがWebで表示されるようになった。

次に追加機能をつけていく。

developmentモードの設定

先ほど上手くデプロイできたが、どこかを修正するたびに

  • セーブ→yarn startを実行→ページをリロード

という3つのプロセスを踏まなければ最新の状態がわからない。セーブはエディタ、yarn startはターミナル、ページのリロードはブラウザで行うので、これはめんどくさい。

webpack-dev-serverやwebpack-dev-middlewareを使えば、create react appと同様に、セーブするだけでブラウザが自動でリロードされるようだが、react-hot-loaderもそれっぽい。わからん。

とりあえず

ということなので、HMRの機能を持っていた方が良さそう。

目に入ったものを調べたところ

  • webpack-dev-middleware
    セーブすると自動でブラウザ上で最新の状態にしてくれる、express serverにマウントするミドルウェア。おそらくこいつが本体。
  • webpac-dev-server
    webpack-dev-middlewareとexpressがセットになったイメージ。また、HMRも取り扱う。つまりサーバーサイドを一切書かないときに使う奴。webpackに説明があるので公式だと思われる。
  • webpack-hot-middleware
    webpack-dev-serverの代替品だが、サーバー自体を起動せずに、webpack-dev-middlewareと一緒に、expressサーバーにマウントするもの?よくわからん。
  • webpack-hot-server-middleware
    webpack-dev-middleware, webpack-hot-middlewareと一緒に使うサーバーレンダリングアプリ?よくわからん。
  • react-hot-loader
    Reactで使える。stateを失うことなくリロードできる。とりあえず使うならReact boiler plateもある。

ちなみにwebpack-serveというのもあったらしいが、現在は使わない方がいいらしい。

ググった感じどれが最強ってわけでもなさそうだし、(react-hot-loaderならstateが残るのかな?) サーバーサイドはとりあえず書かないので、公式ドキュメントに従うことに。webpack-dev-serverと、その解説ページにのってるreact-hot-loaderを使う。

その前にReactで追加のファイルの作成と現在のファイルを修正。(複数のコンポーネントで確かめたい)

// src/index.jsimport React from 'react';
import ReactDOM from 'react-dom';
import Index from './components/app.js';
ReactDOM.render(
<Index />,
document.getElementById('root')
);
// src/components/app.jsimport React from 'react';
import Test from './test';
import Counter from './counter';
const Index = () => (
<div>
<Counter />
<Test />
<p>これがIndexコンポーネント</p>
</div>
);
export default Index;// src/components/counter.jsimport React from 'react';class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 1,
};
this.clickHandler = this.clickHandler.bind(this);
}
clickHandler() {
const increment = this.state.number + 1;
this.setState({
number : increment
});
}
render() {
return (
<div>
<h3>{this.state.number}</h3>
<button type='button' onClick={this.clickHandler}>click</button>
</div>
);
}
}
export default Counter;// src/components/test.jsimport React from 'react';const Test = () => (
<div>
<p>diremkfds</p>
</div>
);
export default Test;

webpack-dev-serverのインストールと設定

webpack-dev-serverを使う。ローカルホストを起動することで、ブラウザ上で最新の状態に自動更新してくれる。設定はここら辺を参照。

まずはインストール

yarn add -D webpack-dev-server

次にpackage.jsonのscriptを変更

// package.json..."scripts": {
"start": "webpack-dev-server --open",
"build": "webpack --config webpack.config.js"
},
...

yarn startでwebpack-dev-serverが実行される。 — openをつけることでブラウザを開いてくれる。

そしてwebpack.config.jsも変更

// webpack.config.jsmodule.exports = {
mode: 'production',
entry: './src/index.js',
devServer: {
contentBase: path.join(__dirname, 'public'),
port: 3000,
compress: true,
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public')
},
  • devServer: webpack-dev-serverの設定
  • contentBase: どこのフォルダの動作を検証するか。
  • port: localhostのポート。この場合localhost:3000になる。
  • compress: gzip圧縮をする。よくわからないが転送量が減るらしいのでtrueにしてみた。

これらを設定してターミナルでyarn startを実行すると、ブラウザの新しいタブが開き、コードを書き換えると自動でブラウザ上での表示もアップデートされるようになった。

Hot Module Replacementの設定

前述の通り、HMRを使うとページ全体ではなく変更があった箇所のみリロードされるようなので、こちらの設定をする。

重要なポイントはこれ↓を最初に読むとバージョン3の古いやり方になってしまう点。GitHubを読む。(古いやり方でも動作はする。)

まずはreact-hot-loaderのインストール

yarn add -D react-hot-loader

scriptにhotオプションをつける。

// package.json"scripts": {
"start": "webpack-dev-server --hot --open",
"build": "webpack --config webpack.config.js"
},

.babelrcにプラグインとして追加

// .babelrc
{
"plugins": ["react-hot-loader/babel"]
}

app.jsにhotを追加。(react-hot-loader/rootをimportして、hot(Index)と書くのはこの4.6.5バージョンだと違うようだ。)

//src/app.jsimport React from 'react';
import Counter from './counter';
import { hot } from 'react-hot-loader'
const Index = () => (
<div>
<Counter />
<p>これはapp.js</p>
</div>
);
export default hot(module)(Index);

そしてwebpack.config.jsを書き換える。devserverは削除する必要がある。(ここでハマった。一度素の状態から作り直したときに気が付いた。)

const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public'),
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
},
performance: {
hints: 'warning'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};

とりあえず表示されたが、オートリロードが効かなくなった。調べてみると、webpack-dev-serverではbundle.jsを作るのではなく、メモリにファイルを作るとのこと。確かに毎回bundle.jsを作っているとしたら時間もかかる。

これを解決するのにhtml-webpack-pluginを追加する。

  • html-webpack-plugin: jsファイル以外の変更も監視し、変更があった場合自動リロードするようにしてくれる。
yarn add -D html-webpack-plugin

webpack.config.jsを修正

const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path');
const webpack = require('webpack');
const htmlPlugin = new HtmlWebpackPlugin({
template: "./public/index.html",
filename: "./index.html"
});
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public'),
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
},
performance: {
hints: 'warning'
},
plugins: [
htmlPlugin,
new webpack.HotModuleReplacementPlugin()
]
};

最初にstateを5に増やして、app.jsを変更してセーブしても、stateが5のままになっているのがわかる。ただ、stateを持っているcomponentとは別のTestを修正するとstateはリセットされてリロードされる。この辺がよくわからないが、一応RHLが効いているようだ。

そこでcounter.jsとtest.jsにもhotを記述してみると、どこを更新してもstateが保たれるようになった。

だが、そのような記述は見当たらないのでよくわからない。先ほどのhot moduleの記述方が使えない点も含めて”react-hot-loader”: “4.6.5”で何かが変わっているのかもしれないが、とりあえず動作するようになったので、今回はこれ以上深追いはしない。

最後にyarn startで出ている警告を消す。こちらも対処方を調べると時間がかかるので、とりあえずオフにする。

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
bundle.js (1.45 MiB)
WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
main (1.45 MiB)
bundle.js
main.be15c4035c66fa7ce956.hot-update.js
WARNING in webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of yourapplication.
For more info visit https://webpack.js.org/guides/code-splitting/

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path');
const webpack = require('webpack');
const htmlPlugin = new HtmlWebpackPlugin({
template: "./public/index.html",
filename: "./index.html"
});
module.exports = {
mode: 'production',
entry: './src/index.js',

output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public'),
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
},
performance: {
hints: false
},
plugins: [
htmlPlugin,
new webpack.HotModuleReplacementPlugin()
]
};

【わからなかったこと】

  • React-Hot-Loader全般。ブラックボックスだらけ。とりあえず開発に支障はなさそう(そもそもstateが保たれなくてもいい。)ドキュメントがあまり信頼出来ないので、深追いはしない。

【感想】

かなり時間をかけてドキュメントを読み込んだおかげで、だいぶ理解は出来たと思う。特にwebpack.config.jsが何をやってるかだいぶクリアになってきた。難点はドキュメントがどこにあるかとにかくわかりにくい。とりあえずその辺も大分奮闘し、手順を丁寧に書いたので、次回ハマったときに役に立ちそうで、時間をかけた甲斐があったと思う。

今回はReactの部分だけで終わったが、ここがおそらく基礎となるはず。次回はCSS, SCSS, ESLintの導入をする予定。

Front end engineer. React, TypeScript, Three.js

Front end engineer. React, TypeScript, Three.js