はじめに
この記事は限界開発鯖 Advent Calendar 2020の7日目です。
何気にAdvent Calendar初挑戦です。
もともと「高速化のためのWebpack」というタイトルでWebpackのチューニングについて書こうと思っていたのですが、あまりにもニッチ過ぎたのでやめました。
1コマンドで環境構築ができるcreate-react-app
皆さんReactの開発はやったことありますか?Reactのチュートリアルでは環境構築としてcreate-react-app
が紹介されています。このcreate-react-app
は以下のコマンドを叩くだけですぐにReactの開発環境を作れる夢のようなアイテムです。
npx create-react-app my-app
このコマンドはたしかにReact初心者の方にはおすすめのコマンドですが、React(Webフロント)中級者になってくると、ビルド時のコンフィグをいじったり新しいツールチェインを導入したくなると思います。
create-react-app
はコンフィグをほとんど隠してしまうため、いじることのできる箇所が非常に少ないです。
一応npm run eject
でcreate-react-app
が隠しているコンフィグをすべてファイルにエクスポートすることができますが、今度は膨大なコンフィグファイルの量で逆にわからなくなります。
今回はそんなcreate-react-app
から卒業するために各種ツールチェインの設定を1から丁寧に解説していこうと思います。
今回の構築する開発環境
Webpack
複数のソースコードを1ファイルにまとめる(バンドルと言います)バンドラーと呼ばれるツールです。バンドルする際に各種ツールにソースコードを通したり画像のファイルパスを自動的に解決してくれたりと万能です。「モダンなWebフロント開発=バンドラーを使った開発」 とも言えるでしょう。
TypeScript
JavaScriptに安全性をもたらす上位互換な言語です。JavaScriptは他言語だとエラーになるようなことも当たり前のようにエラーにならなかったりするので型やnull安全性をもたらしてくれるTypeScriptはほぼ必須の存在です。
Babel
新しいJS/TSの文法を古い文法に変換してくれるツールです。Webフロントでは実行環境がブラウザに依存するため様々なブラウザ・バージョンをサポートしなければなりません。新しいJSの文法が使いたいのに古いブラウザへのサポートもしたい…!そんなときにはぜひともBabelを使いましょう。
SCSS
CSSに変数やfor文、if文を追加したCSSの上位互換な言語です。他にもスタイルをネストして書けたりするので快適にCSSを書くことができます。
PostCSS
CSSに対して色々な処理を行うことができるツールです。今回は自動的にベンダープレフィックスを付けてくれるautoprefixerとCSSのコードサイズを縮小するcssnanoを導入します。
※ESLintやPrettierも導入したいのですが、そこまで解説するととんでもない文字数になってしまいそうなので今回は見送らせていただきます。
環境構築
Node.js
はじめにNode.jsプロジェクトのセットアップを行います。今回はnpmではなくyarnを用いて解説します。npmでもpnpmでもお好きなパッケージマネージャをお使いください。
yarn init -y
package.json
が作成されます。デフォルトで問題ありませんが、必要に応じて変更してください。
TypeScript
yarn add -D typescript
# TypeScriptのヘルパーライブラリです。
yarn add tslib
次にtsconfig.json
を作成します。古いバージョンへのトランスパイルはbabelに任せるので、TypeScriptで出力するコードはesnextにします。
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"jsx": "react",
"sourceMap": true,
"removeComments": true,
"importHelpers": true,
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Webpackの初期設定
Webpackの設定ファイルはJavaScriptで記述することが多いですが、どうせなら型による安全性を設定ファイルにも導入しましょう。いくつかのTypeScriptのパッケージを導入してWebpackの設定を行います。
yarn add -D webpack webpack-cli @types/webpack ts-node @types/node
webpack.config.ts
を作成する前に、tsconfig.json
に少し手を加えます。ts-node
を用いて実行したときのみ反映される設定を追加します。
{
"compilerOptions": {
// ...
},
"ts-node": {
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"lib": ["es2020"]
}
}
}
webpack.config.ts
を作成します。loaderやpluginの設定は後回しにして、ベースとなる設定のみをここで行います。
src/index.tsx
をエントリーポイントにして、dist/
ディレクトリを出力とします(お好みで変更してください)。
Webpackにはmodeという概念があり、production modeとdevelopment modeの2種類がありますが、今回はNODE_ENV
という環境変数を読み込んで判断することにします(未設定の場合はdevelopment modeになります。)。同様に、ベースとなるパス関してもBASE_URL
という環境変数を読み込んで設定します(未設定の場合は/
になります。)
import { Configuration } from "webpack";
import * as path from "path";
const isProduction = process.env.NODE_ENV === "production";
const isDevelopment = !isProduction;
const baseURL = process.env.BASE_URL ?? "/";
const config: Configuration = {
target: "web",
mode: isProduction ? "production" : "development",
entry: {
index: path.join(__dirname, "src", "index.tsx"),
},
output: {
path: path.join(__dirname, "dist"),
publicPath: baseURL,
filename: "assets/scripts/[name].[contenthash:8].js",
chunkFilename: "assets/scripts/chunk.[contenthash:8].js",
},
};
export default config;
まだloaderを一つも設定していないのでsrc/index.tsx
というファイルを作って実行してもバンドルに失敗します。
Babel
Babelでは過去の文法への変換は行ってくれますが、最新の標準ライブラリの補完は行ってくれません。そこでcore-js
も導入します。
yarn add -D @babel/core @babel/preset-env
yarn add core-js
Babelの設定ファイルである.babelrc
を作成します。
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
Babelでは.browserslistrc
というファイルを読み込んでどのバージョンの文法へトランスパイルするかを判断する機能があります。基本的にデフォルト設定で構いませんが、「IE許すまじ」の意志を込めてIEをサポートから外しておきます。
defaults
not ie 11
具体的にどんなブラウザをサポートする設定になっているかは、以下のコマンドで確認することができます。必要に応じてサポートするブラウザやバージョンを変更しましょう。
npx browserslist
loaderの設定
とりあえずTypeScriptとBabelの設定が行えたのでWebpackに各種loaderを設定します。
TypeScriptとBabelのloaderを導入します。
yarn add -D ts-loader babel-loader
webpack.config.ts
にloaderの設定を追加します。
const config: Configuration = {
// ...
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
// ...
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
},
{
loader: "ts-loader",
},
],
},
],
},
// ...
};
Reactの導入
yarn add react react-dom
yarn add -D @types/react @types/react-dom
ここまでの動作確認も含めていくつかファイルを作って実際に動作させてみます。
import React, { StrictMode } from "react";
import { render } from "react-dom";
import App from "./App";
render(
<StrictMode>
<App />
</StrictMode>,
document.getElementById("root"),
);
import React, { FC } from "react";
const App: FC = () => (
<div>
<h1>Hello, world!</h1>
</div>
);
export default App;
{
// ...
"scripts": {
"build": "webpack"
}
// ...
}
yarn build
dist/
下にファイルが出力されたら成功です。
SCSS
SCSSのプロセッサーにはC++製のnode-sass
とDart製のdart-sass
の2種類がありますが、公式の推奨がdart-sass
のため、こちらを導入します。
yarn add -D sass fibers @types/sass @types/fibers style-loader css-loader sass-loader
webpack.config.ts
にCSS系のloader設定を追加します。CSS Moduleを使用したいため、[name].module.(css|sass|scss)
だった場合はCSS Moduleを有効にするようにします。
// ...
import sass from "sass";
import fibers from "fibers";
const config: Configuration = {
// ...
module: {
rules: [
// ...
{
test: /\.(?:c|sa|sc)ss$/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
options: {
sourceMap: isDevelopment,
importLoaders: 1,
modules: {
auto: true,
localIdentName: isProduction ? "[hash:base64:8]" : "[path][name]__[local]",
exportLocalsConvention: "dashesOnly",
},
},
},
{
loader: "sass-loader",
options: {
sourceMap: isDevelopment,
implementation: sass,
sassOptions: {
fiber: fibers,
},
},
},
],
},
// ...
],
},
// ...
};
このままではTypeScriptのコンパイル時に、「CSS/Sass/SCSSを読み込むことができない」というエラーが発生してしまうのでsrc/global.d.ts
を作成して読み込めるようにします。
declare module "*.module.css" {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module "*.module.scss" {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module "*.module.sass" {
const classes: { readonly [key: string]: string };
export default classes;
}
動作確認のためにSCSSのファイルを作成します。
.title {
font-size: 3rem;
color: lightblue;
}
src/App.tsx
を変更してSCSSを適用させます。
import React, { FC } from "react";
import styles from "./App.module.scss";
const App: FC = () => (
<div>
<h1 className={styles.title}>Hello, world!</h1>
</div>
);
export default App;
yarn build
を実行してエラーが発生しなければ成功です。
PostCSS
今回はPostCSSのプラグインとしてautoprefixerとcssnanoを導入します。
yarn add -D postcss autoprefixer cssnano postcss-loader
.postcssrc
を作成します。
{
"plugins": ["autoprefixer", "cssnano"]
}
webpack.config.ts
にPostCSSのloaderを追加します。
const config: Configuration = {
// ...
module: {
rules: [
// ...
{
test: /\.(?:c|sa|sc)ss$/,
use: [
// ...
{
loader: "css-loader",
options: {
// ...
importLoaders: 2,
// ...
},
},
{
loader: "postcss-loader",
options: {
sourceMap: isDevelopment,
},
},
{
loader: "sass-loader",
// ...
},
],
},
// ...
],
},
// ...
};
プラグインの導入
Webpackのプラグインを導入します。今回は、バンドルしたJavaScriptをHTMLタグとして注入してくれるhtml-webpack-plugin
とWebpackの出力先にファイルをそのままコピーするcopy-webpack-plugin
を導入します。
yarn add -D html-webpack-plugin copy-webpack-plugin @types/html-webpack-plugin @types/copy-webpack-plugin
webpack.config.ts
にプラグインの設定を追加します。
const config: Configuration = {
// ...
plugins: [
new HtmlWebpackPlugin({
inject: "head",
minify: isProduction,
template: path.join(__dirname, "src", "index.html"),
scriptLoading: "defer",
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, "public"),
},
],
}),
],
// ...
};
html-webpack-plugin
で用いるHTMLを作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Hello, world!</title>
</head>
<body>
<noscript>
<strong>This site uses JavaScript. Please enable it.</strong>
</noscript>
<div id="root"></div>
</body>
</html>
copy-webpack-plugin
の動作確認のためにpublic/
ディレクトリを作成し、robots.txt
を作成します。
user-agent: *
yarn build
を実行して動作確認をします。
開発用のローカルサーバーのセットアップ
webpack-dev-server
を導入します。ホットリロード付きなので開発中の動作確認が楽になります。
yarn add -D webpack-dev-server @types/webpack-dev-server
webpack.config.ts
に設定を追加します。
const config: Configuration = {
// ...
devServer: {
historyApiFallback: true,
},
// ...
};
package.json
にscriptを新しく追加します。
{
// ...
"scripts": {
// ...
"dev": "webpack serve"
}
// ...
}
yarn dev
を実行してWebページがきちんと表示されれば成功です。
ソースマップの設定
ソースマップがあると開発時のデバッグがやりやすくなります。
const config: Configuration = {
// ...
devtool: isDevelopment ? "eval-source-map" : "nosources-source-map",
// ...
};
これで終了です。お疲れさまでした。
おわり
最後に何度も書き換わったpackage.json
とwebpack.config.ts
の全体像を示しておきます。
{
"scripts": {
"build": "webpack",
"dev": "webpack serve"
},
"dependencies": {
"core-js": "^3.8.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"tslib": "^2.0.3"
},
"devDependencies": {
"@babel/core": "^7.12.9",
"@babel/preset-env": "^7.12.7",
"@types/copy-webpack-plugin": "^6.3.0",
"@types/fibers": "^3.1.0",
"@types/html-webpack-plugin": "^3.2.4",
"@types/node": "^14.14.10",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/sass": "^1.16.0",
"@types/webpack": "^4.41.25",
"@types/webpack-dev-server": "^3.11.1",
"autoprefixer": "^10.0.4",
"babel-loader": "^8.2.2",
"copy-webpack-plugin": "^6.3.2",
"css-loader": "^5.0.1",
"cssnano": "^4.1.10",
"fibers": "^5.0.0",
"html-webpack-plugin": "^4.5.0",
"postcss": "^8.1.14",
"postcss-loader": "^4.1.0",
"sass": "^1.30.0",
"sass-loader": "^10.1.0",
"style-loader": "^2.0.0",
"ts-loader": "^8.0.11",
"ts-node": "^9.1.0",
"typescript": "^4.1.2",
"webpack": "^5.10.0",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.11.0"
}
}
import { Configuration } from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
import CopyWebpackPlugin from "copy-webpack-plugin";
import sass from "sass";
import fibers from "fibers";
import * as path from "path";
const isProduction = process.env.NODE_ENV === "production";
const isDevelopment = !isProduction;
const baseURL = process.env.BASE_URL ?? "/";
const config: Configuration = {
target: "web",
mode: isProduction ? "production" : "development",
entry: {
index: path.join(__dirname, "src", "index.tsx"),
},
output: {
path: path.join(__dirname, "dist"),
publicPath: baseURL,
filename: "assets/scripts/[name].[contenthash:8].js",
chunkFilename: "assets/scripts/chunk.[contenthash:8].js",
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
},
{
loader: "ts-loader",
},
],
},
{
test: /\.(?:c|sa|sc)ss$/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
options: {
sourceMap: isDevelopment,
importLoaders: 2,
modules: {
auto: true,
localIdentName: isProduction ? "[hash:base64:8]" : "[path][name]__[local]",
exportLocalsConvention: "dashesOnly",
},
},
},
{
loader: "postcss-loader",
options: {
sourceMap: isDevelopment,
},
},
{
loader: "sass-loader",
options: {
sourceMap: isDevelopment,
implementation: sass,
sassOptions: {
fiber: fibers,
},
},
},
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
inject: "head",
minify: isProduction,
template: path.join(__dirname, "src", "index.html"),
scriptLoading: "defer",
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, "public"),
},
],
}),
],
devServer: {
historyApiFallback: true,
},
devtool: isDevelopment ? "eval-source-map" : "nosources-source-map",
};
export default config;
最後に
明日は @Siketyan による謎のコミュニティ「限界開発鯖」を支える技術です。私の所属している限界開発鯖の雰囲気がわかる(?)と思うのでぜひご覧ください!