VueでWebpackを使ってみた

# はじめに
Vue　TypeScript Vuetifyという環境でWebpackを使用してみたので、構築手順やWebpackの説明などを残しておきます。

## 必要なパッケージをインストール

webpackと、webpackで使用するloaderをインストールします。

```
npm install -D webpack webpack-cli css-loader style-loader ts-loader vue-loader @vue/compiler-sfc
```

:::note info
loader（ローダー）とは、Webpack が .js 以外のファイルを理解・変換できるようにする仕組みのことです。

✅ わかりやすく言うと…
Webpack はもともと JavaScript ファイルしか扱えません。
でも、現代のWeb開発では .vue、.ts、.scss、.png など様々な形式のファイルを扱います。

Webpack にこう伝える必要があります：

「.vue ファイルはこう処理してね！」
「.ts は TypeScript だから JavaScript に変換してね！」

この「処理方法を教える機能」が loader（ローダー） です。
:::

## Vueコンポーネントを型として認識させるファイルを作成
`src/shims-vue.d.ts`

```ts
declare module '*.vue' {
  import { DefineComponent } from 'vue';
  const component: DefineComponent<{}, {}, any>;
  export default component;
}
```

:::note info
shims-vue.d.ts は、**TypeScript に Vue コンポーネント（.vue ファイル）を型として認識させるための宣言ファイル（型定義）**です。

✅ なぜ必要なのか？
TypeScript は .vue ファイルの中身がどんな構造になっているかをデフォルトでは理解できません。

例えば、これがあると：

ts
コピーする
編集する
import MyComponent from './components/MyComponent.vue';
TypeScript は「.vue って何？ モジュールじゃないでしょ？」とエラーを出します。

✅ shims-vue.d.ts がやってること
ts
コピーする
編集する
declare module '*.vue' {
  import { DefineComponent } from 'vue';
  const component: DefineComponent<{}, {}, any>;
  export default component;
}
この宣言は：

「.vue ファイルをインポートしたら、それは Vue コンポーネント（DefineComponent）として扱っていいよ」

と TypeScript に教えています。
:::

# tsconfigに設定を追記

```json:tsconfig.json
{
  // 追加 
  "include": ["src/**/*.ts", "src/**/*.vue"]
}
```

# webpack.config.jsを追加

packaga.jsonと同じフォルダに以下のファイルを作成する

```js:webpack.config.js
import path from 'path';
import { fileURLToPath } from 'url';
import { VueLoaderPlugin } from 'vue-loader';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  entry: './src/main.ts',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  resolve: {
    extensions: ['.ts', '.js', '.vue'],
    alias: {
      vue$: 'vue/dist/vue.esm-bundler.js',
    },
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader',
      },
      {
        test: /\.ts$/,
        use: {
          loader: 'ts-loader',
          options: {
            appendTsSuffixTo: [/\.vue$/],
          },
        },
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [new VueLoaderPlugin()],
  mode: 'development', // or 'production'
};
```

### パラメータ解説
#### entry
最初にビルドするファイル
ビルドしてファイルをまとめていくことをバンドル処理といい、その最初の開始ファイルをここで設定する

:::note info
「バンドル処理」とは、複数のファイルやモジュール（JS・CSS・画像など）を1つまたは少数のファイルにまとめる処理のことです。Webpack の主な役割がこの「バンドル」です。
:::

entryにはどのファイルを指定したらええねんとなりましたが、
Vue プロジェクトでいうと 下記のように createApp() を呼び出してアプリを起動するファイルにします。

```ts
import { createApp } from 'vue';
import App from './App.vue';

import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import '@mdi/font/css/materialdesignicons.css';
import 'vuetify/lib/styles/main.css';

const vuetify = createVuetify({
  components,
  directives,
});

const app = createApp(App);
app.use(vuetify);
app.mount('#app');
```

:::note info
entryには複数ファイル指定できる
その場合、以下のように設定すると、app.bundle.jsとadmin.bundle.jsが作成される
```js
  entry: {
    app: './src/app.js',
    admin: './src/admin.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },

```
:::

#### resolve
Webpack における モジュール解決のルールを定義する設定

:::note info

簡単に言うと、「import や require が書かれたときに、どのようにファイルを探すか？」を指定します。

✅ コードの解説
```js
resolve: {
  extensions: ['.ts', '.js', '.vue'],
  alias: {
    vue$: 'vue/dist/vue.esm-bundler.js',
  },
}
```
この中に 2 つの重要な設定があります。

1. ` extensions: ['.ts', '.js', '.vue']`
これは、拡張子を省略したときにどの順で探すかを指定します。
`import Hello from './components/Hello';`
このとき Webpack は、次の順でファイルを探します
    - ./components/Hello.ts
    - ./components/Hello.js
    - ./components/Hello.vue
✅ 拡張子が .vue のファイルを読み込むには、.vue をここに含める必要があります。
2. `alias: { vue$: 'vue/dist/vue.esm-bundler.js' }`
これは、import 'vue' と書かれたときに、どのファイルを読み込むか指定するものです。
Vue は複数のビルド形式がありますが、.vue ファイルを扱うには バンドル対応かつコンパイル付きの ESM ビルドを使う必要があります。
vue → デフォルトはランタイムオンリー
vue/dist/vue.esm-bundler.js → template をコンパイルできる完全版
✅ .vue ファイルを扱うなら vue/dist/vue.esm-bundler.js を明示的に指定する必要があります。
:::

また便利な使い方としては下記のように相対パスを省略するといったことも可能です。
下記のように設定すると
```js
alias: {
  "@components": path.resolve(__dirname, "src/main/components"),
  "@pages": path.resolve(__dirname, "src/main/pages"),
}
```

インポートする側のファイルでは下記のように書けます
```ts
// 相対パスが長い書き方
// import MyButton from '../../../main/components/MyButton.vue';

// エイリアスを使った書き方
import MyButton from '@components/MyButton.vue';

import HomePage from '@pages/HomePage.vue';
```

#### modules
Webpack に「どのファイルをどう処理するか（変換・読み込みなど）」を教える設定を書く

:::note info
✅ ざっくり言うと：
「このファイルの拡張子にはこのローダーを使ってね」とルールを書く場所です。

✅ 基本構造
```js
module: {
  rules: [
    {
      test: /\.拡張子$/,      // 対象となるファイル（正規表現）
      use: 'ローダー名'        // 使用するローダー（または use: [...] で複数）
    }
  ]
}
```
:::

#### plugins
plugins は、Webpack の「追加機能」を拡張するための仕組みです。
module.rules が「ファイル変換のルール」だとしたら、plugins は「ビルドの過程でやりたいことを追加する場所」です。

:::note info
✅ plugins には何を書く？
new プラグイン名(設定) の形で、使いたいプラグインを plugins: [] の中に書きます。

✅ よく使うプラグイン例（Vue + TypeScript想定）
```js
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
  // ...entry, output, module.rulesなど
  plugins: [
    new VueLoaderPlugin(), // ← Vueファイルを正しく処理するために必要
    new HtmlWebpackPlugin({
      template: './dist/index.html', // ← HTMLテンプレートにJSを注入する
    }),
  ],
};
```

✅ 各プラグインの役割
| プラグイン名              | 主な役割                                                   |
|---------------------------|------------------------------------------------------------|
| VueLoaderPlugin           | `.vue` ファイルを使うために必須（`vue-loader` とセット）  |
| HtmlWebpackPlugin         | HTML ファイルを自動生成し、`<script src="bundle.js">` を挿入 |
| DefinePlugin              | 環境変数を埋め込む（例：`process.env.NODE_ENV`）          |
| MiniCssExtractPlugin      | CSS を別ファイルとして出力する（`style-loader` の代替）   |
| CleanWebpackPlugin        | `dist` フォルダをビルド前に自動で削除する                 |
:::

#### mode
mode には、ビルドの目的に応じて Webpack に最適な動作をさせるモードを指定します。

:::note info
✅ mode に指定できる値
- 'development'	
開発用モード。デバッグしやすくなる	ソースマップ有効、圧縮なし、watch有効
- 'production'	
本番用モード。パフォーマンス重視	自動で最小化（JS/CSS圧縮）、console削除など
- 'none'	
最適化なしの素の状態でビルド	自動最適化なし（必要なら細かく設定）

✅ 書き方（webpack.config.js）
```js
module.exports = {
  mode: 'development', // or 'production' or 'none'
  entry: './src/index.ts',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // ...
};
```

✅ よくある使い分け
- 開発中 → 'development'
- 本番リリース → 'production'
- カスタム最適化や自前ビルド管理 → 'none'
:::


# index.htmlを作成
package.jsonと同じ階層にdist/index.htmlを作成します

:::note info
後述しますが、HtmlWebpackPluginを使って自動で生成することもできます
:::

`<script src="bundle.js"></script>`の`bundle.js`は、`webpack.config.js`の`output`の`filename`と合わせます。

```html
<!doctype html>
<html>
  <body>
    <div id="app"></div>
    <script src="bundle.js"></script>
  </body>
</html>
```

# build
ここまで設定したら`npx webpack`でビルドできます。
ビルドすると、distフォルダにファイルが出力されます


# serve
webpack-dev-server使うと、localhostで、Webサーバーとして起動できます

```
npm install -D webpack-dev-server
```

インストールできたら、webpack.config.jsも編集します
devServerを追加します。

```diff_javascript
export default {
+ devServer: {
+   static: 'dist',
+   open: true,
+ },
```

追加したら、`npx webpack serve`を実行すると、サーバーが起動します
サーバーを起動すると、ホットリロードが効きますので、tsやVueのファイルを編集すると即座に変更が反映されます
Ctrl+cで終了できます

### serveではファイルは出力されない
深く理解したい方はお読みください

サーバーを起動していたら終了して、`npx webpack`を実行した方は、distフォルダ内のindex.html以外を削除してみてください
そして、もう一度、`npx webpack serve`を実行し、サーバーを立ち上げます
その後、distフォルダを確認してください
何も出力されてないはずです

私はここが引っかかりました
なぜなら、配置したindex.htmlでは、`<script src="bundle.js"></script>`で`bundle.js`を読み込んでいるが、bundle.jsは出力されていない
なぜ正常に動くのか？

:::note info
✅ 結論 Webpack Dev Server がメモリ上に bundle.js を作って提供しているから

🔧 背景
Webpack Dev Server（webpack-dev-server）は、開発中に以下のような動作をします
- bundle.js をファイルとして書き出さない(開発中はディスクに保存されない)
- bundle.js を メモリ上で生成(内部的にメモリでバンドルし、HTTP 経由で提供する)
- index.html が `<script src="bundle.js">` を参照(Web サーバーが bundle.js を仮想的に提供するので正常に動く)

つまり、実際のファイルシステムには存在しなくても、Webpack Dev Server は bundle.js を仮想的に提供しているので、ブラウザは問題なくそれを読み込めます。

✅ 本番環境では注意
開発中はメモリ上でOKですが、本番ビルド時には`npx webpack --mode production`などで dist/bundle.js を実ファイルとして出力する必要があります。そうしないと index.html から参照されている bundle.js が読み込めず、404 エラーになります

:::

# index.htmlも自動で作成する

手動で、dist/index.htmlを作成していましたが、これも自動で作成するようにします

### html-webpack-pluginをインストール
自動作成するプラグインである html-webpack-pluginをインストールします
```
npm install -D html-webpack-plugin
```

### テンプレートファイルを作成する
html-webpack-pluginでindex.htmlを作成するのですが、テンプレートとなるファイルを作成します

src/index.htmlを作成する
```html
<!doctype html>
<html>
  <body>
    <div id="app"></div>
    <!--これは自動で挿入されるので必要なし-->
    <!--<script src="bundle.js"></script>-->
  </body>
</html>
```

テンプレートファイルはなくても、デフォルトのファイルを作成してくれるらしいのですが、以下のように最小限のhtmlになるため、Vueで`<div id="app"></div>`を使いたい場合はテンプレートが必要になるようです。
```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
    <script src="bundle.js"></script>
  </body>
</html>
```

### webpack.config.jsにpluginの設定を追加する

```diff_javascript
import path from 'path';
import { fileURLToPath } from 'url';
import { VueLoaderPlugin } from 'vue-loader';
+import HtmlWebpackPlugin from 'html-webpack-plugin';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  devServer: {
    static: 'dist',
    open: true,
  },
  ...
  plugins: [
    new VueLoaderPlugin(),
+   new HtmlWebpackPlugin({
+     template: './src/index.html', // ← テンプレートとして使うHTML
+     filename: 'index.html', // outputのpathのフォルダに出力される
+     inject: 'body',
+   }),
  ],
  mode: 'development', // or 'production'
};
```

追記したら、`npx webpack serve`で起動します。
bundle.jsと同じく、`npx webpack serve`で起動した場合、dist/index.htmlは出力されず、メモリ上に展開されます。


### 生成したindex.htmlに`<script>`タグが挿入されない場合

私の環境だけかもしれませんが、`npx webpack serve`で立ち上げると画面が真っ白になりました。
開発者ツールで確認する`<script>`タグが挿入されておらず、bundle.jsが読み込めてないみたいでした。

色々試行錯誤したのですが、src/index.htmlを下記に変更して、`npx webpack`でビルドしてみると、生成物に`<script>`タグ挿入されるようになりました。
そして、distフォルダの中身を全て消して、`npx webpack serve`を実行すると正しく起動しました。
一度挿入されると、元のファイルに戻しても、正しく挿入されるので原因わからずです。

```
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Webpack Vue App</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>
```

### devServeのstatic: 'dist'は不要
今回のサンプルでは、npx webpack serveで起動した場合、必要なファイル(index.htmlや、bundle.js)が全て、メモリ上に展開されるため、distには何も出力されないです。
したがって、devServeのstatic: 'dist'は不要となります。

:::note info
devServer.static: 'dist'	通常のVueアプリ開発では 不要
使いたいケース	robots.txt や favicon.ico など Webpackで処理しない静的ファイルを別に配置したいとき
通常は使わずOK。静的ファイルが必要なら public/ フォルダなどを使うのが一般的みたいです
:::