[譯] 關於 SPA,你需要掌握的 4 層 (1)
此文已由作者張威授權網易雲社群釋出。
歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。
我們從頭來構建一個 React 的應用程式,探究領域、儲存、應用服務和檢視這四層
每個成功的專案都需要一個清晰的架構,這對於所有團隊成員都是心照不宣的。
試想一下,作為團隊的新人。技術負責人給你介紹了在專案程序中提出的新應用程式的架構。
然後告訴你需求:
我們的應用程式將顯示一系列文章。使用者能夠建立、刪除和收藏文章。
然後他說,去做吧!
Ok,沒問題,我們來搭框架吧
我選擇 FaceBook 開源的構建工具 Create React App,使用 Flow 來進行型別檢查。簡單起見,先忽略樣式。
作為先決條件,讓我們討論一下現代框架的宣告性本質,以及涉及到的 state 概念。
現在的框架多為宣告式的
React, Angular, Vue 都是宣告式的,並鼓勵我們使用函數語言程式設計的思想。
你有見過手翻書嗎?
一本手翻書或電影書,裡面有一系列逐頁變化的圖片,當頁面快速翻頁的時候,就形成了動態的畫面。 [1]
現在讓我們來看一下 React 中的定義:
在應用程式中為每個狀態設計簡單的檢視, React 會在資料發生變化時高效地更新和渲染正確的元件。 [2]
Angular 中的定義:
使用簡單、宣告式的模板快速構建特性。使用您自己的元件擴充套件模板語言。 [3]
大同小異?
框架幫助我們構建包含檢視的應用程式。檢視是狀態的表象。那狀態又是什麼?
狀態
狀態表示應用程式中會更改的所有資料。
你訪問一個URL,這是狀態,發出一個 Ajax 請求來獲取電影列表,這是也狀態,將資訊持久化到本地儲存,同上,也是狀態。
狀態由一系列不變物件組成
不可變結構有很多好處,其中一個就是在檢視層。
下面是 React 指南對效能優化介紹的引言。
不變性使得跟蹤更改變得更容易。更改總是會產生一個新物件,所以我們只需要檢查物件的引用是否發生了更改。
領域層
域可以描述狀態並儲存業務邏輯。它是應用程式的核心,應該與檢視層解耦。Angular, React 或者是 Vue,這些都不重要,重要的是不管選擇什麼框架,我們都能夠使用自己的領。
因為我們處理的是不可變的結構,所以我們的領域層將包含實體和域服務。
在 OOP 中存在爭議,特別是在大規模應用程式中,在使用不可變資料時,貧血模型是完全可以接受的。
對我來說,弗拉基米爾·克里科夫(Vladimir Khorikov)的這門課讓我大開眼界。
要顯示文章列表,我們首先要建模的是Article實體。
所有 Article 型別實體的未來物件都是不可變的。Flow 可以通過使所有屬性只讀(屬性前面帶 + 號)來強制將物件不可變。
// @flowexport type Article = { +id: string; +likes: number; +title: string; +author: string; }
現在,讓我們使用工廠函式模式建立 articleService。
檢視 @mpjme 的這個視訊,瞭解更多關於JS中的工廠函式知識。
由於在我們的應用程式中只需要一個articleService,我們將把它匯出為一個單例。
createArticle 允許我們建立 Article 的凍結物件。每一篇新文章都會有一個唯一的自動生成的id和零收藏,我們僅需要提供作者和標題。
**Object.freeze()** 方法可凍結一個物件:即無法給它新增屬性。 [5]
createArticle 方法返回的是一個 Article 的「Maybe」型別
Maybe 型別強制你在操作 Article 物件前先檢查它是否存在。
如果建立文章所需要的任一欄位校驗失敗,那麼 createArticle 方法將返回null。這裡可能有人會說,最好丟擲一個使用者定義的異常。如果我們這麼做,但上層不實現catch塊,那麼程式將在執行時終止。 updateLikes 方法會幫我們更新現存文章的收藏數,將返回一個擁有新計數的副本。
最後,isTitleValid 和 isAuthorValid 方法能幫助 createArticle 隔離非法資料。
// @flowimport 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 服務
// @flowexport 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 建立一篇關於我最喜歡的書的文章,並更新它的收藏數。
// @flowimport {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" }; */
儲存層
建立和更新文章所產生的資料代表了我們的應用程式的狀態。
我們需要一個地方來儲存這些資料,而 store 就是最佳人選
狀態可以很容易地由一系列文章來建模。
// @flowimport type {Article} from "./Article"; export type ArticleState = Article[];
ArticleState.js
ArticleStoreFactory 實現了釋出-訂閱模式,並匯出 articleStore 作為單例。
store 可儲存文章並賦予他們新增、刪除和更新的不可變操作。
記住,store 只對文章進行操作。只有 articleService 才能建立或更新它們。
感興趣的人可以訂閱和退訂 articleStore。
articleStore 儲存所有訂閱者的列表,並將每個更改通知到他們。
// @flowimport {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();
我們的 store 實現對於演示的目的是有意義的,它讓我們理解背後的概念。在實際運作中,我推薦使用狀態管理系統,像 Redux, ngrx, MobX, 或者是可監控的資料管理系統
好的,現在我們有了領域層和儲存層的結構。
讓我們為 store 建立兩篇文章和兩個訂閱者,並觀察訂閱者如何獲得更改通知。
// @flowimport 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 來處理這些特性。這個服務將取用一個狀態,就是作者的名字,將其構建到專案中,可返回大寫的版本給呼叫者。
// @flowexport const displayAuthor = (author: string) => author.toUpperCase();
讓我們看一個如何使用這個服務的演示!
// @flowimport {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);// 將輸出 JORDAN PETERSONif (article) { console.log(article.author); // 將輸出 Jordan Peterson}
app-service-demo.js
更多網易技術、產品、運營經驗分享請點選。