#~注意~
本記事はもともと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を作っておく必要があります。
内容はこんな感じです。
{
"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
戦略実行のためのスクリプトはこんな感じになります。
"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
の指定も必要になります。
今のうちにやっておきましょう。
"engines": {
"node": "13.x",
"npm": "6.x"
}
#webpack.common.js
本番用(webpack.dev.js
)と開発用(webpack.prod.js
)でファイルを分けますが、
重複部分はwebpack.common.js
に書き出して、両ファイルでmerge
する形にします。
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
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
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を
送信するためのサーバを立ち上げます。
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とか使いたかったけど、
それはまた後々と言う感じで……。