仕事で新しい機能を開発する際に、フロントエンドにはVue.jsをよく採用しています。しかし個人的な開発でReactを使ってみたところ、書き味を気に入ってしまって、最近はReact推しです。
さて、開発環境構築の記事を書くにあたって、記事の有効期限が気になりました。JavaScriptアプリケーション周辺技術の栄枯盛衰の速度を考えると、単純に叩いたコマンドやファイルの内容を列挙していくだけでは、使用しているツールのバージョンの違い等で容易にエラーを起こすことが考えられます。ツール単体の導入方法は公式ページにあるんですから、そっちを見たら良いですしね。使用するツールのバージョンを全て固定することも考えられますが...。
そんなわけで、有効期限が少しでも長くなることを狙ったReactアプリ開発環境構築の記事を書く試みです。
方針:
- コマンドや設定の詳細は適時省き、一次ソース(公式ページ)を参照できるようにします。
- 最終的に index.html、main.js、index.cssを出力し、それだけで完結するSPAアプリになります。
この記事で選択する技術です。今自分が新しいJSアプリを作るならこうなります。選択理由も併記。
- React: 好き。TypeScriptと相性がいい。
- TypeScript: 型。Elmが恋しい。
- Webpack: ビルド構成の自由度の高さ。Create React AppはPostCSSを足しにくいので却下。
- PostCSS: 最低限autoprefixerを入れたい。
- Prettier: 機械にできることは機械任せ、人間は創造力を発揮しましょう。
それでは順番に導入していきましょう。
プロジェクト作成
mkdir my-app
cd my-app
git init
npm init -y
WebpackでJavaScriptをビルドできるようにする
https://webpack.js.org/guides/getting-started/
このページを見ながら、Pure JavaScriptをビルドできるようにします。
ただし、index.jsは以下の内容でいいでしょう。lodashのインストールは不要です。
alert('JS Loaded');
最終的に以下のようなファイル構成になります。
my-app
|- package.json
|- webpack.config.js
|- /dist
|- main.js
|- index.html
|- /src
|- index.js
|- /node_modules
このステップでの確認事項:
-
package.jsonに
npm run build
コマンドの内容を記述し、webpackコマンドが実行されるようにした。 - webpack.config.jsで入力と出力を設定した。
-
npm run build
コマンドを打った時、src/index.jsを入力としてdist/main.jsが出力される。
webpack-dev-serverを導入する
開発時に便利なlive reloading(ファイルを更新したら画面を自動でリロードしてくれる)とHot Module Replacement(HMR、ファイルを更新したら画面全体をリロードするのではなくモジュール単体を更新してくれる)はこの時点で入れておきます。
https://webpack.js.org/guides/development/#using-webpack-dev-server
このページの"Using webpack-dev-server"を参考に設定し、npm run start
した時にブラウザでdist/index.htmlが開かれるようになればOKです。
https://webpack.js.org/guides/hot-module-replacement/
このページを参考に、HMRを有効にしておきます。まだ別モジュールを作っていないのですが、このページで行っているように別ファイルを作って試してみるのもいいでしょう。
このステップでの確認事項:
- webpack.config.jsにdev server用の設定をした。
-
package.jsonで
npm run start
コマンドの内容を記述し、そこではwebpack-dev-serverが起動するようにした。
Prettierでコードフォーマットできるようにする
https://prettier.io/docs/en/install.html
このページを見てインストール。
https://prettier.io/docs/en/cli.html
このページを見て使い方を知る。prettier --write .
と書けばいいそうですから、
{
...
"scripts": {
...
"format": "prettier --write ."
}
}
としておきましょう。実行してみると分かりますが、distディレクトリ内のファイル等、フォーマットしてほしくないファイルもあります。
https://prettier.io/docs/en/ignore.html#ignoring-files
このページを参考に、.prettierignoreファイルを用意して、distディレクトリはフォーマットしないようにします。
このステップでの確認事項:
- package.jsonでnpm run formatコマンドの内容を記述し、prettierが実行されるようにした。
TypeScriptで書けるようにする
https://webpack.js.org/guides/typescript/
WebpackのページにTypeScriptのセットアップ方法があります。
この記事を書いた時点ではコンパイラとしてts-loaderを使い、ES5まで変換しています。ゆえにbabelは使いません。
https://www.typescriptlang.org/docs/handbook/compiler-options.html
tsconfig.json compilerOptionsの設定値は確認しておきましょう。ただ、設定値に悩まないコツは、不要な設定をしないことだと今のところ思います。何か問題が起こって、必要に迫られた時に設定を追加変更していくぐらいでも良いです。逆に最初からstrictに行くぜ!って人は、"strict": true, "allowJs": false, "noImplicitAny": true, "strictNullChecks": true
でどうでしょう。
このステップでの確認事項:
-
npm run build
すると、TypeScriptで書かれたファイルがJavaScirptファイルに変換された形でdistディレクトリに出力される。
React導入
ここでどこを参照したら良いか悩みます。いくつかの情報を組み合わせることにしました。
https://blog.usejournal.com/creating-a-react-app-from-scratch-f3c693b84658
WebpackでReactアプリを作成する手順として、React公式ページからリンクされた記事です。今babelを使わないのでそれを差っ引いて参考にします。
https://www.typescriptlang.org/docs/handbook/react-&-webpack.html
TypeScript公式ページの、ReactとWebpackセットアップ解説。Node.jsでホストすることを想定しているようなのでそれを踏まえて参考にします。
https://reactjs.org/docs/components-and-props.html
React v16現在、コンポーネントの定義方法がFunctionとClassの2つあります。前者が新しい方法です。
https://reactjs.org/docs/hooks-intro.html
便利なReact HooksもFunctionコンポーネント前提です。Functionコンポーネントを採用することにしましょう。
https://reactjs.org/docs/getting-started.html
もしかしたら将来React Getting Startedのページに適切な一次情報が増えるかもしれないので、そちらも探してみてください。
パッケージインストール
npm install react react-dom
npm install --save-dev @types/react @types/react-dom source-map-loader
esModuleInteropについて
TypeScript公式ページ通りにtsconfig.jsonを設定してみます。
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es6",
"jsx": "react"
}
}
src/index.tsxをこう書くと...
import React from "react";
import ReactDOM from "react-dom";
import App from "./App.js";
ReactDOM.render(<App />, document.getElementById("root"));
エラー。
https://stackoverflow.com/a/56348146
なぜこうなるかの説明がありました。ReactはCommonJSモジュールのようですね。
このままだと以下のようにimportしないといけなくなるわけです。
import * as React from "react";
import * as ReactDOM from "react-dom";
tsconfig.jsonに"esModuleInterop": true
設定を足すと、最初に書いたようにimportできるようになります。こっちを採用します。
Reactセットアップ
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es6",
"jsx": "react",
"esModuleInterop": true
}
}
const path = require("path");
module.exports = {
entry: "./src/index.tsx",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/
},
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader"
}
]
},
resolve: {
extensions: [".tsx", ".ts", ".js"]
},
devtool: "source-map",
devServer: {
contentBase: "./dist",
hot: true
}
};
import React from "react";
import ReactDOM from "react-dom";
import App from "./app/App.tsx";
ReactDOM.render(<App />, document.getElementById("root"));
import React from "react"
const App: React.FC = () => {
return <div>Hello React App</div>
}
export default App
<!DOCTYPE html>
<html>
<head>
<title>Getting Started</title>
</head>
<body>
<div id="root"></div><!-- Reactをマウントする場所を用意 -->
<script src="main.js"></script>
</body>
</html>
ここまで書けたらnpm run startしてみてください。コンソールにエラーが出ず、画面にHello React Appと表示されたら成功です。
src/app/App.tsxのReact.FCについて補足しておきます。JavaScriptでReact Functionコンポーネントを定義する時は下のようになります。
const App = () => {
return <div>Hello React App</div>
}
Functionコンポーネント用のinterface FunctionComponentが@types/reactに定義されています(公式での説明が見つかりませんでしたが)。この省略形としてtype FCが定義されています。propsの型チェック等に便利なので使用していきます。
HMR対応
さて、npm run start(webpack-dev-serverで画面表示)している状態で、src/app/App.tsxを編集保存してみてください。ブラウザが自動リロードして画面が更新されると思います。本来HMRではAppが描画している部分だけが更新してほしいので、対応します。
https://webpack.js.org/guides/hot-module-replacement/
https://webpack.js.org/concepts/hot-module-replacement/
https://webpack.js.org/api/hot-module-replacement/
import React from "react";
import ReactDOM from "react-dom";
const render = () => {
const App = require('./app/App').default;
ReactDOM.render(<App />, document.getElementById("root"));
}
render();
if (module.hot) {
module.hot.accept('./app/App', () => {
render();
})
}
module.hot.acceptの第一引数で指定したファイルが更新されると第二引数の関数が実行されるので、再度renderします。
TypeScriptで書いているので、module.hotに関する型情報が必要です。
npm i -D @types/webpack-env
src/app/App.tsxを編集保存してブラウザがリロードせずに画面が更新されれば成功です。
PostCSS導入
https://github.com/postcss/postcss-loader
src/index.cssを用意しsrc/index.tsxで import "./index.css"; した上で、上のページのとおり設定していきます。
このステップでの確認事項:
- index.tsxからindex.cssをimportした
- webpack.config.jsでpostcss-loaderのルールを追加した
- postcss.config.jsを作成し設定を行った
CSSを外部ファイル化したい場合は、以下を見るといいでしょう。
https://github.com/postcss/postcss-loader#extract-css
https://webpack.js.org/plugins/mini-css-extract-plugin/
Webpack設定の補足など
webpack.config.jsのmodeを今まで無視してきました。
https://webpack.js.org/configuration/mode/
npm run build時とnpm run start時それぞれでmodeを切り替えたい。上のページでもCLIからmodeを渡す方法を紹介しています。
https://webpack.js.org/guides/production/
このようにwebpack-mergeを使っているプロジェクトもよく見ます。
個人的には、postcss.config.js等の外部ファイルでも動作を切り替えたいので、cross-envでNODE_ENVを定義することが多いです。
https://www.npmjs.com/package/cross-env
今回dist/index.htmlは自前で作成しましたが、HtmlWebpackPluginを使うと、Webpackが出力するJSやCSSをロードするHTMLファイルを生成してくれます。
https://webpack.js.org/plugins/html-webpack-plugin/
最後に
以上で解説は終了です。
環境構築記事では、このツールとこのツールを組み合わせることが可能か、組み合わせるのにはどうしたら良いかという点にオリジナリティが出ます。そこを書くにはどうしても設定方法を詳細に書くことになります。詳細に書くと記事の有効期限が短くなりがち。結論、環境構築記事は生もの!常に更新しない限りすぐ腐る。ただ、信頼できる一次ソースを参考文献としてちゃんと示すと、読んだ人が判断しやすい記事になりそうだなとは思いました。