generated at
フロントエンドワークショップ フロントエンドツールチェインについて

JavaScript関連のツールチェイン
Reactに慣れてもらったあとはJavaScriptを扱う際のツールチェインについての外観を掴んでもらうパートです

create-react-app はあらゆるツールをラップしている
例えば、 npm run start npm run build などは https://github.com/facebook/create-react-app/tree/main/packages/react-scripts に置いてあるスクリプトを叩いている
Try npm run eject で隠匿されている各種設定ファイルやラッパーなどを全部露出させられるので眺めてみよう

そもそもなんですが
create-react-app create-next-app などで始めるとラップして隠匿されている
基本的にはこれらは良い感じに更新とかもされていてベストプラクティスが詰まっているので、積極的に剥がす必要はない(はず・・・)
剥がしたくなったらまずはその剥がしたさは本当に必要なものなのか考えてみよう
と言っても、まぁ何も分からないブラックボックスになっていても辛いと思うので、フロントエンド周りの特にJSファイルのデリバリーに関わるツールについて紐解いていきたいと思います

TSで書かれたReactプロジェクトのコードをユーザーに届けるまでに必要なことを考えてみる
*.tsx? をJSにする
JSXで書かれた部分をJSで解釈可能な形に置き換える
module化されている複数のファイルや参照されているnpmモジュールなどを1つ(または複数)のファイルにする

これらをそれぞれどういうツールで実践するか
*.tsx? をJSにする→transpiler(ts)
JSXで書かれた部分をJSで解釈可能な形に置き換える→transpiler(jsx)
場合によってはユーザーのブラウザが古い場合の互換性のための対応が欲しい→transpiler(es)
module化されている複数のファイルや参照されているnpmモジュールなどを1つ(または複数)のファイルにする→bundler

transpiler(ts)としての機能を備えるツールの例
tsc
babel
esbuild

transpiler(jsx)としての機能を備えるツールの例
tsc
babel
esbuild

transpiler(es)としての機能を備えるツールの例
tsc
esbuild
babel

コラム: transpilerとpolyfill
transpilerは記法を変換する
polyfillは新たなメソッドを環境に生やす
この中でpolyfillの注入をやってくれるのはbabelのみ

bundlerとしての機能を備えるツールの例
webpack
esbuild

ツールを組み合わせて実行する
webpack
bundlerとしてwebpackを使うことになる
transpilerはloaderの形で利用する
npm script

それぞれからツールを選んで組み合わせるとユーザーに提供できるJSを得ることが出来る
1つのツールが複数の役割を担うことがある
その結果、色々なツールが乱立しているように見える
どの役割をどのツールに担わせて、どういう組み合わせの戦略を立てるか
どこが交換可能か
どこは兼務させられるか

組み合わせの例
webpackを用いて、babelを利用する
全てをesbuildで行い、npm scriptsで実行する
tscでjsにしたものをesbuildでbundleし、npm scriptsで実行する
全てをesbuildでも出来るが、tscはtypecheckerとしての役割を挟むことでbuild時に検証できる
逆にtscの型チェックをしないとbuild時は型チェックを無視できる

ツールの選択をどのように行うか
polyfillを入れるならbabelをどこかで噛ませたい
速度が欲しいのでwebpackよりesbuildを使いたい

ここまでは所謂ビルド時の出来事、その他にもCIなどでやりたいこともある
それぞれの代表例
typecheck
tsc
test
jest
linter
eslint
formatter
prettier

webpack + babelでビルドをやってみよう
npm run eject しても良いけど、なんか色々くっついてくるので、まずは最小構成の設定を手で書いてみる

作戦紹介
webpackのloaderとして babel-loader を使ってTypeScriptとJSXの変換をやる
webpackの設定とbabelの設定をやります
importしているcssを別のファイルとして書き出せるようにする
cra同様にwebpack-dev-serverを立ち上げて配信できるようにする

必要なものを入れる
webpack関連グッズ
npm install -D webpack webpack-cli webpack-dev-server css-loader style-loader
babelとpreset達
npm install -D babel-loader @babel/core @babel/preset-typescript @babel/preset-react

webpackの設定を書く①
まずはファイルを用意する
webpack.config.js
entrypointと出力先を書く
webpack.config.js
module.exports = { entry: "./src/index", output: { path: __dirname + "/public/dist", publicPath: "/dist", filename: "bundle.js" }, }
拡張子 .tsx などを扱えるようにする
webpack.config.js.diff
path: __dirname + "/public/dist", publicPath: "/dist", filename: "bundle.js" }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, }
babel-loaderを設定する
webpack.config.js.diff
resolve: { extensions: ['.tsx', '.ts', '.js'], }, + module: { + rules: [ + { + test: /\.m?[tj]sx?$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + } + }, + ] + } }
babelの設定を書く
babel.config.js に書いても良いが、簡単のために webpack.config.js にそのまま書いてしまう
webpack.config.js.diff
exclude: /node_modules/, use: { loader: 'babel-loader', + options: { + presets: [ + ['@babel/preset-react', { + "runtime": "automatic" + }], + '@babel/preset-typescript' + ] + } } }, ]

webpackの設定を書く②
.css の扱いについて
import './index.css'; のようなCSSのimportはそのままJSとしては解釈できないので、webpackのloaderを用いてここも変換を噛ませる
css-loader
JSの中でimportされているCSSをJSとして扱えるように変換してくれる
style-loader
JSの中にあるCSSをDOMの中に挿入してくれる(デフォルトは <style> だが、 <link> を使うことも出来る)
この2つを使って、JSの中でimportされるCSSをパースして、 <style> として埋め込めるようにする

2つのloaderの設定を追加する
webpack.config.js
} } }, + { + test: /\.css$/i, + use: ["style-loader", "css-loader"], + }, ] } }

webpackの設定を書く③
webpack-dev-server を使う
npm exec webpack-dev-server で立ち上げる

ちょっと手を入れて動くようにする
public.html %PUBLIC_URL% がパース不能でコケているはず
%PUBLIC_URL% は無くても、今は一旦大丈夫なので全部消す
Try react-scriptsでは interpolate-html-plugin を使ってこの置き換えをやっている
成果物を読み込む <script> もプラグインで実現されているので、これも手で書いておく

minifyについて
minify: JSのサイズが小さくなるように最適化する
「難読化」とは区別されることが多い
最適化の例
変数名や関数名の省略
コメントの削除
ライセンスコメントは残す設定に出来る
モジュールの不要なコードを削るTreeShaking
不要な分岐の削除
js
if (process.env.NODE_ENV !== 'production') { hoge() }
がwebpackのEnvironmentPluginによって NODE_ENV=production の環境で埋め込まれると
js
if ('production' !== 'production') { hoge() }
となり、この結果は自明なので、全体が削除される
その他細やかなテクニックによる圧縮の例
true !0 に置換
, による結合

本番環境向けのbuildのときだけminifyする
webpackのmodeについて
webpackは mode=production のときは、minifyを自動で有効にしてくれる
手元の開発時は実行時に --mode development を渡すようにして区別する

minifyとsourcemap
minifyや各種transpilerによって変換されたコードを元のコードにマッピングできるようにするのがsourcemap
トレースを表示する際などに利用されることで元のコードの行番号や位置を使って表示させることが出来る
webpackの devtool オプションや各種ツールのオプションを使ってsourcemapは各種変換のときに受け継がれるようにする
手元で開発する際は成果物にくっつけたりしておいて、本番向けにビルドするときは hidden-source-map などにしておいて、エラーレポートなどにだけ利用するようにする

webpackの設定を変更して振る舞いを変える
前述の通りそれぞれの役割は可換であることを確認する
babelにブラウザ互換性のための設定を入れる
preset-env を追加してサポートブラウザに合わせて出力をtranspileする
babelをtscに差し替えてみる
babel-loader を剥がして、 ts-loader に置き換える
build時に型チェックが走るようになる
オプションで transpileOnly true にすると型チェックは剥がせる

babelをtscに差し替える
webpack.config.js.diff
use: { - loader: 'babel-loader', - options: { - presets: [ - ['@babel/preset-react', { - "runtime": "automatic" - }], - '@babel/preset-typescript' - ] - } + loader: 'ts-loader', }
tsconfig.json.diff
"isolatedModules": true, - "noEmit": true, "jsx": "react-jsx"
noEmit を有効にすると型チェックだけが出来る。これを外さないと成果物を生成出来ない
逆にCIで実行する際は --noEmit などで実行するようにする

Try webpackを剥がしてesbuildにする
esbuildは現時点ではCSSのimportをサポートしていないので、 import をやめたりして分離させる必要があることに注意
npm install -D esbuild
esbuild --bundle src/index.tsx

まとめ
JSのファイルをデリバリーするまでの各ステップと、各ツールがどういった役割を担うかについて紹介しました
交換可能なツールはどこか、利用したい機能は何かを検討してツールの洗濯をやっていくことになる
といっても、craに乗っかれるままいけるなら(ある程度までは)それが良いと思います