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を確認する。
- @babel/core: babelのコアファイル?例えばES6で書かれたコードをES5に変換してくれる。
- @babel/preset-env: 細かい管理をしなくても最新のJavaScriptを使えるようにしてくれるpreset
- @babel/preset-react: react用のpluginのパッケージ。これらのプラグインのセットのようだ。@babel/plugin-syntax-jsx, @babel/plugin-transform-react-jsx, @babel/plugin-transform-react-display-name, @babel/plugin-transform-react-jsx-self, @babel/plugin-transform-react-jsx-source
- babel-loader: ローダー。ローダーとはwebpackの前処理で使用するもののこと。JSだけでなく様々な静的ファイルがバンドル出来る。これがWebpackの変換を助ける。
以上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.jsWARNING 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の導入をする予定。