この記事では、Gulp 4 でサーバーサイドからフロントエンドまでのフルスタックな開発環境を構築する方法を紹介します。サーバーサイドには Node.js を採用しますが、ES2015 以降の最新の JavaScript 構文を使うために Babel によるトランスパイルを施します。そしてフロントエンドには React と Sass を使用します。
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
ディレクトリ内の実行コード再起動を繋げるために、以下の手順を踏むことになります。
src
ディレクトリ内のサーバサイドファイルを変更watchFiles
タスクによりbabel
タスクが呼び出されるdist
ディレクトリ内のサーバサイドのファイルが変更されるnodemon
タスクによりアプリケーションが再起動される- 変更内容が実行中のアプリケーションプロセスに反映される
watchFiles
と nodemon
はファイルを監視するため、起動し続けるタスクになります。
以上を念頭に Gulp タスクを書いていきましょう。
gulpfile を ES2015 で書く準備
Gulp の設定ファイル自体を ES2015 で記述したいので、そのための準備を行います。
依存パッケージ
依存パッケージをインストールします。
$ npm install -D gulp @babel/core @babel/preset-env @babel/register
.babelrc
Babel の設定ファイル .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
ディレクトリに配置します。
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
タスクは上で説明した通りです。
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
タスクを実行します。
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
というファイル名にしてください。
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
ファイル監視を行う watchFiles
と nodemon
は本番ビルド時には不要なので、これらを行わない prod
タスクも作成しました。
npm スクリプト
コマンドラインから実行する npm スクリプトを追加します。
"scripts": {
"dev": "gulp dev",
"prod": "gulp prod"
}
アプリケーションコード
ここまでの動作確認用に、サンプルの Node アプリを載せておきます。
$ npm install -S express
src/server/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
を作成します。
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 タスクを作成します。
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 を用います。
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
タスクにフロントエンド向けの監視処理を追加します。
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 起動処理を追加します。
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
を完成させます。
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
を作成します。
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.jsx'
ReactDOM.render(
<App />,
document.getElementById('app')
)
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
を作成します。
$title-color: #FF6F61;
h1 {
color: $title-color;
}
HTML
src/front/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
を編集します。
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 の開発環境を構築する方法を紹介しました。これを参考に色々アレンジして、自分なりの開発環境を作ってみていただければと思います。