業務でSpring Bootにwebpackを用いてjs周りを環境構築した際の備忘録

### はじめに
業務の新人施策で、
チーム内で合意形成し、SpringBootをベースにする、
その上で社内で使える技術なら自由に選んでいいよと言われたので、真っ先にnpmが浮かびました。

その理由は、ただjsファイルを用意するだけでなく、
パッケージマネージャーやバンドラーなどの周辺技術を組み合わて、Spring Bootで開発できたらなと思ったからです。
というわけで、とりあえずnpmを入れてみます。

ソースコードはこちらになります。
(コミットメッセージが超適当です。ごめんなさい。)

https://github.com/tkknot/spring-npm

### 作成者スペック
<ul>
<li>Jsはそこそこに書けるけど、実務の経験はないので客観的にできるとは言えないかも？</li>
<li>モダンJSはReactもVueも一応書ける</li>
<li>モジュールバンドラーからは逃げてきました</li>
<li>Spring Bootはまだまだ学習中のため、本業務で身につけていきたい</li>
<li>Spring Boot周りの開発技術はもっとわからない、、、頑張って勉強します</li>
</ul>

### SpringBootプロジェクトの開始
前提としてローカルにnpmがある方が便利ですので、npmもとい、Node.jsを用意してやってみてください。

https://nodejs.org/ja/

業務のメインはEclipseですが、せっかくなのでvsCodeで始めていこうと思います。
コマンドパレットから`Spring Initializer:Create a Gradle Project ... `で作っていきます。
![スクリーンショット (45).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1047811/b95f6e2a-ba6d-43b6-c080-506c077f83d9.png)


Javaは17系でやってみます。
なお、依存関係は業務でやってるものに合わせました。

```gradle:build.gradle
plugins {
	id 'org.springframework.boot' version '2.7.2'
	id 'io.spring.dependency-management' version '1.0.12.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'mysql:mysql-connector-java'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

```

拡張機能はこちらの記事が参考になります。

https://www.zunouissiki.com/vscode-springboot-gradle/

下記記事を参考（簡単ですが）に、一通り動くものを作ってみます。

https://medium-company.com/spring-boot-thymeleaf/

今回は下記画像のディレクトリ構造にします。

![スクリーンショット (49).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1047811/b3dbd4dc-85c8-08f2-2a62-8c654f24e38c.png)

### gradleでnpmを走らせるためのコード
ここからは以下の記事を頼りに（ベースに）環境構築をしました。
業務中はこの記事に幾度となく救われた…ありがたや…

https://qiita.com/h1romas4/items/8345e313b351c992df08#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB

まずはgradleでnpmをビルドできるように、
プラグインを導入した後、コマンドを書いていきます。

```gradle:build.gradle
plugins {
	id 'org.springframework.boot' version '2.7.2'
	id 'io.spring.dependency-management' version '1.0.12.RELEASE'
	id 'java'
  id "com.github.node-gradle.node" version "3.2.1"
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

bootRun {
    // for static resource hot reloading
    // クラスパスの変更を自動的に監視してくれる
    //  https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/html/#running-your-application-reloading-resources
    sourceResources sourceSets.main
}

repositories {
	mavenCentral()
}

node {
    // 16系はパッケージとの互換性がない可能性がある
    version = '16.14.2'
    download = true
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'mysql:mysql-connector-java'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

// task script-name in pacakge-json (dependsOn: ['npm_run_command'])
task webpack(dependsOn: ['npm_run_webpack'])
task dev(dependsOn: ['npm_run_dev'])
task format(dependsOn: ['npm_run_format'])

tasks.named('test') {
	useJUnitPlatform()
}
```

### ローカルでpacakage.jsonを作る
ローカルでnpmをinitしてください。
（Npmがローカルにない方はpackage.jsonをゼロから作らないといけません。大変。）
```
npm init -y
```

次に必要なパッケージをインストールしていきましょう。
```
npm install jquery tailwindcss axios
npm install --save-dev @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server  css-loader sass sass-loader mini-css-extract-plugin path webpack-remove-empty-scripts css-minimizer-webpack-plugin
```

色々入れました。また、webpackを実行するスクリプトを書いて整えたのが以下のファイルです。
```json:package.json
{
  "name": "demo",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "webpack": "webpack --mode=development"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/tkknot/spring-npm.git"
  },
  "author": "tkknot",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/tkknot/spring-npm/issues"
  },
  "homepage": "https://github.com/tkknot/spring-npm#readme",
  "dependencies": {
    "axios": "^0.27.2",
    "jquery": "^3.6.0",
    "tailwindcss": "^3.1.8"
  },
  "devDependencies": {
    "@babel/core": "^7.18.10",
    "@babel/preset-env": "^7.18.10",
    "babel-loader": "^8.2.5",
    "css-loader": "^6.7.1",
    "css-minimizer-webpack-plugin": "^4.0.0",
    "mini-css-extract-plugin": "^2.6.1",
    "node-sass": "^7.0.1",
    "path": "^0.12.7",
    "sass": "^1.54.4",
    "sass-loader": "^13.0.2",
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.10.0",
    "webpack-remove-empty-scripts": "^0.8.1"
  }
}
```
おそらく`node-sass`や`sass-loader`あたりのscssコンパイル周りのバージョンが、
現在のnodeやwebpackのバージョンが適合しない可能性がございますので、こちらはまた後ほど。

### style.scssとstyle.cssとmain.jsを作る
表題通りです。以下のパスで作ってください。
<ul>
<li>..(省略)/static/css/style.scss</li>
<li>..(省略)/static/css/style.css</li>
<li>..(省略)/static/js/main.js</li>
</ul>

### Webpackの設定ファイルを書く
configファイルがないとwebpackを動かせないので、作っていきます。
```js:webpack.config.js
const webpack = require("webpack");
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const RemoveEmptySctiptsPlugin = require("webpack-remove-empty-scripts");

module.exports = {
  entry: {
    // バンドル対象ファイルが一つの場合
    // 生成されるファイル名:対象ファイルの位置
    main: path.join(__dirname, "/src/main/resources/static/js/main.js"),
    style: path.join(__dirname, "/src/main/resources/static/css/style.scss"),
    // バンドル対象のファイルが複数の場合
    vendor: [
      "axios",
      "jquery",
    ],
  },
  target: 'node',
  // バンドルファイルの出力先
  output: {
    path: path.join(__dirname, "/src/main/resources/static"), // eslint-disable-line
    publicPath: "/",
    filename: "js/[name].bundle.js",
  },
  plugins: [
    new webpack.ProvidePlugin({
      $: "jquery",
      jQuery: "jquery",
    }),
    new MiniCssExtractPlugin({
      // cssを別ファイルとして書き出し
      filename: "css/[name].css",
    }),
    
    // バンドルの際に不要なファイルを削除
    new RemoveEmptySctiptsPlugin(),
  ],
  optimization: {
    minimizer: [
      new CssMinimizerPlugin()
    ]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: ["@babel/preset-env"],
            },
          },
        ],
      },
      {
        test: /\.(sc|c)ss$/,
        use: [
          MiniCssExtractPlugin.loader,
          // .css 内の URL パスなどをそれぞれの publicPath に合わせてくれる
          "css-loader",
          // .sass のビルド
          "sass-loader",
        ],
      },
    ],
  },
  resolve: {
    extensions: [".js"],
    modules: ["node_modules"],
  },
};

```
以下記事がエラーの解決やCSSを分割する際に役立ちました。

https://ui.appleple.blog/JS/entry-100.html

### Eslint,Prettierの設定
モダンJS開発では当たり前なコード解析及び整形ツールをこちらでも入れてみます。
```
npm install --save-dev eslint prettier eslint-plugin-prettier eslint-config-standard eslint-plugin-promise eslint-plugin-import eslint-config-prettier
```

eslintとprettierは以下で設定ファイルを作ります。

```json:.eslintrc.json
{
  "env": {
    "node": true,
    "es6": true,
    "commonjs": true,
    "browser": true
  },
  "parserOptions": {
    "sourceType": "module",
    "ecmaVersion": "latest"
  },
  "plugins": ["prettier"],
  "extends":[
    "eslint:recommended",
    "prettier"
  ],
  "rules": {"no-unused-vars": "off"}
}
```
使われていないモジュールなどに対してエラー判定を出す`no-unused-vars`はオフにします。
```json:.prettierrc.json
{
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "quoteProps": "as-needed",
  "trailingComma": "es5",
  "bracketSpacing": true,
  "arrowParens": "always",
  "bracketSameLine": true
}
```

整形する必要のないファイルはignoreしましょう。
```.prettierignore
node_modules
package-lock.json
```
npmで実行するためのコマンドを書きます。
Eslintで静的解析をし、Prettierで整形という運びになっています。
```js:package.json
{
  "scripts": {
    "webpack": "webpack --mode=development",
    //追加
    "format" : "eslint --cache --fix src/ && prettier --write src/" 
  },
}
```

実行してエラーを吐かなければ成功です。
これで一通りnpmをSpring Boot内で扱えるようになりました！！
```
npm run format
    or
./gradlew format
```
### Scssを用意する
今回は`/css`に`variables.scss`という別ファイルを作ってインポートしてみます。
```scss: style.scss
@import "./variables.scss";

header {
  color: #ffffff;
  background: $color-primary;
}
```
```scss: variables.scss
$color-primary: #36e8ff;
```

また公式には読み込んでいるJsファイルに.scssファイルをインポートする文を書けとあります。
```js:main.js
import '../css/style.scss';
```
`npm run webpack`もしくは`./gradlew webpack`をすると、
sass-loaderによってコンパイルしてくれます。

`style.css`が以下のようになればOKです
```css:style.css
header {
  color: #ffffff;
  background: #36e8ff;
}
```

### 【番外編】できなかったこと
##### tailwindcssが最大限に使えない
tailwindcssのconfigファイルを作って、package.jsonに実行コマンドを用意しても、

適切に読み込まれず、特定のクラスが使えない、プラグインが使えないなど、、、
tailwindcssが真価を発揮するように実装することができませんでした。

業務の方はBootstrapに変えようかな、、、
一応公式ドキュメントのnpxコマンドでcssファイルは生成できたので、次の項では読み込んでみます。

有識者いらっしゃいましたら教えていただけると幸いです。

https://tailwindcss.com/docs/installation

### 簡単な画面を作ってみる
せっかくなので簡単に画面を作ってみましょう。

```html:index.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>Hello</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" th:href="@{/css/tailwind.css}">
    <link rel="stylesheet" th:href="@{/css/style.css}">
  </head>
  <body>
    <header class="text-center font-bold"><span th:text="${message}" class="text-lg"></span></header>

    <script th:src="@{/js/main.bundle.js}"></script>
  </body>
</html>
```
```scss:style.scss
@import "./variables.scss";

header {
  color: #ffffff;
  background: $color-primary;
  height: 100px;
  font-size: 64px;
}
```
```js:main.js
import $ from 'jquery';

$(() => {
  console.log('hello');
});
```
こちらを貼り付けて、`npm run webpack`もしくは`./gradlew webpack`をすると、
簡単に挨拶をしている画面が出てきます！

![スクリーンショット (51).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1047811/478b3b82-1511-ad4b-6471-4d466e928a48.png)

コンソールでも挨拶してますね。
（コンソール内のエラーはおそらく拡張機能のものです。）

##### tailwindcssの諸問題を解消できたので、daisyUIを導入して使ってみる（2022/08/18）
postcss-loaderをwebpackで使えば、postcssのプラグインが動作して、
標準通りtailwindcssが使えます。

せっかくなのでtailwindcssのplugin、`daisyUI`を使ってやってみましょう。
```
npx tailwindcss init -p
npm install --save-dev postcss-loader daisyui
```

```html:index.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>Hello</title>
    <meta charset="utf-8" />
    <!-- tailwindcss のスタイルシートを消去 -->
    <link rel="stylesheet" th:href="@{/css/style.css}" />
  </head>
  <body>
    <header class="text-center font-bold">
      <span th:text="${message}" class="text-5xl"></span>
    </header>

    <!-- button 追加 -->
    <div class="w-full h-24 text-center">
      <button class="btn btn-primary">OK</button>
    </div>


    <script th:src="@{/js/main.bundle.js}"></script>
  </body>
</html>

```
```js:main.js
import '../css/style.scss'
```
```js:postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
```
```js:tailwindcss.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
 content: ["./src/**/*.{html,js}"],
  theme: {
    extend: {},
  },
  // daisyuiを呼び出す
  plugins: [require('daisyui')],
}

```
```js:webpack.config.js
...
    new MiniCssExtractPlugin({
      // cssを別ファイルとして書き出し
      filename: './css/style.css',
    }),
...
      {
        test: /\.(sc|c)ss$/,
        use: [
          MiniCssExtractPlugin.loader,
          // .css 内の URL パスなどをそれぞれの publicPath に合わせてくれる
          'css-loader',
          // postcssを使う
          'postcss-loader',
          // .sass のビルド
          'sass-loader',
        ],
      },
...
```
以下あまりレイアウト整えてませんが、daisyUIも盛り込めました！

![スクリーンショット (54).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1047811/a84e1cda-9271-6b57-aa8e-bcc655a92e55.png)

なお以下の記事でdaisyUIについて紹介しています（自分が）。

https://qiita.com/tkdayo/items/953d847e2d88f09d92c8

### 終わりに
正直React+Spring Bootができるなら、
簡単にフロントエンドを動かせる仕組みを導入できたり、サーバーサイドとの分離もしやすいと思うので、
慣れ親しんでる方はそちらのほうがいいなと感じました。

ただ、今回業務内のチームんメンバー全員が開発及びjs初学者・初心者なので、
このようなjs周りが学べる若干レガシーな構成にしました。

ここまで長ったらしい文章を読んでいただきありがとうございました。