VSCodeでイマドキのJavaScriptの開発環境をサクッと作ろうぜ(デプロイ編)

# はじめに
イマドキのJavaScript開発環境記事第3弾。

過去記事は以下。

- [第1弾 VSCodeの設定～Linter](https://qiita.com/neruneruo/items/c940af6c85519fd63648)
- [第2弾 自動テスト](https://qiita.com/neruneruo/items/edcf02eb0ebdb40a4876)

今回は、モジュールバンドラーを用いてアプリケーションのデプロイをする設定を作っていく。

# モジュールバンドラーの選定
モジュールバンドラーはいろいろあるが、Vue CLIを使った場合は最優先で選択される等、一日の長があるため、Webpackを使う。
なお、Webpackに関する参考記事として以下を読んでおくと導入が早い。

- [いちばんやさしいwebpack入門](https://zenn.dev/sprout2000/articles/9d026d3d9e0e8f)

# 全体のファイル構成
プロジェクトと同列にdist/bandle.jsというアウトプットを作成するようにする。

```
<プロジェクトルート>
├── .babelrc.js
├── .webpackrc.common.js
├── .webpackrc.dev.js
├── .webpackrc.prod.js
├── contents
│   ├── app.js
│   ├── employeeMain.vue
│   └── mixin.js
└── package.json

dist
└── bandle.js
```

# Webpack関連のモジュールのインストール
これまでと同様、以下をpackage.jsonに追記して、npm installしよう。
例によって、バージョン指定が面倒なので、今動いているバージョンを書いておく。

```json:package.json(抜粋)
  "devDependencies": {
    "@babel/core": "^7.17.8",
    "@babel/plugin-transform-runtime": "^7.17.10",
    "@babel/preset-env": "^7.16.11",
    "babel-loader": "^8.2.3",
    "clean-webpack-plugin": "^4.0.0",
    "terser-webpack-plugin": "^5.3.1",
    "vue-loader": "^15.9.8",
    "webpack": "^5.70.0",
    "webpack-cli": "^4.9.2",
  }
```

# Webpack設定ファイルの配置
Webpackの設定はwebpack.config.jsというファイル名だが、この手の設定ファイルはツールによってバラバラで分かりにくいので、前回記載した通り、今回の一連の記事では、ドット始まりで統一する。

また、[Webpack公式のガイド](https://webpack.js.org/guides/production/)に記載されている通り、dev環境とprod環境で設定を分けるセオリーとしては、共通の設定と、それぞれの環境の設定ファイルを用意してマージすることらしいので、それに従う。

基本の設定は以下の部分になる。

- ```entry:``` には、起点となるファイル(今回はcontents/app.js)を書く
- ```output:```に出力先の情報(今回は、../dist/bundle.js)を書く。なお、絶対パスで記載する必要があるため、pathモジュールを使って相対パスの解決をする
- 

```javascript:.webpackrc.common.js
// 標準のrequire
const webpack = require('webpack')

// webpack実行時に出力先フォルダを掃除
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

// Vue.js用のrequire
const { VueLoaderPlugin } = require('vue-loader')

// グローバル変数を環境変数で置換する際に使用するrequire
const fs = require('fs')

// ファイル出力時の絶対パス指定に使用
const path = require('path')

module.exports = {
  // エントリーポイント(メインのjsファイル)
  entry: './contents/app.js',
  // ファイルの出力設定
  output: {
    // 出力先(絶対パスでの指定必須)
    path: path.resolve(__dirname, '../dist'),
    // 出力ファイル名
    filename: 'bundle.js',
    // libraryExport
    libraryExport: 'default',
  },
  // ローダーの設定
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader',
        ],
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      },
      {
        // ローダーの対象 // 拡張子 .js の場合
        test: /\.js$/,
        // ローダーの処理対象から外すディレクトリ
        exclude: /node_modules/,
        // Babel を利用する
        loader: 'babel-loader',
        // Babel のオプションを指定する
        options: {
          presets: [
            // プリセットを指定することで、ES2019 を ES5 に変換
            '@babel/preset-env',
          ],
          plugins: [
            '@babel/plugin-transform-runtime'
          ],
        },
      },
    ],
  },
  resolve: {
    // Webpackで利用するときの設定
    alias: {
      vue$: 'vue/dist/vue.esm.js',
    },
    extensions: [
      '*',
      '.js',
      '.vue',
      '.json',
    ],
  },
  plugins: [
    // webpack実行時に出力先フォルダを掃除
    new CleanWebpackPlugin(),
    // Vueを読み込めるようにするため
    new VueLoaderPlugin(),
    // 環境変数の置換
    new webpack.DefinePlugin((() => {
      const jsonObject = JSON.parse(fs.readFileSync('../terraform/terraform.tfstate', 'utf8'))
      return {
        'process.env.APIGATEWAY_INVOKE_URL': JSON.stringify(jsonObject.outputs.apigateway_invoke_url.value),
      }
    })(),
    ),
  ],
}
```

```javascript:.webpackrc.dev.js
// development/production共通の設定をマージするためのrequire
const { merge } = require('webpack-merge')
const common = require('./.webpackrc.common.js')

module.exports = merge(common, {
  // 動作モードの定義
  mode: 'development',
  // ソースマップの設定
  devtool: 'source-map',
  // js, css, html更新時自動的にブラウザをリロード
  devServer: {
    // サーバーの起点ディレクトリ
    // contentBase: "dist",
    // バンドルされるファイルの監視 // パスがサーバー起点と異なる場合に設定
    publicPath: '/dist/js/',
    // コンテンツの変更監視をする
    watchContentBase: true,
    // 実行時(サーバー起動時)ブラウザ自動起動
    open: true,
    // 自動で指定したページを開く
    openPage: 'index.html',
    // 同一network内からのアクセス可能に
    host: '0.0.0.0',
  },
})
```

```javascript:.webpackrc.prod.js
// development/production共通の設定をマージするためのrequire
const { merge } = require('webpack-merge')
const common = require('./.webpackrc.common.js')

// js最適化
const TerserPlugin = require('terser-webpack-plugin')

module.exports = merge(common, {
  // 動作モードの定義
  mode: 'production',
  // ソースマップ有効
  devtool: undefined,
  // mode:productionでビルドした場合のファイル圧縮
  optimization: {
    minimizer: [
      // jsファイルの最適化
      new TerserPlugin({
        // すべてのコメント削除
        extractComments: 'all',
        // console.logの出力除去
        terserOptions: {
          compress: { drop_console: true }
        },
      }),
    ]
  },
})
```

なお、標準と異なる設定ファイルを用いる場合は、--configオプションでファイルを指定する。package.jsonに以下の設定を追加しよう。

```diff_json:package.json(抜粋)
  "scripts": {
+   "webpackDev": "webpack --config ./.webpackrc.dev.js",
+   "webpack": "webpack --config ./.webpackrc.prod.js"
  }
```

# Babelの設定
とりあえず、Vue.jsのコンパイルとトランスパイルをまとめてするためにBabelを動かす。
Babelの設定ファイルは.babelrc.jsだ。以下を書いておく。

```javascript:.babelrc.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        modules: false,
      },
    ]
  ],
  env: {
    test: {
      presets: [
        [
          '@babel/preset-env',
          {
            targets: {
              node: 'current'
            }
          }
        ]
      ]
    }
  }
}
```

Webpackでは以下の部分がBabelの変換に関する設定となる。

```diff_javascript:.webpackrc.common.js(抜粋)
module.exports = {
  // ローダーの設定
  module: {
    rules: [
      {
+       // Babel を利用する
+       loader: 'babel-loader',
+       // Babel のオプションを指定する
+       options: {
+         presets: [
+           // プリセットを指定することで、ES2019 を ES5 に変換
+           '@babel/preset-env',
+         ],
+         plugins: [
+           '@babel/plugin-transform-runtime'
+         ],
+       },
      },
    ],
  },
```


# プラグインあれこれを入れる
## Vueの単一ファイルコンポーネントの変換
Vue.jsの単一ファイルコンポーネント(.vue)ファイルはWebpackのプラグインを使って変換を行う。以下の部分を追記していこう。

```diff_javascript:.webpackrc.common.js(抜粋)
+ // Vue.js用のrequire
+ const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
  // ローダーの設定
  module: {
    rules: [
+     {
+       test: /\.vue$/,
+       loader: 'vue-loader',
+     },
    ],
  },
  resolve: {
+   alias: {
+     vue$: 'vue/dist/vue.esm.js',
+   },
    extensions: [
+      '.vue',
    ],
  },
  plugins: [
+   // Vueを読み込めるようにするため
+   new VueLoaderPlugin(),
  ],
}
```

## clean-webpack-pluginで出力先フォルダを掃除する
以下の部分を追加することで、ビルドごとに出力先フォルダのゴミ掃除をしてからバンドルをしてくれる。
試行錯誤であれこれやっているときにゴミが生じるので、便利なプラグインだ。

```diff_javascript:.webpackrc.common.js(抜粋)
// webpack実行時に出力先フォルダを掃除
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  plugins: [
+   // webpack実行時に出力先フォルダを掃除
+   new CleanWebpackPlugin(),
  ],
}
```

## 環境変数のバンドルファイルへの導入
厳密にはプラグインではないが、```plugins:```には関数を書くこともできるため、以下の部分を追記しておく。
こうしておくことで、バンドルするアウトプット内で環境変数を読み込んでいる部分を置換することができる。

AWSが払い出すエンドポイントのようにランダム生成される文字列を取り込むのに使えるだろう。
※そんなことするくらいだったらRoute53のエイリアスを作れという話もあるが、いずれにしろ環境単位にURLが変わるわけで、外部から値を注入してやる必要があるはずだ。

今回は、Terraformのtfstateから値を引っ張ってきているが、CodeBuildのようなCI/CDのツールを使う場合は、ツールのインプットや環境変数経由で値を渡してあげよう。

```diff_javascript:.webpackrc.common.js(抜粋)
// 標準のrequire
+ // グローバル変数を環境変数で置換する際に使用するrequire
+ const fs = require('fs')

module.exports = {
  plugins: [
+   // 環境変数の置換
+   new webpack.DefinePlugin((() => {
+     const jsonObject = JSON.parse(fs.readFileSync('../terraform/terraform.tfstate', 'utf8'))
+     return {
+       'process.env.APIGATEWAY_INVOKE_URL': JSON.stringify(jsonObject.outputs.apigateway_invoke_url.value),
+     }
+   })(),
+   ),
  ],
}
```

実際のコードの記載は以下のような感じだ。

```javascript:contents/mixin.js
export const mixin = {
  data: function () {
    return {
      APIGATEWAY_INVOKE_URL: process.env.APIGATEWAY_INVOKE_URL,
    }
  },
}
```

```diff_vue:employeeMain.vue(抜粋)
<script>
+ import { mixin } from './mixin'

export default {
+ mixins: [
+   mixin
+ ],
}
```

## コードの最適化
Webpackでバンドルする場合、node_modulesの内容含めてバンドルするためにファイルが非常に大きくなる。
これを最適化するために、minimizerを使おう。minimizerもuglify-js等色々あるが、更新頻度が高く人気があるTerserを採用する。

さらにTerserの設定も色々あるが、今回はコメント削除と、console.log削除を行う。
prod環境で余計な情報を見せないようにしよう。

```diff_javascript:.webpackrc.prod.js(抜粋)
+ // js最適化
+ const TerserPlugin = require('terser-webpack-plugin')

module.exports = merge(common, {
  // mode:productionでビルドした場合のファイル圧縮
  optimization: {
    minimizer: [
      // jsファイルの最適化
+     new TerserPlugin({
+       // すべてのコメント削除
+       extractComments: 'all',
+       // console.logの出力除去
+       terserOptions: {
+         compress: { drop_console: true }
+        },
+     }),
    ]
  },
})

```

# いざ、動かす！
さて、あとはこれで、```npm run webpack```すれば、dist/bundle.js が出力されるので、このファイルをコンテンツと組み合わせて表示させよう！期待したVueの単一ファイルコンポーネントを確認することができるはずだ！

これで、3話かけてやってきたJavaScriptの開発環境を作ってデプロイするまでの流れは終わり。
まだCI/CDに組み込んだり色々と考慮しなければいけない点はあるだろうが、基本的なところはこれで作れるようになったかと思う。