Webpackを一歩一歩確実に理解してReact + TypeScript環境を作る
動機
毎回 npx create-react-app で React アプリを生成していて Webpack の勉強から逃げていたが、いい加減向き合いたいと思ったため。
昔 Webpack に入門しようとしたら難しいという先入観を抱いたので、できるだけシンプルに、一歩一歩づつ入門する。
最終的に、React + TypeScript の環境を作れるようにする。
リポジトリ
各ステップごとにコミットしているので逐次さかのぼってください。
前提
- 最低限の
yarnまたはnpmコマンドの使い方がわかること。 npx create-react-appで React プロジェクトを作成した事があること。
※VSCode 使用を前提としています。
Config なしで実行する
最低限の構成で Webpack が何をするのか理解する。
準備
# run at project directory yarn init -y # generate package.json yarn add webpack webpack-cli -D mkdir src touch src/index.js touch src/a.js touch .gitignore
# .gitignore node_modules/ dist/
// src/index.js import a from "./a"; a();
// src/a.js export default () => console.log("function called");
実行
2 つの JavaScript ファイルは import/export で読み込まれている。
これらを Webpack で Bundle(梱包、まとめる)を行うことで依存関係が解決された 1 つの JavaScript を生成できる。
# mode を指定しない場合は自動的に production モードで実行される
yarn webpack --mode production
Hash: 812c3c2bfa797a640163
Version: webpack 4.43.0
Time: 132ms
Built at: 2020-06-18 23:07:37
Asset Size Chunks Chunk Names
main.js 982 bytes 0 [emitted] main
Entrypoint main = main.js
[0] ./src/index.js + 1 modules 79 bytes {0} [built]
| ./src/index.js 26 bytes [built]
| ./src/a.js 53 bytes [built]
✨ Done in 1.16s.
// dist/main.js !(function (e) { // 省略 ... })([ function (e, t, n) { "use strict"; n.r(t); console.log("function called"); }, ]);
Webpack は Config ファイルなしでは dist/main.js にまとめられる。
https://webpack.js.org/configuration/
Out of the box, webpack won't require you to use a configuration file. However, it will assume the entry point of your project is src/index.js and will output the result in dist/main.js minified and optimized for production.
Config ファイルを設定する
Webpack の最低限の働きが理解できたので、次は Config ファイルを設定する。 まずは Bundle するファイル、Bundle で出力されるファイルを明示的に指定する。
準備
webpack.config.js を作成して設定する。設定用の JavaScript オブジェクトを書いて、module.exports すれば良い。
ついでに、npm script を設定して実行を単調なコマンドで実行できるようにする。
touch webpack.config.js
// webpack.config.js const path = require("path"); module.exports = { // 入力元 entry: "./src/index.js", // 出力先 output: { path: path.resolve(__dirname, "output"), filename: "bundle.js", }, };
// package.json - } + }, + "scripts": { + "build": "webpack --mode production" + }
実行
出力先を変更したので実行すると output/bundle.js に出力される。
yarn build
$ webpack --mode production
Hash: c4b26abe6d1df8044083
Version: webpack 4.43.0
Time: 173ms
Built at: 2020-06-18 23:46:27
Asset Size Chunks Chunk Names
bundle.js 982 bytes 0 [emitted] main
Entrypoint main = bundle.js
[0] ./src/index.js + 1 modules 79 bytes {0} [built]
| ./src/index.js 26 bytes [built]
| ./src/a.js 53 bytes [built]
✨ Done in 1.63s.
自動更新と開発用サーバーを実行する
現段階では変更があるたびに毎回 yarn build する必要がある。これでは面倒なので、ファイルが保存されるたびに自動的に変更を反映させる。
HTML ファイル内で JavaScript を実行したときの環境を作成する。
準備
開発環境用サーバー& build 用に webpack-dev-server と html-webpack-plugin を導入する。
導入する Plugin は webpack.config.js の plugins に追加する。
public ディレクトリーに index.html を作成する。JavaScript が HTML 内で動作していることを確認するするため、実行されると <div id='root'> 内の文字が変わるようにした。
html-webpack-plugin の設定にて template: に作成した HTML のパスを設定する。これによって開発用サーバーが立ち上がると指定した HTML に自動的に JavaScript を注入し、変更があるたびに更新する。
https://github.com/jantimon/html-webpack-plugin#options
yarn add webpack-dev-server html-webpack-plugin -D mkdir public touch public/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="root">JavaScript will be injected here.</div> </body> </html>
// a.js const endPoint = document.getElementById("root"); export default function a() { endPoint.innerHTML = "JavaScript is injected!"; }
// webpack.config.js const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/index.js", output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js", }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "public/index.html"), }), ], };
// package.json
"scripts": {
"build": "webpack --mode production",
+ "start": "webpack-dev-server"
},
実行
yarn start で開発用サーバーが http://localhost:8080/ で立ち上がる。
無事 JavaScript が実行されている場合、 JavaScript will be injected here. の箇所が JavaScript is injected! に置き換わっている。
a.js の文字列を書き換えて保存すると自動的に反映される。
また html-webpack-plugin を導入したので yarn build 時に index.html も出力してくれる。これを導入しない場合、 bundle.js しか出力されない。
https://webpack.js.org/plugins/html-webpack-plugin/#root
# launch dev sever at http://localhost:8080/` yarn start # build dist/bundle.js and dist/index.html yarn build
TypeScript を扱えるようにする
現段階では JavaScript しか扱えない。Webpack では様々なファイルを取り扱えるようにするため、各ファイルごとに loader が提供されている。
今回は TypeScript の loader、 ts-loader を導入する。
https://webpack.js.org/loaders/#root
準備
typescript と ts-loader を導入する。
TypeScript を実行するときは tsconfig.json も必要なので設定する。
今回は yarn tsc --init で生成したものからコメント行を削除した。
また、tsconfig.json にて "module": "ESNext" を指定すること。(標準では"CommonJS" が指定されている)
Webpack の Tree Shaking(未使用の JavaScript を削ってファイルサイズを軽減する機能)を働かせるために必要。
webpack.config.js には ts-loader が拡張子 .ts のファイルに対して働くようにする。
また、 resolve.extensions で拡張子を指定しておくと、その拡張子は import 時に省略できる。
TypeScript ファイルで実行するため JavaScript ファイルの拡張子を .js から .ts に変更する。
yarn add typescript ts-loader -D # generate tsconfig.json yarn tsc --init mv src/index.js src/index.ts mv src/a.js src/a.ts
// tsconfig.json { "compilerOptions": { "target": "es5", "module": "ESNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "moduleResolution": "node", "forceConsistentCasingInFileNames": true } }
// webpack.config.js const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/index.ts", output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js", }, module: { rules: [ { test: /\.ts$/, use: "ts-loader", }, ], }, resolve: { extensions: [".ts", ".ts", ".js"], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "public/index.html"), }), ], };
// a.ts const endPoint = document.getElementById("root"); export default function a() { // null check for endPoint if (endPoint) endPoint.innerHTML = "JavaScript is injected, from TypeScript"; }
実行
先程と同じ。TypeScript は JavaScript に変換され、Webpack によって 1 つの JavaScript ファイルにまとめられる。
# launch dev sever at http://localhost:8080/` yarn start # build dist/bundle.js and dist/index.html yarn build
React + TypeScript を扱えるようにする
最後に、TypeScript 環境で React(TSX)を扱えるようにする。が、実は前項にてほぼ準備は済んでいる。 TypeScript のコンパイラは TSX をサポートしているため、設定を少し加えて React のライブラリと型ファイルを読み込むめば済む。
https://www.typescriptlang.org/docs/handbook/jsx.html
準備
React のライブラリと型ファイルを導入する。
webpack.config.js には、entry 、loader 用の test、resolve.extensions を変更する。
tsconfig.json には "jsx": "react" を追記するだけ。
yarn add react react-dom @types/react @types/react-dom touch src/index.tsx touch src/App.tsx
// webpack.config.js const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/index.tsx", output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js", }, module: { rules: [ { test: /\.tsx?$/, use: "ts-loader", }, ], }, resolve: { extensions: [".tsx", ".ts", ".ts", ".js"], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "public/index.html"), }), ], };
// tsconfig.json { "compilerOptions": { "jsx": "react", "target": "es5", "module": "ESNext", "strict": true, "esModuleInterop": true, "moduleResolution": "node", "skipLibCheck": true, "forceConsistentCasingInFileNames": true } }
// App.tsx import React from "react"; export default function App() { return <div>Hello React with TypeScript</div>; }
//index.tsx import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; ReactDOM.render(<App />, document.getElementById("root"));
実行
前項と同じ。yarn start と yarn build
まとめ
というわけで0から順を追ってちょっとづつ追加した。 説明においては「これはおまじないです」というのをできるだけ避けた。
初見ではWebpackのConfigファイルが複雑に見えるかもしれないが、設定を1つ1つ、意味を確認しながら追加すれば難しくなかった。
あとはCSSを読み込むために style-loader を入れたりして作りたいものに合わせてカスタマイズする。
参考
https://mizchi.hatenablog.com/entry/2020/05/03/151022