Help us understand the problem. What is going on with this article?

脱create-react-app ~ 真面目に express × react 環境を構築する~

はじめに

この記事ではcreate-react-appを使わないReact開発環境の構築方法を紹介します。

この記事で紹介する開発環境は以下に置いています。
詰まった場合、もしくは手っ取り早く開発環境が欲しい方は こちらを利用して下さい。
https://github.com/ohs30359-nobuhara/react-starter

なぜ create-react-app を使わないのか?

これはあくまで自分の考えですがcreate-react-appは初心者の入門のためのツールであり
実開発者の...ましてサービスレベルで使うものではないと思っています。

このツールが解決していることは複雑なビルド周りの設定を隠蔽して
手早くReactを始められるようにすることであり、
実開発における開発環境の構築を目的としているわけではありません。

実際のサービス開発ではテンプレートがそのまま使える,もしくは使い続けることは非常に難しく
何らかの理由でビルドに手を加える必要に迫られる可能性は非常に高いはずです。
そうなった際に ブラックボックス化された環境に手を加えることができるでしょうか?
( 一応configを外出しする設定はありますが、そもそも理解していなけば触ることすらできないでしょう。)

そうならないためにも create-react-appを使わずに
環境を構築する場合にはどうすればいいのかを理解しておく必要があるということです。

この記事で構築する開発環境

今回構築する環境はExpress × Reactをベースに、
開発に必要なログや設定ファイルなども導入していきます。

■ 一覧

  • react
  • express
  • lint
  • test ( mocha )
  • configuration
  • logging

この記事で説明しないこと

  • Reactの基本的な説明
  • expressの基本的な説明
  • それぞれのパッケージの詳しい使い方
  • create-react-appの基本的な説明

構成イメージ

スクリーンショット 2018-11-18 0.22.50.png

■ 開発時
webpack-dev-serverという開発サーバーを利用することでHMRが可能になるため
基本的にReactを触るときはwebpack-dev-server上で行います。
逆にexpressの方はwebpack-dev-serverは不要なため オートリロードさせることで
修正を反映させていきます。

■ 本番
HMRは不要なため webpack-dev-serverを使わずに
expressから直接Reactを読み込んだhtmlに対してルーティングをかけます。

React(client)の環境構築

packageの導入

はじめにfrontのビルドに必要となるパッケージを導入していきます。

■ webpack

$ npm i -D webpack-cli webpack webpack-dev-server html-webpack-plugin

この記事を書いた時点でwebpackはv3だったためwebpack-cliを導入しています。
webpack-dev-server, html-webpack-pluginに関しては それぞれ以下の機能を提供します。

package 説明 link
webpack-dev-server HMRやProxyを提供する開発用Server https://github.com/webpack/webpack-dev-server
html-webpack-plugin distされたscriptをhtmlに埋め込んで出力する https://github.com/jantimon/html-webpack-plugin

■ babel

$ npm i -D babel-core babel-loader@7 babel-preset-env babel-preset-react  

babel-loaderだけverを指定しているのは以下のエラーが出るためです

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側を上げても良かったのですが 今回は babel-loader側を下げることで回避しています。

■ loader

$ npm i -D css-loader style-loader

※ 今回はCSSのみを対象にしていますが必要であれば Sassなども 入れてください。

■ React

$ npm i -S react react-dom

Reactをビルドして画面に表示する

今回は "Hello React"と表示するシンプルなコンポーネントを実装します。

まず、ベースとなるHTMLを作成します。

src/client/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>React and Webpack4</title>
</head>
<body>
<section id="index"></section>
</body>
</html> 

続いて "Hello React!" を描画するシンプルなReactを実装します。

src/client/index.js
import React from "react";
import ReactDOM from "react-dom";

const Index = () => {
  return <div>Hello React!</div>;
};

ReactDOM.render(<Index />, document.getElementById("index")); 

これで最低限必要な実装は完了しました。
webpackの設定ファイルを作成し、実際にビルドをしていきます。

config/webpack.config.js
const HtmlWebPackPlugin = require("html-webpack-plugin");
const path = require('path')

const htmlWebpackPlugin = new HtmlWebPackPlugin({
  template: "./src/client/index.html",
  filename: "./index.html"
});
 module.exports = {
  entry: "./src/client/index.js",
  output: {
    path: path.resolve('dist'),
    filename: '[name].js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      }
    ]
  },
  plugins: [htmlWebpackPlugin]
}; 

ここでは先程 インストールした html-webpack-pluginを設定しています。
html-webpack-pluginはtemplateとfile名を指定することで
distされたhtmlに自動的にReactのコードへのリンクが埋め込まれます。

const HtmlWebPackPlugin = require("html-webpack-plugin");
const path = require('path')

const htmlWebpackPlugin = new HtmlWebPackPlugin({
  template: "./src/client/index.html",
  filename: "./index.html"
});

続いて.babelrcファイルを作成します。

{
  "presets": ["env", "react"]
}

最後に package.jsonに コンパイル用のスクリプト追記します。
ここではwebpack-dev-serverを利用し、開発用のサーバーを立ち上げています。

※ webpack-dev-serverのMHRはビルドされたファイルはメモリ上に展開されるので
 ファイルの実体にアクセスできないことに注意して下さい

package.json
"scripts": {
    "client": "webpack-dev-server --config ./config/webpack.config.js --open --mode development",
    "build": "webpack --config ./config/webpack.config.js --mode development"
  },
タスク 内容
client front 開発サーバーを立ち上げます
build front コードをビルドします

試しにスクリプトを実行させます。

$ npm run client

-> ブラウザが立ち上がり Hello React!が 表示されます。

$ npm run build

-> dist/ 配下に buildされたコードがdistされます。

ここまでで最低限のReactの環境は構築できました。
続いて express を使った server の構築をしていきます。

express (server)の環境構築

packageの導入

frontの時と同じくserver側に必要なパッケージを追加していきます。

■ express

$ npm i -S express

■ babel
本来 ES5環境であれば server側にビルドは不要ですが
今回はES6を利用したいのでfrontと同じくbabelを利用します。

ES6のビルドのために以下のパッケージを導入します

$ npm i -D babel-preset-es2015 babel-preset-stage-0 babel-cli

.babrlrcにinstallしたパッケージの設定を追加します。

.babelrc
{
  "presets": ["env", "react", "es2015", "stage-0"]
}

■ その他 実行用

$ npm i -D nodemon concurrently

nodemon, concurrentlyに関しては開発時に利用するパッケージになります。
機能については以下の表を確認して下さい。

package 説明 link
nodemon 対象のファイルを監視しnodeプロセスを再起動する https://github.com/remy/nodemon
concurrently 複数のコマンドを並列に実行する https://github.com/kimmobrunfeldt/concurrently

expressサーバーの立ち上げ

シンプルなexpressサーバーを実装していきます。
今回は clientへのルーティング と シンプルなバックエンドAPIを提供します。

src/server/server.js
import express from 'express';
import path from 'path';

const app = express();

app.use(express.static(path.join('./', 'dist')));

app.get('/api', (req, res) => {
  res.send({api: 'test'});
})

app.get('*', function (req, res) {
  res.sendFile(path.join('./', 'dist', 'index.html'))
})

app.listen(3000, ()=> {
  console.log('server running');
})

続いて実行用のscriptをpackage.jsonに追記します。

package.json
"scripts": {
 "server": "nodemon src/server/server.js  --exec babel-node",
  "dev": "concurrently \"npm run client\" \"npm run server\""

※ babel-cliで直接ES6を実行しているので server側のコードはコンパイルされません。

この状態で devタスクを実行すると 下記サーバーが並列に立ち上がります。

サーバー host 用途
web-dev-server localhost:8080 webpack-dev-server
express server localhost:3000 expressサーバー
$ npm run dev

この時、 8080, 3000共に ルートにアクセスすると "Hello React!"のページが表示されます。
以降、Frontの開発時は常に8080を利用し、バックエンドAPIを3000から利用します。

ServerとFrontの連携

さてExpressサーバーを建てたことでバックエンドAPIを利用できるようになりました。
しかし、このままの状態ではリクエストした際にポートが違うためCORSで弾かれてしまいます。

それを回避するためにProxyを開発サーバー側にProxyを建てます。
まず、Proxyは開発時しか必要ないためdev専用のwebpack.config.jsを用意します。

前準備としてwebpack-mergeをインストールします。

$ npm i -D webpack-merge

webpack-mergeは2つのwebpack.configをマージすることが出来るpluginです。
これを利用し、開発用の設定のみを切り出します。

config/webpack.config.dev.js
const merge = require('webpack-merge');
const webpackConfig = require('./webpack.config.js');

module.exports = merge(webpackConfig, {
  mode: 'development',
  devServer: {
    historyApiFallback: true,
    inline: true,
    open: true,
    host: 'localhost',
    port: 8080,
    proxy: {
      '/api/**': {
        target: 'http://localhost:3000',
        secure: false,
        logLevel: 'debug'
      }
    },
  }
})

これにより webpack-dev-serverで/api/ でアクセスした場合のみ
3000に向けてリクエストするProxyが立ち上がります。

この設定をdev時にのみ読み込むようにscriptを編集します。
最終的なタスクは以下のようになります。

package.json
"scripts": {
    "server": "nodemon src/server/server.js  --exec babel-node",
    "client": "webpack-dev-server --config ./config/webpack.config.dev.js",
    "build": "webpack --config ./config/webpack.config.js --mode production",
    "dev": "NODE_ENV=development concurrently \"npm run client\" \"npm run server\"",
    "start": "NODE_ENV=production npm run build && npm run server"
  },
タスク 説明
server expressの立ち上げ
client webpack-dev-serverの立ち上げ
build frontコードのコンパイル
dev 開発モード。expressサーバーとwebpack-dev-serverが立ち上がる
start 本番モード。frontコードをコンパイルし、expressを立ち上げる

では実際にバックエンドAPIにリクエストをかけてみます

src/client/index.js
import React from "react";
import ReactDOM from "react-dom";

fetch('/api/').then(response => {
  console.log(response.json());
})

export const Index = () => {
  return <div>Hello React!</div>;
};

ReactDOM.render(<Index />, document.getElementById("index")); 

この状態でlocalhost:8080にアクセスするとバックエンドAPIの結果が返ってくるはずです。

ここまでで最低限開発に必要な設定は終わりましたが
まだlintやconfigの設定が残っています。

次項からはそれらの導入法を紹介していきます。

lintの導入

lintはコード検査ツールと呼ばれるもので
コーディングルールを設定ファイルに記載することで
実装されたコードがルールに違反していないかを検査することができます。

基本的にサービス開発では不特定多数の人間が開発を行うため
lintを導入することで最低限の品質を保証することが出来ます。

packageの導入

■ babel

$ npm i -D babel-eslint

■ eslint

$ npm i -D eslint eslint-plugin-react

■ precommit

$ npm i -D husky lint-staged prettier

precommitはcommitする直前に必ず対象ファイルに対してlintを実行するための機能です。
これによりlintの実行漏れを防ぐことが出来ます。

lintの設定

コーディングルールを定義した設定ファイルを追加します。
細かい設定の内容については割愛しますが詳しい内容を確認したい場合は以下を参照して下さい。
https://eslint.org/

eslintrc.json
{
  "parser": "babel-eslint",
  "plugins": [
    "react"
  ],
  "parserOptions": {
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "env": {
    "browser": true,
    "node": true
  },
  "rules": {
    "quotes": [2, "single"],
    "strict": [2, "never"],
    "react/jsx-uses-react": 2,
    "react/jsx-uses-vars": 2,
    "react/react-in-jsx-scope": 2,
    "no-console": 0
  },
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended"
  ]
}

続いてpackage.jsonに設定を追加します。

package.json
  "scripts": {
    "precommit": "lint-staged",
    "lint": "eslint src --fix -c .eslintrc.json 'src/**/*.js'",
  :
  :

  "pre-commit": [
    "lint-staged"
  ],
  "lint-staged": {
    "*.{js,jsx}": [
      "eslint --fix",
      "git add"
    ]
  },

今回のlintタスクでは src配下のファイルに対してautofixをかけて検査をするように設定しています。
autofixは自動でコードを修正する設定で --fix をつけることで有効にできます。
また今回は明示的に設定ファイルを定義していますが 本来は.eslintrc.jsoを自動で参照します。

"lint": "eslint src --fix -c .eslintrc.json 'src/**/*.js'",

試しに $ npm run lintを実行してみて下さい。
もしlintに反した記載をしている場合は検知内容と自動修正がかかるはずです。
( 自動で修正できないものに関しては自身での修正が必要になります )

precommitは以下の部分で定義しています。
lint-stagedで実行する対象と実行するコマンドを指定しており、
今回の場合は *.{js,jsx} なので
js, jsxに対してlintを実行し、commit するという設定になります。
( この際、autofixで修正できない物がある場合は commit することは出来ません。)

"pre-commit": [
    "lint-staged"
  ],
  "lint-staged": {
    "*.{js,jsx}": [
      "eslint --fix",
      "git add"
    ]
  },

実際にlintに違反する様にコードを改修して commit してみます。

src/client/index.js
// Spaceを開けてlintエラーを起こす
fetch(  '/api/'   ).then(response => {
  console.log(response.json());
})

この状態で commit を行うと autofixがかかり整形されたコードがcommitされるはずです。

configurationの導入

configurationは環境によって設定を切り替える際に利用する機能です。
例えば 開発時と本番時でAPIのhostが違っていたり
request時のportが違ったりする際に その差異をconfigurationに持たせることで
コードベースから環境設定を切り離すことが出来ます。

packageの導入

$ npm i config

続いて環境ごとにconfigファイルを作成します。
今回は expressのportを設定ファイルで指定してみます。
もしdev環境のport値を変更する場合は Proxyを設定しているので必ずそちらも修正して下さい。

config/default.yml
server:
  port: 3000
config/development.yml
server:
  port: 3000
config/production.yml
server:
  port: 3000

読み込まれる設定ファイルは NODE_ENVに指定された環境に依存し、
何も指定がない場合はdefaultが呼び出されます。
ただし、読み込まれるファイル名には優先度があり、
誤って その名前を使うと意図しないファイルが読まれることになるので注意が必要です。

ファイルが読み込まれる順番は以下のとおりです。

default.EXT
default-{instance}.EXT
{deployment}.EXT
{deployment}-{instance}.EXT
{short_hostname}.EXT
{short_hostname}-{instance}.EXT
{short_hostname}-{deployment}.EXT
{short_hostname}-{deployment}-{instance}.EXT
{full_hostname}.EXT
{full_hostname}-{instance}.EXT
{full_hostname}-{deployment}.EXT
{full_hostname}-{deployment}-{instance}.EXT
local.EXT
local-{instance}.EXT
local-{deployment}.EXT
local-{deployment}-{instance}.EXT
(Finally, custom environment variables can override all files)

configの読み込み実装

実際に 設定ファイルのportを読み込むように server.jsを修正します。

src/server/server.js
import express from 'express';
import path from 'path';
import config from 'config';

const app = express();

const serverConfig = config.get('server');

app.use(express.static(path.join('./', 'dist')));

app.get('/api', (req, res) => {
  res.send({data: 'test'});
})

app.get('*', function (req, res) {
  res.sendFile(path.join('./', 'dist', 'index.html'))
})

app.listen(serverConfig.port, ()=> {
  console.log(`server starting -> [port] ${serverConfig.port} [env] ${process.env.NODE_ENV}`);
})

(当たり前ですが) クライアント側で読み込む事はできないので注意して下さい。

loggingの導入

こちらに関してはあまり言及することがないので さくっと実装していきます。

packageの導入

$ npm i log4js 

こちらのパッケージはlogの出力をjson又はymlで設定する必要があります。
ここでは設定の詳しい説明はしませんが
今回はinfoとerrorをログ出力する様に設定しています。

詳しい設定に関しては以下を参照して下さい。
https://log4js-node.github.io/log4js-node/layouts.html

default.yml
log:
  appenders:
    console:
      type: console
      category: system
    file:
      type: dateFile
      filename: logs/system.log
      pattern: "-yyyy-MM-dd"
      alwaysIncludePattern: false
      category: system
  categories:
    default:
      appenders:
      - console
      - file
      level: info
    error:
      appenders:
      - console
      - file
      level: error

※ 他の環境ファイルにも同じ様に設定して下さい。

設定が終わったら 実際にログを出力するためのクラスを実装します。
直接呼んでも良いのですが、ライブラリへの依存を収めることが出来るので
後々のことを考えてラップしたクラスを実装しておくほうが良いでしょう。

src/server/logger.js
import { getLogger, configure } from 'log4js';
import config from 'config';
configure(config.get('log'));

class Log {
  constructor() {
    this.logger = getLogger();
  }
   info(log) {
    this.logger.info(log);
  }
   error(log) {
    this.logger.error(log);
  }
}

export const logger = new Log(); 

では実際に使ってみます。
簡単にexpressが立ち上がった際にログを出力してみます。

src/server/server.js
app.listen(serverConfig.port, ()=> {
  logger.info(`server starting -> [port] ${serverConfig.port} [env] ${process.env.NODE_ENV}`)
})

実際にexpressを立ち上げてみると logs配下にファイルが出来ていることが確認できるはずです。
※ こちらも当然ですが frontでは実行できません。

Testの導入

最後にtestの導入です。
Reactの公式ではjestをスタンダートとしていますが
今回はmochaを利用していきたいと思います。( 個人的に好きなので )

packageの導入

■ mocha

$ npm i -D chai mocha sinon

■ reactのテストに必要なもの

$ npm i -D enzyme enzyme-adapter-react-16 jsdom react-addons-test-utils chai-enzyme

defaultのmochaではReactのテストをすることは出来ません。
そのため、React用のpluginを別途インストールしています。

testの実装

test用のsetupスクリプトを用意します。

test/enzyme.js
const { JSDOM } = require('jsdom');

const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
const { window } = jsdom;

function copyProps(src, target) {
  Object.defineProperties(target, {
    ...Object.getOwnPropertyDescriptors(src),
    ...Object.getOwnPropertyDescriptors(target),
  });
}

global.window = window;
global.document = window.document;
global.navigator = {
  userAgent: 'node.js',
};
global.requestAnimationFrame = function (callback) {
  return setTimeout(callback, 0);
};
global.cancelAnimationFrame = function (id) {
  clearTimeout(id);
};
copyProps(window, global);

const Adapter = require('enzyme-adapter-react-16')

require('enzyme').configure({adapter: new Adapter()})

色々やっていますが、簡単に説明すると
test時にはDOMが存在しないため
globalにjsdomで作ったwindowオブジェクトを差し込んでいます。
また 同時にenzymeの設定も ここでやっています。

const Adapter = require('enzyme-adapter-react-16')
require('enzyme').configure({adapter: new Adapter()})

次にテストを書きます。

import { Index } from '../../src/client'
import React from "react"
import { expect } from 'chai'
import { shallow } from 'enzyme';

describe('react test sample', () => {
  it('rendering <div>Hello React!</div>', () => {
    const result = shallow(<Index />).contains(<div>Hello React!</div>)
    expect(result).to.be.true
  });
});

実行用のスクリプトを追記します。
こちらもES6で書いているのでbabelを利用して実行させています。
この際、--require ./test/enzyme.js`で先程のファイルを指定するのを忘れないように注意して下さい。

package.json
 "scripts": {
    "test": "mocha --require ./test/enzyme.js --compilers js:babel-register --recursive $(find test -name '*.spec.js')",

テストを実行してGreenになれば完了です。

$ npm run test

最後に

以上で開発環境構築は完了です。
駆け足ではありますがcreate-react-appを使わずに一通り必要な機能の構築をしていきました。

冒頭にも記載しましたが 重要なのは create-react-appに依存しないことです。
今回の一連の流れで ブラックボックス化されていたビルド周りを
実際に設定するどうなるかを紹介しましたが、これが全てではありません。

今回の記事は 特定のツールに依存した環境から抜け出すことが目的なので
あくまで今回の内容はベースであり、
ここからプロジェクトにあった設定を肉付けしていく必要があります。
そのためには webpack, babelなどのリファレンスを確認すること、
そして 諦めない心を持つことが肝心です。

ohs30359-nobuhara
WEB系エンジニア k8s/nodejs(ts)/php/java
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした