【TypeScript】VSCodeでTypeScriptの開発環境を構築する②（webpack ver.5）

<!-- ================================================ -->
#概要
<!-- ================================================ -->
TypeScriptのモジュールを使用したプロジェクトで、ESモジュールを使用すると以下のような問題点があります。

* コンパイルしたファイルがバンドルされない
* import/export文を使用するためモダンなブラウザでしか動作しない
* ブラウザで読み込みに時間がかかる
* コンパイルしたファイルが最適化（ミニファイ）されない

これを解決するために、`webpack（モジュールバンドラー）`を使用した開発環境を構築します。

※ モジュールバンドラーとは、コンパイルしたファイルをひとつにまとめる機能のことです。
　 さらにwebpackはコンパイルも同時に行います。

---
この記事は、以下のコースの受講に伴って、備忘録として書いています。詳しく知りたい方は、是非受講してみてください。

https://www.udemy.com/share/103e4OAEYdcVlbTX4D/

<!-- ================================================ -->
#環境
<!-- ================================================ -->
* Windows10
* TypeScript 4.2.4
* webpack 5.36.2
* VSCode
* Chrome

<!-- ================================================ -->
#構築
<!-- ================================================ -->
プロジェクトは、[【TypeScript】VSCodeでTypeScriptの開発環境を構築する](https://qiita.com/nemutas/items/0a266ec58c76ae3ee43a)で作成したものを引き続き使います。

###パッケージのインストール
VSCodeでプロジェクトを開き、ターミナルからwebpackとそれに必要なパッケージをインストールします。

```
npm install --save-dev webpack webpack-cli webpack-dev-server typescript ts-loader clean-webpack-plugin
```
`--save-dev`
開発時のみ使うパッケージを表す設定です

`webpack`
TypeScriptファイルをJavaScriptファイルにコンパイルし、さらにコンパイルしたファイルを一つにまとめます（バンドル）

`webpack-cli`
webpackのコマンドをプロジェクトで実行する

`webpack-dev-server`
開発用のwebサーバー。裏でwebpackを実行し、ファイルの変更を監視して変更があれば自動で再コンパイルを行います

`typescript`
プロジェクト単位でインストールすることで、バージョンを自由に指定（固定）できます

`ts-loader`
webpackがtsファイルをjsファイルにコンパイルする方法を知るためのパッケージ。裏ではtypescriptを使います

`clean-webpack-plugin`
コンパイル時に、コンパイル先のフォルダをクリーンします

```package.json
"devDependencies": {
    "clean-webpack-plugin": "^4.0.0-alpha.0",
    "ts-loader": "^9.1.1",
    "typescript": "^4.2.4",
    "webpack": "^5.36.2",
    "webpack-cli": "^4.6.0",
    "webpack-dev-server": "^3.11.2"
}
```

###webpack.config.js
ルートディレクトリに、`webpack.config.js`という名前でwebpackの設定ファイルを追加します。
webpackを使用してビルドをしたときに、webpackはこのファイル名を内部的に参照します。（つまりファイル名は固定です）

```webpack.config.js
const path = require('path');

module.exports = {
	mode: 'development',
	entry: './src/app.ts',
	output: {
		filename: 'bundle.js',
		path: path.resolve(__dirname, 'dist')
	},
	devServer: {
		publicPath: '/dist/'
	},
	devtool: 'inline-source-map',
	module: {
		rules: [
			{
				test: /\.ts$/,
				use: 'ts-loader',
				exclude: /node_modules/
			}
		]
	},
	resolve: {
		extensions: ['.ts', '.js']
	}
};
```

`mode: 'development'`
開発者モードでビルドします（ミニファイが行われません）

`entry: './src/app.ts'`
ビルドの対象となる基準ファイルです

`output: {filename: 'bundle.js', path: path.resolve(__dirname, 'dist')}`
filenameは、ビルドしたファイル名です（通常、bundle.js）
pathは、出力するディレクトリです。絶対パスで指定します

`devServer: {publicPath: '/dist/'}`
サーバー起動時に、再ビルドを行ったときのメモリ上のバンドルファイルをどこに置くかのディレクトリを指定します

`devtool: 'inline-source-map'`
生成したソースマップを、バンドルファイルに紐づけます

`module: {rules: [{test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/}]}`
webpackが見つけたファイルに対して、何をするのかを指定します
「拡張子が.tsのファイルに対して、ts-loaderを使用してコンパイルを行う。ただし、node_modulesフォルダは除く」

`resolve: {extensions: ['.ts', '.js']}`
インポートしたモジュールをどのように解決するのかを指定します。上記のように指定することで、モジュールをimportするときにファイル名の拡張子を書かないでよくなります。

###ファイルの変更

####tsconfig.json
webpack.config.jsで`entry: './src/app.ts'`を指定したので、以下はコメントアウトします。

```tsconfig.json
// "rootDir": "./src"
```

####index.html
参照するファイル名が変わったので、scriptを変更します。

```index.html
<script type="module" src="dist/bundle.js" defer></script>
```

####app.ts
モジュールの読み込みルールが変わったので、import文を変更します。

```app.ts
import { greeting } from './modules/sub'; // 拡張子を付けない

console.log('Hello, TypeScript');
console.log('こんばんは！');

greeting('Nemutas');
```

<!-- ================================================ -->
#ビルド＆デバッグ
<!-- ================================================ -->
###ビルド
npmコマンドからwebpackのビルドとサーバーアクセスができるように、package.jsonに以下の設定を追加します。

```package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack", // 追加
    "start": "webpack serve" // 追加
  }
```

ターミナルで、以下を実行してビルドします。

```
npm run build
```

そうすると、`dist/bundle.js`が作成されます。

###デバッグ
ビルドをした後に、ターミナルで以下を実行します。
startは、`webpack serve`（ローカルサーバーアクセス）を実行するコマンドです。

```
npm run start
```

エラーがなければ、以下のように表示されます。

```
i ｢wds｣: Project is running at http://localhost:8080/
```

ポートが`8080`だとわかったので、`.vscode/launch.json`を変更します。

```launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "pwa-chrome",
            "request": "launch",
            "name": "Launch Chrome against localhost",
            "url": "http://localhost:8080", // 変更
            "webRoot": "${workspaceFolder}"
        }
    ]
}
```

ローカルサーバーを起動した状態で、F5を押してデバッグを開始します。
app.tsにブレークポイントをいれて再実行（ブラウザリロード）すると、そこで停止することがわかります。

![無題10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/10d754a4-e98a-e05c-a17a-0718a85d4fe2.png)

サーバーを停止する場合は、ターミナルで`Ctrl + C`を押します。

###リビルド
ここで、デバッグ実行を行っている状態（ローカルサーバーを起動している状態）で、tsファイルを編集して保存すると、即座にリビルドされブラウザに反映されます。
しかし、ローカルの`dist/bundle.js`は更新されません。これはリビルドがメモリ上でしか行われないためです。ブラウザはメモリ上のbundle.jsを参照します。

<!-- ================================================ -->
#本番用ファイルの生成
<!-- ================================================ -->
デバッグ環境でビルドされた`bundle.js`は、ミニファイされていません。また、ソースマップなどの余計な情報が含まれています。このため、ファイル容量は大きくなり読み込む速度も低下します。
実際には、余計なものを削ぎ落した本番用ファイルを生成する必要があります。

###webpack.config.prod.js
本番用のbundle.jsを作成するための設定ファイルをルートディレクトリに作成します。名前は何でもいいですが、ここでは、`prod（product）`と付けたファイルとします。

```webpack.config.prod.js
const path = require('path');
const CleanPlugin = require('clean-webpack-plugin');

module.exports = {
	mode: 'production',
	entry: './src/app.ts', 
	output: {
		filename: 'bundle.js',
		path: path.resolve(__dirname, 'dist')
	},
	devtool: false,
	module: {
		rules: [
			{
				test: /\.ts$/,
				use: 'ts-loader',
				exclude: /node_modules/
			}
		]
	},
	resolve: {
		extensions: ['.ts', '.js']
	},
	plugins: [
		new CleanPlugin.CleanWebpackPlugin()
	]
};
```
webpack.config.jsから、

**変更した設定**
`mode: 'production'`
`devtool: false`

**削除した設定**
`devServer: {publicPath: '/dist/'}`
サーバー用（デバッグ用）の設定なので削除

**追加した設定**
`const CleanPlugin = require('clean-webpack-plugin');`
`plugins: [new CleanPlugin.CleanWebpackPlugin()]`
ビルド時に、出力フォルダ（dist）の中をクリーンして余計なファイルを残さないようにする

###package.json
以下のように指定する。

```package.json
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config webpack.config.prod.js",
    "start": "webpack serve"
}
```
※ コマンドで、`webpack`の後に何も指定しないと、`webpack.config.js`が参照されてビルドが行われます。

###実行
ターミナルで以下を実行して、`webpack.config.prod.js`に基づいたビルドをします。

```
npm run build
```

distフォルダに、ミニファイされたbundle.jsが作成されます。

<!-- ================================================ -->
#確認
<!-- ================================================ -->
最終的なプロジェクト構成は、以下のようになります。
![無題11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/e52178cf-b972-1cc7-13d1-b2662c106a39.png)

webpack.config.prod.jsを元にビルドされたbundle.jsは、ミニファイされます。
またファイルはバンドルされているので、コンパイルされたjsファイルにimport/export文はありません。つまりレガシーなブラウザでも動作します。

<!-- ================================================ -->
#Tips
<!-- ================================================ -->
###webpack ver.4 の設定

```package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config webpack.config.prod.js",
    "start": "webpack-dev-server"
  }
```
```webpack.config.js
output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: 'dist'
},
```
```webpack.config.prod.js
devtool: 'none',
```

###webpack.config.js - output.filename
このプロパティには、ビルドされてバンドルされた後のファイル名を指定します。たとえば、`bundle.js`です。
この名前には、以下のように変数を使用することができます。

`bundle.[contenthash].js`

このようにすると、ビルドされるたびにハッシュ値が含まれ、前のビルドファイルとは違う名前になります。
主に、ブラウザのキャッシュに残っているファイルを読み込ませたくない場合に使います。

<!-- ================================================ -->
#作成したプロジェクト
<!-- ================================================ -->

https://github.com/nemutas/typescript-project-webpack5

※ ご利用される場合は、READMEを読んでください。

<!-- ================================================ -->
#参考
<!-- ================================================ -->
* [【世界で7万人が受講】Understanding TypeScript - 2020年最新版](https://www.udemy.com/share/103e4OAEYdcVlbTX4D/)