23
13

More than 3 years have passed since last update.

【脱create-react-app】React開発環境を1から作ってみる

Last updated at Posted at 2020-12-06

はじめに

この記事は限界開発鯖 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 ejectcreate-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にします。

tsconfig.json
{
  "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を用いて実行したときのみ反映される設定を追加します。

tsconfig.json
{
  "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という環境変数を読み込んで設定します(未設定の場合は/になります。)

webpack.config.ts
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を作成します。

.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

Babelでは.browserslistrcというファイルを読み込んでどのバージョンの文法へトランスパイルするかを判断する機能があります。基本的にデフォルト設定で構いませんが、「IE許すまじ」の意志を込めてIEをサポートから外しておきます。

.browserslistrc
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の設定を追加します。

webpack.config.ts
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

ここまでの動作確認も含めていくつかファイルを作って実際に動作させてみます。

src/index.tsx
import React, { StrictMode } from "react";
import { render } from "react-dom";

import App from "./App";

render(
  <StrictMode>
    <App />
  </StrictMode>,
  document.getElementById("root"),
);
src/App.tsx
import React, { FC } from "react";

const App: FC = () => (
  <div>
    <h1>Hello, world!</h1>
  </div>
);

export default App;
package.json
{
  // ...
  "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を有効にするようにします。

webpack.config.ts
// ...
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を作成して読み込めるようにします。

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のファイルを作成します。

src/App.module.scss
.title {
  font-size: 3rem;
  color: lightblue;
}

src/App.tsxを変更してSCSSを適用させます。

src/App.tsx
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を作成します。

.postcssrc
{
  "plugins": ["autoprefixer", "cssnano"]
}

webpack.config.tsにPostCSSのloaderを追加します。

webpack.config.ts
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にプラグインの設定を追加します。

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を作成します。

src/index.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を作成します。

public/robots.txt
user-agent: *

yarn buildを実行して動作確認をします。

開発用のローカルサーバーのセットアップ

webpack-dev-serverを導入します。ホットリロード付きなので開発中の動作確認が楽になります。

yarn add -D webpack-dev-server @types/webpack-dev-server

webpack.config.tsに設定を追加します。

webpack.config.ts
const config: Configuration = {
  // ...
  devServer: {
    historyApiFallback: true,
  },
  // ...
};

package.jsonにscriptを新しく追加します。

package.json
{
  // ...
  "scripts": {
    // ...
    "dev": "webpack serve"
  }
  // ...
}

yarn devを実行してWebページがきちんと表示されれば成功です。

ソースマップの設定

ソースマップがあると開発時のデバッグがやりやすくなります。

webpack.config.ts
const config: Configuration = {
  // ...
  devtool: isDevelopment ? "eval-source-map" : "nosources-source-map",
  // ...
};

これで終了です。お疲れさまでした。

おわり

最後に何度も書き換わったpackage.jsonwebpack.config.tsの全体像を示しておきます。

package.json
{
  "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"
  }
}
webpack.config.json
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 による謎のコミュニティ「限界開発鯖」を支える技術です。私の所属している限界開発鯖の雰囲気がわかる(?)と思うのでぜひご覧ください!

23
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
13