🔪

マニュアルでReactのフロント環境構築(前編)

2021/06/25に公開
2

この記事について

Reactを使ったフロントエンド開発環境構築について説明します。
タイトルの「マニュアルで」とは、 create-react-appを使わなで自力でセットアップを行うという意味です。
また、この記事は個人開発の備忘録を兼ねたものなので実用的な視点から見ると間違いなどあるかもしれません。何か気づいたことなどあれば教えてもらえると嬉しいです。というか絶対どっかしら間違ってます、ごめんなさいおこらないで。

作成するプロジェクトについて

  • ライブラリにReactを使用する(フレームワークは使わない)
  • TypeScript(ES6)で記述する
  • スタイルにはstyled-componentsを使用する
  • webpackでビルドする
  • トランスパイラにTypeScript, Babelを使用する
  • リンター、フォーマッターにはESLint, Prettierを使用する
  • GitHubでコード管理をする

Git Hubに作業後のコードを公開しているので、そちらを参照して記事を読み進めてください。とりあえず動かすだけならREADMEを読んでください。

環境

  • OS: Windows10
  • ランタイム: Node.js 12系
  • エディタ: Visual Studio Code
  • パッケージマネージャ: yarn
    nodeやyarnのインストールは割愛します。

記事の構成

  • 前編(この記事): ディレクトリの準備/ビルド、トランスパイルの設定
  • 後編: リント関連の設定、GitHub関連の設定

長いので前編/後編に分かれています。前編はプロジェクトを動かすのに必要な設定のパート、後編は開発を効率的に行うための設定のパート、というふうにとなっています。

1.ディレクトリの準備

プロジェクト用のディレクトリと、package.jsonを用意する。

# react-ts-prjはプロジェクト名
$ mkdir react-ts-prj
$ cd react-ts-prj

# package.jsonが作成される(-yでオプションにすべてyesで答えスキップする)
$ yarn init -y

$ mkdir dist, src
# dist: ビルド時にバンドルしたファイルを格納する
# src: ソースコードを格納する

(フォークせずに手動で手順を追っている人は、gitリポジトリからsrc以下のファイルも持ってきておく)
あとは、VSCodeでワークスペースの設定をしておく。
この設定をしておくと、拡張機能の設定などを追記できます。今回だと、リンター、フォーマッターのところで使用します。

src以下の構成
component以下には名前の通りコンポーネントの定義ファイルを配置します。
pages直下には、ページごとにひとつのフォルダを用意し、エントリーポイントとなるtsxファイルを配置します。
また、ルートディレクトリのpublic/htmlには、コンパイルされたjsを読み込むテンプレートファイルが置かれています。

2.各種パッケージのインストール

上記で案内したリポジトリのpackage.jsonから、dependenciesとdevDependenciesの内容をコピペして下さい。
そうしたら、ルートディレクトリでyarn installしてください。これで必要なパッケージがすべてインストールされました。

dependenciesのパッケージは後の解説には出てこないのでここで書いておきます。
react: ライブラリとしてのReact本体。
Reactコンポーネントを定義する機能のみ。renderなどDOM操作を行うためにreact-domとセットで使用する
react-dom: DOM操作固有のメソッドが入っている
@types/react: reactの型定義
@types/react-dom: react-domの型定義

3.ビルドの設定(webpack)

関連モジュール
webpack5系: webpack本体
webpack-cli: webpack用CLI
webpack-dev-server: 開発用に動かすローカルサーバー
webpack-merge: 本番/開発むけに分割された設定ファイルを統合して読み込む
html-webpack-plugin: webpackでhtmlを出力する際に用いる
ts-loader: webpackからTypeScriptを読み込む
babel-loader: webpackからBabelを読み込む

使用するファイル
webpack.common.js: webpack共通設定
webpack.prod.js: webpack本番設定
webpack.dev.js: webpack開発設定

ビルドの方針

  • ビルドの処理はwebpackにまとめる
  • MPAに対応する
  • トランスパイラの設定をロードする
  • 本番ビルド
    • テンプレートhtmlとjs形式にバンドルしたファイルをdist配下に出力する
  • 開発ビルド
    • 開発中はNode.js上にフロント専用のサーバーを立てて実行する
    • 表示するhtmlページごとにローカルサーバー上で有効なurlを与え、接続できるようにする

共通/本番/開発で設定ファイルを分ける

こちらを参考にしました。
まずは上記のファイルを用意します。
本番ビルド時はcommonとprodを
開発時はcommonとdevを統合して読み込みたいので、

  • webpack-mergeをそれぞれのファイルでインポート
  • webpack.prod.js, webpack.dev.jsでそれぞれ、APIを使用してcommonの内容とマージ

すれば完了です。
どうやって実行時に意図したモードで読み込むかですが、実行時のスクリプト内でファイル名を指定します。これは後で解説します。
続いてそれぞれのファイルの内容を抜粋して解説します。

webpack共通設定

1ページつにつき、一つのhtmlファイルと、そこに乗せるjsファイルを出力するようにします。

エントリーポイントを設定する
エントリーポイントの設定と、ビルド時の出力設定をします。

webpack.common.js
  entry: {
    index: path.resolve(__dirname, './src/pages/index/main.tsx'),
    secondpage: path.resolve(__dirname, './src/pages/secondpage/main.tsx'),
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'js/[name].js',
  },

entry:以下にはページごとのエントリーポイントを記します。
行頭の'index', 'secondpage'がエントリーポイントの名前となります。

output:には、ビルド時にバンドルされるファイルの出力先とファイル名を記します。
ページごとに記載のあるentryに対してoutputには共通の設定だけが記載されています。
[name]にはentryで設定した名前が適用され、ルートディレクトリのdist/js配下に○○.jsといったかたちで出力されます。

htmlの出力設定
ページごとに一つのhtmlを出力し、そこに上記entryで設定したエントリーポイントを紐づけます。html-webpack-pluginを利用して、1ページごとに一つのインスタンスを作成し設定を記載します。

webpack.common.js
  plugins: [
    new HtmlWebpackPlugin({
      chunks: ['index'],
      template: path.resolve(__dirname, './src/public/template.html'),
      filename: 'html/index.html',
      title: 'index',
    }),
    new HtmlWebpackPlugin({
      chunks: ['secondpage'],
      template: path.resolve(__dirname, './src/public/template.html'),
      filename: 'html/secondpage.html',
      title: 'secondpage',
    }),
  ],
  • mode: 'production'が指定されています。webpackはこの実行環境の指定が必須です。
  • plugins: html-webpack-pluginを用いてテンプレートファイルの出力設定がされています。
    • chunks: webpack.common.jsのentry:で指定した名前を書きます。これによりhtmlとひとまとまりに扱われ、出力されたjsファイルを読み込む記述が追加されます。
    • template: 使用するテンプレートファイルを指定します。
    • filename: 出力するファイル名を指定します。
    • title: タブ名を指定可能です。html側でtitleタグに<title><%= htmlWebpackPlugin.options.title %></title>と書いておく必要があります。templateで指定したhtmlファイルを確認してみてください。

トランスパイラの設定を読み込む
次に、ビルドする際に各トランスパイラの設定を読み込むようにします。
js以外の言語をjsにトランスパイルするためです。具体的にはBabelとTypeScriptの2つです。
TypeScriptはJavaScriptのスーパーセットですが、トランスパイラーの機能も持っています。
webpackから、それぞれの設定ファイルを読み込むよう記載します。

webpack.common.js
module: {
        rules: [
            {
                test: [/\.tsx?$/, /\.ts?$/],
                use: [
                    { loader: 'babel-loader' },
                    { loader: 'ts-loader' }
                    ]
            }
        ]
    },
resolve: {
        extensions: [".tsx", ".ts"]
},

test:には、トランスパイルの対象となるファイル拡張子を記載。
use:には、使用するトランスパイラーを記載。
resolve.extensions:ここに記載した拡張子のファイルは、importする際に拡張子を省略することができる。同名で異なる拡張子のファイルがある場合は、ここに記載されている順に拾いに行く。具体的には、src以下の適当なファイルを開いてみると、import先のファイルは拡張子が省略されているのがわかります。

webpack本番設定

本番の設定内容は少なく、やっているのは環境変数の指定とcommonとの連結だけです。

webpack.prod.js
process.env.NODE_ENV = 'production';
module.exports = merge(common, {
  mode: 'production',
});

process.env.NODE_ENVでnode.jsの、modeでwebpackの環境変数を設定しています。

webpack開発設定

開発用にフロントエンド専用のサーバーを立てる設定。

webpack.dev.js
process.env.NODE_ENV = 'development';
module.exports = merge(common,{
    mode: 'development',
    devtool: 'eval-source-map',
    devServer: {
        port: 3000,
	contentBase: '/dist',
	watchContentBase: true,
	open: true,
	openPage: 'index',
	hot: true,
        historyApiFallback: {
            rewrites: [
                { from: /\/index/, to: '/index.html' }, // index.html に飛ばす
                { from: /\/secondpage/, to: '/secondpage.html' },
            ]
        }
    }
});
  • process.env.NODE_ENV = 'development'で、本番同様nodeの環境を指定します。
  • mode: webpackの実行環境を指定します。
  • devtool: ソースマップを出力します。ソースマップは開発者ツールで使用するものです参考
  • devServer: webpack-dev-serverでフロント開発用サーバーを立てるための設定が入ります。
    • port: 使用するポートを指定。80代だと被りやすいので3000を指定。
    • contentBase: 公式には、静的コンテンツのserve元となるディレクトリを指定するとある。これだけでは理解できず調べたところ、どうやらバンドルファイルの出力先と同じディレクトリを指定する必要があるらしい。以下を参考にした。
      (webpack-dev-serverで開発用サーバを立てる)
    • watchContentBase: contentBaseで指定された静的ファイルに対しても変更監視を行い、変更を検知した場合にライブリロードします。
    • open: サーバー起動時にブラウザを開く
    • openPage: openを指定したときに、開くページを指定する。
      今回の場合だとhttp://localhost:3000/indexが開く。
    • hot: HMRという機能をonにします。JSのコードが変更された場合、その部分だけリロードしてくれます。ページ全体の自動リロードはinlineというオプションで制御できますが、デフォルトでonになっています。
    • historyApiFallback.rewrites: リクエストurlに対してファイルパスを割り当てる。本番ビルドで出力されるhtmlと一緒の名前にしておく。

参考(※この記事はwebpack5を使っているので、webpack4の記事は一部参考です)

4.トランスパイラーの設定(Babel, TypeScript)

webpackから各トランスパイラの設定を読み込むように記述しました。
以下では、それらトランスパイラの設定を書いていきます。

ここでのトランスパイラの役割

  • Babel
    • jsx構文、typescriptのjavascriptへのトランスパイル
    • 難読化
  • TypeScript
    • 出力時のECMAScriptのバージョン指定
    • 型チェック

4-1.Babelの設定

関連モジュール
@babel/core: babel本体
@babel/preset-env: トランスパイル時のターゲットを指定する
@babel/preset-react: Reactを解釈する。
jsx記法を解釈したり、React.createClassで生成された要素名をディスプレイする。
@babel/preset-typescript: TypeScriptを解釈する。
babel-plugin-styled-components: styled-componentsの変換についてbabelから設定できます。スタイルのssrやminify、開発者ツール上でのコンポーネントのクラス名表示などについて設定ができます。(styled componentsの使い方)

使用するファイル
babel.config.js Babel設定ファイル

babel.config.js
module.exports = function (api) {
    // nodeの環境変数を取得
    const isProd = api.env('production');
    const presets = [
        [
            '@babel/preset-env',
            {
                targets: { chrome: 80 },
		// ターゲットをchromeのバージョン80に設定
            },
        ],
        '@babel/preset-react',
        '@babel/preset-typescript',
    ];
    const plugins = [
        [
            "babel-plugin-styled-components", {
                "ssr": false,
                "minify": isProd,
                "pure": true,
                "transpileTemplateLiterals": true,
                "fileName": true
            }
        ]
    ];

ファイル上部でnodeの環境変数をisProdに格納し、本番/開発での動きを制御しています。
取得方法はbabelから提供されるapiを使用しています。

  • presets
    targets: targetをchromeのバージョン80に指定します
    @babel/preset-react, @babel/preset-typescript: インストールしたモジュール名を記載します
  • plugins
    babel-plugin-styled-components: styled-components用のプラグインです。トランスパイル時のオプションを指定します。

設定ファイルの仕様について少し説明を加えます。
presetspluginsという項目がありますが、presetsはpluginsのコレクションです。大雑把にいうと両者とも機能を拡張するために使います。注目すべき点として、適用される順番は決まっており、pluginspresetsplugins内は上から、presets内は下から適用されます。見た目だと以下の例のような順番です。

babel-example
"presets": [
    "6th",
    "5th",
    "4th"
  ]
},
"plugins": [
    "1st",
    "2nd",
    "3rd"
  ]
}

余談: 新しいJSXトランスフォーム
React 17 には新機能はありませんが、「新しい JSX トランスフォーム」という変更が加えられています。
コード上の変化としては、ファイルの先頭でreactをimportしなくてもよくなった点ですが、利便性が劇的に変わるわけでなく、その割に設定が複数のファイルにわたるものだったので、今回は取り入れませんでした。やってみたい人は以下リンクを参考にしてください。

BabelとTypeScriptの設定・・・React17におけるJSXの新しい変換を理解する
ESLintの設定・・・React v17 create-react-app で作ったアプリで ESLint に怒られまくった

参考

4-2.TypeScriptの設定

関連モジュール
typescript: TypeScript本体

使用するファイル
tsconfig.json: TypeScript設定ファイル

出力先や形式はwebpackとbabelで設定してあり、同様の機能はtsにもあるのですが重複するので使用しません。トランスパイラとしての機能もbabelでほとんどカバーされています。
tsの主な用途は型チェックです。また、ビルドに致命的な影響を与えないので、tsconfig.jsonに関しては細かい説明は割愛します。出力はes6形式という点だけ書いておきます。
公式サイトで一覧があるので参照して下さい。

tsconfig.json
{
    "compilerOptions": {
        "noImplicitAny": true,
        "module": "commonjs",
        "target": "es6",
        "jsx": "react",
        "allowJs": true,
	"strict": true
    }
}

5.ビルド用のスクリプトと動作確認

ここまでで実行に必要な最低限の設定は完了です。
npm scriptでビルドを行うので、package.jsonのscriptsに次のように記載します。

package.json
"start": "webpack serve --config ./webpack.dev.js --mode development",
"build": "webpack --progress --config ./webpack.prod.js --mode production",
  • "start" 開発用サーバーを起動するコマンド。
    古いバージョンだとwebpack-dev-serverのコマンドを使ってましたが5系ではこのようにwebpack serveを使います。
  • "build" 本番ビルドを実行するコマンド。
    dist配下にバンドルされたファイルが出力されます。

それぞれ、ルートディレクトリでyarn run startyarn run buildと実行して下さい。
どちらとも、--configで設定ファイルを、--modeで実行モードを指定しています。
このモード指定は、あくまでwebpack内での環境変数の指定で、nodeのものとは無関係です。
nodeの環境変数はwebpackの設定ファイル内でそれぞれなされているので確認しておいてください。
ローカルサーバー起動時の接続確認方法ですが、起動時は設定にあるように、http://localhost:3000/indexが開きます。ほかのページを確認する場合は、webpack.dev.jsの、devServer.historyApiFallback.rewritesの内容を参照してurlを編集し接続してください。


ソースコードの解説

ここまでの設定で表示されるページの描画内容が、どうやって成立しているか解説します。

一番ベースになっている表示内容は、./public/template.htmlです。webpackにおいて、設定したエントリーポイントからコンパイルされたjsファイルを、このhtmlに乗せるように設定されているためです。
このhtmlに書かれている内容は、大きく二つです。
一つ目は背景です。ソースコードでいうと下記の部分が相当します。

html {
        height: 100%;
        width: 100%;
      }
body {
	height: 100%;
        width: 100%;
        background: #0068b7;
        display: flex;
        justify-content: center;
      }

一番背後の青色の背景を指定しているのと、body以下のコンテンツをセンターに配置するようにflexboxで指定しています。

二つ目はjsで描画される内容が乗るdivタグと、そのスタイルが指定されています。

      #app {
        display: flex;
        flex-flow: row wrap;
        width: 80%;
        justify-content: center;
        align-items: center;
        text-align: center;
        background: #e6e6fa;
      }
 ......
<body>
    <div id="app"></div>
  </body>

見たままの説明ですが、divタグに"app"というidを指定して、スタイル情報を付与しています。
では、なぜここにjsの内容が乗るのでしょうか。./src/pages/index/main.tsxの内容をもとに解説します。

main.tsx
class App extends React.Component {
  render() {
    return (
      <Div>
        <Header pageName='index'/>
      </Div>
    );
  }
}

ReactDOM.render(<App />, document.querySelector('#app'));

ここでは、Appというクラスがリアクトコンポーネントとして定義されています。
そして、ファイルの最後では、ReactDomのrenderというAPIが呼び出されており、そこに"App"が引数として渡っています。
公式によると「渡されたcontainerのDOMにReact要素をレンダー」するとあります。以下のようなコードが凡例として挙げられています。

ReactDOM.render(element, container[, callback])

この凡例に今回のパターンを当てはめると、

  • element...class App
  • container...document.querySelector('#app')

となります。containerには、document.querySelector()というJavaScriptのAPIがコールバック関数として渡されており、これは、DOMの中から指定された条件の要素を返す機能を果たします。今回のケースだと、id="app"の要素を返してくれます。
なので結果として、
id="app"の要素にclass appのReact要素をレンダリングする
ということが、ReactDOM.render()によってなされます。

しかしここで、containerである#appの要素はどこを走査しにいくのかという疑問が出てきます。
答えは./public/template.htmlにある<div id="app"></div>ですが、これらを結びつける記述はソースコードには記載されていません。
このことについては、webpackのパートの、html-webpack-pluginを用いたhtmlの出力設定で解説した内容を再度確認してみてください。template.htmlとmain.tsxの関係がわかると思います。

リセットcssについて
destyle.cssを採用しています。
読み込みは上記で出たpublic.htmlから読み込んでいます。
public.htmlからだとうまく読み込まず、webpackの設定から読み込むようにしました。(webpack.common.jsを参照)
具体的な設定としては、destyle.cssをエントリーポイントに設定し、HtmlPluginのchunkを使い、それぞれのページと関連付けました。また、cssをwebpackが読み込めるようにする必要があったため、css-loaderとstyle-loaderのモジュールを追加し、同ファイル内にて設定も追記しました。
参考

推奨するVSCodeの拡張機能

あとはGitHub関連のプラグインも入れておくといいと思います。


問題なくビルドできたら、後編へ進みます。

Discussion

HimenonHimenon

非常に丁寧にまとめられており、知見の共有資料として非常によかったです。
いくつか補足しておくと他の記事と差分が出そうな点、記事がより有意義になりそうな点についてコメントさせてください。

1. webpack.externals

Production環境ではReact/ReactDOMのコードをアプリケーションのコードに対してバンドルすることはしないケースがあります。成果物の容量が増えてしまうのが一番の原因ですが、CDNなどのキャッシュ効率を考えたときにアプリケーションが依存する固定のライブラリは切り出す場合があります。これはwebpackのexternasl optionで実現できます。(おそらく他の記事では書いていない点)

2. BundleAnalyzer

先のexternalsの点に気がつくためにはwebpack-bundle-analyzerを入れておくと理解しやすいです。ファイルサイズは最初から見ておくと異変に気が付きやすくなります。(把握していないですがこれはcreate-react-appをやめると入れやすいかもしれない?)

丁寧にまとまった記事ありがとうございました!(教育的にも非常に良かったです)

手前味噌ですが、参考までに最近書いたwebpack.config.tsも貼っときます。

HalHal

コメント読ませていただきました。
自分のほうでも、CDNを使うパターン、バンドルするパターンでどういった違いがあるのか調べてみて勉強になりました。CDNのメリットは大きいようですね。ご教示いただいた内容はぜひ今後の開発に取り入れてみようと思います。
非常に実用的な内容でとても参考になりました。ありがとうございます!!