docker×webpack×react×typescriptの環境構築(create-react-app不使用)

# はじめに
この記事では[create-react-app](https://github.com/facebook/create-react-app)に頼ることなくreactの環境を作成する。dockerで構築しているが、手元にnode環境があれば飛ばしても良い。

# 完成後の構成
環境構築が終わると以下のようなフォルダ構成になる。

```
.
├── dist
│   ├── index.html
│   └── index.js
├── docker
│   └── node
│       └── Dockerfile
├── node_modules
├── src
│   ├── main.tsx
│   └── sample-component.tsx
└── docker-compose.yml
└── package-lock.json
└── package.json
└── tsconfig.json
└── webpack.config.js
```

# dockerの環境設定
dockerではnodeの環境を構築する。
nodeの`Dokcefile`は以下の通り

```Dockerfile:Dockerfile
FROM node:16-alpine

WORKDIR /var/www/app
```
WORKDIRは適当に設定した。nodeのバージョンは`16-alpine`としたが、[ここ](https://hub.docker.com/_/node)から自由に選んで良い(動作確認は16-alpineでしか行ってない)。

`docker-compose.yml`は以下の通り

```yml:docker-compose.yml
version: "3.8"

services:
  node:
    build: ./docker/node
    tty: true
    volumes:
      - ./:/var/www/app
    working_dir: /var/www/app
    ports:
      - 3000:3000
```
Dokcerfileとdocker-compose.ymlに分けたが、まとめても問題ない。portsなど細かい設定は任意に変えて良い。これらを設定したら、`docker-compose up --build -d`でDocker立ち上げると、Dockerのnode環境は完成。

# Reactの環境設定
Reactの環境設定は`docker-compose exec node sh`でnodeのDokcerに入って全て行う(今後行うコマンド全て)。dockerを使わない場合はシェルにそのまま打てば良い。

## プロジェクトの初期化
npmプロジェクトの立ち上げとして

```
npm init -y
```
と入力する。そうすると

```json:package.json
{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
```
のようなファイルが生成される。立ち上げの際に詳細を設定したい場合は代わりに`npm init`と入力して設定する。

## 必要なパッケージのインストール
今回必要最低限のパッケージとして以下のものをインストールする。

- [webpack](https://webpack.js.org/): JSモジュールのバンドラー
- [webpack-cli](https://www.npmjs.com/package/webpack-cli): webpackのCLI
- [typescript](https://www.typescriptlang.org/)
- ts-loader: typescriptをwebpackで処理するのに必要
- [react](https://ja.reactjs.org/)
- [react-dom](https://www.npmjs.com/package/react-dom): DOMを管理するもの
- @types/react: reactの型定義
- @types/react-dom: react-domの型定義

まず、reactとreact-dom以外は開発専用のパッケージなので

```
npm i -D webpack webpack-cli typescript ts-loader @types/react @types/react-dom
```
のようにインストールする。reactとreact-domは普通に

```
npm i react react-dom
```
のようにインストールする(nodeのバージョンが低ければオプションに-Sが必要)。今後必要なライブラリがあれば上記のようにインストールするとよい。
このようにインストールすることで`package.json`は

```json:package.json
{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/react": "^17.0.20",
    "@types/react-dom": "^17.0.9",
    "ts-loader": "^9.2.5",
    "typescript": "^4.4.2",
    "webpack": "^5.52.0",
    "webpack-cli": "^4.8.0"
  },
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}
```
のように変更される。その他にもpackage-lock.jsonやnode_modulesが生成される。

## typescriptの設定
typescriptの設定は`tsconfig.json`に書く。

```tsconfig.json(最小限)
{
    "compilerOptions": {
        "sourceMap": true,
        "target": "ES6",
        "module": "ES2020",
        "jsx": "react",
        "moduleResolution": "node",
        "lib": [
            "ES2020",
            "DOM"
        ]
    }
}
```
tsconfigの設定は[公式](https://www.typescriptlang.org/tsconfig)が見やすいのでこれを見て変更するとよい。何も考えずに動かす分には上記のものを使用すると良い。

## webpackの構成

```js:webpack.config.js
const path = require("path");

module.exports = {
  mode: "development",
  entry: "./src/main.tsx",
  output: {
    filename: "main.js",
    path: path.resolve("dist"),
    publicPath: "/",
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader"
      }
    ]
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"]
  },
};
```
この記事ではts,tsxファイルを`src`に置き、その中でもメインファイルを`main.tsx`としているので`entry`は`"./src/main.tsx"`となった。また、出力先を`dist`、コンパイル後のファイル名を`index.js`としたので、`output`は

```js
{
    filename: "main.js",
    path: path.resolve("dist"),
    publicPath: "/",
},
```
となった。`module`ではts-loaderを使用する条件が書かれており、ts,tsxファイルに対して使用される。`resolve`はimport文の名前解決のために記述した。

## typescriptのビルドコマンド追加
実際に上記で設定通りにts,tsxファイルをコンパイルするコマンドを`package.json`のscriptに追加する。

```json:package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack",
    "watch": "webpack -w"
  },
```
このように追加することで`npm run build`でビルド、`npm run watch`でホットリロードが可能となる。その他scriptを追加するときはここに追加することとなる。これらのコマンドによって`src`内の`main.tsx`が`dist`に`index.js`としてコンパイルされる。

## 実際に動かしてみる
テンプレートとして静的ファイルである`index.html`を`dist`に作成する。

```html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <script defer src="./main.js"></script>
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>

```
scriptでコンパイル後のjsファイルを読み込んでいる。`<div id="app"></div>`を埋め込む場所にReactが反映される(appでなくても良い)。
次に`src`にtypescriptで書かれたreactのファイルを配置する。サンプルとして簡単なファイル用意した。`webpack.config.js`のresolveがうまく動いていることを確認するためにtsxのファイルを二個作成した(example-component.tsxがうまくimportされるかを確かめる)。

```tsx:main.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { SampleComponent } from './sample-component';

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>React Example</h1>
        <SampleComponent name="Counter"/>
      </div>
    );
  }
}

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

```
最終行のappが`index.html`のdivのid(app)に対応している。

```tsx:sample-component.tsx
import * as React from 'react';

interface IProps {
  name: string;
}

export const SampleComponent = (props: IProps): React.ReactElement =>  {
  const [count, setCount] = React.useState<number>(0);

  return (
    <div>
      <h1>{props.name}</h1>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>Count Up</button>
    </div>
  );
}

```
上記の三つのファイルを作成したのちに`npm run dev`を行い、`./dist/index.js`にファイルが生成されたのを確認したら、`index.html`を開くと
![スクリーンショット 2021-09-06 15.27.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/5f11da64-ba0b-2575-9be9-63cea3b2c670.png)
こんな感じの簡単なカウンターが現れる。