RMS for React UIはReactとMaterial-UIの利用法や効果を確認することを目的としたRental Management SystemのSPAアプリです
Table of Contents
Next.jsなど混じり気のないReact純度100%のJavaScriptによるSPAアプリです
- React.js 17.0
- Material-UI(MUI) 4.12
- OpenAPI Generator 5.4
State管理に外部ライブラリは利用せずReact純正のuseState
, useReducer
, useContext
の3つで行っています
その他の仕組みはreact-uiで実装している仕組みに記載
Reactと言えばyarn
なので例にyarn
を使っていますがnpm
でも問題ありません(やったことないけど多分大丈夫なハズ)
OpenAPI Generatorで自動生成したClient APIはrms-generated-client-jsとして別パッケージにしています。 ホントであればyarn install
で他のdependencyと一緒にインストールしたいところですが、誰でも簡単にダウンロードできる適当なPackage Registryがないので、npm link
で依存を解決しています
- rms-ui-reactのインストール
# Clone this repository
git clone https://github.com/extact-io/rms-ui-react.git
# Go into the repository
cd rms-ui-react
# Install dependencies
yarn install
- rms-generated-client-jsのインストール
# Clone this repository
git clone https://github.com/extact-io/rms-generated-client-js.git
# Go into the repository
cd rms-generated-client-js
# link to global
npm install
npm link
# Go into the rms-ui-react directory
cd /path/to/your/rms-ui-react_dir
# link to node_modules
npm link @extact-io/rms-generated-client-js
- rms-ui-reactの起動
# Go into the rms-ui-react directory
cd /path/to/your/rms-ui-react_dir
# Run the app
yarn start
yarn start
で自動でブラウザが立ち上がりLogin画面がでれば手順は成功です(ブラウザが立ち上がってからLogin画面がでるまで1,2分掛かります)
ただし、rms-ui-reactはフロントエンドアプリなのでLogin画面以降の動作にはバックエンドのサーバーアプリが必要です。サーバアプリには実物のRMSを使います。動作させる場合の次の手順を行ってください
Javaアプリのためビルドと起動にはJava11以上とMavenが必要となります。この2つはインストールされている前提で手順を説明します
- dependencyのローカルインストールとビルド
# Clone this repository
git clone https://github.com/extact-io/rms.git
# Go into the repository
cd rms
# Install dependencies
mvn -Pcli,all clean install -DskipTests=true
# Go into the app directory
cd rms-server
# Build the app
mvn -Pcli,copy-libs clean package -DskipTests=true
- バックエンドアプリの起動
# Run the app
java -Drms.h2.script=classpath:init-rms-demo.ddl -jar -Ddebug.sleep.enable=true -Ddebug.sleep.time=300 target/rms-server.jar
stacktraceが出力されていなければ起動は成功です。こちらに記載のID/paasswordでログインできます。なお、UIのProgressやBackdropの動作を確認するため、起動パラメータでsleepを入れていますが、これの指定はなくても問題はありません
業務アプリの多くは、一般のユーザが利用する「表」の画面と管理者が利用する「裏」の画面に分かれ、また双方で画面の遷移パターンが異なる場合が多いです
rms-ui-reactはこの考慮として「表」の会員機能はウィザード遷移(レンタル品予約)、「裏」の管理機能はダイアログでCRUDを行う2つの遷移パターンで実装しています
- 会員機能(ウィザード遷移)
- 管理機能(単発遷移+ダイアログ)
マスタメンテナンス画面の多くは扱うマスタが異なるだけで行う操作(処理)は同じになることが多くあります。またこれらをマスタごとに別々に実装すると冗長な実装となります
rms-ui-reactではマスタのCRUD処理や描画処理を共通化したコンポーネントを作成し、個々のマスタメンテナンス画面は差分の実装やパラメータ定義だけすればよくしています
- 実装の説明
レイヤ 機能 実装 説明 core 共通 MasterMainteDataGrid.js MasterContext
定義に従い動作するapp レンタル品 RentalItemMasterContext.js 表示カラムの定義や CRUD API
などを定義予約 ReservationMasterContext.js 同上 ユーザ UserMasterContext.js 同上
マスタメンテナス画面は1項目に対し参照画面では表示項目出力、登録/更新画面では入力項目出力と言ったように出力形式の分岐が必要となります。これに対するよくある実装としては、項目ごとに分岐するか、または参照画面、更新画面と画面レベルで実装を分ける方法がありますが、いずれも実装が冗長となります
rms-ui-reactではモードにより入力(input)と表示(label)を切り替えることができる項目コンポーネント(EditablTextField
)を作成し入力画面と参照画面を分岐なく1つで実装できるようにしています
- EditablTextFieldを使った実装例
<PanelLayout>
<Box mb={3}>
<Typography component="h1" variant="h4" align="center">ユーザプロファイル</Typography>
</Box>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<EditableTextField id="loginId" label="ログインID" editable={false}
value={targetUser.loginId.value}
/>
</Grid>
<Grid item xs={12} sm={6}>
<EditableTextField id="password" label="パスワード" type="password" editable={edit}
fieldValue={targetUser.password} onChange={handleChangeTargeUser}
/>
</Grid>
<Grid item xs={12} sm={6}>
<EditableTextField id="userName" label="ユーザ名" editable={edit}
fieldValue={targetUser.userName} onChange={handleChangeTargeUser}
/>
</Grid>
... snip
</Grid>
<PanelLayout>
(参考)UserProfileの参照/登録/更新を行うUserProfilePanel
の実装はこちら。editable
属性により出力形式が切り替わるようにしています。
入力チェックを画面ごとに実装すると重要なドメインルールの1つであるチェック実装が各画面に散ってしまいます
rms-ui-reactではDOA(DataOrientedApproach)で言う"ドメイン"ごとにフィールドクラスを作成し、画面の入力チェックはフィールドインスタンスですべて行っています。これによりA項目の入力がX画面とY画面に現れた場合でもそのチェック実装はAフィールドクラスに局所化されます。またフィールドクラスには「半角数値のみ」といった画面や処理に依らない普遍的なドメインの情報も一緒に定義可能にしています
- フィールドクラスの1例(PasswordField)を使った実装例
class PasswordFieldValidator extends FieldValidator {
static INSTANCE = new PasswordFieldValidator();
static #PATTERN = new RegExp(/^[a-zA-Z0-9!-/:-@¥[-`{-~]*$/);
constructor() {
super();
this.bindThis(this);
}
doValidate(value) {
if (!value) {
return null;
}
if (value.length <= 5) {
return '文字数不足(5文字以上)';
}
if (value.length > 10) {
return '文字数オーバー(10文字以内)';
}
if (!PasswordFieldValidator.#PATTERN.test(value)) {
return '半角英数記号以外の使用不可';
}
return null;
}
}
export class PasswordField extends ValidatableFieldDelegator {
constructor(value = null, infoMessage = '半角5文字以上10文字以下') {
super(value, PasswordFieldValidator.INSTANCE.validate, true, infoMessage);
}
}
UserProfile画面とユーザメンテナンスダイアログの双方にパスワードの入力がありますがどちらもPasswordFieldに実装されています。また、これは他のすべてのフィールドについても同様ですべてのフィールドはfieldディレクトリに実装されています
Googleカレンダーのように開始日を次の日にしたら終了日を自動でズラすCustomHook(useFromToDateTime
)を作成しています(Pickerからの入力だけであればそれほど実装は難しくないですが、今回は手入力も可としているので手入力された場合の不正に日付のバリデーションも考慮する必要があるため実装にかなり苦労しました...)
- useFromToDateTimeを使った例
const fromToDateTime = useFromToDateTime();
return (
<FtdKeyboardDatePicker id="startDate" label="開始日"
fromToDateTime={fromToDateTime} ...
...
/>
<FtdKeyboardDatePicker id="endDate" label="終了日" minDate={fromToDateTime.startDate.value}
fromToDateTime={fromToDateTime}
...
/>
上の実装はレンタル品を検索するRentalItemListPanel
の実装の一部となります。開始日, 開始時刻, 終了日, 終了時刻の4つのフィールドはuseFromToDateTime
フックで管理され複雑な相関チェックやエラーメッセージなど開始終了日時に関するすべてのことがフックに隠蔽されています。
利用者側はこれら複雑なことを気にする必要はありません。また、この開始終了日時のドメインルールはレンタル品予約など他の画面で必要となりますが、いずれもuseFromToDateTime
フックで実現されています
バックエンドのAPI呼び出しはバックエンド側が公開しているOAS(openapi.yml)を入力しとしてOpenAPI Generator
で自動生成したものを利用しています。また、自動生成されたコードに対する追加や置き換えはすべてApiClientFactory
で行い、再生成に対する考慮をしています
- ApiClientFactoryの実装の一部
class ApiClientFactory {
// constructor
constructor(baseUrl = process.env.REACT_APP_API_ENDPOINT) {
this.apiClient = new ApiClient(baseUrl);
this.apiClient.authentications = this.authTypeDef;
this.errorHandler = new ErrorHandler();
}
// methods
getAuthenticateApiFacade() {
if (!this.authenticateApiFacade) {
this.authenticateApiFacade = new AuthenticateApiFacade(
new AuthenticateApi(this.apiClient),
this.errorHandler
);
}
return this.authenticateApiFacade;
}
... snip
}
// replace ApiClient method.
ApiClient.parseDate = DateUtils.parseDateFromJsonFormat;
UserType.constructFromObject = (userType) => {
if (userType instanceof ModelUserType) {
return userType.value;
}
return userType; // String
};
ApiClientFactory.instance = new ApiClientFactory();
export { ApiClientFactory };
ApiClientFactory
の利用コード例(Loginでの利用)
const authApiFacade = ApiClientFactory.instance.getAuthenticateApiFacade();
const result = await authApiFacade.authenticate(loginId, password);
バックエンドの変更がアプリケーションの広範に及ばないようにするため、自動生成されたAPIを直接利用するのではなく、すべてfacade
を経由させるようにしています。facade
インスタンスはApiClientFactory
から取得することで、APIに対する追加や差分が常に織り込まれたインスタンスが使われるようにしています