おじんブログ

Webアプリに関する知見とか雑記です

Webpackを一歩一歩確実に理解してReact + TypeScript環境を作る

動機

毎回 npx create-react-app で React アプリを生成していて Webpack の勉強から逃げていたが、いい加減向き合いたいと思ったため。 昔 Webpack に入門しようとしたら難しいという先入観を抱いたので、できるだけシンプルに、一歩一歩づつ入門する。

最終的に、React + TypeScript の環境を作れるようにする。

リポジトリ

各ステップごとにコミットしているので逐次さかのぼってください。

github.com

前提

  • 最低限の 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-serverhtml-webpack-plugin を導入する。

導入する Plugin は webpack.config.jsplugins に追加する。

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

準備

typescriptts-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 用の testresolve.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 startyarn build

まとめ

というわけで0から順を追ってちょっとづつ追加した。 説明においては「これはおまじないです」というのをできるだけ避けた。

初見ではWebpackのConfigファイルが複雑に見えるかもしれないが、設定を1つ1つ、意味を確認しながら追加すれば難しくなかった。

あとはCSSを読み込むために style-loader を入れたりして作りたいものに合わせてカスタマイズする。

参考

https://mizchi.hatenablog.com/entry/2020/05/03/151022

https://ics.media/entry/16329/

https://webpack.js.org/guides/