20210907のReactに関する記事は8件です。

Clean Architecture on Frontend (翻訳)

こちらはDEV Communityに2021年9月2日に投稿され、現在反響を巻き起こしているフロントエンドにおけるクリーンアーキテクチャの実装についてのAlexさんの記事になります(原文はこちら)(twitterにて翻訳掲載許可取得済み)。 かなり大ボリュームな超大作記事となっておりますが、Reactなどを使ったフロントエンドプロジェクトのディレクトリー構成やファイルごとの責務の切り分けのベストプラクティスなどの決定版といえるものがまだまだ出てこない中で、個人的にまさに待ち侘びていたような内容の記事かと思い、是非日本のフロントエンドコミュニティでも知見が共有されればと思いました。 それでは以下、本文です。 *翻訳は大部分をDeepL翻訳によって行っていますが、適宜修正してあります。 Clean Architecture on Frontend 少し前に、私はフロントエンドにおけるクリーンアーキテクチャについての講演を行いました。この記事では、その講演の概要を説明し、少し拡張しています。 まず、参照いたただくと便利なものへのリンクを貼っておきます。 これから設計するアプリケーションのソースコード 実際のアプリケーションのサンプル 計画の内容 まず、クリーンアーキテクチャとは何かを説明し、ドメイン、ユースケース、アプリケーション・レイヤーなどの概念を理解します。その上で、フロントエンドにどのように適用されるのか、また、その価値があるのかどうかを議論します。 次に、クリーンアーキテクチャのルールに従って、クッキー屋さんのフロントエンドをデザインします。そして最後に、ユースケースの1つをゼロから実装して、使えるかどうかを確認します。 このストアのUIフレームワークにはReactが使われていますが、これはこのアプローチがReactにも使えることを示すためです。(そして、この記事の元になった講演は、すでにReactを使っている開発者に向けたものだったからです?) Reactは必須ではありませんが、この記事で紹介しているものは、他のUIライブラリやフレームワークでも使うことができます。 コードには少しだけTypeScriptが登場しますが、エンティティを記述するための型やインターフェイスの使い方を説明するためだけのものです。ここで見るものはすべてTypeScriptなしでも使えますが、コードの表現力は落ちます。 今日はOOPの話はほとんどしませんので、この記事が重度のアレルギーを引き起こすことはありません。最後に一度だけOOPについて言及しますが、アプリケーションの設計を止めることはありません。 また、テストはこの記事のメイントピックではないので、今日は省略します。しかし、テスト可能性については念頭に置き、それを改善する方法については途中で言及するつもりです。 アーキテクチャと設計 設計とは、基本的には、物事を分解して...元に戻せるようにすることです。...物事を構成可能なものに分離すること、それが設計です。 - Rich Hickey. Design Composition and Performance システム設計とは、ここで引用文したように、システムを後で組み立て直せるように分離することです。そして最も重要なことは、あまり手間をかけずに簡単に組み立てることが出来るようにすることです。 私もそう思います。しかし、私はアーキテクチャのもうひとつの目標は、システムの拡張性だと考えています。プログラムに求められるものは常に変化しています。新しい要求に対応するために、プログラムの更新や修正が容易であることが望まれます。クリーンアーキテクチャは、この目標を達成するために役立ちます。 The Clean Architecture クリーン・アーキテクチャとは、アプリケーション・ドメインに近接しているかどうかによって、責任と機能の一部を分離する方法である。 ドメインとは、プログラムでモデル化した実世界の一部を意味します。これは、データの変換が実世界の変換を反映していることです。例えば、製品の名前を更新した場合、古い名前を新しい名前に置き換えることがドメイン変換です。 The Clean Architectureは、その中の機能が層に分かれていることから、3層アーキテクチャと呼ばれることもあります。The Clean Architectureに関するオリジナルの投稿では、レイヤーを強調した図が掲載されています。 ドメイン層 中心にあるのがドメイン層です。アプリケーションの対象領域を表すエンティティやデータ、そしてそのデータを変換するためのコードを指します。ドメインは、1つのアプリケーションと他のアプリケーションを区別する核心です。 ドメインとは、ReactからAngularに移行しても、あるいはいくつかのユースケースを変更しても変わらないものだと考えることができます。ストアの場合は、商品、注文、ユーザー、カート、そしてそれらのデータを更新する機能などです。 ドメインエンティティのデータ構造と、その変換の本質は、外界から独立しています。外部イベントはドメイン変換のきっかけとなるが、どのように変換されるかは決定しません。 アイテムをカートに追加する機能は、アイテムがどのように追加されたかを気にしません。ユーザー自身が「購入」ボタンを押して追加したのか、プロモーションコードを使って自動的に追加したのかを問いません。どちらの場合でも、アイテムを受け入れ、追加されたアイテムを含む更新されたカートを返します。 アプリケーション層 ドメインの周りには、アプリケーション層があります。この層はユースケース、つまりユーザーシナリオを記述します。あるイベントが発生した後のことを担当します。 例えば、「カートに入れる」というシナリオはユースケースです。ボタンがクリックされた後に取るべきアクションを記述します。これは、次のような「オーケストレーター」の一種です。 サーバへリクエストを送る→ それにドメイン変換を行う→ レスポンスデータを使ってUIを再描画する また、アプリケーション層にはポートがあります。これは、アプリケーションが外部とどのように通信するかを指定するものです。通常、ポートはインターフェイスであり、動作契約(behavior contarct)でもあります。 ポートは、アプリケーションの希望と現実の間の「バッファーゾーン」の役割を果たします。入力ポートは、アプリケーションが外界からどのように連絡を受けたいのかを示します。出力ポートは、アプリケーションが外界とどのように通信して準備を整えるのかを示します。 ポートについては、後で詳しく説明します。 アダプタ(Adapters)層 一番外側のレイヤは、外部サービスへのアダプタを含みます。アダプタは、外部サービスの互換性のない API を、アプリケーションの希望に沿った API に変換するために必要です。 アダプタは、私たちのコードとサードパーティのサービスのコードとの間の結合度を下げるための優れた方法です。結合度が低いと、他のモジュールが変更されたときに1つのモジュールを変更する必要性が減ります。 アダプタはよく次のように分けられます。 ドライビング(driving) - アプリケーションに信号を送信するもの。 ドリブン(driven) - アプリケーションから信号を受け取るもの ユーザーは最も頻繁にドライビングアダプタと対話します。例えば、UIフレームワークがボタンのクリックを処理するのは、ドライビングアダプタの仕事です。ドライビングアダプタは、ブラウザのAPI(基本的にはサードパーティのサービス)と連携し、イベントをアプリケーションが理解できる信号に変換します。 ドリブンアダプタは、インフラストラクチャと対話します。フロントエンドでは、インフラのほとんどはバックエンドサーバーですが、検索エンジンなどの他のサービスと直接やりとりすることもあります。 中心から離れれば離れるほど、コードの機能は「サービス指向」になり、アプリケーションのドメイン知識から離れていくことに注意してください。これは後に、どのモジュールをどのレイヤーに所属させるかを決める際に重要になります。 依存関係のルール 3層構造のアーキテクチャには、外側の層だけが内側の層に依存できるという依存関係のルールがあります。これは次のことを意味します。 ドメインは独立していなければならない。 アプリケーション層はドメインに依存できる。 外側の層は何にでも依存できる。 このルールを乱用しない方が良いのですが、時にはこのルールに違反することもあります。例えば、依存関係がないはずのドメインでも、「ライブラリのような」コードを使うと便利な場合があります。このような例は、ソースコードを見るときにチェックしてみましょう。 依存関係の方向性が制御されていないと、複雑で混乱したコードになってしまいます。例えば、依存関係のルールを破ると、次のようになります。 モジュールAがBに依存し、BがCに依存し、CがAに依存するような循環的な依存関係。 一部をテストするためにシステム全体をシミュレートしなければならないような、テスト容易性の低下。 結合度が高すぎて、結果的にモジュール間の相互作用が脆弱になる。 クリーンアーキテクチャの利点 さて、このコードの分離がもたらすものについて説明しましょう。いくつかの利点があります。 ドメインの分離 すべての主要なアプリケーション機能は、ドメインという1つの場所に隔離され、集められています。 ドメイン内の機能は独立しているため、テストが容易になります。モジュールの依存関係が少なければ少ないほど、テストに必要なインフラストラクチャも少なくなり、モックやスタブも必要なくなります。 また、独立したドメインは、ビジネス上の要件に対してテストしやすいです。これは開発者が、アプリケーションが何をすべきかを把握するのに役立ちます。さらに、独立したドメインは、ビジネス言語からプログラミング言語への「翻訳」におけるエラーや不正確さをより早く発見するのに役立ちます。 独立したユースケース アプリケーションシナリオ、ユースケースは別々に記述されます。これらによって、私たちがどのようなサードパーティのサービスを必要とするかが決定されます。私たちは外の世界を私たちのニーズに合わせるのであって、その逆ではありません。そのため、サードパーティのサービスをより自由に選択することができます。例えば、現在の決済システムが高額な料金を請求するようになったら、すぐに変更することができます。 また、ユースケースのコードはフラットになり、テスト可能で拡張性があります。この点については、後ほど例を挙げて説明します。 交換可能なサードパーティのサービス 外部サービスが置き換え可能になるのは、アダプタのおかげです。インターフェイスを変更しない限り、どの外部サービスにインターフェイスが組み込まれるかは問題ではありません。 このようにして、他人のコードの変更が自分のコードに直接影響しないように、変更の伝播に対する障壁を作ります。また、アダプタはアプリケーションのランタイムにおけるバグの伝播を制限します。 クリーンアーキテクチャのコスト アーキテクチャとはまず道具です。他のツールと同様に、クリーンアーキテクチャにも、その利点の他にコストがあります。 時間がかかる 主なコストは時間です。設計時だけでなく、実装時にも時間が必要です。なぜなら、アダプタを書くよりもサードパーティのサービスを直接呼び出す方が常に簡単だからです。 また、システムのすべてのモジュールの相互作用を事前に考えることは困難です。なぜなら、すべての要件と制約が事前にわからないからです。設計時には、システムがどのように変化するかを念頭に置き、拡張の余地を残しておく必要があります。 過剰なまでの冗長性 一般的に、クリーンなアーキテクチャの正統的な実装は、必ずしも便利ではなく、時には有害でさえあります。プロジェクトの規模が小さい場合、完全な実装はやり過ぎで、新規参入の敷居を高くしてしまいます。 予算や納期を守るために、設計上のトレードオフが必要になるかもしれません。このようなトレードオフとはどういうことか、具体的に例を挙げて説明します。 オンボーディングが困難になる可能性 クリーンアーキテクチャーを全面的に導入すると、オンボーディングが困難になります。 プロジェクトの初期段階で過剰なエンジニアリングを行うと、後になって新しい開発者を迎え入れるのが難しくなります。このことを念頭に置いて、コードをシンプルに保つ必要があるのです。 コード量の増加 フロントエンドに特有の問題として、クリーンなアーキテクチャによって、最終的なバンドル内のコード量が増加することがあります。ブラウザに渡すコードが増えれば増えるほど、ブラウザはダウンロード、解析、解釈をしなければなりません。 コードの量を監視し、どこで手を抜くかを決定しなければなりません。 ユースケースをもう少しシンプルに表現してみる。 ユースケースをもう少しシンプルに記述するとか、ユースケースをバイパスしてアダプターから直接ドメインの機能にアクセスするとか。 コードの分割を微調整する必要があるかもしれません。…などなど コスト削減の方法 手を抜き、アーキテクチャの「クリーンさ」を犠牲にすることで、かかる時間とコードの量を減らすことができます。私は一般的に急進的なアプローチは好きではありません。ルールを破ることがより現実的(例えば、潜在的なコストよりもメリットが大きい)であれば、私はそれを破ります。 ですから、しばらくの間はクリーンなアーキテクチャーのいくつかの側面に抵抗があっても、まったく問題はありません。しかし、絶対に割く価値のある必要最低限のリソースは、次の2つです。 ドメインの抽出 抽出されたドメインは、私たちが設計しているものを全般的に理解し、それがどのように機能すべきかを理解するのに役立ちます。抽出されたドメインは、新しい開発者がアプリケーションやそのエンティティ、それらの間の関係を理解するのを容易にします。 他のレイヤーをスキップしたとしても、コードベース上に広がっていない抽出されたドメインを使って作業やリファクタリングを行うことが容易になります。他のレイヤーは必要に応じて追加できます。 依存関係のルールに従う 捨ててはいけない2つ目のルールは、依存性のルール、というかその方向性です。外部のサービスは我々のニーズに合わせなければならず、そうでないものはありません。 もし、検索APIを呼び出せるようにコードを「微調整」していると感じたら、何かが間違っています。問題が広がる前にアダプタを書いたほうがいいでしょう。 アプリケーションの設計 理論的な話をしてきましたが、いよいよ実践に入ります。あるクッキー屋さんのアーキテクチャを設計してみましょう。 この店では、原材料が異なるさまざまな種類のクッキーを販売します。ユーザーはクッキーを選んで注文し、第三者の決済サービスで代金を支払います。 ホーム・ページには、購入できるクッキーのショーケースがあります。クッキーを購入できるのは、ユーザーが認証された場合のみとなります。ログインボタンを押すと、ログインページが表示され、ログインできるようになります。 (私はウェブデザイナーではないので、見た目は気にしないでください?)。) ログインに成功すると、カートにクッキーを入れることができるようになります。 クッキーをカートに入れると、注文することができます。支払いが完了すると、リストに新しい注文が追加され、ショッピングカートがクリアされます。 ここでは、チェックアウトのユースケースを実装します。残りのユースケースについては、ソースコードをご覧ください。 まず、どのような種類のエンティティ、ユースケース、機能を持つのかを定義します。そして、それらがどのレイヤーに属するべきかを決めましょう。 ドメインの設計 アプリケーションで最も重要なものは、ドメインです。これは、アプリケーションの主要なエンティティとそのデータ変換を行う場所です。アプリのドメイン知識をコードで正確に表現するためには、ドメインから始めることをお勧めします。 ストアのドメインには以下が含まれます。 各エンティティのデータタイプ:user、cookie、cart、order。 各エンティティを作成するためのファクトリ、またはOOPで記述する場合はクラス。 そして、そのデータの変換関数です。 ドメイン内の変換関数は、ドメインのルールにのみ依存し、それ以外には依存しないものとします。そのような関数とは、例えば次のようなものです。 トータルコストを計算する関数。 ユーザーの味の好みの検出 ショッピングカートに入っているかどうかの判断、などなど。 アプリケーション層の設計 アプリケーション層には、ユースケースが含まれます。ユースケースには、必ずアクター、アクション、そして結果があります。 店舗では、次のように区別することができます。 商品購入のシナリオ 決済:サードパーティの決済システムの利用 商品や注文とのやりとり:更新、閲覧 役割に応じたページへのアクセス。 ユースケースは通常、対象分野の観点から説明されます。例えば、「チェックアウト」のシナリオは、実際にはいくつかのステップで構成されています。 ショッピングカートから商品を取り出し、新しい注文をする。 注文の支払いを行う。 支払いに失敗した場合、ユーザーに通知する。 カートを清算し、注文を表示する。 ユースケース関数は、これらのシナリオを記述するコードになります。 また、アプリケーション層には、外部との通信を行うためのポート-インターフェースがあります。 アダプタ層の設計 アダプタ層では、外部サービスへのアダプタを宣言します。アダプタは、サードパーティのサービスの互換性のないAPIを我々のシステムに適合させます。 フロントエンドでは、アダプタは通常、UIフレームワークとAPIサーバーのリクエストモジュールです。ここでは、以下のようにします。 UIフレームワーク API リクエストモジュール ローカルストレージ用のアダプタ アプリケーション層へのAPI回答のアダプターとコンバーター 機能が「サービス的("service-like")」であればあるほど、図の中心から離れていくことに注意してください。 MVCアナロジーの利用 あるデータがどのレイヤーに属しているのかを知るのが難しいことがあります。ここでは、MVCのちょっとした(そして不完全な!)例えが役に立つかもしれません。 * モデルは通常、ドメインエンティティです * コントローラはドメイン変換、アプリケーション層です * Viewはドライビングアダプターです 概念は細部では異なりますが、非常によく似ており、このアナロジーはドメインとアプリケーションのコードを定義するために使用することができます。 詳細設計: ドメイン 必要なエンティティが決まったら、それらがどのように動作するかを定義していきましょう。 早速、プロジェクトのコード構造を見てみましょう。分かりやすくするために、コードをフォルダ-レイヤーに分けています。 src/ |_domain/ |_user.ts |_product.ts |_order.ts |_cart.ts |_application/ |_addToCart.ts |_authenticate.ts |_orderProducts.ts |_ports.ts |_services/ |_authAdapter.ts |_notificationAdapter.ts |_paymentAdapter.ts |_storageAdapter.ts |_api.ts |_store.tsx |_lib/ |_ui/ ドメインは domain/ ディレクトリに、アプリケーション層は application/ に、アダプタは services/ にあります。このコード構造に代わるものについては、最後に説明します。 ドメインエンティティの作成 ドメインには4つのモジュールを用意します。 product; user; order; shopping cart 主役はユーザーです。ユーザーに関するデータは、セッション中にストレージに保存されます。このデータを型付けしたいので、ドメインのユーザータイプを作成します。 ユーザータイプには、ID、名前、メール、好みやアレルギーのリストが含まれます。 domain/user.ts export type UserName = string; export type User = { id: UniqueId; name: UserName; email: Email; preferences: Ingredient[]; allergies: Ingredient[]; }; ユーザーはカートにクッキーを入れます。カートと商品の型を追加しましょう。商品には、ID、名前、1円単位の値段、原材料のリストが入ります。 domain/product.ts export type ProductTitle = string; export type Product = { id: UniqueId; title: ProductTitle; price: PriceCents; toppings: Ingredient[]; }; ショッピングカートでは、ユーザーが入れた商品のリストのみを保持します domain/cart.ts import { Product } from "./product"; export type Cart = { products: Product[]; }; 支払いが完了すると、新しい注文が作成されます。注文のエンティティタイプを追加しましょう。 注文の型には、ユーザーID、注文商品のリスト、作成日時、ステータス、注文全体の合計金額が含まれます。 domain/order.ts export type OrderStatus = "new" | "delivery" | "completed"; export type Order = { user: UniqueId; cart: Cart; created: DateTimeString; status: OrderStatus; total: PriceCents; }; エンティティ間の関係の確認 このようにエンティティタイプを設計することの利点は、その関係図が現実と一致しているかどうかを既に確認できることです。 以下のことを確認することができます 主役が本当にユーザーであるかどうか。 注文に十分な情報があるかどうか。 あるエンティティを拡張する必要があるかどうか。 将来的に拡張性に問題がないかどうか。 また、この段階ではすでに、エンティティ同士の互換性や、エンティティ間の信号の方向性に関するエラーを型によって強調することができます。 すべてが期待通りであれば、ドメイン変換の設計を始めることができます。 データトランスフォームの作成 先ほど設計した型のデータには、さまざまなことが起こります。カートに商品を追加したり、カートを消去したり、商品やユーザー名を更新したりすることになります。これらの変換のために、それぞれ別の関数を作成します。 例えば、ある成分や好みに対してユーザーがアレルギーを持っているかどうかを調べるには、hasAllergy関数とhasPreference関数を書きます。 domain/user.ts export function hasAllergy(user: User, ingredient: Ingredient): boolean { return user.allergies.includes(ingredient); } export function hasPreference(user: User, ingredient: Ingredient): boolean { return user.preferences.includes(ingredient); } 関数 addProduct と contains は、アイテムをカートに追加したり、アイテムがカートに入っているかどうかを確認するために使用されます。 domain/cart.ts export function addProduct(cart: Cart, product: Product): Cart { return { ...cart, products: [...cart.products, product] }; } export function contains(cart: Cart, product: Product): boolean { return cart.products.some(({ id }) => id === product.id); } また、商品リストの合計金額を計算する必要があります。これには関数 totalPrice を使用します。必要であれば、この関数に追加して、プロモーションコードや季節的な割引など、さまざまな条件を考慮することができます。 domain/product.ts export function totalPrice(products: Product[]): PriceCents { return products.reduce((total, { price }) => total + price, 0); } ユーザーが注文を作成できるようにするために、createOrderという関数を追加します。この関数は、指定されたユーザーとそのカートに関連付けられた新しい注文を返します。 domain/order.ts export function createOrder(user: User, cart: Cart): Order { return { user: user.id, cart, created: new Date().toISOString(), status: "new", total: totalPrice(products), }; } どの関数でも、データを快適に変換できるようにAPIを構築していることに注意してください。私たちは引数を取り、思い通りの結果を返します。 設計段階では、まだ外部からの制約はありません。そのため、データ変換をできるだけ対象領域に近い形で反映させることができます。また、変換内容が現実に近ければ近いほど、動作確認も容易になります。 詳細設計 共有カーネル ドメインタイプを説明する際に使用したいくつかの型にお気づきでしょうか。例えば、Email、UniqueId、DateTimeStringなどです。これらはタイプエイリアスです。 sgared0kernel.d.ts type Email = string; type UniqueId = string; type DateTimeString = string; type PriceCents = number; プリミティブへの偏重をなくすために、通常はタイプエイリアスを使います。 単なるstringではなく、DateTimeStringを使うのは、どんな文字列が使われているかを明確にするためです。型が対象領域に近ければ近いほど、エラーが発生したときの対処が容易になります。 指定された型は、shared-kernel.d.tsというファイルにあります。共有カーネルとは、コードとデータのことで、これらに依存することでモジュール間の結合が強まることはありません。この概念については、「DDD、Hexagonal、Onion、Clean、CQRS、...How I put it all together」で詳しく説明しています。 実際には、共有カーネルはこのように説明できます。私たちはTypeScriptを使用し、その標準型ライブラリを使用していますが、それらを依存関係とは考えていません。なぜなら、それらを使用しているモジュールは、おそらくお互いのことを何も知らず、非結合のままであるからです。 すべてのコードが共有カーネルに分類できるわけではありません。主な、そして最も重要な制限は、そのようなコードはシステムのどの部分とも互換性がなければならないということです。アプリケーションの一部がTypeScriptで書かれていて、別の部分が別の言語で書かれている場合、共有カーネルには、両方の部分で使用できるコードのみを含めることができます。例えば、JSON形式のエンティティ仕様は問題ありませんが、TypeScriptヘルパーは問題です。 今回のケースでは、アプリケーション全体がTypeScriptで書かれているので、組み込み型に対するタイプエイリアスも共有カーネルに分類できます。このようなグローバルに利用可能な型は、モジュール間の結合を増やさず、アプリケーションのどの部分でも利用することができます。 詳細設計 アプリケーション層 ドメインがわかったところで、次はアプリケーション層に移りましょう。この層にはユースケースが含まれます。 コードでは、シナリオの技術的な詳細を記述します。ユースケースとは、アイテムをカートに入れたり、チェックアウトに進んだりした後に、データに何が起こるべきかを説明するものです。 ユースケースには外部とのやりとりが含まれるため、外部サービスを利用することになります。外界とのインタラクションは副作用です。副作用のない機能やシステムの方が、作業やデバッグがしやすいことがわかっています。そして、私たちのドメイン関数のほとんどは、すでに純粋な関数として書かれています。 クリーンな変換と不純な世界との相互作用を組み合わせるために、アプリケーション層を不純なコンテキストとして使用することができます。 純粋変換のための不純なコンテクスト 純粋変換のための不純なコンテキストとは、以下のようなコード構成のことです。 最初に副作用を実行して、あるデータを取得します。 次に、そのデータに対して純粋な変換を行います。 そして、その結果を保存または渡すために、再び副作用を実行します。 「カートに商品を入れる」というユースケースでは、次のようになります。 まず、ハンドラはストアからカートの状態を取得します。 次に、追加するアイテムを渡してカート更新関数を呼び出します。 そして、更新されたカートをストレージに保存します。 全体のプロセスは、副作用、純粋な関数、副作用という「サンドイッチ」のようになっています。主なロジックはデータ変換に反映され、世界とのコミュニケーションはすべて命令型シェルに隔離されています。 不純なコンテクストは、不純なシェルの中の機能的なコアと呼ばれることもあります。Mark Seemann氏がブログでこのことを書いています。ユースケース関数を書くときには、このアプローチを使います。 ユースケースの設計 ここでは、チェックアウトのユースケースを選択して設計します。非同期で多くのサードパーティのサービスと連携しているため、最も代表的なケースです。その他のシナリオやアプリケーション全体のコードはGitHubで見ることができます。 このユースケースで実現したいことを考えてみましょう。ユーザーのカートにはクッキーが入っており、ユーザーがチェックアウトボタンをクリックすると、カートの中にはクッキーが入っています。 新しい注文をします。 サードパーティの決済システムで支払いを行います。 支払いに失敗した場合は、その旨をユーザーに通知します。 支払いに失敗した場合は、その旨をユーザーに通知し、支払いに成功した場合は、注文をサーバーに保存します。 注文をローカルデータストアに追加し、画面に表示する。 APIと関数のシグネチャに関しては、ユーザーとカートを引数として渡し、それ以外のことは関数が勝手にやってくれるようにしたいと考えています。 type OrderProducts = (user: User, cart: Cart) => Promise<void>; もちろん理想的には、このユースケースは2つの別々の引数を取るのではなく、すべての入力データを自身の中にカプセル化するようなコマンドを取るべきです。しかし、コード量を肥大化させたくはないので、このままにしておきます。 アプリケーション層のポートを書く ユースケースの手順を詳しく見てみましょう。注文の作成自体はドメイン機能です。それ以外はすべて、私たちが利用したい外部サービスです。 ここで重要なのは、外部サービスは我々のニーズに合わせなければならず、そうでない場合は適応しないということです。そこで、アプリケーション層では、ユースケースそのものだけでなく、これらの外部サービスへのインターフェースであるポートを記述します。 ポートは、まず、アプリケーションにとって便利なものでなければなりません。もし、外部サービスのAPIが我々のニーズに合わない場合は、アダプタを作成します。 必要となるサービスを考えてみましょう。 決済システム イベントやエラーをユーザーに通知するサービス データをローカル・ストレージに保存するサービス ここでは、サービスの実装ではなく、そのインターフェースについて説明しています。この段階では、要求される動作を記述することが重要です。なぜなら、この動作は、シナリオを記述する際にアプリケーション層で頼りにする動作だからです。 この動作を具体的にどのように実装するかは、まだ重要ではありません。これにより、どの外部サービスを使用するかの決定を最後の最後まで延期することができ、コードを最小結合にすることができます。実装については後ほど説明します。 また、インターフェースを機能別に分けていることにも注目してください。決済関連はすべて1つのモジュールに、ストレージ関連は別のモジュールにまとめています。このようにすることで、異なるサードパーティのサービスの機能が混ざらないようにすることが容易になります。 決済システムのインターフェース クッキーストアはサンプルアプリケーションなので、決済システムは非常にシンプルなものになります。tryPayメソッドがあり、支払いに必要な金額を受け取り、それに応じて問題がないかどうかの確認を送信します。 application/ports.ts export interface PaymentService { tryPay(amount: PriceCents): Promise<boolean>; } エラー処理については、別の大きな記事のトピックになりますので、ここでは扱いません?。 そう、通常、決済はサーバー上で行われますが、これはサンプル例なので、すべてをクライアント上で行いましょう。決済システムと直接通信するのではなく、私たちのAPIと簡単に通信することができます。ちなみにこの変更は、このユースケースにのみ影響を与え、他のコードはそのまま残ります。 ローカルストレージインターフェース 新しい注文をローカルリポジトリに保存します。 このストレージは、ReduxでもMobXでも何でも構いません。リポジトリは、異なるエンティティのためのマイクロストレージに分割することも、すべてのアプリケーションデータのための1つの大きなリポジトリにすることもできます。これらは実装上の詳細ですので、今は重要ではありません。 私は、ストレージのインターフェースを各エンティティごとに分けるのが好きです。ユーザーデータストアには別のインターフェイス、ショッピングカートには別のインターフェイス、オーダーストアには別のインターフェイスを用意します。 application/ports.ts export interface OrdersStorageService { orders: Order[]; updateOrders(orders: Order[]): void; } ここでの例では、オーダーストアのインターフェースのみを作成していますが、その他の部分はソースコードをご覧ください。 ユースケース機能 作成したインターフェースと既存のドメイン機能を使って、ユースケースを構築できるかどうか見てみましょう。先ほど説明したように、スクリプトは以下のステップで構成されます。 データを確認する 注文を作成する 注文の支払い 問題があれば通知する 結果の保存 まず、使用するサービスのスタブを宣言しましょう。インターフェイスを適切な変数に実装していないとTypeScriptに指摘されますが、今のところは問題ありません。 application/orderProducts.ts const payment: PaymentService = {}; const notifier: NotificationService = {}; const orderStorage: OrdersStorageService = {}; これらのスタブは、実際のサービスのように使用することができます。スタブのフィールドにアクセスしたり、メソッドを呼び出したりすることができます。これは、ユースケースをビジネス言語からソフトウェア言語に「翻訳」するときに便利です。 次に、orderProductsという関数を作成します。内部では、まず最初に新しい注文を作成します。 application/orderProducts.ts async function orderProducts(user: User, cart: Cart) { const order = createOrder(user, cart); } ここでは、インターフェイスが動作の契約(contract for behavior)であるという事実を利用しています。つまり、将来的にはスタブが現在期待している動作を実際に行うことになります。 application/orderProducts.ts async function orderProducts(user: User, cart: Cart) { const order = createOrder(user, cart); // Try to pay for the order; // Notify the user if something is wrong: const paid = await payment.tryPay(order.total); if (!paid) return notifier.notify("Oops! ?"); // Save the result and clear the cart: const { orders } = orderStorage; orderStorage.updateOrders([...orders, order]); cartStorage.emptyCart(); } このユースケースでは、サードパーティのサービスを直接呼び出さないことに注意してください。インターフェースに記述されている動作に依存しているため、インターフェースが同じであれば、どのモジュールがどのように実装しているかは気にしません。これにより、モジュールの交換が可能になります。 詳細設計 アダプター層 ユースケースをTypeScriptに「翻訳」しました。あとは、現実がニーズにマッチしているかどうかをチェックしなければなりません。 通常はそうではありません。そこで、アダプタを使って外の世界を自分のニーズに合うように調整します。 UIとユースケースの結合 1つ目のアダプタは、UIフレームワークです。ブラウザのネイティブAPIとアプリケーションを接続します。注文の作成の場合は、"チェックアウト "ボタンとそのクリックハンドラで、ユースケースの関数を起動します。 ui/components/Buy.tsx export function Buy() { // Get access to the use case in the component: const { orderProducts } = useOrderProducts(); async function handleSubmit(e: React.FormEvent) { setLoading(true); e.preventDefault(); // Call the use case function: await orderProducts(user!, cart); setLoading(false); } return ( <section> <h2>Checkout</h2> <form onSubmit={handleSubmit}>{/* ... */}</form> </section> ); } ユースケースをフックで提供してみましょう。中にあるすべてのサービスを取得し、その結果、フックからユースケースの関数そのものを返すことになります。 application/orderProducts.ts export function useOrderProducts() { const notifier = useNotifier(); const payment = usePayment(); const orderStorage = useOrdersStorage(); async function orderProducts(user: User, cookies: Cookie[]) { // … } return { orderProducts }; } フックを "crooked dependency injection(屈折した依存性注入) "として使用しています。まずフックの useNotifier, usePayment, useOrdersStorage を使ってサービスインスタンスを取得し、次に useOrderProducts 関数のクロージャを使って orderProducts 関数の中でそれらを利用できるようにします。 ここで重要なのは、ユースケース関数がまだ他のコードから分離されていることで、これはテストのために重要です。記事の最後でレビューとリファクタリングを行う際に、完全に引き出してさらにテストしやすい状態にします。 決済サービスの実装 このユースケースでは、PaymentServiceインターフェースを使用しています。これを実装してみましょう。 支払いには、偽のAPIスタブを使用します。繰り返しになりますが、今すぐサービス全体を書かなければならないわけではありません。 services/api.ts export function fakeApi<TResponse>(response: TResponse): Promise<TResponse> { return new Promise((res) => setTimeout(() => res(response), 450)); } fakeApi関数は、450ms後に起動されるタイムアウトで、サーバーからの応答の遅れをシミュレートしています。この関数は、引数として渡されたものを返します。 services/paymentAdapter.ts import { fakeApi } from "./api"; import { PaymentService } from "../application/ports"; export function usePayment(): PaymentService { return { tryPay(amount: PriceCents) { return fakeApi(true); }, }; } usePaymentの戻り値を明示的にタイプしています。こうすることで、TypeScriptはこの関数が実際にインターフェイスで宣言されたすべてのメソッドを含むオブジェクトを返すかどうかをチェックします。 通知サービスの実装 通知はシンプルなアラートとします。このコードはデカップリングされているので、後でこのサービスを書き直すことは問題ありません。 services/notificationAdapter.ts import { NotificationService } from "../application/ports"; export function useNotifier(): NotificationService { return { notify: (message: string) => window.alert(message), }; } ローカルストレージの実装 ローカルストレージをReact.Contextとhooksとします。新しいコンテキストを作成し、その値をプロバイダに渡し、プロバイダをエクスポートし、フックを介してストアにアクセスします。 store.tsx const StoreContext = React.createContext<any>({}); export const useStore = () => useContext(StoreContext); export const Provider: React.FC = ({ children }) => { // ...Other entities... const [orders, setOrders] = useState([]); const value = { // ... orders, updateOrders: setOrders, }; return ( <StoreContext.Provider value={value}>{children}</StoreContext.Provider> ); }; それぞれの機能に対応したフックを書く予定です。こうすることで、ISPを壊すことはありませんし、ストアも、少なくともインターフェイスに関しては、アトミックになります。 services/storageAdapter.ts export function useOrdersStorage(): OrdersStorageService { return useStore(); } また、このアプローチでは、ストアごとに追加の最適化をカスタマイズすることができます。セレクタの作成やメモの作成などが可能です。 データフロー図の検証 作成したユースケースの中で、ユーザーがどのようにアプリケーションと通信するかを検証してみましょう。 ユーザーはUI層と対話しますが、UI層はポートを通じてのみアプリケーションにアクセスできます。つまり、必要に応じてUIを変更することができるのです。 ユースケースはアプリケーション層で処理され、どのような外部サービスが必要かを正確に教えてくれます。メインロジックとデータはすべてドメイン内にあります。 外部サービスはすべてインフラの中に隠れており、我々の仕様に従うことになります。メッセージを送信するサービスを変更する必要がある場合、コードの中で修正しなければならないのは、新しいサービスのためのアダプターだけです。 この方式により、コードは交換可能で、テスト可能で、変化する要件に合わせて拡張可能になります。 改善すべき点 全体としては、これで十分にスタートでき、クリーンアーキテクチャについてのザッとした理解を得られたといえるでしょう。しかし、この例をよりシンプルにするために簡略化した点を説明したいと思います。 このセクションは任意ですが、「手抜きのない」クリーンなアーキテクチャがどのようなものか、理解を深めることができるでしょう。 できることをいくつか紹介します。 価格には数字ではなくオブジェクトを使う 私が価格を数字で表現していることにお気づきでしょうか。これはあまり良いことではありません。 shared-kernel.d.ts type PriceCents = number; 数字は数量を示すだけで、通貨を示すものではありませんし、通貨のない価格は意味がありません。理想的には、priceはvalueとcurrencyの2つのフィールドを持つオブジェクトとして作られるべきです。 type Currency = "RUB" | "USD" | "EUR" | "SEK"; type AmountCents = number; type Price = { value: AmountCents; currency: Currency; }; これにより、通貨を保存する際の問題が解決され、通貨の変更や追加の際の手間や神経を大幅に省くことができます。複雑にならないように、例ではこの型を使っていません。しかし、実際のコードでは、この型に近いものになるでしょう。 それとは別に、価格の値についても触れておきましょう。私は常に、流通している通貨の中で最も小さい端数の金額を設定しています。例えば、1ドルの場合はセントです。 このように価格を表示することで、割り算や分数を意識せずに済むのです。お金の場合、浮動小数点演算の問題を避けたい場合には、特に重要です。 レイヤーではなく機能でコードを分割 コードは「レイヤーごと」ではなく「フィーチャーごと」にフォルダ分けすることができます。1つの機能とは、下の模式図のパイの一部のことです。 この構造はさらに好ましいもので、特定の機能を別々にデプロイすることができ、これはしばしば便利なことです。 「DDD, Hexagonal, Onion, Clean, CQRS, ...How I put it all together」で読むことをお勧めしました。 また、概念的にはコンポーネントコード分割と非常に似ていますが、より理解しやすい「Feature Sliced」も見てみることをお勧めします。 コンポーネントの使い分けに注意 システムをコンポーネントに分けて考えるのであれば、コンポーネント間でのコードの使用についても言及する価値があります。オーダー作成機能を思い出してみましょう。 import { Product, totalPrice } from "./product"; export function createOrder(user: User, cart: Cart): Order { return { user: user.id, cart, created: new Date().toISOString(), status: "new", total: totalPrice(products), }; } この関数は、別のコンポーネントである製品のtotalPriceを使用しています。このような使い方をすること自体は問題ないのですが、コードを独立した機能に分割したい場合、他の機能の機能に直接アクセスすることはできません。 この制約を回避する方法は、「DDD、Hexagonal、Onion、Clean、CQRS、...How I put it all together」や「Feature Sliced」でも読むことができます。 エイリアスではなく、ブランド化された型を使う 共有カーネルでは、タイプエイリアスを使用しました。型エイリアスは操作が簡単で、新しい型を作成して、例えば文字列を参照するだけです。しかし、その欠点は、TypeScriptにはエイリアスの使用を監視し、それを強制するメカニズムがないことです。 誰かがDateTimeStringの代わりにstringを使ったとしても、コードは問題なくコンパイルされます。 問題はまさに、より広い型が使われているにもかかわらず、コードがコンパイルされてしまうことです(賢い言い方をすれば、前提条件が弱められている)。なぜなら、特別な品質の文字列だけでなく、どんな文字列でも使用できるため、エラーが発生する可能性があるからです。 第二に、真実の情報源が2つになるので、読むのに混乱します。本当に日付だけを使う必要があるのか、それとも基本的にどんな文字列でも良いのかが不明瞭です。 TypeScriptに特定の型が必要であることを理解させるには、ブランディング、つまりブランド化された型を使う方法があります。ブランディングを使えば、型がどのように使われているかを正確に把握することができますが、コードは少し複雑になります。 ドメインに依存する可能性に注意 次に気になるのは、createOrder関数でドメイン内に日付が作られていることです。 import { Product, totalPrice } from "./product"; export function createOrder(user: User, cart: Cart): Order { return { user: user.id, cart, // Вот эта строка: created: new Date().toISOString(), status: "new", total: totalPrice(products), }; } new Date().toISOString()はプロジェクト内で頻繁に繰り返されると思われるので、何らかのヘルパーに入れておきたいと思います。 lib/datetime.ts export function currentDatetime(): DateTimeString { return new Date().toISOString(); } そしてそれをドメインで使います domain/order.ts import { currentDatetime } from "../lib/datetime"; import { Product, totalPrice } from "./product"; export function createOrder(user: User, cart: Cart): Order { return { user: user.id, cart, created: currentDatetime(), status: "new", total: totalPrice(products), }; } しかし、ドメイン内の何かに依存することはできないことをすぐに思い出します。つまり、どうすればいいのでしょうか?createOrderは、注文のためのすべてのデータを完全な形で受け取るのが良いでしょう。日付は最後の引数として渡すことができます。 function someUserCase() { // Use the `dateTimeSource` adapter, // to get the current date in the desired format: const createdOn = dateTimeSource.currentDatetime(); // Pass already created date to the domain function: createOrder(user, cart, createdOn); } これにより、ドメインの独立性が保たれ、テストもしやすくなります。 例題では、2つの理由からこの点に焦点を当てないことにしました。主旨から外れてしまうことと、言語機能のみを使用する場合には、独自のヘルパーに依存することに問題はないと考えているからです。このようなヘルパーは、コードの重複を減らすだけなので、共有カーネルと考えることもできます。 カートとオーダーの関係に注目 この例では、カートは商品のリストを表しているだけなので、OrderはCartを含んでいます。 export type Cart = { products: Product[]; }; export type Order = { user: UniqueId; cart: Cart; created: DateTimeString; status: OrderStatus; total: PriceCents; }; 注文とは関係のない追加のプロパティが Cart にある場合、この方法ではうまくいかないことがあります。このような場合は、データ・プロジェクションや中間DTOを使用するのが良いでしょう。 オプションとして、「商品リスト」エンティティを使用することもできます。 type ProductList = Product[]; type Cart = { products: ProductList; }; type Order = { user: UniqueId; products: ProductList; created: DateTimeString; status: OrderStatus; total: PriceCents; }; ユーザーケースをよりテスト可能なものにする ユースケースにも議論すべき点がたくさんあります。今のところ、orderProducts関数はReactから切り離してテストするのは難しく、これは悪いことです。理想的には、最小限の労力でテストできるようにすべきです。 現在の実装の問題点は、ユースケースからUIへのアクセスを提供するフックにあります application/orderProducts.ts export function useOrderProducts() { const notifier = useNotifier(); const payment = usePayment(); const orderStorage = useOrdersStorage(); const cartStorage = useCartStorage(); async function orderProducts(user: User, cart: Cart) { const order = createOrder(user, cart); const paid = await payment.tryPay(order.total); if (!paid) return notifier.notify("Oops! ?"); const { orders } = orderStorage; orderStorage.updateOrders([...orders, order]); cartStorage.emptyCart(); } return { orderProducts }; } 規範的な実装では、ユースケース関数はフックの外側に配置され、サービスは最後の引数またはDIを介してユースケースに渡されます。 type Dependencies = { notifier?: NotificationService; payment?: PaymentService; orderStorage?: OrderStorageService; }; async function orderProducts( user: User, cart: Cart, dependencies: Dependencies = defaultDependencies ) { const { notifier, payment, orderStorage } = dependencies; // ... } そして、そのフックがアダプターになります function useOrderProducts() { const notifier = useNotifier(); const payment = usePayment(); const orderStorage = useOrdersStorage(); return (user: User, cart: Cart) => orderProducts(user, cart, { notifier, payment, orderStorage, }); } そうすると、フックコードはアダプタとみなされ、ユースケースだけがアプリケーション層に残ることになります。orderProducts関数は、必要なサービスのモカを依存関係として渡すことでテストすることができます。 自動依存性注入の設定 そこで、アプリケーション層では、サービスを手動で注入するようになりました。 export function useOrderProducts() { // ここではフックを使って各サービスのインスタンスを取得しています。 // orderProductsのユースケースの中で使用されます。 const notifier = useNotifier(); const payment = usePayment(); const orderStorage = useOrdersStorage(); const cartStorage = useCartStorage(); async function orderProducts(user: User, cart: Cart) { // ...ユースケースの中でこれらのサービスを使用します } return { orderProducts }; } しかし、一般的には、これを自動化し、依存性注入で行うことができます。すでに最後の引数を通じて最もシンプルなバージョンのインジェクションを見ましたが、さらに進んで自動注入を設定することもできます。 この特定のアプリケーションでは、DIを設定することにあまり意味がないと思いました。要点がずれてしまうし、コードが複雑になりすぎてしまうからです。また、Reactやフックの場合は、指定したインターフェイスの実装を返す「コンテナ」として使うことができます。たしかに手作業ではありますが、エントリーの敷居が高くなることはなく、新人開発者にとっては読みやすいものになっています。 実際のプロジェクトではもっと複雑になることも 投稿の例は洗練されていて、意図的にシンプルにしてあります。人生は、この例よりもはるかに驚きと複雑さに満ちていることは明らかです。そこで、クリーンアーキテクチャで作業する際に起こりうる一般的な問題についてもお話したいと思います。 ビジネスロジックの分岐 最も重要な問題は、知識が不足している対象分野です。あるお店に、商品、値引き商品、評価損商品があるとします。これらのエンティティをどのように適切に記述すればよいのでしょうか? 拡張される「ベース」となるエンティティがあるべきでしょうか?このエンティティは具体的にどのように拡張されるべきでしょうか?追加のフィールドが必要ですか?これらのエンティティは相互に排他的であるべきですか?単純なエンティティではなく、別のエンティティがある場合、ユーザーケースはどのように振る舞うべきですか?重複をすぐに減らすべきでしょうか? チームもステークホルダーも、システムが実際にどのように動作すべきかをまだ知らないため、質問が多すぎて答えが出ないことがあります。仮定しかない場合は、分析麻痺に陥る可能性があります。 具体的な解決策は特定の状況に依存しますが、私がお勧めできるのは一般的なものをいくつか挙げることだけです。 たとえそれが「拡張」と呼ばれるものであっても、継承は使用しないでください。たとえ、インターフェースが本当に継承されているように見えたとしても。明らかに階層化されているように見えたとしてもです。ちょっと待ってください。 コードのコピーペーストは必ずしも悪ではなく、ツールなのです。ほとんど同じものを2つ作り、現実にどのように振る舞うか、観察してみましょう。ある時点で、両者が大きく異なったものになっているか、あるいは本当に1つの分野でしか異なっていないことに気づくでしょう。ありとあらゆる条件やバリエーションに対するチェックを作るよりも、似たような2つのエンティティを1つに統合する方が簡単です。 それでも何かを拡張しなければならない場合は...。 共分散、反比例、不変性に留意し、誤って必要以上の作業をしてしまわないようにしましょう。 異なるエンティティや拡張機能を選択する際には、BEMのブロックやモディファイアとの類似性を利用してください。BEMの文脈で考えれば、コードを独立したエンティティか「モディファイア-エクステンション」かを判断するのにとても役立ちます。 相互依存のユースケース 2つ目の大きな問題は、関連するユースケースで、あるユースケースのイベントが別のユースケースを誘発することです。 この問題を解決する唯一の方法は、ユースケースをより小さく、アトミックなユースケースに分割することです。そうすれば、簡単にまとめることができます。 一般的に、このようなスクリプトの問題は、プログラミングにおけるもう一つの大きな問題であるエンティティの構成に起因しています。 エンティティを効率的に構成する方法については、すでに多くの文献があり、数学のセクションもあります。それについては別の記事を書くことにします。 結論 この記事では、フロントエンドのクリーンなアーキテクチャに関する私の講演の概要を説明し、少し展開してみました。 これはゴールドスタンダードではなく、異なるプロジェクト、パラダイム、言語での経験をまとめたものです。コードを切り離して、独立したレイヤー、モジュール、サービスを作ることができる便利なスキームだと思っています。これらは別々にデプロイして公開できるだけでなく、必要に応じてプロジェクトからプロジェクトへ移行することもできます。 OOPに触れていないのは、アーキテクチャとOOPが直交しているからです。確かに、アーキテクチャはエンティティの構成について述べていますが、構成の単位を何にするか、つまりオブジェクトにするか、関数にするかについては指示していません。例で見てきたように、異なるパラダイムで作業することができます。 OOPについては、先日、クリーンアーキテクチャをOOPで使う方法についての記事を書きました。この投稿では、canvas上で木の絵を生成するものを書いています。 このアプローチをチップスライシング、ヘキサゴナル・アーキテクチャ、CQSなどの他のものと具体的にどのように組み合わせることができるかについては、「DDD、Hexagonal、Onion、Clean、CQRS、...How I put it all together」やこのブログの一覧のシリーズを読むことをお勧めします。非常に洞察力に富み、簡潔で的を得ています。 文献 この記事とソースコード: * ソースコード * サンプルアプリケーション 実践的な設計: * The Clean Architecture * Model-View-Controller * DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together * Ports & Adapters Architecture * More than Concentric Layers * Generating Trees Using L-Systems, TypeScript, and OOP Series' Articles システム設計: Domain Knowledge Use Case Coupling and cohesion Shared Kernel Analysis Paralysis 設計とコーディングについての本 * Design Composition and Performance * Clean Architecture * Patterns for Fault Tolerant Software TypeScript, C# や他の言語の概念: Interface Closure Set Theory Type Aliases Primitive Obsession Floating Point Math Branded Types и How to Use It パターン、メソドロジー: Adapter, pattern SOLID Principles Impureim Sandwich Design by Contract Covariance and contravariance Law of Demeter BEM Methodology
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactの表示される仕組みについて〜DOMとは?〜

DOM? ブラウザがJavascriptを実行してDOMを作る。 正確にはDocument Object Model。 1つ1つのタグをJavascriptのオブジェクト({プロパティ:値}のやつ)として表す。(Document.〇〇とかで使われるやつ…。)Documentオブジェクト=DOMの認識 オブジェクトの値を変えるとタグの表示が変更される。 DOMで用意されるオブジェクトにはエレメントとノードがある。 エレメント エレメントオブジェクトはHTMLのタグを扱う。エレメントを操作することでタグの表示をJSから操作できる。 ノード ノードは、一番小さい単位、エレメントもノードの一種。改行や、タグの前の半角スペースもノードになる。 Reactの表示を読み込む仕組み ①querySelectorメソッドを使ってオブジェクトを取得する idがrootの要素を取得 let dom = document.querySelector('#root') ②仮想エレメントの作成 仮想DOMのElementオブジェクト。 React.createElementはReactの仮想DOMによるElementを作成するもの。 React.createElement(タグ名,属性,中に組み込まれるもの) 引数について、 第一引数:pタグを作るときはp 第二引数:エレメントに用意される属性のオブジェクトを記入。特に必要なければ空のオブジェクトで{}と入れる。 第三引数:作成するエレメントの内部に組み込まれるもの。テキストを表示する場合はそのテキストを指定する。 let element = React.createElement( 'p',{},'Hello React!' ) ③レンダリングの実行 作成した仮想DOMのエレメントはブラウザがレンダリング(実際に画面に表示されるデータを生成する)を行って画面に表示される。 ReactDOM.render(エレメント,dom) まとめると 場所を指定して React.createElementでまっさらな仮想DOMに材料であるエレメントを作る(準備させる) ReactDOM.renderで仮想DOMから実際に表示させるDOMを作って表示できるようにする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React Native】ただ写真ライブラリから写真を取得するだけでなく、加工して表示させる方法

写真ライブラリから写真を取得し、自分の好きなサイズに加工して表示させる ReactNativeで写真ライブラリから写真を取得する方法はいくつかありますが、その中で加工や編集も行えるライブラリを紹介します。 ライブラリのGitHub https://github.com/ivpusic/react-native-image-crop-picker もともとreact-native-image-pickerというライブラリがあり、それを使えば「カメラを起動させるか」「写真ライブラリを開くか」の選択欄も出て、どちらも実装できる便利なものがありましたが、最近のアップデートで選択欄が出なくなったり何かと使いにくなりました。 image-pickerのGitHub https://github.com/react-native-image-picker/react-native-image-picker またこのライブラリで写真を取得してもそのまま表示されるため、実際にアプリに入れてみないと調整が難しいです。 例えば、アイコンを設定する際に写真全体を表示するのではなく、右下の一部分だけを表示したいとなった時に、従来のimage-pickerだと、まず写真ライブラリ使う写真の加工をして、それを保存してから使わないといけません。 そこがimage-crop-pickerを使うことでアプリ側で加工するためのviewを挟んでくれるため、その場で写真の加工を行えます 実際に実装し、両方を比べてみる 実際にimage-pickerとimage-crop-pickerの両方を実装し、比べてみます。 1、ライブラリをインストール react-native-image-crop-picker:yarn add react-native-image-crop-picker react-native-image-picker:yarn add react-native-image-picker このライブラリでは写真ライブラリにアクセスしたり、カメラを起動させるためinfo.plistに設定が必要です。 ios iOSディレクトリ内のinfo.plistに追記してください <key>NSPhotoLibraryUsageDescription</key> <string>${プロジェクト名など} would like to upload photos from your photo gallery</string> <key>NSCameraUsageDescription</key> <string>${プロジェクト名など} requires to access camera for uploading photos to your profile or posts</string> <key>NSPhotoLibraryAddUsageDescription</key> <string>${プロジェクト名など} would like to save photos to your photo gallery</string> <key>NSMicrophoneUsageDescription</key> <string>${プロジェクト名など}requires to access Audio recording to record and uplod videos</string> コマンドラインで (cd ios && pod install)を打ち込む ちなみに()で囲うことで現在のディレクトリから移動せずにpod installしてくれます。 あるあるなのがiosディレクトリにしたままyarn iosをしててエラーになるパターンです。 割と気づかず何でエラーなのか分からなくなってしまいます。 Android AndroidManifest.xmlに以下を追記してください。 <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 2、コーディング プロジェクトを作成し、今回用のファイルを用意します。 ライブラリをインポート import ImagePicker from 'react-native-image-crop-picker'; import { launchCamera, launchImageLibrary } from 'react-native-image-picker'; image-pickerでは写真ライブラリを開きたいときはlaunchImageLibrary、カメラを起動するときはlaunchCameraを使用します。 launchCameraもインポートしている状態ですが、今回は使わないので必要ないです。 余談ですが、アプリなどのアイコンを設定する際に大体カメラを起動するか写真ライブラリから取得するか選択できると思いますが、その場でカメラを起動して撮った写真をアイコンに設定する人なんているんですかね。 僕は一度もカメラを使ったことないし、友人のアイコンを見る限り全員既存の写真を取得して使ってると思います(加工したものをアイコンに使うから)。 image-crop-pickerを実装 このonCropImageをonPressしたタイミングで呼び出せば写真ライブラリから取得できます。 openPickerの中身には現在は画像サイズなど初期の設定しかしてませんが、たくさん種類があるため自分の好みに合わせて色々付け加えてみてください。 https://github.com/ivpusic/react-native-image-crop-picker 取得時にsetImagePath内に格納してますがこれはuseStateで管理している値なので、今はエラーのままで大丈夫です。 const onCropImage = () => { ImagePicker.openPicker({ width: 300, height: 300, cropping: true, }).then(image => { setImagePath(image.path) }); } image-pickerを実装 こちらもoptionsで色々設定できますが今回は説明を割愛します。 https://github.com/react-native-image-picker/react-native-image-picker 表示するためのuriはresponseのassetsの配列内にあるのでそこにある値をuseStateで管理します const options = { mediaType: 'photo', maxWidth: 1000, maxHeight: 1000, quality: 0.8, saveToPhotos: true, }; const choosePhoto = () => { launchImageLibrary(options, (response) => { if (response.didCancel) { console.log('User cancelled image picker'); } else if (response.error) { console.log('ImagePicker Error: ', response.error); } else { console.log(response.assets[0].uri) setImagePath(response.assets[0].uri) } }); } 全体を実装 今回アイコンをタップした際に出てくる選択欄(アクションシート)はライブラリを使って実装してますが、別の記事で実装方法を取り上げてるのでそこを参照ください。 https://qiita.com/flutter_daisuki/items/949869756b3d10944b7f import React ,{useState}from 'react'; import {StyleSheet, TouchableOpacity, View,Image} from 'react-native'; import {ActionSheet} from 'react-native-cross-actionsheet'; import Icon from 'react-native-vector-icons/FontAwesome'; import ImagePicker from 'react-native-image-crop-picker'; import { launchCamera, launchImageLibrary } from 'react-native-image-picker'; export const ImageCropPicker = () => { const [imagePath ,setImagePath] = useState('') const onCropImage = () => { ImagePicker.openPicker({ width: 300, height: 300, cropping: true, }).then(image => { setImagePath(image.path) }); } const options = { mediaType: 'photo', maxWidth: 1000, maxHeight: 1000, quality: 0.8, saveToPhotos: true, }; const choosePhoto = () => { launchImageLibrary(options, (response) => { if (response.didCancel) { console.log('User cancelled image picker'); } else if (response.error) { console.log('ImagePicker Error: ', response.error); } else { console.log(response.assets[0].uri) setImagePath(response.assets[0].uri) } }); } const onPressAction = () => { return ActionSheet.options({ options: [ {text: 'image-picker', onPress:choosePhoto}, {text: 'image-crop-picker', onPress:onCropImage}, ], cancel: {text: 'キャンセル'}, }); }; return ( <View> <TouchableOpacity onPress={onPressAction}> <View style={styles.iconButton}> {imagePath ? ( <Image style={styles.Photo} source={{uri: imagePath}} /> ) : ( <Icon style={styles.icon} name="user-o" size={70} /> )} </View> </TouchableOpacity> </View> ); }; const styles = StyleSheet.create({ iconButton: { borderWidth: 1, width: 100, height: 100, borderRadius: 100, marginLeft: 'auto', marginRight: 'auto', }, icon: { marginLeft: 'auto', marginRight: 'auto', marginTop: 'auto', marginBottom: 'auto', }, Photo: { width: 100, height: 100, borderRadius: 50, }, }); コードを解説するとアイコン全体をtouchableOpacityでタップできるようにし、タップした際にアクションシートを出すようにしています。 またアイコンはuseStateのimagePathがある場合はそのまま写真を表示するようにして、ない場合はIconを表示するようにしています。 三項演算子を使ってますが、めちゃくちゃ使う場面が多いのでぜひ覚えてください。 そして、アクションシート内で上をタップするとimage-pickerの処理が、下をタップするとimage-crop-pickerの処理が動くようになってます。 3、実際にどんな感じか確認してみる 初期画面 タップ時 image-pickerを選択時 ライブラリが開き、右上の滝をタップすると 表示されました! ただ、今回は綺麗に映りましたが、もし写真の一部分だけをアイコンに使いたいのにこうされると思うようにアイコンが設定できないです。 image-crop-pickerを選択時 ライブラリが開き、さっきと同じ滝の写真をタップすると。。。 写真編集用のviewが出てきました! 滝を拡大させて、アイコンいっぱい滝にしよう できました! ユーザーのことを考えるとこっちの方がいいですね✨ 皆さんもぜひ使ってみてください
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コンポーネントとprops

React入門 最近学習を始めたのですが、propsとコンポーネントという概念がReactを触るという点において重要だと感じたのでメモ代わりにまとめました。 コンポーネントとは ・見た目と機能を持つUI ・コンポーネントを組み合わせてページを作る ・大きく2種類のコンポーネントに分かれる  Class Component (クラスコンポーネント)  Functional Component (関数コンポーネント) //今はこっちで書くことが主流 なぜコンポーネントを使うのか ・再利用するため  ✔同じ記述を何度もする必要がない ・コードの見通しをよくするため  ✔1コンポーネント=1ファイル  ✔べつファイルに分けることでコードが読みやすくなる ・変更に強くするため  ✔修正は1箇所だけでOK コンポーネントの基本的な使い方 ・ファイルは大文字 ・子コンポーネントでexport ・親コンポーネントでimport コンポーネントの再利用 ・配列をmap()メソッドで処理することが一般的 ・同じコンポーネントをいくつも呼び出すことができる propsでデータを受け渡す(★) propsとは(★) 親から子にデータを受け渡す際の橋渡し役 Reactでよく使用する App.jsx(親) import Article from "./components/Article"; function App() { return( <div> <Article title={'これはタイトルです'} ←呼び出し側で値を指定する content={'これはコンテンツです'} </div> ); } export default App; components/Article.jsx(子) const Article = (props) => { return( <div> <h2>{props.title} <p>{props.content}</p> </div> ) } export default Article; propsで渡せるデータ ・文字列、数値、真偽値、配列、オブジェクト、日付などなんでもOK ・変数も可能 ・文字列は{}なしでもOK ・propsのデータは{}に記述
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React Native】axiosを使ってAPIの共通処理を実装する

APIの共通処理を実装 大規模な開発になるとフロントからたくさんのAPIを叩くことになります。 そこでAPIを叩く際の共通処理を作成し、毎回詳しく記述しなくて済むようにしたいと思います。 今回はaxiosを使って実装していきますのでインストールしておいてください yarn add axios APIの共通処理を実際に作っていく api.tsを作成 import axios from 'axios' export const api = axios.create({ baseURL:'https://jsonplaceholder.typicode.com/', }) // api.interceptors.request.use( // config.headers.authorization= `Bearer ${ここにアクセストークンを入れる}` // ) api.interceptors.response.use( response => { return response }, function(error) { switch(error.response.status) { case 400: case 401: console.log('認証エラーです'); case 404: console.log('URL先がないです'); default: } } ) 実際の開発では認証用にアクセストークンをつけてリクエストを投げると思いますが、今回はjsonplaceholderを使って手軽に検証しているのでコメントアウトにしています。 初めにbaseURLを設定します。 そして、アクセストークンをつける必要がある場合はコードの用にrequest.use内に設定します response.useではAPIから返ってきたレスポンスの処理を書きます。 成功時にはそのままreturnで返し、エラーがある場合はステータスコードに合わせて処理を書くことができます。 これを毎回API毎に記述するのはコード量も増すし、めんどくさいので共通にまとめておくことでわかりやすいですね! 実際にAPIを呼ぶ側の処理を作っていく 新規ファイルを作成し、 import React, {useEffect, useState} from 'react'; import {StyleSheet, Text, FlatList, SafeAreaView} from 'react-native'; import {api} from '../api/api'; export const ApiResearch = () => { const [postsData, setPostsData] = useState([]); const getPostsData = async () => { const res = await api.get('posts'); setPostsData(res.data); }; useEffect(() => { getPostsData(); },[]); return ( <SafeAreaView> <Text style={styles.title} >ここからpostデータです</Text> <FlatList data={postsData} renderItem={({item}) => <Text>{item.title}</Text>} keyExtractor={item => item.id} /> </SafeAreaView> ); }; const styles = StyleSheet.create({ title: { fontSize:20, fontWeight:'bold', paddingBottom:10 } }); まず、先ほど作ったAPIの共通処理をimportし、初期表示時にgetPostDataでAPIを呼ぶように記述しています。 getPostsData内ではimportしたapiの後にgetメソッドを記述し、先程baseURLで設定したURLの続きを記述しています。 これでAPIを呼ぶことができるようになります。 ちなみに表示はFlatListを使って返ってきたレスポンス(配列)をマッピングし、全部表示しています 別のAPIも呼んでみる import React, {useEffect, useState} from 'react'; import {StyleSheet, Text, FlatList, SafeAreaView} from 'react-native'; import {api} from '../api/api'; export const ApiResearch = () => { const [postsData, setPostsData] = useState([]); const [usersData, setUsersData] = useState([]); const getPostsData = async () => { const res = await api.get('posts'); setPostsData(res.data); }; const getUsersData = async () => { const res = await api.get('users'); setUsersData(res.data); }; useEffect(() => { getPostsData(); getUsersData(); },[]); return ( <SafeAreaView> <Text style={styles.title} >ここからpostデータです</Text> <FlatList data={postsData} renderItem={({item}) => <Text>{item.title}</Text>} keyExtractor={item => item.id.toString()} /> <Text style={styles.title}>ここからuserデータです</Text> <FlatList data={usersData} renderItem={({item}) => <Text>{item.name}</Text>} keyExtractor={item => item.id.toString()} /> </SafeAreaView> ); }; const styles = StyleSheet.create({ title: { fontSize:20, fontWeight:'bold', marginBottom:10 } }); 次はjsonplaceholderからuserデータを呼んでいますが、先ほどと同じような記述で簡単に呼び出すことができます 今回はjsonplaceholderで検証したので、わかりにくかったかもしれないですが、実際の大規模の開発だととても恩恵を受けることになるので覚えておいて損はないです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Axios】速度改善のため、毎度APIを呼ばないようにする(〇〇秒キャッシュ)

ページに遷移するたびにAPIを呼ぶのは効率が悪いため、ある程度の時間は再レンダリングしてもAPIを呼ばないようにする 独学の時はAPIを呼ぶのは基本一つしかなく、あまり速度が遅いとは感じませんでしたが、 本格的にアプリを作っていくと1ページに4種類のAPIを呼ばないといけないことになります。 その時に毎回全部のAPIを呼んでいると速度が遅くなる恐れがあります。 そこで、毎回呼ぶ必要のないAPIは次にAPIを呼ぶまでの時間を設定し、その間はレンダリングしてもAPIを呼ばないようにします。 実装していく 実際にコーディングしていきます。 1、APIを呼ぶ設定 まずはAPIの呼び出し共通処理を記述します import axios from 'axios'; export const api = axios.create({ baseURL: 'https://jsonplaceholder.typicode.com/' }); api.interceptors.response.use( response => { return response; }, function (error) { switch (error.response.status) { case 400: case 401: console.log('認証エラーです'); case 404: console.log('URL先がないです'); default: } }, ); 次に実際にAPIを呼び出す処理を実装します。 jsonplaceholderの「posts」と「users」の二つを呼び出しています。 そしてここでは検証用に毎秒レンダリングするように処理しています。(setInterval) こうすることで、1秒経つ毎に毎回二つのAPIを呼び出しているようにしています import React, {useEffect, useState} from 'react'; import {StyleSheet, Text, FlatList, SafeAreaView} from 'react-native'; import {api} from '../api/api'; export const ApiResearch = () => { const [count, setCount] = useState(0); const [postsData, setPostsData] = useState([]); const [usersData, setUsersData] = useState([]); const getPostsData = async () => { const res = await api.get('posts'); setPostsData(res.data); }; const getUsersData = async () => { const res = await api.get('users'); setUsersData(res.data); }; useEffect(() => { getPostsData(); getUsersData(); const intervalId = setInterval(() => { setCount(prevCount => prevCount + 1); }, 1000); return () => clearInterval(intervalId); },[count]); return ( <SafeAreaView> <Text>{count}</Text> <Text style={styles.title} >ここからpostデータです</Text> <FlatList data={postsData} renderItem={({item}) => <Text>{item.title}</Text>} keyExtractor={item => item.id.toString()} /> <Text style={styles.title}>ここからuserデータです</Text> <FlatList data={usersData} renderItem={({item}) => <Text>{item.name}</Text>} keyExtractor={item => item.id.toString()} /> </SafeAreaView> ); }; const styles = StyleSheet.create({ title: { fontSize:20, fontWeight:'bold', marginBottom:10 } }); あとはこのApiResearce.jsをApp.jsで呼んでください。 では実際にReactNative Debbugerで確認してみましょう。 画像の通り、1秒毎にpostsとusersが呼び出されてるのがわかると思います。 2、axios cache adapterを使い、APIを呼ぶタイミングを設定する 必要なライブラリをインストール yarn add axios-cache-adapter APIの共通処理にcacheの設定をしていきます import axios from 'axios'; import {setupCache} from 'axios-cache-adapter'; const cache = setupCache({ maxAge: 10 * 1000, }); export const api = axios.create({ baseURL: 'https://jsonplaceholder.typicode.com/', adapter:cache.adapter }); api.interceptors.response.use( response => { return response; }, function (error) { switch (error.response.status) { case 400: case 401: console.log('認証エラーです'); case 404: console.log('URL先がないです'); default: } }, ); setupCacheをインポートし、キャッシュする時間を設定します。 今回は10秒経ったら再度取得するようにしたいので10*1000としています(ミリ秒なので*1000する必要があります)。 Debuggerで確認してみましょう 今回は初期の一度のみ呼ばれているだけで、毎秒呼ばれてないですね。 10秒経ったので再度API通信がされました! 3、API毎にキャッシュする時間を設定する 最後にAPI毎に時間を設定していきます。 こうすることでリアルタイムにAPIを取得したいものと、何分、何時間に一回でいいもので分けることができます。 実装方法は簡単です。先程は共通処理に記述したので、APIを呼び出す処理の第二引数に加えるだけです。 先程の共通処理に記述したコードを削除し、ApiReseach.js(APIを呼び出しているところ)に追加で記述していきます import React, {useEffect, useState} from 'react'; import {StyleSheet, Text, FlatList, SafeAreaView} from 'react-native'; import {api} from '../api/api'; import {setupCache} from 'axios-cache-adapter'; const tenCache = setupCache({ maxAge: 10 * 1000, }); const thirtyCache = setupCache({ maxAge: 30 * 1000, }); export const ApiResearch = () => { const [count, setCount] = useState(0); const [postsData, setPostsData] = useState([]); const [usersData, setUsersData] = useState([]); const getPostsData = async () => { const res = await api.get('posts',{ adapter:tenCache.adapter }); setPostsData(res.data); }; const getUsersData = async () => { const res = await api.get('users',{ adapter:thirtyCache.adapter }); setUsersData(res.data); }; useEffect(() => { getPostsData(); getUsersData(); const intervalId = setInterval(() => { setCount(prevCount => prevCount + 1); }, 1000); return () => clearInterval(intervalId); },[count]); return ( <SafeAreaView> <Text>{count}</Text> <Text style={styles.title} >ここからpostデータです</Text> <FlatList data={postsData} renderItem={({item}) => <Text>{item.title}</Text>} keyExtractor={item => item.id.toString()} /> <Text style={styles.title}>ここからuserデータです</Text> <FlatList data={usersData} renderItem={({item}) => <Text>{item.name}</Text>} keyExtractor={item => item.id.toString()} /> </SafeAreaView> ); }; const styles = StyleSheet.create({ title: { fontSize:20, fontWeight:'bold', marginBottom:10 } }); 10秒毎に呼び出すAPIと30秒毎に呼び出すAPIで分けています。 では実際にDebuggerで確認してみましょう 画像の通りusersの方はpostsが3回呼ばれると1回呼ばれているのがわかると思います。 10秒、20秒のタイミングではusersは呼ばれていません! エンジニア初学者でエンジニア転職を目指されている方はここまで実装する必要はないかもしれないですが、 転職用のポートフォリオを作る際にこのようにAPI毎にキャッシュ時間を設定して速度改善をしていると他の人のポートフォリオと差別化が図れますし、かなりポイントが高いと思います 自分も初めて実装しましたが、想像以上に簡単だったので使ってみてください!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ES6】変数宣言const・let・var のまとめ

これは何? ES6記法で書く機会が増え、const, let, varの使い分けがよくわかっていないため、どのようなな違いがあるかまとめてみました 結論 基本的にはconstを使う。 forループの処理などのように再代入が必要なケースのみletを使用する。 varは、挙動が把握しづらく、バグの温床になる可能性があるため使わない。 用語 それぞれの特徴を理解する際に、出てくる用語のまとめ - 再代入 = 一度宣言した変数を更新すること - 再宣言 = 一度宣言した変数を、同じ変数名を使って再び宣言し直すこと - ブロックスコープ = ブロック{}ごとに囲まれた範囲のこと - 関数スコープ = 関数のブロック{}ごとに作られる範囲のこと それぞれの特徴 const 再代入不可 再宣言不可 ブロックスコープを持つ ブロックスコープ外からのアクセスを制限。つまり、ブロックスコープ外から参照ができない。 関数スコープを持つ 関数スコープ外からのアクセスを制限。つまり、関数スコープ外から参照ができない。 let 再代入可能 再宣言不可能 ブロックスコープを持つ ブロックスコープ外からのアクセスを制限。つまり、ブロックスコープ外から参照ができない。 関数スコープを持つ 関数スコープ外からのアクセスを制限。つまり、関数スコープ外から参照ができない。 var 再代入可能 再宣言可能 ブロックスコープを持たない ブロックスコープ外からのアクセスを許可する。つまり、ブロックスコープ外から参照ができる。 関数スコープを持つ 関数スコープ外からのアクセスを制限。つまり、関数スコープ外から参照ができない。 再現 再代入 const constValue = 'Hello!!'; let letValue = 'Hello!!'; var varValue = 'Hello!!'; // 再代入 constValue = 'Goodby!!'; letValue = 'Goodby!!'; varValue = 'Goodby!!'; console.log(constValue); console.log(letValue); console.log(varValue); // エラー内容 Uncaught TypeError: Assignment to constant variable. Goodby!! Goodby!! constのみ再代入しようとするとエラーが吐かれる。 再宣言 const constValue = 'Hello!!'; let letValue = 'Hello!!'; var varValue = 'Hello!!'; // 再宣言 const constValue = 'Goodby!!'; let letValue = 'Goodby!!'; var varValue = 'Goodby!!'; console.log(constValue); console.log(letValue); console.log(varValue); // エラー内容 Uncaught SyntaxError: Identifier 'constValue' has already been declared Uncaught SyntaxError: Identifier 'letValue' has already been declared" Goodby!! constとletは再宣言しようとするとエラーが吐かれる。 ブロックスコープ const isTest = true; if(isTest) { const constValue = 'Hello!!'; let letValue = 'Hello!!'; var varValue = 'Hello!!'; } console.log(constValue); console.log(letValue); console.log(varValue); // 出力 Uncaught ReferenceError: constValue is not defined Uncaught ReferenceError: letValue is not defined "Hello!!" constとletは、if文などのブロックスコープ外から参照できない 関数スコープ const setValuables = (text) => { const constValue = text; let letValue = text; var varValue = text; } setValuables('Hello!!'); console.log(constValue); console.log(letValue); console.log(varValue); // 出力 Uncaught ReferenceError: constValue is not defined Uncaught ReferenceError: letValue is not defined Uncaught ReferenceError: varValue is not defined const、letとvar全て、関数ブロック外から参照できない const, letの方が優れている varだと宣言したブロック以外にも影響をあたえてしまうため、意図しないところで値が変わってしまう。const, letだと宣言したブロック以内に影響が収まるため、意図しないところで値が変わってしまうことを防ぐことができます。 constは定数のように見えて、定数ではない constは再代入はできないが、オブジェクト型(Array, Objectなど)の中身を変更することができるようです。 例えば、配列に配列を再代入するとエラーになるが、配列の中身の一部を変更する時はエラーにらずに変更することができます。 そのため、constは定数のように見えて、定数ではないと言えます。 まとめ まとめて見て感じたことは、制限の少ないvarを使うのではなく、制限の多いconst、let を使う方が良さそうです。 また、基本的には変数はconstで定義してあげて、for文などの処理で、変数を数え上げたりする場合はletを使用するという使い分けで良さそうです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【laravel】laravel+Reactを使ってSPAにしてみる

初書:2021/09/06 mac : 11.5.2 php:v8.0.6 laravel:v8.57.0 前書き 最近Reactを知ったのでせっかくならLaravelでも使ってみよう、というメモ。 前提 Laravelインストール済み。 素のPHPしか触ってなかった人がlaravelを触ってみる - Qiita インストール まずはuiをインストール % composer require laravel/ui その後はuiをreactに変更 % php artisan ui react インストールとmix % npm install && npm run dev もし、Please run Mix again.と言われた場合はもう一度npm run devします。 以上 Typescriptにする ReactといえばTypescript…と、勝手に思ってるので、Typescriptで書けるように追加していく。 インストール まずは必要なものをインストール % npm install --save-dev ts-loader typescript react-router-dom @types/react @types/react-dom @types/react-router-dom react-router-domと@types/react-router-domはSPAを作る場合。laravel側でルーティングする場合は不要。 でもこの記事では使います。 tsconfig.jsonの設定 % npx tsc --init これでtsconfig.jsonが生成される。 設定は好みだが、一応今設定しているものを置いておく。 tsconfig.json { "compilerOptions": { "target": "ES2020", "lib": [ "ES2020", "DOM", ], "jsx": "react-jsx", "module": "AMD", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true }, "include": [ "resources/ts/**/*" ] } targetはES2020を選択。頻繁なアプデを好まない層(IEとか古いiOSとか)がこないサイトなら最新のものでいいと思う。 moduleはAMDを選択した。es2020とかとの違いはあんまり分かってない。(非同期で読み込める点?でものちに1ファイルにまとめる・・・。) 各ブラウザの対応状況:ECMAScript 2016+ compatibility table ページを作成する 今回はメインページとサブページを作る。 laravelでtsを置くのは基本的にresourcesディレクトリなので、この下に作成する。 resources/ts/app.tsx import ReactDOM from "react-dom"; import { BrowserRouter, Route, Switch } from "react-router-dom"; import main from "./main"; import sub from "./sub"; function App() : JSX.Element { return ( <BrowserRouter> <Switch> <Route exact path="/" component={main} /> <Route exact path="/sub" component={sub} /> </Switch> </BrowserRouter> ); } ReactDOM.render(<App />, document.getElementById("root")); resources/ts/main.tsx import React from "react"; import { Link } from "react-router-dom"; const element = (): JSX.Element => { return ( <div> <p>メインページです。</p> <Link to="/sub">サブページへ</Link> </div> ); }; export default element; resources/ts/sub.tsx import React from "react"; const element = (): JSX.Element => { return <p>サブページです。</p>; }; export default element; React、最近は関数型の書き方の方が良いって聞いたので関数型を使ってみた。 基盤となるapp.tsxとページを表示するmainとsubを作成。 これが出来れば、それを表示するためのwelcome.blade.phpを変更する resources/views/welcome.blade.php <!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Laravel</title> </head> <body class="antialiased"> <div id="root"> </div> </body> <script src="{{ mix('js/app.js') }}"></script> </html> 表示用のdivと、読み込みスクリプトがあれば後は自由。 ページは以上 laravel mixを使う そのままではapp.jsなるファイルは存在していないので、作成する。 普通にインストールしていると、webpack.mix.jsが既に存在しているので、これを編集する。 webpack.mix.js const mix = require('laravel-mix'); mix.js('resources/ts/app.tsx', 'public/js') .react() .webpackConfig({ module: { rules: [ { test: /\.tsx?$/, loader: 'ts-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: ['*', '.js', '.jsx', '.ts', '.tsx'], }, }) .version(); react()が何するのかよく分かってない。誰か教えてください(他人頼り) これで、npm run devを実行すると、先ほど作ったapp.tsx関連がapp.jsにまとめられる。 一度アクセスしてみる これで一度アクセスしてみる。 php artisan serveでサーバーを立て、http://127.0.0.1:8000/にアクセスすると、先程のメインページですが表示されているはず。 また、サブページへを押すと、http://127.0.0.1:8000/subにアクセスされ、サブページですが表示される。 一見これでいいように見えるが、直接http://127.0.0.1:8000/subにアクセスすると、laravel側からurlが存在していないと言われる。 これは、subページがReact内だけで作成されているだけのため。 この先は仕様次第だが、今回はReact側でアクセス処理を任せることにする 全てのアクセスをapp.tsxにまとめる アクセスはroutes/web.phpで管理しているので、ここを変える routes/web.php Route::get('/{any}', function () { return view('welcome'); })->where('any', '.*'); 元々あった/を/{any}に変え、条件を全てに変更した。 where文があるのは、/もanyに含まれるようにするため。 これで、直接http://127.0.0.1:8000/subにアクセスしても、サブページですが表示される。 終わりに この後ブラウザとサーバー側でデータのやりとりをするため、APIの設定等をしないといけないが、長くなりそうなので今回は一旦ここで切り上げ。 まだ試行錯誤段階なので、やり方異なってたり、よりいい方法とかあれば更新する…かも。 参考サイト React.JS + Laravel 8 + MySQL + API REST でCRUDを作ってみた。 【環境構築】LaravelでReactとTypescriptを使う方法 - Qiita 【Typescript×React】tsconfig.jsonの設定項目を詳しく紹介 - Qiita laravel-mixでtypescript + reactをビルドする - Qiita
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む