はじめに
Spring Boot で構築するウェブアプリケーションに、ES Modules、babel や Sass/CSS コンバートなどのフロントエンド技術を含めてビルドする TIPS を書いてみました。普段はバックエンドばかりで webpack よく分からないよ、という方の参考にもなるかもしれません(自分ですが…!)
この記事中の全てのソースコードは以下のリポジトリーから参照できます。なるべくバックエンドとフロントエンド担当の方がリポジトリーを共有して作業できることを心がけました。
また、Gitpod からビルドして動作する様子も確認できます。
# (Gitpod ターミナルで)Spring Boot アプリケーション起動
./gradlew bootRun
# (別なターミナルで)webpack の watch 開始
./graldew watch
初期設定
Spring Boot プロジェクトのビルドとディレクトリー構成
プロジェクトは Spring Boot の spring-boot-starter-web
と spring-boot-starter-thymeleaf
で構成し、ビルドは Gradle を用いています。
また、多くのフロントエンド系技術は Node.js ランタイムに依存するため、Gradle で Node.js をラップする com.github.node-gradle.node
を使います。
このプラグインは指定バージョンの Node.js をプロジェクトローカルに自動的に導入できるので CI を組む時にも便利です。(Heroku などでも動作します)
build.gradle
は次のようになります。
https://github.com/h1romas4/springboot-template-web/blob/master/build.gradle
// build.gradle 抜粋
plugins {
id 'org.springframework.boot' version '2.2.6.RELEASE'
id "com.github.node-gradle.node" version "2.2.3"
}
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'com.github.node-gradle.node'
node {
version = '12.16.2'
// プロジェクトローカルに OS に応じた nodejs を自動ダウンロード
// 具体的にはプロジェクトルート .gradle/nodejs 配下に配置される
download = true
}
bootRun {
// スタティックファイルの変更を直接反映させる
// Thymeleaf .html や .css/.js などの修正が即反映
sourceResources sourceSets.main
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-devtools'
}
Spring Boot(starter-web) 標準のディレクトリ構成は次のようになります。Node.js/npm を構成するために package.json
を追加しています。
src/main/
java/
# resources 配下のファイルがデプロイされる
resources/static
css/
fonts/
images/
js/
resources/templates
index.html
build.gradle
package.json # 追加
TIPS はこのディレクトリ構成を前提として記載していますので、異なる場合は適宜読み替えてください。
フロントエンド系ソースをビルドするために Node.js のパッケージを追加する
package.json
に使いたいビルド系ツール(devDependencies
)と依存パッケージ(dependencies
)を記述します。
このサンプルの依存パッケージは Baootstrap と Vue.js と axios としています。(適宜変更してください) ビルド系ツールの定義部分には、何をするものなのかソースコードにコメントを入れてみました。
https://github.com/h1romas4/springboot-template-web/blob/master/package.json
// package.json 抜粋
{
// ビルド系依存
"devDependencies": {
// babel: ES6以降の JavaScript ソースをブラウザー ECMAScript5 で動作させる
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"babel-loader": "^8.1.0",
// webpack: ES Modules バンドル及び .vue、CSS/Sass ビルド用
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11",
// nodejs のみでフロントエンド開発する場合に使う httpd サーバー(後述)
"webpack-dev-server": "^3.10.3",
// プロダクションと開発で webpack.config を分ける(後述)
"webpack-merge": "^4.2.2",
// webpack: Vue.js の .vue モジュールをバンドル
"vue-loader": "^15.9.1",
"vue-template-compiler": "^2.6.11",
// webpack: .css は JS にバンドルしない
"mini-css-extract-plugin": "^0.9.0",
"css-loader": "^3.5.2",
// webpack: 画像やフォントも JS にバンドルしない
"file-loader": "^6.0.0",
// webpack: Sass ビルド
"sass": "^1.26.3",
"sass-loader": "^8.0.2"
},
// JavaScript ソースから依存するパッケージ・ライブラリー
"dependencies": {
// HTTP クライアント
"axios": "^0.19.2",
// bootstrap
"bootstrap": "^4.4.1",
// bootstrap の依存
"open-iconic": "^1.1.1",
"popper.js": "^1.16.1",
"jquery": "^3.4.1",
// Vue.js ライブラリー
"vue": "^2.6.11"
},
}
また、package.json
に、フロントエンドのビルドを行う webpack コマンドが呼べるように script
を追加します。
// package.json 抜粋
{
"scripts": {
"webpack": "webpack",
}
}
続けて packege.json
の script
を Gradle から呼べるように build.gradle
に分かりやすいラッパータスクを定義します。
// build.gradle 抜粋
task webpack(dependsOn: ['npm_run_webpack'])
ビルドの実行は次のように行います。
./gradlew npmInstall # 初回
./gradlew webpack
もちろんクライアントPC に build.gradle
で指定したバージョンと同じ Node.js が導入されているのであれば、
npm install # 初回
npm run webpack
でも同様の結果を得ることができます。(フロントエンドのみを担当する方はこちらを使うことができます)
TIPS: フロントエンド系のソースコードを Spring Boot の構造に合わせてビルドする webpack 例
CSS(Sass)や JavaScript のソースはビルド対象となりますので、resources/static
に直接置かずに src/main
に上げ、ビルド後の結果を resources/static
に配置するように webpack ビルドを組みます。
webpack 初めてだよという方は処理の流れを、
- webpack はビルドを定義する
webpack.config.js
のentry
キーに設定された.js
を起点に処理を追っていきimport
をみつけたら指定されているモジュールを合体(バンドル)-
import
の指定がパスではなくモジュール名であればpackage.json
内に指定した同名モジュールをnode_modules
ディレクトリから検索してバンドル
-
- バンドル前に import する拡張子に応じて
rule
に設定されたプログラムを呼び出し変換(ビルド) -
import
を追っていく中で.js
だけでなく.css
や CSS からリンクされている画像ファイルなどのリソースも.js
にバンドルできる機能も有する。
とざっくり思っておくと読みやすいかもしれません。
このビルドでは CSS は全画面でひとつを共有(もちろん @import によるモジュール分割は可能)、JavaScript は各画面でファイル分割という構成を想定しています。
.css
は JavaScript にバンドルすると画面がパタつくので、.js
にはバンドルせずに MiniCssExtractPlugin
プラグインを利用し単一の.css
ファイルとして出力する方針としています。
ソースコードの構成:
src/main/
java/
css/
# .css もしくは .sass
style.css
images/
# CSS から url('./images/image.png') で参照する画像
image.png
js/
common/
# ここで bootstrap 系と .css を import して webpack 対象に
common.js
components/
# Vue.js のコンポーネント
hello.vue
# 画面ごとの JavaScript
screen1.js
screen2.js
screen3.js
resources/
templates/
index.html
# 追加
webpack.config.js
このビルドを行うための webpack.config.js
は次のようになります。コメントを入れましたので適宜修正してお使いください。
// webpack.config.js 抜粋
const webpack = require('webpack');
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: 'production',
entry: {
// 共通系の JavaScript ライブラリーは vendor.bundle.js としてひとつにまとめる
// .css は bootstrap の .css と合成するため vendor に含めておく
vendor: [
'vue',
'axios',
'jquery',
'popper.js',
'bootstrap',
'./src/main/css/style.css'
],
// 画面ごとの .js は分割して出力する(この設定のファイルを起点にバンドル開始)
screen1: './src/main/js/screen1.js',
screen2: './src/main/js/screen2.js',
screen3: './src/main/js/screen3.js',
},
output: {
// ビルドの出力先を /src/main/resources/static にする
path: path.join(__dirname, '/src/main/resources/static'), // eslint-disable-line
// Spring Boot のコンテキストパス(/)を設定する
publicPath: "/",
// JavaScript は js/ 配下に配置
filename: 'js/[name].bundle.js'
},
optimization: {
// 共通 JavaScript/CSS を vendor という名前で出力する
splitChunks: {
cacheGroups: {
vender: {
name: 'vendor',
chunks: 'initial'
}
}
}
},
plugins: [
// bootstrap 特有の設定
// bootstrap 内の JavaScript が $/jQuery 名でグローバルスコープの jQuery を
// 参照するためコンテキストに入れてあげて解決する
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
}),
// .vue をビルド・バンドルするプラグイン
new VueLoaderPlugin(),
// .css は JavaScript にバンドルせずにファイル出力するプラグイン
// css/ 配下に出力
new MiniCssExtractPlugin({
filename: 'css/[name].bundle.css',
}),
],
module: {
rules: [
{
// .js は babel を通してブラウザーで動作する JavaScript に変換
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}]
},
{
// .vue の Vue.js コンポーネントをビルドしてバンドル
test: /\.vue$/,
loader: 'vue-loader'
},
{
// .css|.sass ファイルをビルドして css/ ディレクトリに出力
test: /\.(sa|c)ss$/,
use: [
// 各ローダーは定義の下から順に適用される(Sass-> CSS -> Extract)
// https://webpack.js.org/concepts/loaders/#configuration
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '/css',
},
},
// .css 内の URL パスなどをそれぞれの publicPath に合わせてくれる
"css-loader",
// .sass のビルド
"sass-loader"
]
},
{
// bootstrap などで使われるフォントファイルは fonts/ ディレクトリーに
// ファイル出力
test: /\.(ttf|otf|eot|woff|woff2)$/,
use: [{
loader: 'file-loader',
options: {
name: "[name].[ext]",
outputPath: 'fonts/',
publicPath: '/fonts'
}
}]
},
{
// 画像ファイルは images/ ディレクトリーにファイル出力
test: /\.(png|jpg|svg)$/,
use: [{
loader: 'file-loader',
options: {
name: "[name].[ext]",
outputPath: 'images/',
publicPath: '/images'
}
}]
},
]
},
resolve: {
// .js と .vue 拡張子は import で付いてなくても解決
extensions: ['.js', '.vue'],
// import するモジュールでパス付きでないものは npm の node_modules に入ってる
modules: [
"node_modules"
],
alias: {
// import Vue from 'vue'; は Product 用の ES Modules 版を使う
'vue$': 'vue/dist/vue.esm.js'
}
}
};
各画面から import する common/common.js
などの分かりやすい部分に次の記述を追加して、bootstrap 系の依存と作成した .css をバンドルするようにします。(entry
から辿れる .js のどこかに一度書けば OK です)
// common.js 抜粋
// for bootstrap
import 'bootstrap/dist/css/bootstrap.css'
import 'open-iconic/font/css/open-iconic-bootstrap.css';
import $ from 'jquery';
// for common css
import '../../css/style.css';
各画面の JavaScript は次のようになります。
// screen1.js 抜粋(各画面)
// common.js を import
import './common/common.js'
// 各画面で使いたいライブラリーを import
import Vue from 'vue';
import axios from 'axios';
// Vue の .vue コンポーネントをインポートする例
// webpack.config の resolve に .vue が入っているので拡張子は不要
import hello from './components/hello'
この状態でビルドすると、Spring Boot がリソースとして認識する resources/static
配下に、次のようにファイルが出力されアプリケーションから読み込めます。(これらの出力ファイルは .gitignore
しておいても良いかもしれません)
resources/static
css/
# bootstrap の .css + css/style.css(もしくは .sass のビルド結果)
vendor.bundle.css
fonts/
# モジュール(bootstrap)が参照するフォントファイル
open-iconic.woff
...
images/
# モジュール(.css など)が参照する画像ファイル
150x150.png
# モジュール(bootstrap)が参照する画像ファイル
open-iconic.svg
js/
# vendor 指定したライブラリーがひとつにバンドルされた共通 .js
vendor.bundle.js
# 各画面用の .js
screen1.bundle.js
screen2.bundle.js
screen3.bundle.js
配置されたファイルを読むための resources/templates/*.html
は次のようになります。
<!doctype html>
<!-- screen1.html 例 -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<!-- 全 .css がバンドルされた vendor.bundle.css を指定 -->
<!-- 画像パスなどは webpack(各 publicPath 設定) により自動的に解決されている -->
<link href="/css/vendor.bundle.css"
th:href="@{/css/vendor.bundle.css}" rel="stylesheet" />
</head>
<body>
<!-- 共通ライブラリーがバンドルされた vendor.bundle.js -->
<!-- 各画面で同一ファイルとなるためブラウザーキャッシュが効く -->
<script src="/js/vendor.bundle.js" th:src="@{/js/vendor.bundle.js}"></script>
<!-- 各画面の .js がバンドルされた screen1.bundle.js -->
<script src="/js/screen1.bundle.js" th:src="@{/js/home.bundle.js}"></script>
</body>
</html>
HTML 属性で Thymeleaf の th
なし href
や src
属性にもパスを記述することで、次項の Java 環境なしでのフロントエンド開発が可能になります。
TIPS: Node.js 環境のみでフロントエンド開発を行う webpack-dev-server
フロントエンド側の開発を行う場合は、バックエンド側のプログラムの動作を気にせずに自由に好きな画面を開いて開発したいです。このような時は開発用の webpack.dev.js
を追加して webpack-dev-server を起動して作業すると便利です。
// webpack.dev.js 抜粋
const merge = require('webpack-merge');
const path = require('path');
const common = require('./webpack.config.js');
// 本家の webpack.config.js に設定をマージ(オーバーライド)
module.exports = merge(common, {
// ブラウザー開発者ツール用に JavaScript のソースマップを出力
devtool: 'source-map',
// 開発用サーバーを 9080 ポートで起動
devServer: {
contentBase: [
// .html のパス(コンテントルート)を指定
// webpack でビルドされる JS/CSS などは修正がウォッチされ、
// ビルド結果がメモリー上に展開され contentBase と合成される
path.join(__dirname, '/src/main/resources/templates')
],
// .html ファイルの修正も反映対象にする
watchContentBase: true,
port: 9080,
// ブラウザーを自動起動
open: true,
openPage: "screen1.html"
},
resolve: {
alias: {
// webpack.config.js の設定を上書き
// import Vue from 'vue'; で開発版を読ませることで Vue のデバッグが可能になる
'vue$': 'vue/dist/vue.js'
}
}
});
package.json
の script
に server
を次のように追加します。
// package.json 抜粋
{
"scripts": {
"webpack": "webpack",
// 作成した webpack.dev.js をコンフィグに指定
"server": "webpack-dev-server --config webpack.dev.js"
}
}
次のコマンドでサーバーが 9080 ポートで起動し .js
.css
の修正が自動的にビルドされ反映されます。
npm run server
TIPS: API 呼び出し時に開発用 .json を返す
開発中のフロントエンドから API をたたく処理をかく場合、バックエンド不在のまま開発に都合の良い .json を返したいことがあります。
// screent1.js 抜粋
new Vue({
methods: {
ajax: function() {
// ここで http リクエスト(バックエンドがいない…!)
axios.get('/api/v1/home').then((res) => {
this.items = res.data;
});
}
}
});
このような場合は webpack-dev-server
の設定で、内部的使われている Express サーバーの動作を定義することにより、ファイルで配置した .json を返すことができるようになります。
// webpack.dev.js 抜粋
module.exports = merge(common, {
devServer: {
before: function(app) {
// /api でアクセスがきたら
app.use('/api', function (req, res) {
// Content-Type 設定
res.type('application/json')
// /src/test/js/json 配下にある .json を返却(ディレクトリトラバーサル注意)
res.sendFile(path.join(__dirname, '/src/test/js/json', req.originalUrl + ".json"))
})
},
},
});
返したい JSON をソースの test
ディレクトリーに次のように配置します。
src/test/js/json
/api/v1/
home.json
...
API のエンドポイントがファイルシステムだけで表現できない場合は、Expressのドキュメントを参考に、疑似 API サーバーをつくってあげると便利だと思います。
終わりに
リポジトリーのほうはさらに Gradel/Node.js の組み合わせでビルドする定義なども入っていますので、必要であれば確認していただければと思います。なお、このリポジトリは、本文書よりライブラリやビルドの依存関係を上げられる範囲でアップデート・メンテナンスしている場合があります。
おかしな部分を発見しましたら、プルリクなどいけだけると嬉しいです。