調整さん (https://chouseisan.com/ )のパクりサービス。リスペクト!
React・TypeScript・Expressの習得用に開発している。
※有識者レビュー対象:
https://github.com/isoittech/tsugou-kun-react/releases/tag/REL_0.5.0
開発工程用サーバ起動手順を示す。
本プロジェクトでは、バックエンド用・フロントエンド用サーバを分けて開発している。
(2020/09/26現在、一緒にビルドする方法が分からないのと、効率が良さそうだと感じたため)
下記にバックエンドサーバ、フロントエンドサーバの順に動作手順を示す。
-
clone
-
バックエンドサーバセットアップのため、下記コマンドを実行する。
$ 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で起動
-
フロントエンドサーバセットアップのため、下記コマンドを実行する。
$ 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 # # アプリバージョンアップ # # =削除-->リリース手順実施
-
参加日記入機能開発
-
イベント情報登録後、フォームクリアが働かない
-
DB永続化関連ソースをTypeScript化
-
any根絶
-
イベント編集画面にて、イベント日時候補が無くなってしまうのをチェックする
-
StoryBook導入
マウント時にuseRefで生成したRefオブジェクトを、そのまま使い続ける。
このRefオブジェクトは、コンポーネントがアンマウントされるまで存在し続ける。
Refオブジェクトは最初からcurrentというプロパティを持っており、
useRefに渡した引数がcurrentプロパティの初期値となる。
引数を渡さなかった場合はundefinedが初期値になる。
そしてcurrentは、自由に書き換えることが出来る。
アンマウントされるまで存在し続けること、自由に書き換えることが出来ること、これがRefオブジェクトの特徴
関数コンポーネントを再レンダーした際に、前回のレンダー時のデータを取得することが可能ということを意味する。
-
見た目を担当するコンポーネント
-
独自のマークアップとスタイルを持つ
-
多くの場合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からアクセスすることになる。
-
ロジック(物事の振る舞い)に関与する。
-
通常、ラッピングの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
-
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 を呼ぶのは どこでデータが上書きされるかが複雑に絡み合ってややこしくなるので、できれば避けたほうがいい
-
ページをコンポーネントの階層構造に落とし込み、併せて各コンポーネントの Props を決定する
-
どのコンポーネントを Container にするかを決め、その Local State および connect するProps を決定する
UI 状態を表現する必要かつ十分な state を決定する
state をどこに配置するべきなのかを明確にする -
ページを構成する主要なコンポーネントを、スタイルガイドとして Storybook に登録する
Container にするべきコンポーネントが決まったら、ページを構成する主要な
Presentational Component を Storybook にスタイルガイドとして登録する -
Container が発行する Action と発行に使う Action Creator を作成、それに対応する Reducerも併せて作る
-
その Action が必要とする API ハンドラを作成、ユニットテストも併せて書く
4.の Action に対応した Saga を作成する。それができたら Redux DevTools から
生テキストのAction を Dispatch してみて、その Saga が正しく動作することを確認。
その上で Redux SagaTest Plan を用いて Saga と Reducer のユニットテストを書く。 -
4 と 5 による Saga を作成、ユニットテストも併せて書く
-
Container Component を作成する
-
正常系の E2E テストをCypressで作成する
-
ロジックのテストはちゃんとやる。
※API ハンドラや Redux-Saga の Saga 群。 -
コンポーネントに関しては、費用対効果を考えて最小限にする
-
Storybook にストーリー登録したPresentational Component のスナップショットテストを行う。
-
全体的な動作の保証のために、自動化された E2E テストを正常系に限って行う。
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
$ 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サーバーでのアプリケーションのディレクトリ/ファイル構成
この記事での考え方が一番しっくり来たため採用。