TS+Reactでcreate-react-appなしでHerokuにホストするまで

#～注意～
本記事はもともと2020年秋ごろにフロントエンドの勉強をしていた際に備忘録として執筆されたものであり、Webpackその他ツールの記述方法は当時の仕様に準じたものです。
特にwebpackに関しては、執筆時点から現在の間に4.x→5.xのメジャーアップデートがあったため、ここに書いていることをそのまま実行しても動作しない可能性があります。あらかじめご了承ください。

#基礎戦略
- 修正ビルドのたびにキャッシュを拾わないよう、バンドルファイル名にコンテントハッシュを入れる
- そのためHtml Webpack Pluginを使い、動的にバンドルファイルを追跡する``index.html``が``build``フォルダ内に生成されるようにする
- 開発環境の場合は、コンテントベースを``build``フォルダにしたWebpack Dev Serverを立ち上げる
- 本番環境の場合は、expressでサーバを立て、``build``をパブリックフォルダとして指定し、``res.send()``で``index.html``をブラウザへと送る。

という感じでやっていきましょう。

#全体のツリー図
おおざっぱにこんな感じで行きましょう

```
root/
　├ (将来buildフォルダができる)/
　├ src/Reactファイルの皆さん
　├ template.html
　├ server.js
　├ package.json
　├ webpack.common.js
　├ webpack.dev.js
　└ webpack.prod.js
```

ちなみに……。
クライアントフォルダ（バンドルファイルを出力するフォルダ）と
サーバーフォルダ（server.jsを立ち上げてそれを送信するフォルダ）は
別にするのが良いという意見もあります。

しかし、その場合両方でpackage.jsonに諸々インストールしなければならず、
大変めんどいため、本稿では完全無視で行きます。

また、実際のツリーには当然``node_modules``とか``.gitignore``とかが含まれますが、
本稿では触れないために省略しております。

#tsconfig.json
TypeScriptをコンパイルする場合、tsconfig.jsonを作っておく必要があります。
内容はこんな感じです。

```js:tsconfig.json
{
  "compilerOptions": {
    //ソースマップは作っておく
    "sourceMap": true,
    // TSはECMAScript 5に変換
    "target": "es5",
    // TSのモジュールはES Modulesとして出力
    "module": "es2015",
    // JSXの書式を有効に設定
    "jsx": "react",
    "moduleResolution": "node",
    "lib": [
      "es2020",
      "dom"
    ],
    "allowSyntheticDefaultImports": true
  }
}
```
``"allowSyntheticDefaultImports": true``を設定しないと
``import React from "react"``のような文が書けなくなり、
``import * as React from "react"``と書かなければならなくなるので、要注意です。

#Package.json
戦略実行のためのスクリプトはこんな感じになります。

```json:package.json
  "scripts": {
    "start:dev": "webpack-dev-server --config webpack.dev.js --open",
    "build": "webpack --config webpack.prod.js",
    "start:prod": "node server.js"
  }
```
Webpack Dev Serverを立てる時は``webpack.dev.js``、
WebPackでビルドする時は、``webpack.prod.js``の設定を参照するよう指定します。
本番環境のスタートはnodeで``server.js``を実行するだけです。

あとHerokuに上げるには``engines``の指定も必要になります。
今のうちにやっておきましょう。

```json:package.json
  "engines": {
    "node": "13.x",
    "npm": "6.x"
  }
```
#webpack.common.js
本番用(``webpack.dev.js``）と開発用(``webpack.prod.js``)でファイルを分けますが、
重複部分は``webpack.common.js``に書き出して、両ファイルで``merge``する形にします。

```js:webpack.common.js
const HtmlWebpackPlugin = require("html-webpack-plugin")
const { CleanWebpackPlugin } = require("clean-webpack-plugin")

module.exports = {
  entry: "./src/index.tsx",
  plugins: [
    new CleanWebpackPlugin(),
    //無限増殖していくbundle.[contentHash].jsをお掃除
    new HtmlWebpackPlugin({
      template: "./template.html"
    })],
    //template.htmlを雛形に、buildフォルダ内にindex.htmlを生成
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"]
  }
}
```
この際、resolve内で拡張子ts、tsxを指定しないと
TypeScriptがコンパイルされないので注意しておきましょう。

#webpack.dev.js
```js:webpack.dev.js
const path = require("path")
const common = require("./webpack.common");
const { merge } = require("webpack-merge");

module.exports = merge(common, {
  mode: "development",
  output: {
    //  出力ファイルのディレクトリ名
    path: path.resolve(__dirname, "build"),
    // 出力ファイル名
    filename: "bundle.js"
  },
  devServer: {
    contentBase: path.resolve(__dirname, "build"),
    port: 5000,
    historyApiFallback: true,
  },
  module: {
    rules: [
      {
        test: /\.html$/,
        // 拡張子 .html の場合
        use: ["html-loader"]
      },
      {
        // 拡張子 .ts もしくは .tsx の場合
        test: /\.tsx?$/,
        // TypeScript をコンパイルする
        use: "ts-loader"
      },
      {
        test: /\.(svg|png|jpg|svg)$/,
        use: {
          loader: "file-loader",
          options: {
            name: "[name].[ext]",
            outputPath: "imgs"
          }
        },
      }
    ]
  },
})
```
ファイル別にローダーを実行しながら``/build/bundle.js``にバンドルする感じです。
Webpack Dev Serverのコンテントベースも``/build``ファイルに指定しましょう。

あとは``historyApiFallback: true``を忘れると、
開発環境でルーターが機能しないのがちょっとしたハマりどころです。

#webpack.prod.js
```js:webpack.prod.js
const path = require("path")
const common = require("./webpack.common");
const { merge } = require("webpack-merge");

module.exports = merge(common, {
  mode: "production",
  output: {
    //  出力ファイルのディレクトリ名
    path: path.resolve(__dirname, "build"),
    // 出力ファイル名
    filename: "bundle.[contentHash].js"
  },
  module: {
    rules: [
      {
        test: /\.html$/,
        // 拡張子 .html の場合
        use: ["html-loader"]
      },
      {
        // 拡張子 .ts もしくは .tsx の場合
        test: /\.tsx?$/,
        // TypeScript をコンパイルする
        use: "ts-loader"
      },
      {
        test: /\.(svg|png|jpg|svg)$/,
        use: {
          loader: "file-loader",
          options: {
            name: "[name].[contentHash].[ext]",
            outputPath: "imgs"
          }
        },
      }
    ]
  }
})
```
``/build``フォルダ内のhtmlファイルと画像ファイルにコンテントハッシュが付いた以外
基本``webpack.dev.js``と変わらないので、難しい部分はありません。
むしろDev Serverの設定がないぶん楽ちんになっています。

#server.js

ビルドした後、``build``フォルダ内のindex.htmlを
送信するためのサーバを立ち上げます。

```js:server.js
const express = require('express');
const app = express();
const path = require("path")

app.use(express.static('build'));
//ここでbuildフォルダをパブリックフォルダに指定する
app.get('*', (req, res) => {
//buildフォルダ内のindex.htmlをリクエストに対して返信
  res.sendFile(path.join(__dirname, "build", "index.html"))
});
const port = process.env.PORT || 5000

app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});
```
Herokuで実行するときはPORTは環境変数から拾ってくるので、
「環境変数があれば環境変数、なければ5000」という感じで設定しておくのが吉です。

#まとめ
Mini Css Extract Pluginとか使いたかったけど、
それはまた後々と言う感じで……。