tsugou-kun-react

調整さん (https://chouseisan.com/ )のパクりサービス。リスペクト!
React・TypeScript・Expressの習得用に開発している。
※有識者レビュー対象:
https://github.com/isoittech/tsugou-kun-react/releases/tag/REL_0.5.0

■ローカル環境動作手順

開発工程用サーバ起動手順を示す。
本プロジェクトでは、バックエンド用・フロントエンド用サーバを分けて開発している。
(2020/09/26現在、一緒にビルドする方法が分からないのと、効率が良さそうだと感じたため)

下記にバックエンドサーバ、フロントエンドサーバの順に動作手順を示す。

  1. clone

  2. バックエンドサーバセットアップのため、下記コマンドを実行する。

    $ PJ_HOME=`pwd`/tsugou-kun-react
    $ cd ${PJ_HOME}/backend
    $ npm ci
    $ cd ${PJ_HOME}/backend/src/main/db
    $ ${PJ_HOME}/backend/node_modules/sequelize-cli/lib/sequelize db:migrate --env development
    $ mv ${PJ_HOME}/backend/src/main/db/data/tsugoukun_development.sqlite3 ${PJ_HOME}/backend/data/
    $ cd ${PJ_HOME}/backend
    $ npm start  # またはVSCodeでF5で起動
  3. フロントエンドサーバセットアップのため、下記コマンドを実行する。

    $ cd ${PJ_HOME}/frontend
    $ npm ci
    $ npm start

■本番環境リリース

# # cloneする
# cd tsugou-kun-react
# export HOST_URL=https://XXXXXXX  # https://tsugoukun.0x0.jp
# touch backend/data/tsugouku_XXXXXXXX.sqlite3  # tsugoukun_development.sqlite3等
# docker-compose build --no-cache
# docker-compose up -d
# # ---------------------
# # ・cron 起動によるmydns.jpへのIPアドレス通知
# #   ※*/5 * * * * /usr/bin/wget -O - 'https://mydnsXXXXXX:PWPWPWPWPW@www.mydns.jp/login.html' --no-check-certificate >>/tmp/cronlog.log 2>>/tmp/cronlog-err.log
# # ・https://github.com/isoittech/HttpsSiteRouter の 準備・コンテナ起動
# # ---------------------
# docker-compose logs                        # ログ確認
# docker-compose exec nginx /bin/bash        # ログインして確認

# # ---------------------
# # 再起動
# docker-compose restart
# # ---------------------
# # 削除
# docker-compose down --rmi all --volumes --remove-orphans
# # or
# docker stop xxxx && docker rm xxxx
# # アプリバージョンアップ
# # =削除-->リリース手順実施

■ISSUE・TODO

  • 参加日記入機能開発

  • イベント情報登録後、フォームクリアが働かない

  • DB永続化関連ソースをTypeScript化

  • any根絶

  • イベント編集画面にて、イベント日時候補が無くなってしまうのをチェックする

  • StoryBook導入

■今後の開発用メモ

▼Hooks

◎useMemo

関数の実行結果をメモ化(コンポーネント外に記憶)する。

◎useCallback

関数定義そのものをメモ化する。

◎useRef

マウント時にuseRefで生成したRefオブジェクトを、そのまま使い続ける。
このRefオブジェクトは、コンポーネントがアンマウントされるまで存在し続ける。

Refオブジェクトは最初からcurrentというプロパティを持っており、
useRefに渡した引数がcurrentプロパティの初期値となる。
引数を渡さなかった場合はundefinedが初期値になる。
そしてcurrentは、自由に書き換えることが出来る。

アンマウントされるまで存在し続けること、自由に書き換えることが出来ること、これがRefオブジェクトの特徴
関数コンポーネントを再レンダーした際に、前回のレンダー時のデータを取得することが可能ということを意味する。

▼コンポーネント分類

◎Presentational Component

  • 見た目を担当するコンポーネント

  • 独自のマークアップとスタイルを持つ

  • 多くの場合this.props.childrenとして他に内包される

  • アクションやストアに依存しない

  • データのロードや変更などのロジックの部分は切り離される

  • propsとしてデータとコールバックを受け取れる

  • 稀に独自のstateを持つ、それはデータではなくUIの状態として持つ

  • Presentational Component例:Page, Sidebar, Story, UserInfo, Listが上げられる

  • 基本的にstateには触らず、propsとして与えられるデータを表示することに専念

  • storeにもアクセスしない。dispatchもできない。

  • 例えばボタンを表示しても、onClickではpropsで与えられるコールバック関数を呼ぶだけ

  • 表示するデータや、ボタン押下時の処理を外部から指定することができ、再利用性が上がる

  • dropdownの開閉状態のような、componentの中に閉じ込めた方が良いと判断されるデータの管理には stateを使うこともありる。そういうのは大抵UIに関する状態管理である。 アプリケーションの状態やデータはReduxのstoreに格納し、container comoponentからアクセスすることになる。

◎Container Components

  • ロジック(物事の振る舞い)に関与する。

  • 通常、ラッピングのdivを除いて独自のDOMマークアップはもたない

  • Presentational Componentまたその他のコンポーネントにデータと振る舞いを提供する

  • アクション呼び出しなどをコールバックとしてPresentational Componentに渡す

  • スタイルなどを持たないという点から、データソースとして機能する傾向があるため、基本的に状態保持と処理を行う

  • React Reduxのconnect()、RelayのcreateContainer ()、Flux UtilsのContainer.create()などの上位コンポーネントを使用して生成される。

  • 例としてUserPage、FollowersSidebar、StoryContainer、FollowedUserListが上げられる

◎備考

ただし、この分類を提唱したDan Abramovは、「Hooksがある現状では、分割は勧めていない。(2019)」と言っている。
※元々分割を推奨した理由は、「複雑なステートフルロジックをコンポーネントの
他の側面から切り離すことができたから」とのこと。
https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

▼Component実装ルール

  • Atomic Designを意識する

    • 各レベルのルール

    • 自分のレベル以下の要素で構成する

    • 最初から完璧に設計する必要はない

  • ファイルの命名規則

  • Functional Componentで実装する

  • Container ComponentとPresentational Componentに分けて実装する

  • Templates以下のComponentではuseQuery・useMutationを実行しない

  • global state と local stateの使い分け

    • 下記はglobal

      • そのデータがUI上関連の無いComponent同士で参照される時
        ※ヘッダーとサイドメニューでユーザー情報を参照するなど

      • そのデータから派生データを作成する必要がある時

  • スタイル管理

  • その他

    • 名前を間違えずにimport/exportするため、export defaultを使用しない

      • default exportの場合はimportの際に自由に名前をつけることができるため、
        typoに気づけない、export先の名前が統一されないケースがある。
        また、IDEでのコード補完とも相性が悪い。

    • Componentを作成する際はclassNameを受け取ることが可能なようにpropsを定義する

    • Material-UIを利用する

  • Componentの利用

    • RailsのViewへのReact Componentの埋め込み

    • client/Components/other/以下のComponentは原則利用しない

  • 親コンポーネントが子コンポーネントの具体的なデータや発行する Action を知りすぎないよう、 またひとつのコンポーネントの Props が5個や6個以上にならないよう調整していくといい

  • Presentaitona Component が Container Component を、Container Component が Presentational Component を呼ぶのはいいが、Container が Container を呼ぶのは どこでデータが上書きされるかが複雑に絡み合ってややこしくなるので、できれば避けたほうがいい

▼開発プロセス

  1. ページをコンポーネントの階層構造に落とし込み、併せて各コンポーネントの Props を決定する

  2. どのコンポーネントを Container にするかを決め、その Local State および connect するProps を決定する
    UI 状態を表現する必要かつ十分な state を決定する
    state をどこに配置するべきなのかを明確にする

  3. ページを構成する主要なコンポーネントを、スタイルガイドとして Storybook に登録する
    Container にするべきコンポーネントが決まったら、ページを構成する主要な
    Presentational Component を Storybook にスタイルガイドとして登録する

  4. Container が発行する Action と発行に使う Action Creator を作成、それに対応する Reducerも併せて作る

  5. その Action が必要とする API ハンドラを作成、ユニットテストも併せて書く
    4.の Action に対応した Saga を作成する。それができたら Redux DevTools から
    生テキストのAction を Dispatch してみて、その Saga が正しく動作することを確認。
    その上で Redux SagaTest Plan を用いて Saga と Reducer のユニットテストを書く。

  6. 4 と 5 による Saga を作成、ユニットテストも併せて書く

  7. Container Component を作成する

  8. 正常系の E2E テストをCypressで作成する

▼テスト方針

  • ロジックのテストはちゃんとやる。
    ※API ハンドラや Redux-Saga の Saga 群。

  • コンポーネントに関しては、費用対効果を考えて最小限にする

  • Storybook にストーリー登録したPresentational Component のスナップショットテストを行う。

  • 全体的な動作の保証のために、自動化された E2E テストを正常系に限って行う。

▼Webpack

webpack.config.js上におけるモードの切替・設定値によりリビルド速度やバンドルファイルサイズに差がでる。
参考: https://webpack.js.org/configuration/devtool/

◎速度
・devtool: "inline-source-map"
→ build:slowest, rebuild:slowest
・devtool: "eval-source-map"
→ build:slowest, rebuild:fast

◎バンドルファイルサイズ
・developmentモードxdevtool指定
→ 数 [MB]
・developmentモードxdevtool指定なし
→ 2 [MB]
※デバッグ時、見にくいコードになる。余計な文字列が変数名・関数名に付く。
・productionモード
→ 500 [KB]

■開発者用メモ

自分が辿った道を残す。

▼開発環境構築

◎バックエンド側

プロジェクトフォルダ・TypeScript・Expressの準備を行う。

$ mkdir backend; cd backend
$ npm init
$ npm i -D \
    typescript \
    ts-node \
    ts-node-dev \
    sequelize-cli \
    tslint \
    @types/node \
    @types/express \
    @types/sqlite3 \
    @types/validator \
    @types/bluebird \
    mocha \
    @types/mocha \
    reflect-metadata
$ npm i \
    express \
    sqlite3 \
    sequelize@5.22.3 \      # 6.xはバグのため低いバージョンを使用
    sequelize-typescript \
    base64url \
    connect-history-api-fallback \
    winston \ # Logger
    moment \ # Logger
    @types/winston \ # Logger
    @types/moment  # Logger
$ tsc --version
$ tsc --init

# GraphQL導入用
$ npm i \
    express-graphql \
    graphql \
    type-graphql
○DBマイグレーション・モデル初期構築
$ mkdir -p src/main/db/data
$ cd src/main/db
$ ../../node_modules/sequelize-cli/lib/sequelize init
$ ls
config/  migrations/  models/  seeders/
<この間で config/config.json の接続先等を編集>
$ ../../node_modules/sequelize-cli/lib/sequelize model:create \
    --name moyooshi \
    --underscored \
    --attributes \
        "name:string \
        ,memo:string \
        ,schedule_update_id:string"
$ ../../node_modules/sequelize-cli/lib/sequelize model:create \
    --name moyooshikouho_nichiji \
    --underscored \
    --attributes \
        "kouho_nichiji:string \
        ,moyooshi_id:bigint \
        ,schedule_update_id:string"
$ ../../node_modules/sequelize-cli/lib/sequelize model:create \
    --name sankasha \
    --underscored \
    --attributes \
        "name:string \
        ,moyooshi_id:bigint \
        ,comment:string"
$ ../../node_modules/sequelize-cli/lib/sequelize model:create \
    --name sanka_nichiji \
    --underscored \
    --attributes \
        "sanka_kahi:enum \
        ,event_kouho_nichiji_id:bigint \
        ,sankasha_id:bigint"
<ここで、migration/とmodels/配下のソースに、非null制約・外部キー関連の設定(キーワード:associate, references)を行う>
$ ../../node_modules/sequelize-cli/lib/sequelize db:migrate --env development
<ここで、src/main/db/data配下に出力される.sqlite3ファイルを、data/に移動する>

◎フロントエンド側

○実行コマンド

プロジェクトフォルダ・TypeScript・Webpack・ReactJSの準備を行う。

$ mkdir frontend; cd frontend
$ npm init
$ npm i -D \
    typescript \
    ts-loader \
    tslint \
    @types/react \
    @types/react-dom \
    @types/react-redux \
    @types/react-router-dom \
    @types/jsonwebtoken \
    webpack \
    webpack-cli \
    webpack-dev-server \
    clean-webpack-plugin \
    html-webpack-plugin \
    mini-css-extract-plugin \
    style-loader \
    css-loader \
    dotenv \
    cross-env
$ npm i \
    react \
    react-dom \
    redux \
    react-redux \
    redux-saga \
    @reduxjs/toolkit \
    axios \
    react-router@next \
    react-router-dom@next \
    history \
    redux-actions \
    react-bootstrap \
    bootstrap \
    react-modern-calendar-datepicker \
    react-helmet \
    react-cookie \
    winston \
    moment \
    jsonwebtoken \
    @types/winston \
    @types/moment \
    # Material-UIに変更する
    @material-ui/core \
    @material-ui/styles \
    react-hook-form
$ tsc --version
$ tsc --init
※React Routerについてはβ版の6を使用。競合のReach Routerとの合併版であり、便利かつ直感的であるため。正式版がリリースされ次第「@next」を除去する。
# for test
$ npm i -D ts-jest \
    jest-environment-jsdom-fourteen \
    @testing-library/react \
    @testing-library/user-event \
    @testing-library/jest-dom \
    @types/jest \
    @babel/preset-react \
    @babel/preset-env \
    @babel/preset-typescript \
    babel-preset-react-app \
    babel-jest \
    enzyme \
    jest-enzyme \
    enzyme-adapter-react-16 \
    react-test-renderer

# GraphQL導入用
$ npm i \
    @apollo/client \
    graphql \
    react-apollo-hooks \
    cors # ApolloClientではこれを使わないとだめだった

$ npm i -D \
    @graphql-codegen/cli \
    @graphql-codegen/typescript \
    @graphql-codegen/typescript-operations \
    @graphql-codegen/typescript-react-apollo

※jestはグローバルインストール( npm i -g jest )しておく。
※ts-node:コンパイルした後にnodeで実行してくれるモジュール。
package.jsonのscriptsに ts-node を実行するコマンドを定義する。
※react-test-renderer:スナップショットテスト用
※Jestを単体で使用する:npm install jest --global
※enzymeとtesting-libraryを競合不具合検証のために両方入れている。

# for Storybook
# https://storybook.js.org/docs/react/api/cli-options
$ npx sb init # Storybookインストール開始コマンド。
○フォルダ構成

React+Redux+APIサーバーでのアプリケーションのディレクトリ/ファイル構成
この記事での考え方が一番しっくり来たため採用。