知っておくべき4つのレベルの単一ページアプリ

画像







この記事では、Reactアプリケーションをゼロから開発し、ドメインとそのサービス、ストレージ、アプリケーションサービス、およびビューについて説明します。









4レベルのシングルページ(SPA)アプリケーション







成功するプロジェクトにはすべて、開発チームのすべてのメンバーが理解できる明確なアーキテクチャが必要です。 最近チームに参加したとしましょう。 Timlidは、新しいアプリケーション用に提案されたアーキテクチャについて語っています。













要件について説明します。







アプリケーションは記事のリストを表示します。 同時に、ユーザーは記事を作成および削除したり、いいね!を付けたりすることができます。







そして、チームリーダーはこれを実現するように頼みます!







質問はありません。アーキテクチャから始めましょう



タイプチェック用にCreate React AppFlowを選択しました。 コードを膨張させないために、アプリケーションにはスタイルがありません。 次に、状態の概念に影響を与える現代のフレームワークの宣言的な性質について話しましょう。







最新のフレームワークは宣言的です



React、Angular、Vueは宣言的であり、関数型プログラミングの要素を使用するように促します。







ノートやノートブックのページに描かれた子供の頃の「漫画」を楽しんだことはありますか?







Kineograph (Kineograph)-アニメーション画像を作成するためのデバイスで、ノートに縫い付けられた紙のシートに個々のフレームが適用されます。 ビューアは、特別な方法でノートブックを裏返し、アニメーションの効果を観察します。







そして、これはReactの説明の一部です:







アプリケーションの状態ごとに簡単な表現を作成すると、Reactはデータを変更すると、必要なコンポーネントのみを効果的に更新およびレンダリングします。







そして、これはAngularの説明の一部です:







シンプルで宣言的なテンプレートを使用して、機能をすばやく構築します。 独自のコンポーネントでテンプレート言語を拡張します。







おなじみの音?







フレームワークは、ビューからアプリケーションを構築するのに役立ちます。 ビューは状態を表します。 しかし、条件は何ですか?







状態



ステータスには、アプリケーションで変更されたすべてのデータ部分が表示されます。







URLをフォローしました-これが状態です。 映画のリストを取得するためにAjax呼び出しを行いました-これも状態です。 情報をローカルストレージに格納します-それが状態です。







状態は不変オブジェクトから形成されます。







不変のアーキテクチャには多くの利点があり、その1つは状態レベルに関連しています。







React パフォーマンス最適化ガイドからの引用です:







不変性は、変更を追跡するコストを削減します。 変更は常に新しいオブジェクトの作成につながるため、オブジェクトへの参照が変更されたかどうかを確認するだけです。







ドメインレベル



ドメインは状態を記述し、ビジネスロジックを含みます。 これはアプリケーションの中核を表し、プレゼンテーション(ビュー)のレベルに依存するべきではありません。 フレームワークに関係なく、ドメインを使用する機能が必要です。









ドメインレベル







不変のアーキテクチャで作業するため、ドメインレベルはエンティティとドメインサービスで構成されます。 OOPでの貧血領域モデルの使用は、特に大規模なアプリケーションでは議論の余地がありますが、不変データの操作には非常に適しています。 かつて、ウラジミール・ホリコフのコースは私にとって発見でした。







記事のリストを表示する必要があるため、最初にArticle



の本質をモデル化します。







Article



型の将来のオブジェクトはすべて不変でなければなりません。 Flowは、各プロパティを読み取り専用として定義することにより、 これを強制的に実行できます (各プロパティの前のプラス記号を参照)。







Article.js:







 // @flow export type Article = { +id: string; +likes: number; +title: string; +author: string; }
      
      





次に、「factory」関数テンプレートを使用して、 articleService



作成しarticleService



。 この点はここで完全に説明されています







アプリケーションにはarticleService



が1つしか必要ないため、シングルトンとしてエクスポートします。 createArticle



メソッドをcreateArticle



Article



タイプのcreateArticle



オブジェクトを作成createArticle



。 新しい記事にはそれぞれ、一意に自動生成されたIDと0件のいいね!が付けられ、著者とタイトルのみが示されます。







Object.freeze()



メソッドはオブジェクトをフリーズします。つまり、オブジェクトに新しいプロパティが追加されないようにします。
c







createArticle



メソッドは、多分タイプのArticle



返します。







多分型オプションの型 )は、操作を実行する前にArticleオブジェクトが存在するかどうかを強制的にチェックします。







アーティクルの作成に必要なフィールドが失敗した場合、 createArticle



メソッドはnullを返します。 誰かが、ユーザー定義の例外をスローする方が良いと言うでしょう。 しかし、強制的に実行し、上位レベルが例外キャッチブロックを実装していない場合、プログラムは実行時にクラッシュします。







updateLikes



メソッドは、新しいカウンターでコピーを返すことにより、既存の記事のupdateLikes



の数を更新するのに役立ちます。







最後に、 isTitleValid



isAuthorValid



isAuthorValid



は、 createArticle



が破損したデータを処理できません。







ArticleService.js:







 // @flow import v1 from 'uuid'; import * as R from 'ramda'; import type {Article} from "./Article"; import * as validators from "./Validators"; export type ArticleFields = { +title: string; +author: string; } export type ArticleService = { createArticle(articleFields: ArticleFields): ?Article; updateLikes(article: Article, likes: number): Article; isTitleValid(title: string): boolean; isAuthorValid(author: string): boolean; } export const createArticle = (articleFields: ArticleFields): ?Article => { const {title, author} = articleFields; return isTitleValid(title) && isAuthorValid(author) ? Object.freeze({ id: v1(), likes: 0, title, author }) : null; }; export const updateLikes = (article: Article, likes: number) => validators.isObject(article) ? Object.freeze({ ...article, likes }) : article; export const isTitleValid = (title: string) => R.allPass([ validators.isString, validators.isLengthGreaterThen(0) ])(title); export const isAuthorValid = (author: string) => R.allPass([ validators.isString, validators.isLengthGreaterThen(0) ])(author); export const ArticleServiceFactory = () => ({ createArticle, updateLikes, isTitleValid, isAuthorValid }); export const articleService = ArticleServiceFactory();
      
      





検証は、特にドメインレベルでデータの一貫性を維持するために非常に重要です。 Validators



は、純粋な関数から構築できます。







Validators.js:







 // @flow export const isObject = (toValidate: any) => !!(toValidate && typeof toValidate === 'object'); export const isString = (toValidate: any) => typeof toValidate === 'string'; export const isLengthGreaterThen = (length: number) => (toValidate: string) => toValidate.length > length;
      
      





デモンストレーションのみを目的として、これらのチェックはひとつまみの塩で提出されます。 JavaScriptでは、オブジェクトが実際にオブジェクトであるかどうかを確認するのはそれほど簡単ではありません:)







これで、ドメインレベルが構成されました!







フレームワークに関係なく、コードをすぐに使用できることを嬉しく思います。 次に、 articleService



が私のお気に入りの本の1つに関する記事を作成し、 articleService



の数を更新するarticleService



どのように役立つかを見てみましょう。







domain-demo.js:







 // @flow import {articleService} from "../domain/ArticleService"; const article = articleService.createArticle({ title: '12 rules for life', author: 'Jordan Peterson' }); const incrementedArticle = article ? articleService.updateLikes(article, 4) : null; console.log('article', article); /* const itWillPrint = { id: "92832a9a-ec55-46d7-a34d-870d50f191df", likes: 0, title: "12 rules for life", author: "Jordan Peterson" }; */ console.log('incrementedArticle', incrementedArticle); /* const itWillPrintUpdated = { id: "92832a9a-ec55-46d7-a34d-870d50f191df", likes: 4, title: "12 rules for life", author: "Jordan Peterson" }; */
      
      





ストレージレベル



記事の作成および更新中に取得したデータは、アプリケーションの状態を具体化します。 このデータをどこかに保管する必要があります。この理想的な候補はリポジトリです。









ストレージレベル







記事の配列を使用して状態をシミュレートできます。







ArticleState.js:







 // @flow import type {Article} from "./Article"; export type ArticleState = Article[];
      
      





ArticleStoreFactory



は、パブリッシュ/サブスクライブテンプレートを実装し、 articleStore



をシングルトンとしてエクスポートします。







リポジトリには記事が含まれており、追加、削除、更新の不変の操作を実行します。 リポジトリは記事でのみ動作することに注意してください。 articleService



のみがarticleService



を作成および更新できます。 関係者は、すべてのサブスクライバーのリストを保管し、変更を通知するarticleStore



をサブスクライブおよびサブスクライブ解除できます。







ArticleStore.js







 // @flow import {update} from "ramda"; import type {Article} from "../domain/Article"; import type {ArticleState} from "./ArticleState"; export type ArticleStore = { addArticle(article: Article): void; removeArticle(article: Article): void; updateArticle(article: Article): void; subscribe(subscriber: Function): Function; unsubscribe(subscriber: Function): void; } export const addArticle = (articleState: ArticleState, article: Article) => articleState.concat(article); export const removeArticle = (articleState: ArticleState, article: Article) => articleState.filter((a: Article) => a.id !== article.id); export const updateArticle = (articleState: ArticleState, article: Article) => { const index = articleState.findIndex((a: Article) => a.id === article.id); return update(index, article, articleState); }; export const subscribe = (subscribers: Function[], subscriber: Function) => subscribers.concat(subscriber); export const unsubscribe = (subscribers: Function[], subscriber: Function) => subscribers.filter((s: Function) => s !== subscriber); export const notify = (articleState: ArticleState, subscribers: Function[]) => subscribers.forEach((s: Function) => s(articleState)); export const ArticleStoreFactory = (() => { let articleState: ArticleState = Object.freeze([]); let subscribers: Function[] = Object.freeze([]); return { addArticle: (article: Article) => { articleState = addArticle(articleState, article); notify(articleState, subscribers); }, removeArticle: (article: Article) => { articleState = removeArticle(articleState, article); notify(articleState, subscribers); }, updateArticle: (article: Article) => { articleState = updateArticle(articleState, article); notify(articleState, subscribers); }, subscribe: (subscriber: Function) => { subscribers = subscribe(subscribers, subscriber); return subscriber; }, unsubscribe: (subscriber: Function) => { subscribers = unsubscribe(subscribers, subscriber); } } }); export const articleStore = ArticleStoreFactory();
      
      





リポジトリの実装は、図解として非常に適しており、コンセプト自体を理解するのに役立ちます。 実際のプロジェクトでは、状態管理システムReduxngrxMobX、または少なくともObservable - data servicesを使用することをお勧めします







そのため、ドメインとストレージレベルを構成しました。







2つの記事、2つのリポジトリサブスクライバーを作成し、サブスクライバーが変更を通知する方法を見てみましょう。







store-demo.js:







 // @flow import type {ArticleState} from "../store/ArticleState"; import {articleService} from "../domain/ArticleService"; import {articleStore} from "../store/ArticleStore"; const article1 = articleService.createArticle({ title: '12 rules for life', author: 'Jordan Peterson' }); const article2 = articleService.createArticle({ title: 'The Subtle Art of Not Giving a F.', author: 'Mark Manson' }); if (article1 && article2) { const subscriber1 = (articleState: ArticleState) => { console.log('subscriber1, articleState changed: ', articleState); }; const subscriber2 = (articleState: ArticleState) => { console.log('subscriber2, articleState changed: ', articleState); }; articleStore.subscribe(subscriber1); articleStore.subscribe(subscriber2); articleStore.addArticle(article1); articleStore.addArticle(article2); articleStore.unsubscribe(subscriber2); const likedArticle2 = articleService.updateLikes(article2, 1); articleStore.updateArticle(likedArticle2); articleStore.removeArticle(article1); }
      
      





アプリケーションサービス



このレベルは、サーバーからデータを受信するためのAjax呼び出しや状態予測など、状態フローに関連する操作を実行するのに役立ちます。









アプリケーションサービスレベル







何らかの理由で、デザイナーは著者の名前を大文字で書くことを要求します。 要件は愚かであり、そのためにモデルを台無しにしたくありません。 この機能を使用するには、 ArticleUiService



作成しArticleUiService



。 サービスは州(著者の名前)の一部を取得して投影し、大文字で書かれたバージョンを呼び出し元に返します。







ArticleUiService.js:







 // @flow export const displayAuthor = (author: string) => author.toUpperCase();
      
      





このサービスを使用したデモはこちらです。







app-service-demo.js:







 // @flow import {articleService} from "../domain/ArticleService"; import * as articleUiService from "../services/ArticleUiService"; const article = articleService.createArticle({ title: '12 rules for life', author: 'Jordan Peterson' }); const authorName = article ? articleUiService.displayAuthor(article.author) : null; console.log(authorName); // It will print JORDAN PETERSON if (article) { console.log(article.author); // It will print Jordan Peterson }
      
      





プレゼンテーションレベル



これで、フレームワークに依存しない完全に機能するアプリケーションができました。 Reactが生命を吹き込む準備ができました。 プレゼンテーション層は、プレゼンテーションコンポーネントとコンテナコンポーネントで構成されます。 表示コンポーネントは要素の外観を担当し、コンテナコンポーネントは要素の機能を担当します。 Dan Abramovの記事で、すべての詳細が説明されています。









プレゼンテーションレベル







ArticleFormContainer



ArticleListContainer



構成されるApp



コンポーネントを作成します。







App.js:







 // @flow import React, {Component} from 'react'; import './App.css'; import {ArticleFormContainer} from "./components/ArticleFormContainer"; import {ArticleListContainer} from "./components/ArticleListContainer"; type Props = {}; class App extends Component<Props> { render() { return ( <div className="App"> <ArticleFormContainer/> <ArticleListContainer/> </div> ); } } export default App;
      
      





次に、 ArticleFormContainer



作成しArticleFormContainer



。 React、Angular、それは問題ではありません-フォームは複雑です。 また、Ramdaライブラリを理解し、そのメソッドがコードの宣言的な性質をどのように補完するかを確認することをお勧めします。







フォームはユーザー入力を受け取り、それをarticleServiceに渡します。 このデータに基づいて、サービスは記事を作成し、ArticleStoreに追加して、他のコンポーネントがそこから記事を取得できるようにします。 すべてのロジックは、最初にsubmitFormメソッドに保存されます。







ArticleFormContainer.js:







 // @flow import React, {Component} from 'react'; import * as R from 'ramda'; import type {ArticleService} from "../domain/ArticleService"; import type {ArticleStore} from "../store/ArticleStore"; import {articleService} from "../domain/ArticleService"; import {articleStore} from "../store/ArticleStore"; import {ArticleFormComponent} from "./ArticleFormComponent"; type Props = {}; type FormField = { value: string; valid: boolean; } export type FormData = { articleTitle: FormField; articleAuthor: FormField; }; export class ArticleFormContainer extends Component<Props, FormData> { articleStore: ArticleStore; articleService: ArticleService; constructor(props: Props) { super(props); this.state = { articleTitle: { value: '', valid: true }, articleAuthor: { value: '', valid: true } }; this.articleStore = articleStore; this.articleService = articleService; } changeArticleTitle(event: Event) { this.setState( R.assocPath( ['articleTitle', 'value'], R.path(['target', 'value'], event) ) ); } changeArticleAuthor(event: Event) { this.setState( R.assocPath( ['articleAuthor', 'value'], R.path(['target', 'value'], event) ) ); } submitForm(event: Event) { const articleTitle = R.path(['target', 'articleTitle', 'value'], event); const articleAuthor = R.path(['target', 'articleAuthor', 'value'], event); const isTitleValid = this.articleService.isTitleValid(articleTitle); const isAuthorValid = this.articleService.isAuthorValid(articleAuthor); if (isTitleValid && isAuthorValid) { const newArticle = this.articleService.createArticle({ title: articleTitle, author: articleAuthor }); if (newArticle) { this.articleStore.addArticle(newArticle); } this.clearForm(); } else { this.markInvalid(isTitleValid, isAuthorValid); } }; clearForm() { this.setState((state) => { return R.pipe( R.assocPath(['articleTitle', 'valid'], true), R.assocPath(['articleTitle', 'value'], ''), R.assocPath(['articleAuthor', 'valid'], true), R.assocPath(['articleAuthor', 'value'], '') )(state); }); } markInvalid(isTitleValid: boolean, isAuthorValid: boolean) { this.setState((state) => { return R.pipe( R.assocPath(['articleTitle', 'valid'], isTitleValid), R.assocPath(['articleAuthor', 'valid'], isAuthorValid) )(state); }); } render() { return ( <ArticleFormComponent formData={this.state} submitForm={this.submitForm.bind(this)} changeArticleTitle={(event) => this.changeArticleTitle(event)} changeArticleAuthor={(event) => this.changeArticleAuthor(event)} /> ) } }
      
      





ArticleFormContainer



は、ユーザーに表示されるフォーム、つまり送信されたArticleFormComponent



正確に返すことに注意してください。 このコンポーネントは、コンテナから送信されたデータを表示し、 changeArticleTitle



changeArticleAuthor



changeArticleAuthor



などのイベントを生成します。







ArticleFormComponent.js







 // @flow import React from 'react'; import type {FormData} from './ArticleFormContainer'; type Props = { formData: FormData; changeArticleTitle: Function; changeArticleAuthor: Function; submitForm: Function; } export const ArticleFormComponent = (props: Props) => { const { formData, changeArticleTitle, changeArticleAuthor, submitForm } = props; const onSubmit = (submitHandler) => (event) => { event.preventDefault(); submitHandler(event); }; return ( <form noValidate onSubmit={onSubmit(submitForm)} > <div> <label htmlFor="article-title">Title</label> <input type="text" id="article-title" name="articleTitle" autoComplete="off" value={formData.articleTitle.value} onChange={changeArticleTitle} /> {!formData.articleTitle.valid && (<p>Please fill in the title</p>)} </div> <div> <label htmlFor="article-author">Author</label> <input type="text" id="article-author" name="articleAuthor" autoComplete="off" value={formData.articleAuthor.value} onChange={changeArticleAuthor} /> {!formData.articleAuthor.valid && (<p>Please fill in the author</p>)} </div> <button type="submit" value="Submit" > Create article </button> </form> ) };
      
      





これで、記事を作成するためのフォームができました。リストの順番です。 ArticleListContainer



サブスクライブし、すべての記事を受け取り、 ArticleStore



を表示します。







ArticleListContainer.js:







 // @flow import * as React from 'react' import type {Article} from "../domain/Article"; import type {ArticleStore} from "../store/ArticleStore"; import {articleStore} from "../store/ArticleStore"; import {ArticleListComponent} from "./ArticleListComponent"; type State = { articles: Article[] } type Props = {}; export class ArticleListContainer extends React.Component<Props, State> { subscriber: Function; articleStore: ArticleStore; constructor(props: Props) { super(props); this.articleStore = articleStore; this.state = { articles: [] }; this.subscriber = this.articleStore.subscribe((articles: Article[]) => { this.setState({articles}); }); } componentWillUnmount() { this.articleStore.unsubscribe(this.subscriber); } render() { return <ArticleListComponent {...this.state}/>; } }
      
      





ArticleListComponent



は、プレゼンテーションを担当するコンポーネントです。 プロパティを通じて、利用可能な記事を取得ArticleContainer



コンポーネントをArticleContainer



ます。







ArticleListComponent.js:







 // @flow import React from 'react'; import type {Article} from "../domain/Article"; import {ArticleContainer} from "./ArticleContainer"; type Props = { articles: Article[] } export const ArticleListComponent = (props: Props) => { const {articles} = props; return ( <div> { articles.map((article: Article, index) => ( <ArticleContainer article={article} key={index} /> )) } </div> ) };
      
      





ArticleContainer



は、投稿を担当するArticleComponent



記事データを渡します。 また、 likeArticle



およびremoveArticle



も実装しremoveArticle









likeArticle



メソッドは、 likeArticle



の数を更新し、リポジトリ内の記事を更新されたコピーで置き換え、 removeArticle



メソッドは記事をリポジトリから削除します。







ArticleContainer.js:







 // @flow import React, {Component} from 'react'; import type {Article} from "../domain/Article"; import type {ArticleService} from "../domain/ArticleService"; import type {ArticleStore} from "../store/ArticleStore"; import {articleService} from "../domain/ArticleService"; import {articleStore} from "../store/ArticleStore"; import {ArticleComponent} from "./ArticleComponent"; type Props = { article: Article; }; export class ArticleContainer extends Component<Props> { articleStore: ArticleStore; articleService: ArticleService; constructor(props: Props) { super(props); this.articleStore = articleStore; this.articleService = articleService; } likeArticle(article: Article) { const updatedArticle = this.articleService.updateLikes(article, article.likes + 1); this.articleStore.updateArticle(updatedArticle); } removeArticle(article: Article) { this.articleStore.removeArticle(article); } render() { return ( <div> <ArticleComponent article={this.props.article} likeArticle={(article: Article) => this.likeArticle(article)} deleteArticle={(article: Article) => this.removeArticle(article)} /> </div> ) } }
      
      





ArticleContainer



は、記事データをそれらを表示するArticleContainer



渡します。 また、このメソッドは、適切なコールバックを実行することにより、コンテナコンポーネントに「いいね」または「削除」ボタンのクリックを通知します。







非常識な要求が著者の名前がどのように見えるべきか覚えていますか? ArticleUiService



は、アプリケーションレベルのArticleUiService



を使用して、状態の一部を元の値(大文字のない文字列)から、大文字で書かれた目的の値に投影します。







ArticleComponent.js:







 // @flow import React from 'react'; import type {Article} from "../domain/Article"; import * as articleUiService from "../services/ArticleUiService"; type Props = { article: Article; likeArticle: Function; deleteArticle: Function; } export const ArticleComponent = (props: Props) => { const { article, likeArticle, deleteArticle } = props; return ( <div> <h3>{article.title}</h3> <p>{articleUiService.displayAuthor(article.author)}</p> <p>{article.likes}</p> <button type="button" onClick={() => likeArticle(article)} > Like </button> <button type="button" onClick={() => deleteArticle(article)} > Delete </button> </div> ); };
      
      





いいね!



これで、信頼性が高く理解しやすいアーキテクチャを備えた完全に機能するReactアプリケーションができました。 チームの新参者は誰でもこの記事を読み、自信を持って作業に接続できます:)







完成したアプリケーションはこちら 、GitHubリポジトリはこちらです。








All Articles