20210725のReactに関する記事は9件です。

そのデータ、Reactで管理するか?Reduxで管理するか?

はじめに React × Reduxの組み合わせで開発をする時はデータは基本的に全てReduxのstateで管理してきました。 ところがある日、 ダイアログの表示・非表示を切り替えるためのフラグをReduxのstateに追加した時の話。 TypeScript使ってたのでそのフラグをpropsの型定義に追加、useSelectorで取得してpropsに詰めてコンポーネント間で渡して渡して...と目的のコンポーネントにたどり着かせるまでに、修正量は少ないものの直す箇所がたくさんあってかなり面倒でした。 あれ? これってReduxではなくReactのuseStateでそのコンポーネントで管理すれば楽じゃないかと思ったのですが、統一性を持たせるためにその時はReduxのstateでやり過ごしました。 どう対処するのが正解だったんだろうと思い、調べて記事にしてみようと思いました。 Reduxベストプラクティス やはりRedux公式に記載がありました。 「明確な正解」はないとのこと。 原文 There is no “right” answer for this. Some users prefer to keep every single piece of data in Redux, to maintain a fully serializable and controlled version of their application at all times. Others prefer to keep non-critical or UI state, such as “is this dropdown currently open”, inside a component's internal state. 訳 これに対する「正しい」答えはありません。 一部のユーザーは、アプリケーションの完全にシリアル化および制御されたバージョンを常に維持するために、すべてのデータをReduxに保持することを好みます。 コンポーネントの内部状態内で、「このドロップダウンは現在開いていますか」など、重要ではない状態またはUI状態を維持することを好む人もいます。 まさに今回のケース。 こちらの文章以外に上記リンク先にReduxで管理するのが良い場合の記載が箇条書きされてますが、気になったのはこの一文。 原文 Is the same data being used to drive multiple components? 訳 同じデータが複数のコンポーネントを駆動するために使用されていますか? 同じデータを複数の画面で使っているかってこと? 普段ReduxToolKitのcreateSliceを使ってるのですが、画面ごと専用のslice作っているので画面またいで共有なんてしないはず。。 そもそも画面単位でSliceを作っていたことが正しくなかった これを読んで解決しました。 原文 Root state slices should be defined and named based on the major data types or areas of functionality in your application, not based on which specific components you have in your UI. This is because there is not a strict 1:1 correlation between data in the Redux store and components in the UI, and many components may need to access the same data. Think of the state tree as a sort of global database that any part of the app can access to read just the pieces of state needed in that component. 訳 ルート状態スライスは、UIにある特定のコンポーネントではなく、アプリケーションの主要なデータ型または機能領域に基づいて定義および名前を付ける必要があります。 これは、ReduxストアのデータとUIのコンポーネントの間に厳密な1:1の相関関係がなく、多くのコンポーネントが同じデータにアクセスする必要がある場合があるためです。 状態ツリーは、アプリの任意の部分がそのコンポーネントに必要な状態の一部だけを読み取るためにアクセスできる一種のグローバルデータベースと考えてください。 なるほど。画面単位ではなく機能単位でSliceを設計するのが本来あるべき姿。 そうなるとさっきの一文も納得でした。 確かに見直せばこれ1つで良かったなと思うデータがあるので、state設計サボってしまっていたことが浮き彫りになりました。。 入力フォームの管理方法について 公式のよくある質問ページにも気になる記載がありました。 テキストボックスやセレクトボックスなど入力フォーム系の値はどこで持つのか?です。 原文 Based on those rules of thumb, most form state doesn't need to go into Redux, as it's probably not being shared between components. However, that decision is always going to be specific to you and your application. You might choose to keep some form state in Redux because you are editing data that came from the store originally, or because you do need to see the work-in-progress values reflected in other components elsewhere in the application. On the other hand, it may be a lot simpler to keep the form state local to the component, and only dispatch an action to put the data in the store once the user is done with the form. 訳 これらの経験則に基づくと、ほとんどのフォーム状態は、コンポーネント間で共有されていない可能性があるため、Reduxに入る必要はありません。 ただし、その決定は常にあなたとあなたのアプリケーションに固有のものになります。 元々ストアから取得したデータを編集しているため、またはアプリケーションの他のコンポーネントに反映されている進行中の値を確認する必要があるため、Reduxでフォームの状態を維持することを選択できます。 一方、フォームの状態をコンポーネントに対してローカルに保ち、ユーザーがフォームを使い終わったら、データをストアに配置するアクションをディスパッチする方がはるかに簡単な場合があります。 テキストボックスの場合、入力値をstateへ即時で反映させたい場合はonChangeに更新用アクションをセットすればよいし、バリデーションまで終わってからstateへ反映させたい場合はonSubmitにセットすれば良いって感じでしょうか。確かに入力フォームは、React Hook Formでバリデーションかけて使うことが多かったので、後者のパターンでした。 話が若干逸れてしまいましたが、やはりここでも画面ごとに共有する必要がない場合はReduxで管理する必要がないことに触れられています。 まとめ 基本的に グローバル(画面関係なく全体)で使うならRedux ローカル(特定の画面だけ)で使うならReact で自分の中ではひとまず腑に落ちました。 また、人によってバラバラになる気がするので、チームで開発する時はこの基準を明確にしておくことは大事かと思いました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

はじめてReduxに触れるための基礎

はじめに Reactを使ってアプリの開発を行っているとReduxという単語を目にするようになった。 知らなくてもコードを書くことはできるが、開発をする上で知っておいた方がいいと思ったので調査した。 手探りの状態で調査をしているので認識違いがあったら指摘していただけると幸いです。 Reduxとは何か Reduxとは、状態(state)管理フレームワークである。 状態管理フレームワークはアプリケーション内におけるデータの流れを管理するものである。 状態管理フレームワークを導入することで、開発の規模が大きくなったときにデータの流れを見失いづらくすることができる。 Redux以外の方法で状態管理を行う手法として、 MobX Almin のようにライブラリを使ったものもあれば、 ローカルステート(useState, useReducer等)を使った管理 HooksAPIのuseReducer, useContextを使った管理 のようにReactの機能を使用して独自で実装するもの等がある。 なぜReduxは登場したか Reduxが登場した背景として、Facebookが提唱したFluxという設計思想の出現がある。 FluxはMVCパターンの問題を解決するために提唱された。 MVCパターンで開発する場合、開発規模が拡大するとモデルやビューの依存関係が複雑化しデータの流れの管理が難しくなってしまう。 Fluxではデータの流れを「Action→Dispatcher→Store→View」という単一方向の流れに限定することで管理をしやすくしている。 ただFluxという設計は特定の実装方法を強要するものではないため、Fluxを実現するために様々な手法が検討され、その一つとしてReduxが誕生した。 Reduxの特徴 Reduxの特徴として、FluxにあったDispatcherが存在しない。(「Action→Store→View」) ActionがStateを更新する際にはStore内のReducerと呼ばれる関数を使う。 また以下の三原則が存在する。 Single source of truth アプリケーション全体の状態(state)を単独のオブジェクトとしてStoreに保存する。デバッグを容易にしたり、undo/redoの実装を容易にすることができる。 State is read-only stateは読み取り専用であり、stateを更新するにはActionをStoreにdispatchする必要がある。Viewから直接stateが変更されないことが保証されるため、テストやデバッグの際にオブジェクトであるActionを利用すればいい。 Changes are made with pure functions ReducerはActionとstateから新たなstateを作る純粋な関数である。 Reduxのデメリット シンプルなアプリケーションの場合オーバースペックになる可能性がある。 データの流れが複雑でないアプリケーションでReduxを使用すると、不必要に複雑な実装になってしまう。 CustomHookを使えばより工数の少ないシンプルな実装をすることが可能な場合もある。 参考文献 ベストな手法は? Reactのステート管理方法まとめ - ICS MEDIA SPAにおける状態管理:関数型のアプローチも取り入れるフロントエンド系アーキテクチャの変遷 - エンジニアHub|Webエンジニアのキャリアを考える! アプリの状態管理を安全に行うためのFluxとRedux (1/3):CodeZine(コードジン) Facebook の決断:MVCはスケールしない。ならば Flux だ。 Redux入門【ダイジェスト版】10分で理解するReduxの基礎 - Qiita 結局FluxやらReduxやらって何なのか個人的なまとめ - Qiita 無理してReduxを使わずに、Custom Hookで楽に状態を管理する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【CentOS7】Reactの環境を簡単に構築する

はじめに CentOS7の環境でReactの環境構築がかなり簡単に出来ました。 とりあえずReactを使ってみたい人向けになります。 まずはアップデート。 sudo yum update node.jsとnpmのインストール。 sudo yum install nodejs npm npmとnode.jsのバージョンを確認する。 npm -v # 3.10.10 node -v # v6.17.1 nパッケージをインストール このバージョンだと古く、Reactの環境構築がうまくいかないので、新しいバージョンにする必要がある。 そのために、nパッケージをインストール。 ※nパッケージをインストールするのにnpmとnode.jsが必要なので、上で古いバージョンをとりあえずインストールしています。 sudo npm install -g n nを用いてnpmとnode.jsの新しいバージョンをインストール n stable 古い方のnode.jsとnpmはいらないのでアンインストールする sudo yum remove nodejs npm 再度ログインを行う exec $SHELL -l バージョンを確認 node -v # v14.17.3 npm -v # 6.14.13 開発用のディレクトリを作成 mkdir react ディレクトリの移動 cd react create-react-appを行う create-react-appを用いて「project-name」という名前のプロジェクトを作成 npx create-react-app project-name 作成されたプロジェクトに移動 cd project-name プロジェクトを実行 npm start このような画面が出れば成功です。 以上で環境構築完了です。 自分が想定したよりもかなり楽でビックリしました。 とりあえず環境は出来たので、色々と試していこうと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BitbucketでCI/CDを設定する

概要 Bitbucket pipelineを使ってCI/CDを設定します。 今回はReactのアプリケーションの場合、どのように設定するかをご紹介します。 設定方法が分かれば、他の言語を使ったアプリケーションでも応用が効きます。 bitbucket pipelinesを有効にする。 Repository settingsからpipelinesのSettingsを選択して、有効にします。 bitbucket-pipelines.ymlを設定する 設定したいリポジトリのホームディレクトリにbitbucket-pipelines.ymlを設定します。 以下は、最低限の設定を記載しました。必要に応じて追加してください。 home/bitbucket-pipelines.yml pipelines: default: - step: caches: - node - pip script: - npm install - npm test branches: develop: - step: script: - apt-get update - ssh ec2-user@0.0.0.0 /var/www/home/scripts/devCdScript.sh staging: - step: script: - apt-get update - ssh ec2-user@0.0.0.0 /var/www/home/scripts/stgCdScript.sh master: - step: script: - apt-get update - ssh ec2-user@0.0.0.0 /var/www/home/scripts/prodCdScript.sh 一つ一つ解説して行きます。 どのブランチにマージしたかに関わらず、defaultの部分は、scriptが毎回実行されます。 なので、この部分にはテストコードなど品質を担保するためのscriptを追加すると良いです。 JavaScriptのflowなどを入れても良いと思います。 一方で、branches以下はブランチ名によって、実行されるscriptが異なります。 例えば、developブランチにプルリクエストをマージする場合、defaultとdevelopのscriptが実行されます。 ssh ec2-user@以降のIPアドレスは適宜変更してください。 homeとなっている部分もディレクトリ名を適宜変更してください。 .shファイルは各環境毎に一つずつ用意します。 デプロイ時に実行したいスクリプトをここに書きます。 home/scripts/devCdScript.sh #!/bin/bash cd /var/www/home git pull; if [ $? -eq 0 ]; then echo 'git pull success.' else echo 'git pull failure.' exit 1; fi npm install; npm run build; SSH keyの設定 Repository settingsからSSH keysを選択し、SSH keyの設定ができます。 Generate keysを選択すると公開鍵と秘密鍵を作成できます。 作成した公開鍵をサーバーに設定します。 EC2の場合、以下のファイルにコピーした公開鍵を貼り付けます。 /home/ec2-user/.ssh/authorized_keys Repository settings → SSH keysからKnown hostsを設定します。 設定したいサーバーのIPアドレスを入力して、Fetchを選択。 ここまで設定したら、プルリクエストを対象のブランチにマージした際にpipelineが動くと思います。 なお、pipeline実行時にpermission errorでfailedになった場合は、devCdScript.shなどのスクリプトファイルの権限を変更してみて下さい。 ・参考 https://ja.confluence.atlassian.com/bitbucket/configure-bitbucket-pipelines-yml-792298910.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

reactテストでハマった話 getting TypeError: expect(...).toBeInTheDocument is not a function

タイトル通りです。 2021/07/25において確認された事象。 ライブラリのかみ合わせが良くなかった様子です。 npx create-react-app --template typescript にてreact × typescriptのプロジェクトを作成した後。 各コマンドの確認を行おうと思い、 npm run test を実行したら以下のようなエラーを吐きました。 getting TypeError: expect(linkElement).toBeInTheDocument is not a function エラーをまんま検索すると、 ドンピシャで解決策ありました。 https://github.com/testing-library/jest-dom/issues/167 ちなみに初期のバージョンは以下 "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", これから、 "@testing-library/jest-dom": "^5.5.0", "@testing-library/react": "^10.0.3", こちらに変更することで解決できました。 バージョンのかみ合わせが悪い場合のエラーはハマると中々抜け出せないので大変ですね。 インストール時にエラー吐かれるとホントに困るので早く修正してほしいです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelで同じ実装をドメイン駆動設計(DDD)とMVCで比較してみた(フロントはReact)

概要 Laravelで同じ実装をドメイン駆動設計(DDD)とMVCで比較してみました。フロントはReactで実装しています。設計思想はドメイン駆動設計(以下DDD)、アーキテクチャはクリーンアーキテクチャを採用しました。LaravelのWebアプリケーションをDDDやクリーンアーキテクチャで構築すると、MVCで構築するのと比べて実装量やファイル数が増えるのでは?また、処理速度も遅くなるのでは?という懸念を抱いておりました。ただ、それは実際に試してみないとわからないと思い、同じ機能をDDD、MVCで実装し、比較する事にしました。やるなら実践的なプロジェクトが望ましいし、イメージし易いと考えて、題材はECのデモサイトにしました。構築パターンは、DDD、軽量DDD、MVCの3パターンです。DDD、軽量DDDの違いは、DDDはクリーンアーキテクチャに忠実に実装したのに比べて、軽量DDDはプレゼンター、ViewModel、UseCaseDataを共通クラスに実装して実装量の削減をしています。 結果は下にまとめますが、やはり懸念していた通りパフォーマンスはMVC > 軽量DDD > DDDという結果で、ファイル数やステップ数もDDD > 軽量DDD > MVCと予測通りでした。ただ、パフォーマンスは思っていた程DDDが悪い結果では無かったです。 ちなみに便宜上「軽量DDD」という文言を使っておりますが、 本検証で述べている「軽量DDD」は、参考書籍「エリック・エヴァンスのドメイン駆動設計」でアンチパターンとされているDDDの技術要素だけ取り入れた「軽量DDD」と同意ではありません。(私の理解度が低くDDDを実践しきれていない可能性は否定できませんが・・・)本検証の「軽量DDD」は、ドメイン層については「DDD」の基本構造と全く同じであるためです(ViewModelへのデータ変換用の実装は追加していますが、モデリングは全く同じ)。本検証における「軽量DDD」で実装を削っている箇所は、あくまでもクリーンアーキテクチャの層をまたいだデータ変換処理となります。 なお、フロントはbladeを使っているものの、ほぼ全てReactで実装しました。SPAにも出来たのですが、本プロジェクトの趣旨ではないのでSPAにはあえてしていません。Reactより使い慣れてるVue.jsの方が実装しやすかったのですが、いい機会だったのでついでにReactもかじってみました。material uiを使うことで、スマホアプリのようなリッチなUI表現に仕上がりました。かつ、レスポンシブにも対応しています。 ソースコード laravel-ddd-sample 実装するに当たって、nrslib/StrictLaraClean、shin1x1/laravel-ddd-samplのお二方のgithubのソースを大いに参考にさせて頂きました。とても勉強になりました。ドメインモデルは最初ただのDTOで、定義する事が目的化するだけだし、Eloquentで取得したデータをそのまま使えばこの実装は不要では?と思ってましたが大間違いでした。参考書籍「エリック・エヴァンスのドメイン駆動設計」によると、ドメイン層のモデリングと実装こそがDDDの真骨頂で、最も重要との事です。今考えたら「ドメイン駆動設計」なので、そりゃそうですよね・・・ドメイン駆動設計からドメインモデルを省いたら、「駆動設計」になって駆動させる動力が無くなってしまう・・・(たこの入っていないたこ焼きみたいな・・・)。ドメインモデルはただのプロパティとそのget、setしかないDTOとは似て否なるもので、ドメインモデルにドメイン領域のビジネスルールや仕様等を凝集して実装していくと理解しました。DTOを作っているみたいに単調で面倒ですが、頑張って実装しました。実際やってみると、ドメインモデルが保持するデータと、それにまつわるメソッドが1つのクラスに凝集され、データと操作の関連がひと目で分かる造りになりました。オブジェクト指向でプログラミングしているという実感を感じました(まだ不十分でしたらすみません・・・)。ただ、仕様をやルールの実装が無く、プロパティとそのget、setしかないValueaObjectがあるので、これらは共通クラスで実装削減してもいいかも知れません。 なお、参考書籍「エリック・エヴァンスのドメイン駆動設計」でレイヤ化アーキテクチャ(クリーンアーキテクチャの前身)を推奨している箇所があるのですが、DDDを実践するのにレイヤ化アーキテクチャは必須ではなく、あくまでもレイヤ化アーキテクチャはドメイン層を隔離する手段であり、ドメイン層を隔離出来るなら他のアプローチでもいいと書かれてました。どうしてもレイヤ化アーキテクチャのような技術的アプローチに着目して、それを用いることが目的化しがちですが、ドメイン層の隔離とそのモデリング、実装こそが最も重要との事です。ただ、DDDを実践するに当たってドメイン層の隔離は必須との事なので、何かしらの手段でドメイン層の隔離が必要です。本検証では、そのアーキテクチャにクリーンアーキテクチャを採用しています。 デモサイト 以下にデモサイトを構築しました。 - DDD http://www.take14.shop/ddd/product/list - 軽量DDD http://www.take14.shop/lightddd/product/list - MVC http://www.take14.shop/mvc/product/list 基本認証をかけているので、ご覧になりたい方は以下のID、パスワードをご利用下さい ID パスワード take14 UP6-hL$Z.8ghBn?c ミドルウェア ミドルウェア バージョン Apache 2.4.34 PHP 8.0.7 PostgreSQL 13.3 npm 6.14.12 フレームワーク・ライブラリ フレームワーク・ライブラリ バージョン Laravel 8.48.1 React 17.0.2 material-ui 4.11.4 typescript 4.3.4 sass 1.35.1 ディレクトリ構成 packages/Common 共通モジュール packages/DDDEcSample DDDのモジュール packages/LightDDDEcSample 軽量DDDのモジュール packages/MvcSample MVCのモジュール resources/ts ReactのTypeScriptコード resources/sass Reactで使用するsass tests/Unit/DDDEcSample DDDのUnitTest tests/Unit/LightDDDEcSample 軽量DDDのUnitTest tests/Unit/MvcSample MVCのUnitTest DDDModel UmbrelloというUMLツールで設計したクラス図、シーケンス図を保存した「Umbrelloモデルファイル.xmi」、それらの画像出力した画像ファイル プロジェクトの環境構築について マイグレーションファイル、シーダーファイルを作っているので、それを用いれば環境構築してデバッグ可能です。 以下コマンドで環境構築出来ます。事前に.env.exampleをコピーして.envを作成し、APP_KEY、DB_〜、MAIL_〜を適切に設定する必要があります。 composer install npm insall npm run dev php artisan migrate:fresh php artisan db:seed 機能概要 商品一覧 商品詳細 カート 注文 設計 Umbrelloというソフトでクリーンアーキテクチャのベースとなるクラス図、シーケンス図を作成し、PHPでエクスポートしたソースコードをベースに実装を進めました。本当にIF部分だけのスケルトン生成なので、わざわざクラス図から生成する必要も無いのですが、検証の意味でトライしてみました。クリーンアーキテクチャのクラス図と見比べつつクラスを作成出来たので、やって良かったと考えています。「DDDModel」というフォルダにその結果を収めております。 クラス図 正確にはクリーンアーキテクチャではクラス図左下のViewクラスが画面出力を行うのですが、Laravelのコントローラーはコントローラーのメソッドの戻り値でviewを返却する作りとなっているため、そこは妥協してViewクラスはviewを返却し、それをコントローラーのメソッドまで戻り値で引き継いでコントローラーで返却する造りとしています。やろうと思えばViewクラスでviewを保持させて、Laravelフレームワークを改造してそのviewを使ってレンダリングさせることも出来ますが、そうしたトリッキーな事をするとフレームワークにどういう影響が出るかがわからないので、そこはフレームワークの習わしに従う選択をしています。参考文献「エリック・エヴァンスのドメイン駆動設計」にもフレームワークとは争うなと書いていたので、妥協可能なところは妥協するのがいい選択だと考えます。 シーケンス図 集約 CartはCartItemを保持。CartItemは、Productを保持するといった集約にしました。Orderも同じで、OrderItemを保持し、OrderItemはProductを保持するという集約にしています。当初CartItemはCartDetailという名称にしていましたが、DDDの要のユビキタス言語を実践するのであれば不自然な言葉使いだとはたと気付いて、CartItemにしました。それでも不自然かも知れませんが、DDD初学者の為ご容赦下さい・・・手続き型の実装に馴染んでる私の身としてはCartDetailという名前がしっくりしていたのですが、自然言語としては不自然な呼び名だなと思いました。 参考文献、書籍によると、集約に対する操作は原則集約ルートから行うとあったため、それに気をつけつつ実装しました(参考書籍「ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本」より)。(もし間違っていたら申し訳ありません) ドメインサービス カートアイテムを追加する処理を実装する際、CartReposutoryで取得した結果に、CartItemを追加する必要がありますが、その際Productを取得して設定する必要があります。ProductReposutoryを使ってProductを取得しますが、複数のRepositoryを操作する必要があり、それをUseCase層に実装するのはぎこちなさを感じました。カートに商品を追加する。という一つのUseCaseを実現するのに、UseCaseがProductIdからProductRepositoryを使ってProductを取り出し、それをCartオブジェクトに追加する。という複数のReoositoryを使った一連の操作を行うのですが、そうした一連の操作もドメイン知識なのではと思った為です。そこで、CartServiceというドメインサービスを作りました。今こうして書いていて、UseCaseでProductRepositoryからProductを取り出し、Cartに対してそれを追加するメソッドを実装してコールする形でもよかったなと思い直しており、ドメインサービスを作るまでも無かったのかも知れません、、、ただ、あくまでも検証なので、実装パターンを試すという意味ではこれはこれで良かったと考えています。なお、参考書籍ではドメインサービスは極力使わないで、ドメインモデルで表現・実装するべきである旨が強調して書かれていました。その気になれば、全部ドメインサービスに書けてしまう為です。それによってドメイン貧血症という、仕様やドメイン知識が何も実装されていないドメインモデルが出来てしまうそうです(参考書籍「ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本」より)。何をドメインモデルで実装し、どういった場合にドメインサービスを用いるかは、慎重な検討と熟練した判断や経験が必要になりそうです。 バリデーションチェック ドメインモデルのバリデーションチェックは、LaravelのValidatorを使用しても良かったのですが、あえてフレームワークには依存せず独自の実装を選択しました。それはそれで良かったのですが、注文のバリデーションで少し苦戦しました。ドメインモデルのコンストラクタでバリデーションし、エラーの場合はExceptionをthrowする実装としたのですが、例えば氏名、名前が必須入力なのに両方未入力でバリデーションエラーとなった場合、両方のエラーを検知して画面表示したいのですが、try catch一つだと1個のエラーしか検知して画面表示出来ません。ユーザーにとってはエラーを一つずつしか解消出来ず、これではコンバージョン低下に繋がります。モデル毎にtry catchすればいいですが、ソースコードが冗長になりスマートではありません。そこで、ModelFactoryというドメインサービスを作りました。そのファクトリーにバリデーションしたいモデルクラスを配列で渡したら、配列のモデルクラスのバリデーションを全部やってエラーがあればthrowまでやってくれるクラスにしました。エラーが無ければ、インスタント化したモデルクラスをModelFactoryから取得も出来ます。これでドメインモデルのバリデーションチェックと、そのインスタンス化をスマートに実装出来ました。 自動テスト 全機能に対するUnitTestを実装しました。TDDも勉強していた為、実装の過程で実践しました。何度かリファクタリングをしたのですが、その度にグリーン(自動テストオールOK)だったのが、レッド(自動テストオールNG)になりました。そこからグリーンにするまで修正したら、画面テストがキチンと動作する事を確認出来ました。UnitTestで非常に信頼性の高い検証を行える事が、この事から立証出来たと思います。キチンとUnitTestを実装したら、機能追加やリファクタリングを自信を持って行えると思います。 ステップ数 種別 ファイル数 ステップ数 DDD 152 3,444 軽量DDD 102 2,298 MVC 23 725 上記の通り、懸念していたとおりDDDはファイル数、ステップ数共に圧倒的にMVCと比較して多くなる結果となりました。一方でDDDは1クラスの凝集度は高く、ほとんど50ステップ未満なので、実装工数は数ほどのインパクトがあるわけではありません。DDDでは決まった形式でクラスを実装するケースも多く、ほとんどコピペやロジックの組み立ての時間がかからず作業的に実装するケースもあったので、ステップ数の割には工数はかかっていなかったりします。また、クラスの凝集度が高い分、どこに何が記載されているか、このクラスの役割は何かがひと目で把握しやすいメリットもあります。実装量、ファイル数が多い=悪ではないという事を言いたいのですが、それぞれメリット、デメリットがあります。詳細は以下の「メリット・デメリット」でまとめたいと思います。 パフォーマンス app/Http/Middleware/PerformanceLog.php 上記ファイルにコントローラー実行前後にログを出力し、処理時間、処理に使用したメモリを計測できるようにしました。その計測結果を以下にまとめます。 商品一覧のように大量のレコードをDBから取得して表示する箇所は、MVC > 軽量DDD > DDDという結果でした。これは、DDDの場合取得したレコードをドメインモデルに変換するコストがかかるためです。 一方意外だったのは、その他はほとんど軽量DDDに軍配が上がって軽量DDD > MVC > DDDという結果になった点です。詳しくは分析しておりませんが、MVCは手続き型で実装しているので、処理効率が悪い部分があるのでしょうか。もしくは単なる誤差という可能性もあります。いずれにしても気にするほどの差ではありません。商品一覧だけ100ms単位で差が出ているので、大量データ取得・表示のパフォーマンスに影響が出ると考えるのが良さそうです。なお、注文確定処理が2000ms前後と時間がかかっているのは、注文確定のお知らせメール送信処理が含まれるためです。 結果一覧 機能 DDD時間 DDDメモリ 軽量DDD時間 軽量DDDメモリ MVC時間 MVCメモリ 商品一覧画面表示 289ms 6299KB 178ms 5049KB 85ms 2735KB 商品詳細画面表示 63ms 650KB 18ms 600KB 23ms 627KB カート追加API 153ms 419KB 19ms 418KB 51ms 839KB カート画面表示 36ms 468KB 10ms 382KB 21ms 640KB 注文画面表示 20ms 468KB 9ms 382KB 20ms 640KB 注文バリデーションAPI 34ms 422KB 31ms 506KB 58ms 814KB 注文確定処理 2552ms 1334KB 2517ms 1061KB 1949ms 1376KB エビデンス DDD [2021-07-23 23:00:19] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\ProductController@list メモリ 6298.953125KB [2021-07-23 23:00:19] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\ProductController@list 288.98096084595ミリ秒 [2021-07-23 23:07:51] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\ProductController@detail メモリ 649.6484375KB [2021-07-23 23:07:51] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\ProductController@detail 62.504053115845ミリ秒 [2021-07-23 23:08:28] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\CartController@add メモリ 418.6953125KB [2021-07-23 23:08:28] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\CartController@add 153.38611602783ミリ秒 [2021-07-23 23:08:28] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\CartController@list メモリ 468.3046875KB [2021-07-23 23:08:28] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\CartController@list 36.375999450684ミリ秒 [2021-07-23 23:09:27] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\OrderController@form メモリ 468.3046875KB [2021-07-23 23:09:27] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\OrderController@form 19.876003265381ミリ秒 [2021-07-23 23:09:37] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@check メモリ 421.9609375KB [2021-07-23 23:09:37] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@check 33.753156661987ミリ秒 [2021-07-23 23:09:44] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@save メモリ 1334.2734375KB [2021-07-23 23:09:44] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@save 2552.1411895752ミリ秒 軽量DDD [2021-07-23 23:11:33] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\ProductController@list メモリ 5049.25KB [2021-07-23 23:11:33] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\ProductController@list 177.73199081421ミリ秒 [2021-07-23 23:11:36] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\ProductController@detail メモリ 600.1328125KB [2021-07-23 23:11:36] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\ProductController@detail 18.326997756958ミリ秒 [2021-07-23 23:11:38] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\CartController@add メモリ 417.765625KB [2021-07-23 23:11:38] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\CartController@add 19.247770309448ミリ秒 [2021-07-23 23:11:38] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\CartController@list メモリ 382.2265625KB [2021-07-23 23:11:38] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\CartController@list 10.499000549316ミリ秒 [2021-07-23 23:11:40] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\OrderController@form メモリ 382.2265625KB [2021-07-23 23:11:40] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\OrderController@form 9.2220306396484ミリ秒 [2021-07-23 23:11:49] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@check メモリ 506.1171875KB [2021-07-23 23:11:49] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@check 30.539989471436ミリ秒 [2021-07-23 23:11:53] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@save メモリ 1061.2578125KB [2021-07-23 23:11:53] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@save 2517.3659324646ミリ秒 MVC [2021-07-23 23:10:28] local.DEBUG: Packages\MvcSample\App\Http\Controllers\ProductController@list メモリ 2735.3828125KB [2021-07-23 23:10:28] local.DEBUG: Packages\MvcSample\App\Http\Controllers\ProductController@list 84.88392829895ミリ秒 [2021-07-23 23:10:31] local.DEBUG: Packages\MvcSample\App\Http\Controllers\ProductController@detail メモリ 627.4375KB [2021-07-23 23:10:31] local.DEBUG: Packages\MvcSample\App\Http\Controllers\ProductController@detail 22.572994232178ミリ秒 [2021-07-23 23:10:34] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\CartController@add メモリ 838.9921875KB [2021-07-23 23:10:34] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\CartController@add 50.947904586792ミリ秒 [2021-07-23 23:10:34] local.DEBUG: Packages\MvcSample\App\Http\Controllers\CartController@list メモリ 639.7109375KB [2021-07-23 23:10:34] local.DEBUG: Packages\MvcSample\App\Http\Controllers\CartController@list 20.750045776367ミリ秒 [2021-07-23 23:10:36] local.DEBUG: Packages\MvcSample\App\Http\Controllers\OrderController@form メモリ 639.7109375KB [2021-07-23 23:10:36] local.DEBUG: Packages\MvcSample\App\Http\Controllers\OrderController@form 20.180940628052ミリ秒 [2021-07-23 23:10:45] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\OrderController@check メモリ 813.6875KB [2021-07-23 23:10:45] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\OrderController@check 57.819128036499ミリ秒 [2021-07-23 23:10:53] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\OrderController@save メモリ 1375.6796875KB [2021-07-23 23:10:53] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\OrderController@save 1949.3370056152ミリ秒 メリット・デメリット DDD、軽量DDD、MVCのメリット・デメリットを比較した結果を以下にまとめます。DDDの記述ボリュームが多くなりましたが、DDDよりは、軽量DDDを採用するほうがいいと考えます。軽量DDDはDDDのメリットを全て継承しつつ、DDDのファイル数、ステップ数が多くなるデメリットをある程度軽減出来ている為です。MVCはファイル数、ステップ数が圧倒的に少なく、実装工数を考えるとMVC一択と判断しがちですが、メンテナンス性を考えると一口にはそうは言いきれません。また、DDDや軽量DDDはファイル数、ステップ数が多いですが、その数字=工数とも限りません。殆どが1クラス50ステップ未満の凝集度の高いクラスの為、実装効率が高く実行工数は数ほどのインパクトは無いと考えます。また、DDDでは決まった形式でクラスを実装するケースも多く、ほとんどコピペやロジックの組み立ての時間がかからず作業的に実装するケースもあったので、ステップ数の割には工数はかかっていなかったりします。なお、カートのセッション取得箇所を「SessionRepository::get」でgrepすると、DDD、軽量DDDは共に1箇所ですが、MVCは4箇所でした。設計次第で改善出来るかもしれませんが、こうした実装の重複はMVCや手続き型の実装では避けづらいです。処理の重複や類似の仕様が様々な箇所に分散して実装されていくと、変更しづらく影響範囲も読みづらいプロジェクトになっていきます。DDDが目指すのは変更や拡張をしやすい「しなやかな設計(参考文献「エリック・エヴァンスのドメイン駆動設計」より引用)」ですが、MVCや手続き型の実装ではそうした理想を目指すのが困難だと考えます。運用期間が短かったり、手早く短時間で仕上げたいWEBアプリケーションであれば、MVCでさっと作るのが向いているかもしれませんが、年単位で運用をしながら機能拡張・追加を繰り返して成熟させていきたいWEBアプリケーションやWEBサービスの場合は、DDDか軽量DDDで構築したほうが良さそうだと考えます。 DDD メリット クラスの凝集度が高くなり、1クラス当りのステップ数は少なくなって可読性、メンテナンス性が高くなる どこに何を実装するかの指針が決まっており、クラスの責務がはっきり分かる形で実装出来るため、ソースコードの重複を避けやすい。(完全には防げないが、重複が発生したらそれを検知してリファクタリングしやすい。例えば手続き型の実装だと関数単位で機能が実装されていって関数が増えていき、その中に重複処理があっても発見しにくくなる。) 依存関係逆転の原則を用いることで、疎結合な実装となる 疎結合な為UnitTestを実装しやすい 疎結合な為、DBの変更、フレームワークの変更、HTML・API・コンソール・ViewComposer等のUIやコントローラーの変更をしても、ユースケース層、ドメイン層は影響を受けずそのまま使える。 デメリット ファイル数、ステップ数が多くなる ドメインモデル→UseCaseData→ViewModelとレイヤーをまたぐ毎にデータの変換が必要となり、パフォーマンスが低下する 軽量DDD メリット 高凝集、疎結合、UnitTestの実装のしやすさはDDDのメリットを継承 DDDに比べてファイル数、実装量が削減される。 ドメインモデル→ViewModelを、ドメインモデルの配列化で実現し、DDDと比べて変換回数が少なくなってパフォーマンス改善した デメリット DDDに比べてステップ数が2/3になるが、それでもMVCの3倍以上のステップ数となる DDDと比べてパフォーマンス改善したものの、大量データ取得時のパフォーマンスはMVCと比較すると悪い MVC メリット DDD、軽量DDDと比べてステップ数、ファイル数が圧倒的に少ない(DDDの1/4以下、軽量DDDの1/3以下) DDDと比べてデータの変換が無い為、処理速度が早い傾向にある(特に大量データ取得時) デメリット コントローラー、モデルクラスにビジネスロジックが集中し、メンテナンス性が悪い神クラスが生まれやすくなる 関数単位で機能が実装されていって関数が増えてき、関数同士の関連がわかりづらくなったり、類似処理の重複を避けづらくなってくる フレームワークと密結合な実装となるため、UnitTestが実装し辛く、実装するには工夫が必要となる Eloquentのパフォーマンス 本記事の趣旨ではありませんが、Eloquentのパフォーマンスについて検証結果を述べます。商品一覧のデータ取得は最初Eloquentのモデルクラスで取得しておりましたが、DB::tableで取得する方式へ変更しました。理由はパフォーマンスの悪さです。どうもEloquentは大量データを取得する際のパフォーマンスが悪い模様です。理由はEloquentのインスタンスを作成するコストが嵩むからという見解と、whereInで大量にパラメータ指定する際のデータバインドのコストが原因という見解があるようです(参考サイト「laracasts」の記事より)。今回のクエリはwhereInを使ってなくても遅かったため、Eloquentのインスタンス化のコストが主な遅延要因ではないかと考えます。本記事のデモサイトでは、DB::tableでこの問題に対処しました。それでもほとんど使い方はEloquentのモデルクラスと同等なので支障はありませんでした。なお、この問題が本記事のパフォーマンス検証に影響しないように、DDD、軽量DDD、MVC全て、商品一覧だけはdb::tableを使用するコードでパフォーマンス計測しております。 React 仕事でVue.jsの採用経験があるためVue.jsのほうが慣れておりますが、いい機会なのでReactに挑戦しました。仕事でVue.jsかReactのどちらを使用するか検討した際、Vue.jsのほうがデザイナーにも受け入れてもらいやすそうだと感じてVue.jsを採用しました。あと、私自身JSXアレルギーがあって、Vue.jsのようにテンプレート記法の方が既存の技術(HTML、CSS、js、jQueryを用いた実装)に近い感覚で実装出来て、わかりやすく実装しやすいと思っていました。 ところが今回Reactに挑戦してみて、JSXアレルギーは誤解だったと考えを改めました。jsコードとHTMLをシームレスに書ける事の便利さに驚きました。JSXはプログラミングしている感覚でHTMLを記述出来ます。なんと言っても自由度の高さです。Vue.jsはHTMLはtemplate、jsロジックはscript、CSSはstyleタグ内に記述するように決まっています。そして、jsロジックの実装ルールも定まっていて、それに沿って実装します。Reactはというと・・・一定のルールはあるものの、かなり自由に実装できます。ただこれは副作用もあります。自由ということは、きちんとコーディング規約を作ったり実装方針や設計を定めないと、メンテナンス性の悪いコードも作れてしまいます。MVCはビューとロジックを別で実装する事で、高いメンテナンス性を実現し、プログラマーとデザイナーが分業しやすくなりました。ReactはHTMLとjsを一緒に書けるのは便利な反面、キチンと設計、実装しないとビューとロジックが混雑してデザイナー、プログラマーの分業に支障が出たり、可読性、メンテナンス性の低下を招くと思います。一長一短ですね・・・ あと、元々懸念していたデザイナーにはハードルが高い点は当たっています。デザイナーよりはプログラマー寄りのアーキテクチャーで、プログラマーは幸せになれますが、デザイナーは敷居が高く感じるかもしれません。ただ、ReactやVue.jsの登場で、フロントエンドで出来ることは大幅に増えました。一昔前はHTML、CSSで静的コンテンツを制作し、jsやjQueryで動的コンテンツをちょこっと色づけという感じでしたが、ReactやVue.jsでがっつりフロンドで動的コンテンツを実装し、サーバーサイドはREST APIだけ提供し、データの永続化とその復元といったバックエンド処理に徹するという構成も今では珍しくありません。昨今GraphQLという選択肢もあり、フロントエンドに実装の比重が移りつつあります(GraphQLを採用すると、今回検証したDDDも必要無くなるかも・・・)。SEO対策の点ではSPAは不安もあるのでその点十分考慮する必要はありますが、SSRという解決策も用意されています。その事を考えたら、デザイナーもガリガリとプログラミングに参画するのが時代の流れかも知れません。そうした時代の流れに乗るのであれば、Reactも十分選択肢になり得ると考えます。 今回の趣旨では無いものの、Reactに挑戦してみてReactが広く受け入れられている理由を肌で感じました。少し実装ルールを勉強したら、あとは直感的にどんどん実装していけました。ただ、初めてのReactなのでコード品質は悪いのでそこはご容赦下さい。本格的に導入するには、フォルダ設計、実装方針の策定、CSSのツールの選定等々、色々と事前に勉強・検討する必要がありそうです。 参考文献・記事・ソースコード エリック・エヴァンスのドメイン駆動設計 実践ドメイン駆動設計 (Object Oriented SELECTION) テスト駆動開発 ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本 ドメイン駆動設計 モデリング/実装ガイド nrslib/StrictLaraClean DDDパターンを活用した Laravelアプリケーション開発 shin1x1/laravel-ddd-sampl laracasts
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelで同じ実装をドメイン駆動設計(ddd)とMVCで比較してみた(フロントはReact)

概要 Laravelで同じ実装をドメイン駆動設計(ddd)とMVCで比較してみました。フロントはReactで実装しています。設計思想はドメイン駆動設計(以下ddd)、アーキテクチャはクリーンアーキテクチャを採用しました。LaravelのWebアプリケーションをdddやクリーンアーキテクチャで構築すると、MVCで構築するのと比べて実装量やファイル数が増えるのでは?また、処理速度も遅くなるのでは?という懸念を抱いておりました。ただ、それは実際に試してみないとわからないと思い、同じ機能をddd、MVCで実装し、比較する事にしました。やるなら実践的なプロジェクトが望ましいし、イメージし易いと考えて、題材はECのデモサイトにしました。構築パターンは、ddd、軽量ddd、MVCの3パターンです。ddd、軽量dddの違いは、dddはクリーンアーキテクチャに忠実に実装したのに比べて、軽量dddはプレゼンター、ViewModel、UseCaseDataを共通クラスに実装して実装量の削減をしています。 結果は下にまとめますが、やはり懸念していた通りパフォーマンスはMVC > 軽量ddd > dddという結果で、ファイル数やステップ数もddd > 軽量ddd > MVCと予測通りでした。ただ、パフォーマンスは思っていた程dddが悪い結果では無かったです。 なお、フロントはbladeを使っているものの、ほぼ全てReactで実装しました。SPAにも出来たのですが、本プロジェクトの趣旨ではないのでSPAにはあえてしていません。Reactより使い慣れてるVue.jsの方が実装しやすかったのですが、いい機会だったのでついでにReactもかじってみました。material uiを使うことで、スマホアプリのようなリッチなUI表現に仕上がりました。かつ、レスポンシブにも対応しています。 ソースコード laravel-ddd-sample 実装するに当たって、nrslib/StrictLaraClean、shin1x1/laravel-ddd-samplのお二方のgithubのソースを大いに参考にさせて頂きました。とても勉強になりました。ドメインモデルは最初ただのDTOで、定義する事が目的化するだけだし、Eloquentで取得したデータをそのまま使えばこの実装は不要では?と思ってましたが大間違いでした。参考書籍「エリック・エヴァンスのドメイン駆動設計」によると、ドメイン層のモデリングと実装こそがdddの真骨頂で、最も重要との事です。今考えたら「ドメイン駆動設計」なので、そりゃそうですよね・・・ドメイン駆動設計からドメインモデルを省いたら、「駆動設計」になって駆動させる動力が無くなってしまう・・・(たこの入っていないたこ焼きみたいな・・・)。ドメインモデルはただのプロパティとそのget、setしかないDTOとは似て否なるもので、ドメインモデルにドメイン領域のビジネスルールや仕様等を凝集して実装していくと理解しました。DTOを作っているみたいに単調で面倒ですが、頑張って実装しました。実際やってみると、ドメインモデルが保持するデータと、それにまつわるメソッドが1つのクラスに凝集され、データと操作の関連がひと目で分かる造りになりました。オブジェクト指向でプログラミングしているという実感を感じました(まだ不十分でしたらすみません・・・)。ただ、仕様をやルールの実装が無く、プロパティとそのget、setしかないValueaObjectがあるので、これらは共通クラスで実装削減してもいいかも知れません。 なお、参考書籍「エリック・エヴァンスのドメイン駆動設計」でレイヤ化アーキテクチャ(クリーンアーキテクチャの前身)を推奨している箇所があるのですが、dddを実践するのにレイヤ化アーキテクチャは必須ではなく、あくまでもレイヤ化アーキテクチャはドメイン層を隔離する手段であり、ドメイン層を隔離出来るなら他のアプローチでもいいと書かれてました。どうしてもレイヤ化アーキテクチャのような技術的アプローチに着目して、それを用いることが目的化しがちですが、ドメイン層の隔離とそのモデリング、実装こそが最も重要との事です。ただ、dddを実践するに当たってドメイン層の隔離は必須との事なので、何かしらの手段でドメイン層の隔離が必要です。本検証では、そのアーキテクチャにクリーンアーキテクチャを採用しています。 デモサイト 以下にデモサイトを構築しました。 - ddd http://www.take14.shop/ddd/product/list - 軽量ddd http://www.take14.shop/lightddd/product/list - MVC http://www.take14.shop/mvc/product/list 基本認証をかけているので、ご覧になりたい方は以下のID、パスワードをご利用下さい ID パスワード take14 UP6-hL$Z.8ghBn?c ミドルウェア ミドルウェア バージョン Apache 2.4.34 PHP 8.0.7 PostgreSQL 13.3 npm 6.14.12 フレームワーク・ライブラリ フレームワーク・ライブラリ バージョン Laravel 8.48.1 React 17.0.2 material-ui 4.11.4 typescript 4.3.4 sass 1.35.1 ディレクトリ構成 packages/Common 共通モジュール packages/DDDEcSample dddのモジュール packages/LightDDDEcSample 軽量dddのモジュール packages/MvcSample MVCのモジュール resources/ts ReactのTypeScriptコード resources/sass Reactで使用するsass tests/Unit/DDDEcSample dddのUnitTest tests/Unit/LightDDDEcSample 軽量dddのUnitTest tests/Unit/MvcSample MVCのUnitTest DDDModel UmbrelloというUMLツールで設計したクラス図、シーケンス図を保存した「Umbrelloモデルファイル.xmi」、それらの画像出力した画像ファイル プロジェクトの環境構築について マイグレーションファイル、シーダーファイルを作っているので、それを用いれば環境構築してデバッグ可能です。 以下コマンドで環境構築出来ます。事前に.env.exampleをコピーして.envを作成し、APP_KEY、DB_〜、MAIL_〜を適切に設定する必要があります。 composer install npm insall npm run dev php artisan migrate:fresh php artisan db:seed 機能概要 商品一覧 商品詳細 カート 注文 設計 Umbrelloというソフトでクリーンアーキテクチャのベースとなるクラス図、シーケンス図を作成し、PHPでエクスポートしたソースコードをベースに実装を進めました。本当にIF部分だけのスケルトン生成なので、わざわざクラス図から生成する必要も無いのですが、検証の意味でトライしてみました。クリーンアーキテクチャのクラス図と見比べつつクラスを作成出来たので、やって良かったと考えています。「DDDModel」というフォルダにその結果を収めております。 クラス図 正確にはクリーンアーキテクチャではクラス図左下のViewクラスが画面出力を行うのですが、Laravelのコントローラーはコントローラーのメソッドの戻り値でviewを返却する作りとなっているため、そこは妥協してViewクラスはviewを返却し、それをコントローラーのメソッドまで戻り値で引き継いでコントローラーで返却する造りとしています。やろうと思えばViewクラスでviewを保持させて、Laravelフレームワークを改造してそのviewを使ってレンダリングさせることも出来ますが、そうしたトリッキーな事をするとフレームワークにどういう影響が出るかがわからないので、そこはフレームワークの習わしに従う選択をしています。参考文献「エリック・エヴァンスのドメイン駆動設計」にもフレームワークとは争うなと書いていたので、妥協可能なところは妥協するのがいい選択だと考えます。 シーケンス図 集約 CartはCartItemを保持。CartItemは、Productを保持するといった集約にしました。Orderも同じで、OrderItemを保持し、OrderItemはProductを保持するという集約にしています。当初CartItemはCartDetailという名称にしていましたが、dddの要のユビキタス言語を実践するのであれば不自然な言葉使いだとはたと気付いて、CartItemにしました。それでも不自然かも知れませんが、ddd初学者の為ご容赦下さい・・・手続き型の実装に馴染んでる私の身としてはCartDetailという名前がしっくりしていたのですが、自然言語としては不自然な呼び名だなと思いました。 参考文献、書籍によると、集約に対する操作は原則集約ルートから行うとあったため、それに気をつけつつ実装しました(参考書籍「ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本」より)。(もし間違っていたら申し訳ありません) ドメインサービス カートアイテムを追加する処理を実装する際、CartReposutoryで取得した結果に、CartItemを追加する必要がありますが、その際Productを取得して設定する必要があります。ProductReposutoryを使ってProductを取得しますが、複数のRepositoryを操作する必要があり、それをUseCase層に実装するのはぎこちなさを感じました。カートに商品を追加する。という一つのUseCaseを実現するのに、UseCaseがProductIdからProductRepositoryを使ってProductを取り出し、それをCartオブジェクトに追加する。という複数のReoositoryを使った一連の操作を行うのですが、そうした一連の操作もドメイン知識なのではと思った為です。そこで、CartServiceというドメインサービスを作りました。今こうして書いていて、UseCaseでProductRepositoryからProductを取り出し、Cartに対してそれを追加するメソッドを実装してコールする形でもよかったなと思い直しており、ドメインサービスを作るまでも無かったのかも知れません、、、ただ、あくまでも検証なので、実装パターンを試すという意味ではこれはこれで良かったと考えています。なお、参考書籍ではドメインサービスは極力使わないで、ドメインモデルで表現・実装するべきである旨が強調して書かれていました。その気になれば、全部ドメインサービスに書けてしまう為です。それによってドメイン貧血症という、仕様やドメイン知識が何も実装されていないドメインモデルが出来てしまうそうです(参考書籍「ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本」より)。何をドメインモデルで実装し、どういった場合にドメインサービスを用いるかは、慎重な検討と熟練した判断や経験が必要になりそうです。 バリデーションチェック ドメインモデルのバリデーションチェックは、LaravelのValidatorを使用しても良かったのですが、あえてフレームワークには依存せず独自の実装を選択しました。それはそれで良かったのですが、注文のバリデーションで少し苦戦しました。ドメインモデルのコンストラクタでバリデーションし、エラーの場合はExceptionをthrowする実装としたのですが、例えば氏名、名前が必須入力なのに両方未入力でバリデーションエラーとなった場合、両方のエラーを検知して画面表示したいのですが、try catch一つだと1個のエラーしか検知して画面表示出来ません。ユーザーにとってはエラーを一つずつしか解消出来ず、これではコンバージョン低下に繋がります。モデル毎にtry catchすればいいですが、ソースコードが冗長になりスマートではありません。そこで、ModelFactoryというドメインサービスを作りました。そのファクトリーにバリデーションしたいモデルクラスを配列で渡したら、配列のモデルクラスのバリデーションを全部やってエラーがあればthrowまでやってくれるクラスにしました。エラーが無ければ、インスタント化したモデルクラスをModelFactoryから取得も出来ます。これでドメインモデルのバリデーションチェックと、そのインスタンス化をスマートに実装出来ました。 自動テスト 全機能に対するUnitTestを実装しました。TDDも勉強していた為、実装の過程で実践しました。何度かリファクタリングをしたのですが、その度にグリーン(自動テストオールOK)だったのが、レッド(自動テストオールNG)になりました。そこからグリーンにするまで修正したら、画面テストがキチンと動作する事を確認出来ました。UnitTestで非常に信頼性の高い検証を行える事が、この事から立証出来たと思います。キチンとUnitTestを実装したら、機能追加やリファクタリングを自信を持って行えると思います。 ステップ数 種別 ファイル数 ステップ数 ddd 152 3,444 軽量ddd 102 2,298 MVC 23 725 上記の通り、懸念していたとおりdddはファイル数、ステップ数共に圧倒的にMVCと比較して多くなる結果となりました。一方でdddは1クラスの凝集度は高く、ほとんど50ステップ未満なので、実装工数は数ほどのインパクトがあるわけではありません。dddでは決まった形式でクラスを実装するケースも多く、ほとんどコピペやロジックの組み立ての時間がかからず作業的に実装するケースもあったので、ステップ数の割には工数はかかっていなかったりします。また、クラスの凝集度が高い分、どこに何が記載されているか、このクラスの役割は何かがひと目で把握しやすいメリットもあります。実装量、ファイル数が多い=悪ではないという事を言いたいのですが、それぞれメリット、デメリットがあります。詳細は以下の「メリット・デメリット」でまとめたいと思います。 パフォーマンス app/Http/Middleware/PerformanceLog.php 上記ファイルにコントローラー実行前後にログを出力し、処理時間、処理に使用したメモリを計測できるようにしました。その計測結果を以下にまとめます。 商品一覧のように大量のレコードをDBから取得して表示する箇所は、MVC > 軽量ddd > dddという結果でした。これは、dddの場合取得したレコードをドメインモデルに変換するコストがかかるためです。 一方意外だったのは、その他はほとんど軽量dddに軍配が上がって軽量ddd > MVC > dddという結果になった点です。詳しくは分析しておりませんが、MVCは手続き型で実装しているので、処理効率が悪い部分があるのでしょうか。もしくは単なる誤差という可能性もあります。いずれにしても気にするほどの差ではありません。商品一覧だけ100ms単位で差が出ているので、大量データ取得・表示のパフォーマンスに影響が出ると考えるのが良さそうです。なお、注文確定処理が2000ms前後と時間がかかっているのは、注文確定のお知らせメール送信処理が含まれるためです。 結果一覧 機能 ddd時間 dddメモリ 軽量ddd時間 軽量dddメモリ MVC時間 MVCメモリ 商品一覧画面表示 289ms 6299KB 178ms 5049KB 85ms 2735KB 商品詳細画面表示 63ms 650KB 18ms 600KB 23ms 627KB カート追加API 153ms 419KB 19ms 418KB 51ms 839KB カート画面表示 36ms 468KB 10ms 382KB 21ms 640KB 注文画面表示 20ms 468KB 9ms 382KB 20ms 640KB 注文バリデーションAPI 34ms 422KB 31ms 506KB 58ms 814KB 注文確定処理 2552ms 1334KB 2517ms 1061KB 1949ms 1376KB エビデンス ddd [2021-07-23 23:00:19] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\ProductController@list メモリ 6298.953125KB [2021-07-23 23:00:19] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\ProductController@list 288.98096084595ミリ秒 [2021-07-23 23:07:51] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\ProductController@detail メモリ 649.6484375KB [2021-07-23 23:07:51] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\ProductController@detail 62.504053115845ミリ秒 [2021-07-23 23:08:28] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\CartController@add メモリ 418.6953125KB [2021-07-23 23:08:28] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\CartController@add 153.38611602783ミリ秒 [2021-07-23 23:08:28] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\CartController@list メモリ 468.3046875KB [2021-07-23 23:08:28] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\CartController@list 36.375999450684ミリ秒 [2021-07-23 23:09:27] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\OrderController@form メモリ 468.3046875KB [2021-07-23 23:09:27] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\OrderController@form 19.876003265381ミリ秒 [2021-07-23 23:09:37] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@check メモリ 421.9609375KB [2021-07-23 23:09:37] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@check 33.753156661987ミリ秒 [2021-07-23 23:09:44] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@save メモリ 1334.2734375KB [2021-07-23 23:09:44] local.DEBUG: Packages\DDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@save 2552.1411895752ミリ秒 軽量ddd [2021-07-23 23:11:33] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\ProductController@list メモリ 5049.25KB [2021-07-23 23:11:33] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\ProductController@list 177.73199081421ミリ秒 [2021-07-23 23:11:36] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\ProductController@detail メモリ 600.1328125KB [2021-07-23 23:11:36] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\ProductController@detail 18.326997756958ミリ秒 [2021-07-23 23:11:38] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\CartController@add メモリ 417.765625KB [2021-07-23 23:11:38] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\CartController@add 19.247770309448ミリ秒 [2021-07-23 23:11:38] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\CartController@list メモリ 382.2265625KB [2021-07-23 23:11:38] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\CartController@list 10.499000549316ミリ秒 [2021-07-23 23:11:40] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\OrderController@form メモリ 382.2265625KB [2021-07-23 23:11:40] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\OrderController@form 9.2220306396484ミリ秒 [2021-07-23 23:11:49] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@check メモリ 506.1171875KB [2021-07-23 23:11:49] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@check 30.539989471436ミリ秒 [2021-07-23 23:11:53] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@save メモリ 1061.2578125KB [2021-07-23 23:11:53] local.DEBUG: Packages\LightDDDEcSample\InterfaceAdapters\Controllers\Api\OrderController@save 2517.3659324646ミリ秒 MVC [2021-07-23 23:10:28] local.DEBUG: Packages\MvcSample\App\Http\Controllers\ProductController@list メモリ 2735.3828125KB [2021-07-23 23:10:28] local.DEBUG: Packages\MvcSample\App\Http\Controllers\ProductController@list 84.88392829895ミリ秒 [2021-07-23 23:10:31] local.DEBUG: Packages\MvcSample\App\Http\Controllers\ProductController@detail メモリ 627.4375KB [2021-07-23 23:10:31] local.DEBUG: Packages\MvcSample\App\Http\Controllers\ProductController@detail 22.572994232178ミリ秒 [2021-07-23 23:10:34] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\CartController@add メモリ 838.9921875KB [2021-07-23 23:10:34] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\CartController@add 50.947904586792ミリ秒 [2021-07-23 23:10:34] local.DEBUG: Packages\MvcSample\App\Http\Controllers\CartController@list メモリ 639.7109375KB [2021-07-23 23:10:34] local.DEBUG: Packages\MvcSample\App\Http\Controllers\CartController@list 20.750045776367ミリ秒 [2021-07-23 23:10:36] local.DEBUG: Packages\MvcSample\App\Http\Controllers\OrderController@form メモリ 639.7109375KB [2021-07-23 23:10:36] local.DEBUG: Packages\MvcSample\App\Http\Controllers\OrderController@form 20.180940628052ミリ秒 [2021-07-23 23:10:45] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\OrderController@check メモリ 813.6875KB [2021-07-23 23:10:45] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\OrderController@check 57.819128036499ミリ秒 [2021-07-23 23:10:53] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\OrderController@save メモリ 1375.6796875KB [2021-07-23 23:10:53] local.DEBUG: Packages\MvcSample\App\Http\Controllers\Api\OrderController@save 1949.3370056152ミリ秒 メリット・デメリット ddd、軽量ddd、MVCのメリット・デメリットを比較した結果を以下にまとめます。dddの記述ボリュームが多くなりましたが、dddよりは、軽量dddを採用するほうがいいと考えます。軽量dddはdddのメリットを全て継承しつつ、dddのファイル数、ステップ数が多くなるデメリットをある程度軽減出来ている為です。MVCはファイル数、ステップ数が圧倒的に少なく、実装工数を考えるとMVC一択と判断しがちですが、メンテナンス性を考えると一口にはそうは言いきれません。また、dddや軽量dddはファイル数、ステップ数が多いですが、その数字=工数とも限りません。殆どが1クラス50ステップ未満の凝集度の高いクラスの為、実装効率が高く実行工数は数ほどのインパクトは無いと考えます。また、dddでは決まった形式でクラスを実装するケースも多く、ほとんどコピペやロジックの組み立ての時間がかからず作業的に実装するケースもあったので、ステップ数の割には工数はかかっていなかったりします。なお、カートのセッション取得箇所を「SessionRepository::get」でgrepすると、ddd、軽量dddは共に1箇所ですが、MVCは4箇所でした。設計次第で改善出来るかもしれませんが、こうした実装の重複はMVCや手続き型の実装では避けづらいです。処理の重複や類似の仕様が様々な箇所に分散して実装されていくと、変更しづらく影響範囲も読みづらいプロジェクトになっていきます。dddが目指すのは変更や拡張をしやすい「しなやかな設計(参考文献「エリック・エヴァンスのドメイン駆動設計」より引用)」ですが、MVCや手続き型の実装ではそうした理想を目指すのが困難だと考えます。運用期間が短かったり、手早く短時間で仕上げたいWEBアプリケーションであれば、MVCでさっと作るのが向いているかもしれませんが、年単位で運用をしながら機能拡張・追加を繰り返して成熟させていきたいWEBアプリケーションやWEBサービスの場合は、dddか軽量dddで構築したほうが良さそうだと考えます。 ddd メリット クラスの凝集度が高くなり、1クラス当りのステップ数は少なくなって可読性、メンテナンス性が高くなる どこに何を実装するかの指針が決まっており、クラスの責務がはっきり分かる形で実装出来るため、ソースコードの重複を避けやすい。(完全には防げないが、重複が発生したらそれを検知してリファクタリングしやすい。例えば手続き型の実装だと関数単位で機能が実装されていって関数が増えていき、その中に重複処理があっても発見しにくくなる。) 依存関係逆転の原則を用いることで、疎結合な実装となる 疎結合な為UnitTestを実装しやすい 疎結合な為、DBの変更、フレームワークの変更、HTML・API・コンソール・ViewComposer等のUIやコントローラーの変更をしても、ユースケース層、ドメイン層は影響を受けずそのまま使える。 デメリット ファイル数、ステップ数が多くなる ドメインモデル→UseCaseData→ViewModelとレイヤーをまたぐ毎にデータの変換が必要となり、パフォーマンスが低下する 軽量ddd メリット 高凝集、疎結合、UnitTestの実装のしやすさはdddのメリットを継承 dddに比べてファイル数、実装量が削減される。 ドメインモデル→ViewModelを、ドメインモデルの配列化で実現し、dddと比べて変換回数が少なくなってパフォーマンス改善した デメリット dddに比べてステップ数が2/3になるが、それでもMVCの3倍以上のステップ数となる dddと比べてパフォーマンス改善したものの、大量データ取得時のパフォーマンスはMVCと比較すると悪い MVC メリット ddd、軽量dddと比べてステップ数、ファイル数が圧倒的に少ない(dddの1/4以下、軽量dddの1/3以下) dddと比べてデータの変換が無い為、処理速度が早い傾向にある(特に大量データ取得時) デメリット コントローラー、モデルクラスにビジネスロジックが集中し、メンテナンス性が悪い神クラスが生まれやすくなる 関数単位で機能が実装されていって関数が増えてき、関数同士の関連がわかりづらくなったり、類似処理の重複を避けづらくなってくる フレームワークと密結合な実装となるため、UnitTestが実装し辛く、実装するには工夫が必要となる Eloquentのパフォーマンス 本記事の趣旨ではありませんが、Eloquentのパフォーマンスについて検証結果を述べます。商品一覧のデータ取得は最初Eloquentのモデルクラスで取得しておりましたが、DB::tableで取得する方式へ変更しました。理由はパフォーマンスの悪さです。どうもEloquentは大量データを取得する際のパフォーマンスが悪い模様です。理由はEloquentのインスタンスを作成するコストが嵩むからという見解と、whereInで大量にパラメータ指定する際のデータバインドのコストが原因という見解があるようです(参考サイト「laracasts」の記事より)。今回のクエリはwhereInを使ってなくても遅かったため、Eloquentのインスタンス化のコストが主な遅延要因ではないかと考えます。本記事のデモサイトでは、DB::tableでこの問題に対処しました。それでもほとんど使い方はEloquentのモデルクラスと同等なので支障はありませんでした。なお、この問題が本記事のパフォーマンス検証に影響しないように、ddd、軽量ddd、MVC全て、商品一覧だけはdb::tableを使用するコードでパフォーマンス計測しております。 React 仕事でVue.jsの採用経験があるためVue.jsのほうが慣れておりますが、いい機会なのでReactに挑戦しました。仕事でVue.jsかReactのどちらを使用するか検討した際、Vue.jsのほうがデザイナーにも受け入れてもらいやすそうだと感じてVue.jsを採用しました。あと、私自身JSXアレルギーがあって、Vue.jsのようにテンプレート記法の方が既存の技術(HTML、CSS、js、jQueryを用いた実装)に近い感覚で実装出来て、わかりやすく実装しやすいと思っていました。 ところが今回Reactに挑戦してみて、JSXアレルギーは誤解だったと考えを改めました。jsコードとHTMLをシームレスに書ける事の便利さに驚きました。JSXはプログラミングしている感覚でHTMLを記述出来ます。なんと言っても自由度の高さです。Vue.jsはHTMLはtemplate、jsロジックはscript、CSSはstyleタグ内に記述するように決まっています。そして、jsロジックの実装ルールも定まっていて、それに沿って実装します。Reactはというと・・・一定のルールはあるものの、かなり自由に実装できます。ただこれは副作用もあります。自由ということは、きちんとコーディング規約を作ったり実装方針や設計を定めないと、メンテナンス性の悪いコードも作れてしまいます。MVCはビューとロジックを別で実装する事で、高いメンテナンス性を実現し、プログラマーとデザイナーが分業しやすくなりました。ReactはHTMLとjsを一緒に書けるのは便利な反面、キチンと設計、実装しないとビューとロジックが混雑してデザイナー、プログラマーの分業に支障が出たり、可読性、メンテナンス性の低下を招くと思います。一長一短ですね・・・ あと、元々懸念していたデザイナーにはハードルが高い点は当たっています。デザイナーよりはプログラマー寄りのアーキテクチャーで、プログラマーは幸せになれますが、デザイナーは敷居が高く感じるかもしれません。ただ、ReactやVue.jsの登場で、フロントエンドで出来ることは大幅に増えました。一昔前はHTML、CSSで静的コンテンツを制作し、jsやjQueryで動的コンテンツをちょこっと色づけという感じでしたが、ReactやVue.jsでがっつりフロンドで動的コンテンツを実装し、サーバーサイドはREST APIだけ提供し、データの永続化とその復元といったバックエンド処理に徹するという構成も今では珍しくありません。昨今GraphQLという選択肢もあり、フロントエンドに実装の比重が移りつつあります(GraphQLを採用すると、今回検証したdddも必要無くなるかも・・・)。SEO対策の点ではSPAは不安もあるのでその点十分考慮する必要はありますが、SSRという解決策も用意されています。その事を考えたら、デザイナーもガリガリとプログラミングに参画するのが時代の流れかも知れません。そうした時代の流れに乗るのであれば、Reactも十分選択肢になり得ると考えます。 今回の趣旨では無いものの、Reactに挑戦してみてReactが広く受け入れられている理由を肌で感じました。少し実装ルールを勉強したら、あとは直感的にどんどん実装していけました。ただ、初めてのReactなのでコード品質は悪いのでそこはご容赦下さい。本格的に導入するには、フォルダ設計、実装方針の策定、CSSのツールの選定等々、色々と事前に勉強・検討する必要がありそうです。 参考文献・記事・ソースコード エリック・エヴァンスのドメイン駆動設計 実践ドメイン駆動設計 (Object Oriented SELECTION) テスト駆動開発 ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本 ドメイン駆動設計 モデリング/実装ガイド nrslib/StrictLaraClean DDDパターンを活用した Laravelアプリケーション開発 shin1x1/laravel-ddd-sampl laracasts
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

typescript + vite + reactを使ったChrome拡張機能の土台を作って検証してみた

Chrome拡張機能をtypeScript + webpack + create react appで開発していますが、ビルドが遅いので、開発でストレスになっていた。 一方、viteがビルド速度が早いと巷で話題になっているので、まずは土台を作成し、本当に早いのか検証してみました。 使用環境 会社で使用しているMacなので、かなりハイエンドのものを使用していますが、ここまでのスペックじゃなくても問題なく利用はできると思います。 MacBook Pro (16-inch, 2019) OS: MacOSX 10.15.7 CPU: 2.4 GHz 8コアIntel Core i9 メモリ:64 GB 2667 MHz DDR4 node v12.13.1 ベースで使用したもの react-tsというテンプレートから作成しています。 yarn create @vitejs/app --template react-ts . やったことまとめ 作成したものは、以下においてありますが、やった内容をまとめておきます。 package.jsonの変更 以下の変更を行っています yarn build時に、tscに--noEmitオプションを追加(型検査のみで十分なので) パッケージを追加 chrome関係のものをtypescriptで利用するのに必要 @types/chrome ビルドで必要 rollup-plugin-copy @types/node { "name": "vite_for_chrome_extension", "version": "0.0.0", "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview" }, "dependencies": { "react": "^17.0.0", "react-dom": "^17.0.0" }, "devDependencies": { "rollup-plugin-copy": "3.4.0", "@types/node": "16.4.0", "@types/chrome": "0.0.148", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@vitejs/plugin-react-refresh": "^1.3.1", "typescript": "^4.3.2", "vite": "^2.4.3" } } vite.config.tsを変更 主に、やっていることは以下の通り main.jsやbackgroundスクリプトであるbackground.jsをビルドし、dist直下へ出力 publicファイルをdist/publicへコピー manifest.jsonをdistへコピー index.htmlなどchrome拡張機能で利用するhtmlファイルをdistへコピー import { defineConfig } from 'vite' import reactRefresh from '@vitejs/plugin-react-refresh' import copy from 'rollup-plugin-copy' const { resolve } = require('path') // https://vitejs.dev/config/ export default defineConfig({ build: { // src上にあるts, tsxファイルを一つのjsにまとめて、dist配下へ配置 // ポップアップスクリプト本体であるmain.jsと、バックアップスクリプトbackground.jsのビルド設定を含めている // コンテンツスクリプトは含めていないが、必要であればここに追加していけばよい rollupOptions: { input: { main: resolve(__dirname, 'src/main/main.tsx'), background: resolve(__dirname, 'src/background/background.ts'), }, output: { entryFileNames: '[name].js', }, }, }, plugins: [ reactRefresh(), copy({ verbose: true, hook: 'writeBundle', targets: [ // publicファイル(アイコンなど) を dist/public へコピー { src: 'public/*', dest: 'dist/public' }, // manifest.json を distへコピー { src: 'manifest.json', dest: 'dist', }, // index.htmlなどchrome extensionで使用するhtmlファイルをdistへコピー { src: '*.html', dest: 'dist' } ], }), ] }) src配下にあるコンポーネントファイルをsrc/mainへ移動 vite-env.d.tsを除くすべてのファイルをsrc/mainへ移動させた。コンポーネント関係なので、分離してわかりやすくしたかっただけに過ぎません。 background.tsを作成 background scriptを作成しておきます。とりあえず土台を作成したかったので、何もいれていません。 export {} publicフォルダを作成 ここにはとりあえず、拡張機能で利用するファビコンやロゴなどを入れています index.htmlを作成 拡張機能アイコンを押したときに、呼び出すindex.htmlをルート直下で作成しています。 ここでmain.jsを呼び出すようにしています。 なお、main.jsのパスはビルド時にルート直下で作成されることを想定しているので、以下のようにしています。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="favicon.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vite App</title> </head> <body> <div id="root"></div> <script type="module" src="main.js"></script> </body> </html> manifest.jsonを作成 chrome 拡張機能で必須なファイル manifest.json を作成しておきます。ロゴやバックグラウンドスクリプトbackground.js、ポップアップで表示させるファイルindex.htmlを指定しています。 コンテンツスクリプトは今回含まれていませんが、vite.config.jsでビルドできるようにすればすぐに対応できるようにしています。 { "manifest_version": 3, "name": "vite_for_chrome_extension", "version": "0.0.0.0", "action": { "name": "test", "default_popup":"index.html" }, "web_accessible_resources" : [{ "resources": [ "index.html", "main.js" ], "matches": ["http://*/*", "https://*/*", "*://*/*"], "extension_ids": [] }], "icons": { "32": "public/logo192.png", "64": "public/logo192.png", "128": "public/logo192.png", "512": "public/logo192.png" }, "background" : { "service_worker" : "./background.js" }, "content_scripts" : [], "permissions": [ "tabs", "webNavigation", "clipboardWrite", "clipboardRead", "storage", "unlimitedStorage", "alarms", "scripting" ], "host_permissions" : [ "http://*/*", "https://*/*", "*://*/*" ] } ここまで作成すれば、あとは、yarn buildで拡張機能がビルドできるようになります。 結局早くビルドできるのか? 従来のものと比較して、本当に早くビルドできるのか試してみました。 従来のプロジェクトについては、比較のためにこちらを拝借しました 実際にビルドしてみてかかった時間は以下のとおりです。 vite: 平均 6.46s (6.81s, 6.37s, 6.21s) create-react-app: 平均 5.32s (7.22s, 4.35s, 4.38s) なんと、viteのほうが遅いという残念な結果に。。。 なぜ遅いのか調べてみたところ、vite buildの前にtscを実行していてこれがビルド時間に足かせになっているようです(実際これを省略してみたところ、平均 2.56s (2.82s, 2.46s, 2.40s)で済み、問題なくビルドできているようです)。 ではなぜtscをいれているのかというと、公式マニュアルによると、viteもといビルドに使っているesbuildでは、あくまでトランスパイルしか行わず、型チェックは行っていないからだそうです。なので、型チェックはtscでカバーしているようです。 一方、拡張機能では毎回ビルドすることになるので、当然型チェックは必要です。なので、tscは外せられないと考えています。 規模が大きくなるとどうなるかも検証 何も入れていない状態だと、あまり意味がないのかなと思い、規模が大きくなったら本領が発揮されるという話もあるので、検証してみました。 たとえば、React Material UI ( https://material-ui.com/ ) という比較的重いモジュールを含めてみることにしました。バージョンは4.12.2とします。 その結果がこちらです。 vite: 平均 10.64s (10.66s, 10.53s, 10.74s) vite(tscを除外): 平均 4.46s (4.60s, 4.36s, 4.43s) create-react-app: 平均 6.82s (6.77s, 6.87s, 6.81s) これでもviteのほうが遅い結果に。。。tscを除外したら早いので、ここでもtscが足かせになっているようでした。 他にモジュールをいれてみたら結果は変わるかもしれませんが、キリがなさそうなので、ここで打ち切ることにしました。 まとめ viteで土台を作ってみたら早くなるのかなと検証していましたが、従来のものと比較して早くビルドされるわけではありませんでした tscがビルド時間の足かせになっていることはわかったものの、型チェックは必要なので外せません。なので、chrome拡張機能で採用するにはまだまだ課題がある印象でした。 今後改善してもっと早くなることがわかれば、またトライしてみたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Django(DRF)でAPIからデータ削除時に各モデルに紐づいた画像をディレクトリからも自動削除してくれるdjango_creanupが感動するほど便利だった件

背景 最近、DjagoRestFramework と React を使って簡単なアプリを開発していたところ、curd でデータ削除時に各モデルに紐づいた画像を表面上では削除できるが、django アプリ側のディレクトリに画像ファイルが残ってしまう現象に少々困ったので良いものはないかと探してみると django_creanup なるものがあることを発見し、使用してみたところ、感動するほど便利だったのでこうして共有している所存です。 目的 データ削除時にそのレコードに紐づいた画像ファイルを削除したい view の処理は増やしたくない もちろん手動は嫌なので自動にしたい django_creanup を使ってみた 公式 ( https://github.com/un1t/django-cleanup ) バージョンの確認 ( https://pypi.org/project/django-cleanup/ ) ※ 以下公式より引用、そして翻訳 互換性 Django 2.2, 3.0, 3.1, 3.2 (詳細はDjango公式のサポートバージョンをご覧ください https://www.djangoproject.com/download/#supported-versions) Python 3.5+ sorl-thumbnail https://github.com/jazzband/sorl-thumbnail と互換性があります。 easy-thumbnail https://github.com/SmileyChris/easy-thumbnails と互換性があります。 インストール $ pip install django-cleanup 構成 settings.py の INSTALLED_APPS 以下に django_cleanup を追加します。 (django_cleanup.apps.CleanupConfig でも django_creanup だけでもどちらでも大丈夫です) settings.py INSTALLED_APPS = [ ..., 'django_cleanup.apps.CleanupConfig', or 'django_cleanup', ] これだけです、他には何も必要ありません。 引用にもある通り、使用するためにはこれだけで大丈夫です。 view に何も追加必要する必要がないのが個人的にはありがたいなという感じです。 注意点としては、サードパーティーモジュールなので他のモジュールと衝突してしまう可能性があることです。 その場合はよく考えて使いましょう。 まとめ django_creanup めちゃくちゃ便利!!!!!!!!!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む