Quantcast
Channel: CSSタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8948

Spring Boot ウェブアプリにフロントエンド技術を含める webpack ビルド

$
0
0

はじめに

Spring Boot で構築するウェブアプリケーションに、ES Modules、babel や Sass/CSS コンバートなどのフロントエンド技術を含めてビルドする TIPS を書いてみました。普段はバックエンドばかりで webpackよく分からないよ、という方の参考にもなるかもしれません(自分ですが…!)

この記事中の全てのソースコードは以下のリポジトリーから参照できます。なるべくバックエンドとフロントエンド担当の方がリポジトリーを共有して作業できることを心がけました。

https://github.com/h1romas4/springboot-template-web

また、Gitpod からビルドして動作する様子も確認できます。

Open in Gitpod

# (Gitpod ターミナルで)Spring Boot アプリケーション起動
./gradlew bootRun
# (別なターミナルで)webpack の watch 開始
./graldew watch

初期設定

Spring Boot プロジェクトのビルドとディレクトリー構成

プロジェクトは Spring Boot の spring-boot-starter-webspring-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"}applyplugin:'java'applyplugin:'io.spring.dependency-management'applyplugin:'com.github.node-gradle.node'node{version='12.16.2'// プロジェクトローカルに OS に応じた nodejs を自動ダウンロードdownload=true}bootRun{// スタティックファイルの変更を直接反映させる//  Thymeleaf .html や .css/.js などの修正が即反映sourceResourcessourceSets.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)を記述します。

このサンプルの依存パッケージは BaootstrapVue.jsaxiosとしています。(適宜変更してください) ビルド系ツールの定義部分には、何をするものなのかソースコードにコメントを入れてみました。

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.jsonscriptを Gradle から呼べるように build.gradleに分かりやすいラッパータスクを定義します。

// build.gradle 抜粋taskwebpack(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.jpentryキーに設定された .jsの処理を追っていき importをみつけたら合体(バンドル)
  • バンドル前に import する拡張子に応じて ruleに設定されたプログラムを呼び出し変換(ビルド)
  • .jsだけでなく .cssや画像ファイルなどのリソースも .jsにバンドルできる機能も有する。

とざっくり思っておくと読みやすいかもしれません。

このビルドでは CSS は全画面でひとつを共有(もちろん @import によるモジュール分割は可能)、JavaScript は各画面でファイル分割という構成を想定しています。 .cssは JavaScript にバンドルすると画面がパタつくので、.jsにはバンドルせずに .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 抜粋constwebpack=require('webpack');constpath=require('path');constVueLoaderPlugin=require('vue-loader/lib/plugin');constMiniCssExtractPlugin=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 を// 参照するためコンテキストに入れてあげて解決するnewwebpack.ProvidePlugin({$:'jquery',jQuery:'jquery'}),// .vue をビルド・バンドルするプラグインnewVueLoaderPlugin(),// .css は JavaScript にバンドルせずにファイル出力するプラグイン//  css/ 配下に出力newMiniCssExtractPlugin({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:[{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 bootstrapimport'bootstrap/dist/css/bootstrap.css'import'open-iconic/font/css/open-iconic-bootstrap.css';import$from'jquery';// for common cssimport'../../css/style.css';

各画面の JavaScript は次のようになります。

// screen1.js 抜粋(各画面)// common.js を importimport'./common/common.js'// 各画面で使いたいライブラリーを importimportVuefrom'vue';importaxiosfrom'axios';// Vue の .vue コンポーネントをインポートする例// webpack.config の resolve に .vue が入っているので拡張子は不要 importhellofrom'./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 例 --><htmlxmlns:th="http://www.thymeleaf.org"><head><!-- 全 .css がバンドルされた vendor.bundle.css を指定 --><!-- 画像パスなどは webpack(各 publicPath 設定) により自動的に解決されている --><linkhref="/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なし hrefsrc属性にもパスを記述することで、次項の Java 環境なしでのフロントエンド開発が可能になります。

TIPS: Node.js 環境のみでフロントエンド開発を行う webpack-dev-server

フロントエンド側の開発を行う場合は、バックエンド側のプログラムの動作を気にせずに自由に好きな画面を開いて開発したいです。このような時は開発用の webpack.dev.jsを追加して webpack-dev-serverを起動して作業すると便利です。

// webpack.dev.js 抜粋constmerge=require('webpack-merge');constpath=require('path');constcommon=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.jsonscriptserverを次のように追加します。

// 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 抜粋newVue({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 の組み合わせでビルドする定義なども入っていますので、必要であれば確認していただければと思います。

https://github.com/h1romas4/springboot-template-web

おかしな部分を発見しましたら、プルリクなどいけだけると嬉しいです。


Viewing all articles
Browse latest Browse all 8948

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>