はじめに
勉強目的なのでCreate React App相当の設定を実施するわけではないです!
参考にされる際はこの点ご留意下さい。
モチベーション
Create React AppはサクッとReactアプリの雛形が作成できて便利ですよね。
Reactの環境構築が複雑だからこそ、このようなツールが提供されているのだと思います。
ただ、処理過程を理解しないのは些か不安でもあります。
段階的に実施すればそこまで難しくないのでは?と思い立ち、勉強を兼ねて0からの構築を試してみました。
STEP 0 : Reactをインストールする
プロジェクトの初期化が済んでいない場合は、まずnpm init
コマンドで初期化しておいて下さい。
npm i react react-dom
STEP 1 : Webpackでのバンドルを試す
まずはWebpackでのバンドルを試してみます。
npm i -D webpack webpack-cli
JSファイルのバンドルであればローダーは必要ありません。
そして、React自体はJSで記述できるので、この段階でReactアプリがバンドルできます。
エントリーポイントのindex.js
とReactコンポーネントのApp.js
をsrc配下に作成していきます。
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
const rootEl = document.getElementById('root')
const rootComponent = React.createElement(App)
ReactDOM.render(rootComponent, rootEl)
import React from 'react'
const App = () => React.createElement('h1', null, 'React App')
export default App
package.json
にビルド用のスクリプトを書きます
"scripts": {
"build": "webpack --mode=production"
}
npm run build
を実行すると、distディレクトリ配下にmain.js
が作成されます。
distディレクトリにindex.html
を作成してmain.js
を読み込んでみます。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script src="main.js"></script>
</body>
</html>
そしてindex.html
を直接ブラウザで開き、React Appと表示されていればOKです。
エントリーポイントがsrc/index.js
出力先がdist/main.js
というのはwebpackのデフォルト設定です。なので、webpackの設定ファイルを作成せずともバンドルができました。
とはいえ、流石にJSXなしは辛いのでJSXで記述できるようにします。
STEP 2 : JSXでの記述を試す
JSXは標準のJSではないので、Babelを通して変換します。
まずは必要なパッケージをインストール
npm i -D @babel/core @babel/preset-env @babel/preset-react babel-loader
Babelの設定ファイルを作成し、使用するプリセットを明記します。
後ろに記載したプリセットから処理されるので、配列の順番に意味がある点に注意します。
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react']
}
Webpackの設定ファイルを作成し、Babelを実行するタイミング(babel-loader)を明記します。
また、jsx拡張子はデフォルトで省略できないので、省略できるようにこちらも明記します。
const path = require('path')
// 出力先は絶対パスで記載する
const outputPath = path.resolve(__dirname, 'dist')
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: outputPath
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: ['babel-loader']
}
]
},
resolve: {
extensions: ['.js', '.jsx']
}
}
App.jsだけJSXファイルにしてみます。拡張子をjsxにしてから編集します。
import React from 'react'
const App = () => <h1>React App JSX</h1>
export default App
npm run build
を実行して、React App JSXと表示されればOKです。
JSXファイルで記述でき、かなりReactっぽくなりました。ただindex.html
を直接ブラウザで参照してしまっているので、サーバー経由で参照するようにします。
STEP 3 : 開発サーバーを試す
webpack-dev-serverをインストールして開発サーバーを導入します。
npm i -D webpack-dev-server
Webpackの設定ファイルに、開発サーバーの設定を追記します。
const path = require('path')
// 出力先は絶対パスで記載する
const outputPath = path.resolve(__dirname, 'dist')
module.exports = {
entry: './src/index.js',
output: { /* 省略 */ },
module: { /* 省略 */ },
resolve: { /* 省略 */ },
devServer: {
contentBase: outputPath
}
}
contentBaseにdistディレクトリを指定しているので、distディレクトリをルートにサーバーが起動します。
package.json
にwebpack-dev-serverの実行コマンドを追記します。
(最新版だとwebpack-dev-server
でなくwebpack serve
で起動するようです)
"scripts": {
"build": "webpack --mode=production",
"dev": "webpack serve --mode=development"
}
npm run dev
でサーバーを起動し http://localhost:8080 にアクセスしてSTEP2と同様の結果になればOKです。
ちなみに、webpack-dev-serverはファイルの変更を検知してブラウザを自動リロードしてくれます。便利ですね。
ところで、dist配下に直接index.html
を作成してしまったのですが、基本的にdist配下はバージョン管理しないはずです。
HTMLファイルの生成も、Webpack経由で処理するようにします。
STEP 4 : HTML Webpack Pluginを試す
HTML用のプラグインをインストールします。
npm i -D html-webpack-plugin@next
src配下にindex.html
を作成します。
基本的にはSTEP1と同じですが、scriptタグはプラグインで自動挿入されるので削除しています。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Webpackの設定ファイルを修正します。
HTMLファイルのテンプレートを指定しつつ、devServerのcontentBaseもdistディレクトリに依存しなくなったので、コメントアウトしています。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const outputPath = path.resolve(__dirname, 'dist')
module.exports = {
entry: './src/index.js',
output: { /* 省略 */ },
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
],
module: { /* 省略 */ },
resolve: { /* 省略 */ },
devServer: {
// contentBase: outputPath
}
}
distディレクトリを削除し、開発サーバーを再起動します。distディレクトリは存在しませんが、STEP2と同様の内容が表示されるはずです。
npm run build
を実行すると、distディレクトリが作成され、STEP2と同様のファイルが生成されます。
HTMLファイルが処理できたので、次はCSSファイルもWebpack経由で処理するようにします。
STEP 5 : CSSローダーを試す
基本的には「JSでCSSをimportし、バンドル後のファイルに含める」という考え方です。
ただ、JSに含めたCSSをどのようにしてHTMLに反映させるかについては、選択肢があります。
そのため
①JSでCSSをimportするためのローダー
②JS内のCSSをHTMLに反映させる方法
それぞれ別のパッケージを使用する必要があります。
今回は、①css-loader
②style-loader
を使用します。
style-loader
は取り込んだCSSをheadタグ内のstyleタグとして展開してくれます。
npm i -D css-loader style-loader
Webpackの設定ファイルを修正します。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const outputPath = path.resolve(__dirname, 'dist')
module.exports = {
entry: './src/index.js',
output: { /* 省略 */ },
plugins: [/* 省略 */],
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: ['babel-loader']
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
resolve: { /* 省略 */ },
devServer: { /* 省略 */ }
}
useでは最後に定義したものから順番に処理されるため、必ずcss-loader
を後に記載する必要があります。
では、簡単なCSSファイルを作成し、index.js
で読み込んでみます。
.tomato-title {
color: tomato;
}
import React from 'react'
const App = () => <h1 className="tomato-title">React App JSX</h1>
export default App
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './styles.css'
const rootEl = document.getElementById('root')
const rootComponent = React.createElement(App)
ReactDOM.render(rootComponent, rootEl)
タイトルがトマト色に変わっていたらOKです。
次は静的ファイルもWebpack経由で処理するようにします。
STEP 6 : Asset Modulesを試す
静的ファイルの読み込みはurl-loader
やfile-loader
が使用できますが、Webpack5系から標準で静的ファイル用のtypeプロパティが追加されたので、そちらを使用してみます。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const outputPath = path.resolve(__dirname, 'dist')
module.exports = {
entry: './src/index.js',
output: { /* 省略 */ },
plugins: [/* 省略 */],
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: ['babel-loader']
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|svg|jpg)$/, // 他に必要な拡張子があれば追加
type: 'asset'
}
]
},
resolve: { /* 省略 */ },
devServer: { /* 省略 */ }
}
標準だと、8kb以下のファイルはバンドルファイルに同梱(inline)、それ以上は別ファイルとしてdist配下に配置(resource)されるようです。
適当な画像をバンドルしてみます。
import React from 'react'
import image from './image.png'
const App = () => (
<>
<h1 className="tomato-title">React App</h1>
<img src={image} width="150" height="150" />
</>
)
export default App
指定した画像が表示されればOKです。
以上で、Webpack経由でのバンドルは一通り設定できました。
最後に、このアプリをテストしてみたいと思います。
STEP 7 : テストを試す
テストランナーは多くの種類がありますが、React公式が推奨しているJestを導入します。(Reactと同じFacebook製なので相性が良いです)
npm i -D jest
Jestはnode環境で動作する関係上、ESModules(import/export)が標準では使用できません。
しかし、Babelの設定ファイルを検出すると自動でBabelを通して実行してくれます。
STEP2でBabelの設定ファイルは作成しているので、簡単なテストを作って実行してみます。
import React from 'react'
import { render } from 'react-dom'
import { act } from 'react-dom/test-utils'
import App from './App'
test('render content', () => {
const container = document.createElement('div')
document.body.appendChild(container)
act(() => { render(<App />, container) })
expect(container.textContent).toBe('React App')
})
App.jsx内で画像をimportしていますが、これはWebpackによって解決されるものです。Jestは画像importの処理方法を知らないので、このままだとエラーになります。
画像(ついでにCSSも)をimportするときはモックを使用するように、Jestの設定ファイルで指定します。
module.exports = {
moduleNameMapper: {
"\\.(png|svg|jpg)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
}
}
モックを作成します。
module.exports = 'test-file-stub'
module.exports = {}
package.json
にスクリプトを追加し、npm test
で実行します。
"scripts": {
"build": "webpack --mode=production",
"dev": "webpack serve --mode=development",
"test": "jest"
}
テスト成功で終了すればOKです。
おわりに
以上で、Reactの基本的な環境構築ができました。
各ステップでの細かい設定に関しては調査不足なところがあるので、今後の課題にしていきたいですが、全体の流れが俯瞰できて良い勉強になりました。