
create-react-appを使わないでReactの開発環境を構築する方法を紹介します。
はじめに
近年のJavaScriptフレームワークはCLIを使ってワン・コマンドで設定できるようになっています。Reactではそのためのコマンドとしてcreate-react-appが提供されています。creat-react-appは簡単にReactを設定できる反面、設定が隠蔽されてしまい、細かい設定をするためにはejectする羽目になります。しかし、ejectをしてしまえば最後、create-react-appがアップデートされてもアップデートできなくなってしまいます。では、プロダクションなどで設定を細かく設定して管理したい場合はどうすればよいでしょうか?そういう人は自分でゼロから設定しましょう。
ということで、今回はcreate-react-appを使わないで、Reactの開発環境を構築していきましょう。
Reactの開発環境の構築方法
それでは、Reactの設定をしましょう。
前提
以下の設定が完了している必要があります。
- NodeJSがインストールされていること
- Yarnがインストールされていること
- Dockerがインストールしていること
詳しくは、「環境」を参照して下さい。
ベースの作成とパッケージのインストール
必要なフォルダおよびファイルを作成し、必要なパッケージをインストールします。
$ mkdir my-react-app
$ cd my-react-app/
$ yarn init -y
$ mkdir public src
$ touch public/index.html
$ touch src/index.js src/index.css
$ touch src/App.js src/App.test.js src/App.css
$ touch .babelrc webpack.config.babel.js
$ touch jest.config.js
$ mkdir __mocks__
$ touch __mocks__/styleMock.js
$ touch __mocks__/fileMock.js
$ yarn add --dev flow-bin flow-typed
$ yarn flow init
$ tree -aI node_modules
.
├── .babelrc
├── .flowconfig
├── __mocks__
│ ├── fileMock.js
│ └── styleMock.js
├── build
├── jest.config.js
├── package.json
├── public
│ └── index.html
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── index.css
│ └── index.js
├── webpack.config.babel.js
└── yarn.lock
$ yarn add react react-dom
$ yarn add --dev jest jest-enzyme enzyme enzyme-adapter-react-16
$ yarn add --dev @babel/core @babel/preset-env @babel/preset-react @babel/preset-flow @babel/register babel-jest
$ yarn add --dev webpack webpack-cli webpack-dev-server
$ yarn add --dev html-webpack-plugin mini-css-extract-plugin uglifyjs-webpack-plugin optimize-css-assets-webpack-plugin clean-webpack-plugin
$ yarn add --dev babel-loader css-loader html-loader
$ vi package.json
...
"scripts": {
"start": "webpack-dev-server --open --mode development",
"build": "webpack --mode production",
"test": "jest"
}
...
準備ができました。
設定を書く
それでは、ファイルの中身を書いていきましょう。
.babelrc
{
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"]
}
jest.config.js
module.exports = {
moduleNameMapper: {
'\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'/__mocks__/fileMock.js',
'\.(css|less)$': '/__mocks__/styleMock.js',
},
};
__mocks__/styleMock.js
module.exports = {};
__mocks__/fileMock.js
module.exports = 'test-file-stub';
.flowconfig
[ignore]
.*/node_modules/.*
.*/flow-typed/.*
.*/build/.*
.*\.(test|spec)\.js
.*/build/.*
.*webpack.*
.*jest\.config\.js
<PROJECT_ROOT>/src/index\.js
[include]
[libs]
[lints]
[options]
all=true
[strict]
webpack.config.babel.js
import path from 'path';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import UglifyJsPlugin from 'uglifyjs-webpack-plugin';
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
const outputPath = path.resolve(__dirname, 'build');
export default {
entry: './src/index.js',
output: {
filename: '[name].[hash].js',
path: outputPath,
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.html$/,
loader: 'html-loader',
},
],
},
devServer: {
contentBase: outputPath,
compress: true,
port: 3000,
},
devtool: 'eval-source-map',
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new MiniCssExtractPlugin({
filename: '[name].[hash].css',
}),
new CleanWebpackPlugin(),
],
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
compress: {
drop_console: true,
},
},
}),
new OptimizeCSSAssetsPlugin(),
],
},
};
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>My React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render( , document.getElementById('root'));
index.css
* {
margin: 0;
}
App.js
import React, { useState } from 'react';
import './App.css';
type Props = {
initialCount: number,
};
function App({ initialCount }: Props) {
const [count, setCount] = useState(initialCount);
return (
<div className="App" data-test="component-app">
<header className="App-header">
<p>My React App without Create React App!</p>
<p>Count: {count}</p>
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(initialCount)}>clear</button>
</div>
</header>
</div>
);
}
App.defaultProps = {
initialCount: 0,
};
export default App;
App.css
.App {
text-align: center;
}
.App-header {
background-color: #09d3ac;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 4vmin);
color: white;
}
button {
margin: 0 5px;
padding: 10px;
width: 200px;
font-size: calc(10px + 4vmin);
border-radius: 10px;
}
App.test.js
import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import EnzymeAdapter from 'enzyme-adapter-react-16';
import App from './App';
Enzyme.configure({ adapter: new EnzymeAdapter() });
it('renders without crashing', () => {
const wrapper = shallow(<App />);
const appComponent = wrapper.find(`[data-test="component-app"]`);
expect(wrapper).toBeTruthy();
expect(appComponent.length).toBe(1);
});
完成です。
今回のWebpackの設定は開発用とプロダクション用で分けていませんが、実際には分けて作り、webpack-mergeなどを使って共通化すると良いでしょう。
今回のJestの設定は公式ドキュメントの通りにしてあります。
なお、EslintやPrettierもプロジェクトのスタイリングルールに応じて設定した方が良いでしょう。
動作確認
まずは開発環境を起動してみましょう。
$ yarn start
...
ℹ 「wds」: Project is running at http://localhost:3000/
...
ℹ 「wdm」: Compiled successfully.
ブラウザで「localhost:3000」が表示されます。
カウンターの動作確認もしてみましょう。
OKですね。
次に、テスト実行してみましょう。
$ yarn test
...
PASS src/App.test.js
✓ renders without crashing (10ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.667s
Ran all test suites.
✨ Done in 7.68s.
テストもパスしました。
最後に、ビルドしてみましょう。
$ yarn build
$ ls build/
index.html main.babca1870d3e34b4c183.css main.babca1870d3e34b4c183.js
ビルドも成功しました。
これで、Reactの設定は完了しました。
Docker化する方法
それでは、Docker上で起動するようにしていきましょう。
ファイルの作成
必要なファイルを作成します。
$ touch Dockerfile.dev docker-compose.yml
設定を書く
Dockerの設定を書きましょう。
Dockerfile.dev
FROM node:12.10-alpine
WORKDIR /app
RUN apk update \
&& apk --no-cache add git ca-certificates wget
RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub \
&& wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.30-r0/glibc-2.30-r0.apk \
&& apk --no-cache add glibc-2.30-r0.apk
COPY package.json .
COPY yarn.lock .
RUN yarn install
COPY .flowconfig .
RUN yarn global add flow-typed \
&& flow-typed install
COPY . .
EXPOSE 3000
CMD yarn start
docker-compose.yml
version: "3"
services:
web:
command: "yarn start --host 0.0.0.0 --port 3000"
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- /app/node_modules
- .:/app
完成です。
FlowはAlpine上で実行する際に問題があり、こちらにissueが発行されています。結局、glibcを入れるか、flowのバイナリーを入れるかで対処する必要があり、今回は前者にしました。
動作確認
Dockerで起動できるか確認してみましょう。
$ docker-compose build --no-cache
$ docker-compose up -d
$ docker-compose ps
Name Command State Ports
------------------------------------------------------------------------------------
my-react-app_web_1 docker-entrypoint.sh yarn ... Up 0.0.0.0:3000->3000/tcp
ブラウザで「localhost:3000」を開けば、正しく表示されます。
これでDocker化まで完了しました。
おまけ
さらに、Webpackに開発用の設定を追加していきましょう。
画像を読み込むための設定
画像などのアセットを読み込むための設定を追加します。画像をWebpackで利用するには、url-loaderおよびfile-loaderのローダーを追加します。
$ yarn add --dev file-loader url-loader
$ vi webpack.config.babel.js
{
...
module: {
rules: [
...
{
test: /\.(jpe?g|png|gif|svg)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 10 * 1024,
name: '[name].[hash:8].[ext]',
outputPath: 'assets',
},
},
],
},
...
],
},
...
}
「url-loader」でファイルをbase64に変換してCSSに埋め込みます。ファイルのサイズがリミットを超える場合は「file-loader」でファイルをそのまま読み込みます。base64はファイルを都度読み込む必要がないのでページの読み込みを早くできますが、常にファイルをそのまま扱いたい場合は「file-loader」だけで良いです。
Polyfillの設定方法
古いブラウザに対応するために、BabelにPolyfillを追加しましょう。
$ yarn add core-js@3
.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
],
"@babel/preset-react",
"@babel/preset-flow"
]
}
@babel/polyfillはBabelの7.4.0から非推奨になっています。上記の設定で、必要なPolyfillだけをcorejsからインポートして使われるようになります。詳しくは、公式ドキュメントを参照して下さい。
HMR(Hot Module Replacement)の設定方法
webpack-dev-serverでは、デフォルトでdevServer.inlineが有効になっており、ライブリロードが実行されます。ライブリロードはソースコードを修正するたびにブラウザをリロードする機能ですが、一つのコンポーネントを修正するたびにブラウザをリロードするのは効率がよくありません。そこで、HMR(Hot Module Replacement)を有効することで、リロードすることなく、修正したコンポーネントだけ更新するようにしましょう。
webpack.config.babel.js (抜粋)
...
import webpack from 'webpack';
...
export default {
...
devServer: {
...
hot: true,
...
},
...
plugins: [
...
new webpack.HotModuleReplacementPlugin(),
],
...
};
devServer.hotを有効にし、かつ、HotModuleReplacementPluginを設定しました。
起動してみると、以下のようにHMRが有効になっていることが分かります。
react-hot-loaderの設定方法
Stateを保持したままコンポーネントの修正を反映するために、react-hot-loaderおよび@hot-loader/react-domを設定しましょう。
$ yarn add react-hot-loader @hot-loader/react-dom
.babelrc (抜粋)
{
"plugins": ["react-hot-loader/babel"],
...
}
webpack.config.babel.js (抜粋)
...
export default {
...
resolve: {
alias: {
'react-dom': '@hot-loader/react-dom',
},
},
};
App.js (抜粋)
...
import { hot } from 'react-hot-loader/root';
...
export default hot(App);
最後に
いかがでしたか?これでcreate-react-appに頼ることなく、自分でReactを設定できるようになったことでしょう。それでは。
環境
- NodeJS: v12.11.0
- Yarn: 1.17.3
- Docker: 19.03.2
- react: 16.10.0
- react-dom: 16.10.0
- react-hot-loader: 4.12.14
- @hot-loader/react-dom: 16.9.0
- core-js: 3
- @babel/core: 7.6.2
- @babel/preset-env: 7.6.2
- @babel/preset-flow: 7.0.0
- @babel/preset-react: 7.0.0
- @babel/register: 7.6.2
- babel-jest: 24.9.0
- jest: 24.9.0
- jest-enzyme: 7.1.1
- enzyme: 3.10.0
- enzyme-adapter-react-16: 1.14.0
- flow-bin: 0.108.0
- flow-typed: 2.6.1
- webpack: 4.41.0
- webpack-cli: 3.3.9
- webpack-dev-server: 3.8.1
- clean-webpack-plugin: 3.0.0
- html-webpack-plugin: 3.2.0
- optimize-css-assets-webpack-plugin: 5.0.3
- uglifyjs-webpack-plugin: 2.2.0
- mini-css-extract-plugin: 0.8.0
- babel-loader: 8.0.6
- css-loader: 3.2.0
- html-loader: 0.5.5
- url-loader: 2.2.0
- file-loader: 4.2.0