Believe you can

If you can dream it, you can do it.

WebフレームワークでWebpackを使ってみよう〜SpringBoot+Webpack+Vue.js編〜

第4回目です
Laravel、DjangoRailsときて最終回はSpringBootです
作るものはお約束のこれです
jp.vuejs.org

環境

nodeやWebpackは今までと同じなので省略します
SpringBootはリリースされたばかりの2.0です
特に意識したわけではございませんw

  • Java@1.8
  • SpringBoot@2.0.0

SpringBootプロジェクトの作成

僕はIntellij IDEAユーザなので新規プロジェクト作成でポチポチと作りました
作れたらnpmでモジュールをインストールしていきます

npm init -f
npm install --save-dev webpack webpack-cli wevpack-dev-server vue style-loader css-loader

webpackの設定

今までのフレームワークはwebpack用のモジュールが用意されていたのでコマンドで準備ができていましたが、SpringBootやGradleにはありませんので手動で準備をしてきます

webpack.config.js
コンパイルの入出力やcssの組み込み、wevpack-dev-serverの設定を記載します
コメントになっている output ですが、ここが悩ましかった。。
resources/static にすることでSpringBootのポートでJavaScriptにアクセスできますが、build 配下だと wevpack-dev-server を起動しないとJavaScriptにアクセスできません
どちらが正解なのか悩ましいところですが自分はwevpack-dev-serverを起動しないSpringBoot経由を選びました。。

var path = require('path')
var webpack = require('webpack')
var fs = require('fs')

const vendor = { vendor: ['vue'] }
const entries = fs.readdirSync('./src/main/js')
                    .filter(a => /\.js$/.test(a))
                    .reduce((acc, x) => {
                        acc[x.slice(0, -3)] = './src/main/js/' + x
                        return acc
                    }, vendor)

module.exports = {
    entry: entries,
    output: {
        // path: path.resolve(__dirname, './build/classes/main/static/js'),
        path: path.resolve(__dirname, './src/main/resources/static/js'),
        publicPath: '/js/',
        filename: '[name].js',
    },
    module : {
        rules: [
            {
                test: /\.css/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            url: false,
                            sourceMap: true,
                        },
                    },
                ],
            },
        ]
    },
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        }
    },
    devServer: {
        historyApiFallback: true,
        noInfo: true,
        contentBase: path.join(__dirname, "./bin/static"),
    },
    performance: {
        hints: false
    },
    devtool: '#eval-source-map'
}

package.json
Gradleから呼び出されるwebpackコマンドをscriptsに記載します

{
  "private": true,
  "name": "springboot-webpack-vue",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server --inline --hot",
    "build": "webpack",
    "watch": "webpack --progress --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "css-loader": "^0.28.10",
    "style-loader": "^0.20.2",
    "vue": "^2.5.13",
    "webpack": "^4.0.1",
    "webpack-cli": "^2.0.9",
    "webpack-dev-server": "^3.1.0"
  }
}

Gradleの設定

SpringBootのrunやコンパイルJavaScriptのビルドも行えるようにします
この辺の情報がググっても錯綜していて難しかった。。

build.gradle

buildscript {
    ext {
        springBootVersion = '2.0.0.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

tasks.withType(JavaCompile) {
    sourceCompatibility = '1.8'
    targetCompatibility = '1.8'
    options.encoding = 'UTF-8'
}
repositories {
    mavenCentral()
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('org.springframework.boot:spring-boot-devtools')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

bootJar {
    launchScript()
}

task npmRunBuild {
    doLast {
        def npm = System.getProperty('os.name').contains('Windows') ? 'cmd /c npm' : 'npm'
        if (file('./node_modules').exists() ==  false) {
            "${npm} install".execute().waitForProcessOutput(System.out, System.err)
        }
        "${npm} run build".execute().waitForProcessOutput(System.out, System.err)
    }
}
processResources.dependsOn npmRunBuild

SpringBootの起動スクリプト化は2.0でかわったぽいです

コントローラーや画面、Vue.jaの組み込み

ここからはよくあるSpringBootやサンプルVue.jsなので細かい説明は省きます

IndexController.java

@Controller
public class IndexController {

    @GetMapping("/")
    public String index() {
        return "index";
    }
}

src/main/resources/templates/index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>springboot-webpack-vue</title>
</head>
<body>

    <!-- item template -->
    <script type="text/x-template" id="item-template">
        <li>
            <div
                    :class="{bold: isFolder}"
                    @click="toggle"
                    @dblclick="changeType">
                {{ model.name }}
                <span v-if="isFolder">[{{ open ? '-' : '+' }}]</span>
            </div>
            <ul v-show="open" v-if="isFolder">
                <item
                        class="item"
                        v-for="(model, index) in model.children"
                        :key="index"
                        :model="model">
                </item>
                <li class="add" @click="addChild">+</li>
            </ul>
        </li>
    </script>

    <p>(You can double click on an item to turn it into a folder.)</p>

    <!-- the demo root element -->
    <ul id="demo">
        <item
                class="item"
                :model="treeData">
        </item>
    </ul>

    <script src="/js/vendor.js"></script>
    <script src="/js/index.js"></script>
</body>
</html>

src/main/js/index.js

import '../css/index.css';
import Vue from 'vue'

// demo data
var data = {
    name: 'My Tree',
    children: [
        { name: 'hello' },
        { name: 'wat' },
        {
            name: 'child folder',
            children: [
                {
                    name: 'child folder',
                    children: [
                        { name: 'hello' },
                        { name: 'wat' }
                    ]
                },
                { name: 'hello' },
                { name: 'wat' },
                {
                    name: 'child folder',
                    children: [
                        { name: 'hello' },
                        { name: 'wat' }
                    ]
                }
            ]
        }
    ]
}

// define the item component
Vue.component('item', {
    template: '#item-template',
    props: {
        model: Object
    },
    data: function () {
        return {
            open: false
        }
    },
    computed: {
        isFolder: function () {
            return this.model.children &&
                this.model.children.length
        }
    },
    methods: {
        toggle: function () {
            if (this.isFolder) {
                this.open = !this.open
            }
        },
        changeType: function () {
            if (!this.isFolder) {
                Vue.set(this.model, 'children', [])
                this.addChild()
                this.open = true
            }
        },
        addChild: function () {
            this.model.children.push({
                name: 'new stuff'
            })
        }
    }
})

// boot up the demo
var demo = new Vue({
    el: '#demo',
    data: {
        treeData: data
    }
})

src/main/css/index.css

body {
    font-family: Menlo, Consolas, monospace;
    color: #444;
}
.item {
    cursor: pointer;
}
.bold {
    font-weight: bold;
}
ul {
    padding-left: 1em;
    line-height: 1.5em;
    list-style-type: dot;
}

動作確認

gradlew bootrun をしてもらえればJavaScriptコンパイルが行われSpringBootが起動します

まとめ

4つのフレームワークの中でwebpackのモジュールが用意されていなかったので準備することは多かったSpringBootでした
あと情報も意外と少なかったように思います
結構悩みました...

今回もソースはGitHubにあげてあります
webpack組み込みの参考になれば幸いです(^^)

github.com