2019.02.17

GulpでフルスタックJS開発環境構築!サーバサイドNode + Babel/フロントエンドReact + Sass


この記事では、Gulp 4 でサーバーサイドからフロントエンドまでのフルスタックな開発環境を構築する方法を紹介します。サーバーサイドには Node.js を採用しますが、ES2015 以降の最新の JavaScript 構文を使うために Babel によるトランスパイルを施します。そしてフロントエンドには ReactSass を使用します。

Node を Babel に通したい個人的な理由としては ES Module(export / import)が挙げられます。この記事を書いている時点の Node.js は推奨版が 10.15.1 LTS でたいていの ES2015+ 記法は使えますが、ES Module にはまだ対応していません。Node の独自仕様である exports よりも ES Module の方が、慣れもあってか私には文法的に明快に感じられるので、サーバサイドコードを Babel でトランスパイルする環境について調べました。さらにせっかくなのでフロントエンドも含めた SPA 構成にしてしまおうというのが本記事の内容です。

ディレクトリ構成

最終的なディレクトリ構成は以下の通りです。

├─ dist
│  ├─ public
│  │  ├─ js
│  │  └─ css
│  └─ server
├─ src
│  ├─ frontend
│  │  ├─ js
│  │  └─ scss
│  └─ server
├─ tasks
└─ gulpfile.babel.js
  • src にはトランスパイル前のコードを置きます。
  • dist はトランスパイルされたコードの配置先です。
  • tasks は Gulp タスクをまとめるディレクトリです。

タスクの流れ

最終的な Gulp タスクの流れは以下の通りです。

start ─── parallel ─── parallel
          ├─ babel     ├─ watchFiles
          └─ front     └─ nodemon
  • babel タスクは Babel により Node コードをトランスパイルします。
  • front タスクは Webpack により React と Sass のコードをトランスパイルします。
  • watchFiles タスクはファイルの変更を監視します。サーバサイドのファイルに変更があった場合は babel タスクを再実行します。フロントエンドのファイルに変更があった場合は front タスクを再実行し、BrowserSync でブラウザを自動リロードします。
  • nodemon タスクは dist ディレクトリ内のサーバサイドのファイルを監視し、変更があればアプリケーションを再起動して変更内容を反映します。

最後の nodemon タスクが少しややこしいですが、アプリとして実行するのはトランスパイルしたあと、つまり dist ディレクトリ内のファイルです。そしてフロントエンドでもコードの変更はブラウザをリロードしないと反映されないのと同じように、サーバサイドコードの変更も Node アプリを再起動しないと反映されません。nodemon はファイルの変更を監視して Node アプリを再起動するためのライブラリです。src ディレクトリ内のコード編集から dist ディレクトリ内の実行コード再起動を繋げるために、以下の手順を踏むことになります。

  1. src ディレクトリ内のサーバサイドファイルを変更
  2. watchFiles タスクにより babel タスクが呼び出される
  3. dist ディレクトリ内のサーバサイドのファイルが変更される
  4. nodemon タスクによりアプリケーションが再起動される
  5. 変更内容が実行中のアプリケーションプロセスに反映される

watchFilesnodemon はファイルを監視するため、起動し続けるタスクになります。

以上を念頭に Gulp タスクを書いていきましょう。

gulpfile を ES2015 で書く準備

Gulp の設定ファイル自体を ES2015 で記述したいので、そのための準備を行います。

依存パッケージ

依存パッケージをインストールします。

$ npm install -D gulp @babel/core @babel/preset-env @babel/register

.babelrc

Babel の設定ファイル .babelrc をプロジェクトのルートディレクトリに配置します。

.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": { "node": "current" }
      }
    ]
  ]
}

ここまではあくまで Gulp ファイルを ES2105 以降の文法で記述するための Babel の設定で、以降で紹介するサーバーサイドの Node コードをトランスパイルする設定とは別物です。

Node + Babel

まずはサーバーサイドの設定から行います。タスクの流れは以下の通りです。

start ─── babel ─── parallel
                    ├─ watchFile
                    └─ nodemon

依存パッケージ

依存パッケージをインストールします。

$ npm install -D gulp-babel gulp-nodemon

babel

ここからは tasks ディレクトリを作成し、いくつかのタスクを追加します。

babel タスクは、Node コードに Bebel によるトランスパイルを適用して dist ディレクトリに配置します。

tasks/babel.js
import gulp from 'gulp'
import gulpBabel from 'gulp-babel'

export function babel() {
  return gulp
    .src('src/server/**/*.js')
    .pipe(
      gulpBabel({
        presets: ['@babel/env'],
      })
    )
    .pipe(gulp.dest('dist/server'))
}

nodemon

nodemon タスクは上で説明した通りです。

tasks/nodemon.js
import gulpNodemon from 'gulp-nodemon'

export function nodemon(callback) {
  let started = false

  // 初回起動時および再起動時に実行される
  const onStart = () => {
    if (!started) {
      callback()
      started = true
    }
  }

  gulpNodemon({
    script: 'dist/server/index.js',
    watch: 'dist/server',
  }).on('start', onStart)
}

start イベントは初回起動および再起動時に発行されます。Promise もストリームも返さない Gulp タスクはタスクの完了を通知するコールバックを引数で受け取って実行しなければいけないのですが、初回起動時のみ実行すればいい(再起動時にまで実行されるとエラーになる)ので、started 変数をフラグにしてコールバックの複数回実行を防いでいます。

watch

watchFiles タスクは、サーバサイドのファイルを監視して変更があれば先ほど作成した babel タスクを実行します。

tasks/watch.js
import { series, watch } from 'gulp'
import { babel } from './babel'

export function watchFiles(callback) {
  watch('src/server/**/*.js', babel)

  callback()
}

gulpfile

以上のタスクをまとめて Gulp ファイルを作成します。Gulp ファイルに Babel のトランスパイルが必要であることを示すために、gulpfile.babel.js というファイル名にしてください。

gulpfile.babel.js
import { parallel, series } from 'gulp'
import { babel } from './tasks/babel'
import { nodemon } from './tasks/nodemon'
import { watchFiles } from './tasks/watch'

export const dev = series(
  babel,
  parallel(nodemon, watchFiles)
)

export const prod = babel

ファイル監視を行う watchFilesnodemon は本番ビルド時には不要なので、これらを行わない prod タスクも作成しました。

npm スクリプト

コマンドラインから実行する npm スクリプトを追加します。

package.json
"scripts": {
  "dev": "gulp dev",
  "prod": "gulp prod"
}

アプリケーションコード

ここまでの動作確認用に、サンプルの Node アプリを載せておきます。

$ npm install -S express

src/server/index.js を作成します。

index.js
import express from 'express'

const app = express()

app.get('/hello', (req, res) => {
  res.json({ message: 'Hello from Node' })
})

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`)
})

以下のコマンドで Gulp タスクを実行します。

$ npm run dev

React + Sass

次にフロントエンド向けのタスクを作成します。

React および Sass コードは Webpack でトランスパイルしますが、設定を容易にするために、Webpack のラッパーライブラリである Laravel Mix を利用します。もともと Laravel の一部として開発されたライブラリですが、独立した npm パッケージとして使用できます。

依存パッケージ

依存パッケージをインストールします。

$ npm install -D browser-sync laravel-mix webpack webpack-stream

Webpack(Laravel Mix)

まずは Laravel Mix が参照する設定ファイル webpack.mix.js を作成します。

webpack.mix.js
let mix = require('laravel-mix')

mix
  .react('src/front/js/main.js', './dist/public/js')
  .sass('src/front/sass/app.scss', './dist/public/css')
  .copy('src/front/index.html', './dist/public/index.html')

設定ファイルはこれだけです!複雑な webpack.config.js を記述する必要はありません。Laravel Mix ライブラリが上記の内容を読み取って自動的に webpack の設定を生成します。

非常に明快なインターフェースなので何をやっているかはなんとなく分かると思います。
それぞれ第一引数にエントリポイント、第二引数に出力先を指定します。

感動的な便利さですね 🤩
Laravel Mix のより詳しい使い方に関してはドキュメントをご覧ください。

続いて Webpack 用の Gulp タスクを作成します。

tasks/front.js
import gulp from 'gulp'
import webpack from 'webpack'
import gulpWebpack from 'webpack-stream'

// Laravel Mix が提供するwebpack.config.jsを読み込む
// このファイルからユーザー定義のwebpack.mix.jsが参照される
const config = require('../node_modules/laravel-mix/setup/webpack.config.js')

export function front() {
  return gulp
    .src('src/front/**/*.(html|scss|js|jsx)')
    .pipe(
      gulpWebpack(config),
      webpack
    )
    .pipe(gulp.dest('.'))
}

BrowserSync

ファイルの変更をきっかけにブラウザを自動リロードしたいので、BrowserSync を用います。

tasks/browser-sync.js
import browserSync from 'browser-sync'

const server = browserSync.create()

export function reload(callback) {
  server.reload()
  callback()
}

export function serve() {
  const port = process.env.PORT || 3000

  server.init({
    proxy: `localhost:${port}`,
    port: 8001,
    open: false,
  })
}

serve は Gulp タスクではありません。後述しますが、nodemon タスク内から呼び出される単なる関数です。proxy にサーバサイドアプリの起動 URL を指定することで、サーバからのレスポンスを BrowserSync が仲介して自動リロードのためのスクリプトを仕込んで上記 port の8001ポートからブラウザに返す仕組みです。ちなみに port は他のアプリやシステムと被らなければ好きな値で構いません。

watch

watchFiles タスクにフロントエンド向けの監視処理を追加します。

tasks/watch.js
import { series, watch } from 'gulp'
import { babel } from './babel'
import { front } from './front' // ★ 追加
import { reload } from './browser-sync' // ★ 追加

export function watchFiles(callback) {
  watch('src/server/**/*.js', babel)
  watch('src/front/**/*.*', series(front, reload)) // ★ 追加

  callback()
}

nodemon

nodemon タスクに BrowserSync 起動処理を追加します。

tasks/nodemon.js
import gulpNodemon from 'gulp-nodemon'
import { serve } from './browser-sync' // ★ 追加

export function nodemon(callback) {
  let started = false

  // 初回起動時および再起動時に実行される
  const onStart = () => {
    if (!started) {
      callback()
      serve() // ★ 追加
      started = true
    }
  }

  gulpNodemon({
    script: 'dist/server/index.js',
    watch: 'dist/server',
  }).on('start', onStart)
}

必ずプロキシするサーバアプリが起動してから BrowserSync を立ち上げるべきなので、onStart の内部で先ほどの serve 関数を実行します。

gulpfile

gulpfile を完成させます。

gulpfile.babel.js
import { parallel, series } from 'gulp'
import { babel } from './tasks/babel'
import { front } from './tasks/front' // ★ 追加
import { nodemon } from './tasks/nodemon'
import { watchFiles } from './tasks/watch'

export const dev = series(
  parallel(babel, front), // ★ front 追加
  parallel(nodemon, watchFiles)
)

export const prod = parallel(babel, front) // ★ 変更

これで今回の環境構築は完成しました。

アプリケーションコード

フロントエンドもサンプルコードを載せておきます。

$ npm install -S react react-dom

JavaScript

src/front/js ディレクトリに main.js および App.jsx を作成します。

main.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.jsx'

ReactDOM.render(
  <App />,
  document.getElementById('app')
)
App.jsx
import React from 'react'

export default class App extends React.Component {
  construct() {
    super()
    this.state = { message: '' }
  }

  async componentWillMount() {
    const json = await fetch('/hello').then(res => res.json())
    this.setState({ message: json.message })
  }

  render() {
    return <h1>{this.state.message}</h1>
  }
}

Sass

src/front/sass/app.scss を作成します。

app.scss
$title-color: #FF6F61;

h1 {
  color: $title-color;
}

HTML

src/front/index.html を作成します。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Gulp sample</title>
    <link rel="stylesheet" href="/css/app.css" />
  </head>
  <body>
    <div id="app"></div>
    <script src="/js/main.js"></script>
  </body>
</html>

Node.js

src/server/index.js を編集します。

index.js
import express from 'express'
import path from 'path' // ★ 追加

const app = express()

app.use(express.static(path.join(__dirname, '../public'))) // ★ 追加

/* 以下略 */

public ディレクトリ内の静的ファイルを配信するための記述を追加しています。

繰り返しになりますが、以下のコマンドで Gulp タスクを実行できます。

$ npm run dev

以上、Gulp で Node.js + Babel + React + Sass の開発環境を構築する方法を紹介しました。これを参考に色々アレンジして、自分なりの開発環境を作ってみていただければと思います。