Edited at

React開発環境構築2018


この記事について

この記事では実際にReactの開発環境をセットアップする方法を説明します。ReactなどのSPAでネックになる速度を改善するための手法としてサーバーサイドレンダリングも行います。

フロントエンド開発についてまだよく知らないという人は下の記事を読んでおきましょう。

フロントエンドとはいえ、今回は静的サイトではなく動的サイト(サーバーサイドレンダリングを行うため)を作成するのでサーバー側の話がメインとなります。

実際のところ「ブラウザを立ち上げてページが表示されるまで」には何が起きるのかではブラウザがどのようにページを描画するのかなどについて書いたのですが、ローカルでjsを実行するのはかなり時間がかかる上、jsの実行中はページの描画をストップさせてしまいます。



例えば170KBほどのjsファイルはパース・コンパイル・実行で3.5秒程かかることもあります。そこでサーバーサイドでHTMLを生成しておくことでページの描画を圧倒的に早くするという技術がサーバーサイドレンダリングです。

Reactを使うなら一緒に押さえておくべきだと思います。

ちなみにGoogleの調査ではページのロード時間が1秒から3秒に伸びると直帰率が32%増加するそうです。


必要なソフトをインストールする


エディタ

まずはVSCodeをインストールします。別に他のエディタでも良いのですが、特に信念がないのであればVSCodeを使いましょう。

VSCodeをインストールしたら起動して「ワークスペースフォルダー」を追加して開いておきます。ついでにターミナルで開いておくと後で便利です。

スクリーンショット 2018-09-15 15.53.05.png

それと左の拡張機能タブから「Bracket Pair Colorizer」と「ESLint」をインストールしておきます。

(重要)さらにメニューから設定を開きeditor.insertSpacesにチェックを入れ、editor.tabSizeを2にします。2タブは正義です。

設定画面上の検索窓で検索すれば出てきます。


Node.js

Node.jsはローカルでjavascriptを実行できるようにしてくれるすごいやつです。一緒について来るnpmというパッケージマネージャーが超有能で、今のフロントエンド開発はこのnpmが支えています。


Git

まあ無くても良いっちゃ良いんですが。macなら最初から入っていると思います。

公式サイト


yarn

さっきはnpmが凄いって書いたんですがyarnの方がパッケージのインストールが早いのでyarnも使います。さっき開いたターミナルでnpmを使ってインストールします。

npm i -g yarn

ただし公式のドキュメントではhomebrewを使うように指示されているそうです。silverskyvictoさん指摘ありがとうございます。

パッケージ管理ができるようにpackage.jsonを設定しておきます。

yarn init -y


ESLint

それと構文解析ツールのESLintもインストールしておきます。サーバーにデプロイしてからエラーを発見するよりも、書いた瞬間エラーを表示してくれた方が何かと便利です。

yarn add eslint eslint-plugin-react -D

-DはdevDependenciesにインストールせよという命令で、開発中だけ使用し、本番稼働では使わないようなライブラリはこっちにインストールします。まあ間違えても大差ないですが。

.eslintrc.jsonを作成してeslintの設定をします。


フォルダ構造

qiita/

├─node_modules/ ←yarnでインストールしたライブラリが保存されています。
│ └─...
├─.eslintrc.json ←new!
├─package.json
└─yarn.lock ←パッケージのバージョン情報などが保存されています。

よく分からないと思うのでreact用の設定を用意しておきました。コピーして使いましょう。


.eslintrc.json

{

"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
"no-console":"warn",
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}


サーバーを書く

サーバーサイドはnode.jsで書いていきます。


Expressの設定

続いてサーバーを超簡単にセットアップしてくれるライブラリ「express」をインストール。

yarn add express

serverフォルダを作りserver.jsを作成します。


フォルダ構造

qiita/

...
├─server/ ←new!
│ └─server.js ←new!
...


server.js

import express from 'express';

const app = express();

//GETリクエストでルートにアクセスが会った時の動作
app.get('/', (req, res)=>{
res.send('Hello express');
});

//3000番ポートを使ってサーバーを立ち上げ
app.listen(3000, ()=>{
console.log('app listening on 3000');
});



babel-nodeの設定

nodeがes6を読めるようにbabelの設定をしてあげます。

yarn add babel-core babel-preset-env


フォルダ構造

qiita/

...
├─.babelrc ←new!
...


.babelrc

{

"presets": [
"env"
]
}

nodeに渡す前にbabelがコンパイルしてくれる便利なツールbabel-nodeをインストールします。

yarn add -D babel-cli

babel-clibabel-node が含まれています。


package.json

...

"main": "index.js",
+ "scripts": {
+ "dev": "babel-node server/server.js"
+ },
"license": "MIT",
...

合わせてpackage.jsonを書き換えます。

+の行は追記した行で-は削除された行です。git diffで生成しています。


サーバーを実行

yarn run dev

# ↓
# app listening on 3000


これでhttp://localhost:3000/にアクセスすればHello expressと表示されるはずです。


index.html

publicフォルダを用意してindex.htmlを作成します。


フォルダ構造

qiita/

...
├─public/ ←new!
│ └─index.html ←new!
...


index.html

<!DOCTYPE html>

<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="root"></div>
</body>
</html>

ちなみに今時のhtmlの書き方は普通のHTMLの書き方が参考になります。

htmlを配信できるようにserver.jsを書き換えます。


server.js

 import express from 'express';

+import fs from 'fs';

const app = express();

app.get('/', (req, res)=>{
- res.send('Hello express');
+ const index = fs.readFileSync('./public/index.html', 'utf-8');
+ res.send(index);
});
...


もう一回yarn run devすればindex.htmlが配信されているとわかるかと思います。真っ白で分からないという場合は「⌘+⌥+i」で出現する検証モードを見ましょう。


初めてのReact

yarn add react react-dom


フォルダ構造

qiita/

...
├─src/ ←new!
│ └─index.jsx ←new!
...


index.jsx

import React from 'react';

import ReactDOM from 'react-dom';

ReactDOM.render(
<h1>Hello React</h1>,
document.getElementById('root')
);


ReactDOM.render()は第一引数のjsxを第二引数のエレメントの子要素に加える関数です。


Webpackの設定

ファイルをまとめたりminifyしてくれたりする便利なやつ。

yarn add webpack -D


フォルダ構造

qiita/

...
├─webpack.config.dev.babel.js ←new!
...

webpack.config webpackの設定ファイルである事を示しています。

dev 開発用(本番用では無い)という意味。

babel 設定ファイルを読む前にこの設定ファイル自体をbabelで変換しろという意味になります。

js jsファイルなので。


webpack.config.dev.babel.js

import path from 'path';

export default {
mode: 'development',
entry:[
path.resolve(__dirname, 'src/')
],
output:{
path: path.resolve(__dirname, 'public'),
publicPath: '/',
filename: 'bundle.js'
},
resolve:{
extensions: ['.js','.json','.jsx']
},
module:{
rules:[
{
test: /\.jsx?$/,
use:{
loader: 'babel-loader'
},
include: path.resolve(__dirname, 'src')
}
]
}
};


webpackでファイルをまとめて一つのbundle.jsにするんですが、ES6で書かれたファイルとかjsxとかを読めるようにするためにbabel-loaderというローダーを使用します。

yarn add babel-loader -D


サーバーにWebpackを組み込む

サーバーでコンパイルして配信させるようにします。この設定は開発中のみ使用し、本番ではあらかじめビルドしたものを配信することになります。

yarn add webpack-dev-middleware -D


server.js

 import express from 'express';

import fs from 'fs';
+import webpack from 'webpack';
+import webpackConfig from '../webpack.config.dev.babel';
+import webpackMiddleware from 'webpack-dev-middleware';

const app = express();

+const compiler = webpack(webpackConfig);
+app.use(webpackMiddleware(compiler));
+
app.get('/', (req, res)=>{
const index = fs.readFileSync('./public/index.html', 'utf-8');
...



public/index.html

...

<body>
<div id="root"></div>
+ <script src="/bundle.js"></script>
</body>
</html>

いざ実行!

yarn run dev

ERROR in ./src/index.jsx

Module build failed (from ./node_modules/babel-loader/lib/index.js):
Error: Cannot find module '@babel/core'
babel-loader@8 requires Babel 7.x (the package '@babel/core'). If you'd like to use Babel 6.x ('babel-core'), you should install 'babel-loader@7'.

🙃<ふぁっきゅ

babelを7.x系にアップグレードするかbabel-loaderを7.x系にダウングレードするか選べば良いようです。パッチノートを読むとbabel7.0.0が出たのが19日前となっていてあまり気乗りはしなかったのですが、せっかくの機会なのでbabel7.x系を使ってみます。


以前のバージョンをアンインストール

yarn remove babel-core babel-preset-env babel-cli



新しいバージョンをインストール

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


ついでにbabelがjsxを理解できるようにpreset-reactをインストールしておきました。

yarn add -D @babel/node

babel-nodeを使用するために@babel/nodeをインストールしておきます。


.babelrc

 {

"presets": [
- "env"
+ "@babel/env",
+ "@babel/preset-react"
]
}

yarn run devでサーバーを実行してページを開くと"Hello React"と表示されているはずです。


HMR(Hot Module Replacement)


HMR(Hot Module Replacement)はWebpackが提供する、ブラウザのリロードをすること無くアプリケーションのJSを更新する開発ツールです。


この機能を使用するためにサーバーを書きました。まあサーバーを書いておくと後々Server Side Rendering(SSR)出来たりもしますし。

早速実装していきます。

yarn add webpack-hot-middleware -D


webpack.config.dev.babel.js

+import webpack from 'webpack';

import path from 'path';

export default {
mode: 'development',
entry:[
+ 'webpack-hot-middleware/client',
path.resolve(__dirname, 'src/')
],
output:{
...
publicPath: '/',
filename: 'bundle.js'
},
+ plugins:[
+ new webpack.HotModuleReplacementPlugin()
+ ],
resolve:{
extensions: ['.js','.json','.jsx']
},
...



server/server.js

...

import webpackConfig from '../webpack.config.dev.babel';
import webpackMiddleware from 'webpack-dev-middleware';
+import HMR from 'webpack-hot-middleware';

const app = express();

const compiler = webpack(webpackConfig);
app.use(webpackMiddleware(compiler));
+app.use(HMR(compiler));

app.get('/', (req, res)=>{
...



フォルダ構造

qiita/

...
├─src/
│ └─App.jsx ←new!
...


src/App.jsx

import React from 'react';

const App = ()=>(
<h1>Hello React</h1>
);

export default App;



src/index.jsx

 import React from 'react';

import ReactDOM from 'react-dom';

-ReactDOM.render(
- <h1>Hello React</h1>,
- document.getElementById('root')
-);
+import App from './App';
+
+const render = (_App) =>{
+ ReactDOM.render(
+ <_App />,
+ document.getElementById('root')
+ );
+};
+
+if(module.hot){
+ module.hot.accept('./App', ()=>{
+ const NextApp = require('./App').default;
+ render(NextApp);
+ });
+}
+
+render(App);


yarn run dev

hmr.gif

あとは気の赴くままにReactを書いていきましょう。


サーバーサイドレンダリング(SSR)

Reactでサーバーサイドレンダリングを行うのは非常に簡単です。


public/index.html

...

</head>
<body>
- <div id="root"></div>
+ <div id="root"><%= preloadedApplication %></div>
<script src="/bundle.js"></script>
</body>
...


server/server.js

...

import fs from 'fs';
import webpack from 'webpack';
+import React from 'react';
+import { renderToString } from 'react-dom/server';
import webpackConfig from '../webpack.config.dev.babel';
import webpackMiddleware from 'webpack-dev-middleware';
import HMR from 'webpack-hot-middleware';

+import App from '../src/App';
+
const app = express();
...
app.use(HMR(compiler));

app.get('/', (req, res)=>{
- const index = fs.readFileSync('./public/index.html', 'utf-8');
+ let index = fs.readFileSync('./public/index.html', 'utf-8');
+
+ const appRendered = renderToString(
+ <App />
+ );
+ index = index.replace('<%= preloadedApplication %>', appRendered);
res.send(index);
});
...



src/index.jsx

...

const NextApp = require('./App').default;
render(NextApp);
});
}

-render(App);
+ReactDOM.hydrate(
+ <App />,
+ document.getElementById('root')
+);


何か分かりにくいところやエラーなどあればコメントで教えてください。

次回があれば本番用のビルド&デプロイの設定かReduxの実装とかですかね。