1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        可擴展的前端 -- 常見模式

        共 9526字,需瀏覽 20分鐘

         ·

        2020-06-11 23:22

        (給前端大學(xué)加星標,提升前端技能.

        者:唐江洪

        譯文:https://github.com/mcuking/blog/issues/60

        作者:Talysson de Oliveira

        https://blog.codeminer42.com/scalable-frontend-2-common-patterns-d2f28aef0714

        @唐江洪,網(wǎng)易云音樂前端工程師,三年前端經(jīng)驗的杭漂,目前專注在前端工程方面研究不能自拔

        正文從這開始~~

        讓我們繼續(xù)討論前端可擴展性!在可擴展的前端 -- 架構(gòu)基礎(chǔ)中,我們僅在概念上討論了前端應(yīng)用程序中的架構(gòu)基礎(chǔ)。現(xiàn)在,我們將動手操作實際代碼。

        常見模式

        如可擴展的前端 -- 架構(gòu)基礎(chǔ)所述,我們?nèi)绾螌崿F(xiàn)架構(gòu)?與我們過去的做法有什么不同?我們?nèi)绾螌⑺羞@些與依賴注入結(jié)合起來?

        不管你使用哪個庫來抽象 view 或管理 state,前端應(yīng)用程序中都有重復(fù)出現(xiàn)的模式。在這里,我們將討論其中的一部分,因此請系緊安全帶!

        譯者解讀:結(jié)合上篇文章分成的四層:application 層、domain 層、 infrastructure 層、view 層。下面講解的內(nèi)容中:用例屬于 application 層的核心概念,實體/值對象/聚合屬于 domain 層核心概念,Repositories 屬于 infrastructure 核心概念。

        用例(Use Case)

        我們選擇用例作為第一種模式,因為在架構(gòu)方面,它們是我們與軟件進行交互的方式。用例說明了我們的應(yīng)用程序的頂層功能;它是我們功能的秘訣;application 層的主要模塊。他們定義了應(yīng)用程序本身。

        用例通常也稱為 interactors,它們負責在其他層之間執(zhí)行交互。它們:

        • 由 view 層調(diào)用,

        • 應(yīng)用它們的算法,

        • 使 domain 和 infrastructure 層交互而無需關(guān)心它們在內(nèi)部的工作方式,并且,

        • 將結(jié)果狀態(tài)返回到 view 層。結(jié)果狀態(tài)用來表明用例是成功還是失敗,原因是內(nèi)部錯誤、失敗的驗證、前提條件等。

        知道結(jié)果狀態(tài)很有用,因為它有助于確定要為結(jié)果發(fā)出什么 action,從而允許 UI 中包含更豐富的消息,以便用戶知道故障下出了什么問題。但是有一個重要的細節(jié):結(jié)果狀態(tài)的邏輯應(yīng)該在用例之內(nèi),而不是 view 層--因為知道這一點不是 view 層的責任。這意味著 view 層不應(yīng)從用例中接收通用錯誤對象,而應(yīng)使用 if 語句來找出失敗的原因--例如檢查 error.message 屬性或 instanceof 以查詢錯誤的類。

        這給我們帶來了一個棘手的事實:從用例返回 promise 可能不是最佳的設(shè)計決策,因為 promise 只有兩個可能的結(jié)果:成功或失敗,這就要求我們在條件語句來發(fā)現(xiàn) catch() 語句中失敗的原因。是否意味著我們應(yīng)該跳過軟件中的 promise?不!完全可以從其他部分返回 promise,例如 actions、repositories、services??朔讼拗频囊环N簡單方法是對用例的每種可能結(jié)果狀態(tài)進行回調(diào)。

        用例的另一個重要特征是,即使在只有單個入口點的前端,它們也應(yīng)該來遵循分層之間的邊界,不用知道哪個入口點在調(diào)用它們。這意味著我們不應(yīng)該修改用例內(nèi)的瀏覽器全局變量,特定 DOM 的值或任何其他低級對象。例如:我們不應(yīng)該將 />元素的實例作為參數(shù),然后再讀取其值;view 層應(yīng)該是負責提取該值并將其傳遞給用例。

        沒有什么比一個例子更清楚地表達一個概念了:

        createUser.js

        1. exportdefault({ validateUser, userRepository })=>async(

        2. userData,

        3. { onSuccess, onError, onValidationError }

        4. )=>{

        5. if(!validateUser(userData)){

        6. return onValidationError(newError('Invalid user'));

        7. }


        8. try{

        9. const user =await userRepository.add(userData);

        10. onSuccess(user);

        11. }catch(error){

        12. onError(error);

        13. }

        14. };

        userAction.js

        1. const createUserAction = userData =>(dispatch, getState, container)=>{

        2. container.createUser(userData,{

        3. // notice that we don't add conditionals to emit any of these actions

        4. onSuccess: user => dispatch(createUserSuccessAction(user)),

        5. onError: error => dispatch(createUserErrorAction(error)),

        6. onValidationError: error => dispatch(createUserValidationErrorAction(error))

        7. });

        8. };

        本示例使用 Redux 和 Redux-Thunk。容器將作為 thunk 的第三個參數(shù)注入。

        請注意,在 userAction 中,我們不會對 createUser 用例的響應(yīng)進行任何斷言;我們相信用例將為每個結(jié)果調(diào)用正確的回調(diào)。另外,即使 userData 對象中的值來自 HTML 輸入,用例對此也不了解。它僅接收提取的數(shù)據(jù)并將其轉(zhuǎn)發(fā)。

        就是這樣!用例不能做的更多。你能看到現(xiàn)在測試它們有多容易嗎?我們可以簡單地注入所需功能的模擬依賴項,并測試我們的用例是否針對每種情況調(diào)用了正確的回調(diào)。

        實體、值對象和聚合(Entities, value objects, and aggregates)

        實體是我們 domain 層的核心:它們代表了我們軟件所處理的概念。假設(shè)我們正在構(gòu)建博客引擎應(yīng)用程序,在這種情況下,如果我們的引擎允許,我們可能會有一個 User 實體,Article 實體,甚至還有 Comment 實體。因此,實體只是保存數(shù)據(jù)和這些概念的行為的對象,而不用考慮技術(shù)實現(xiàn)。實體不應(yīng)被視為 Active Record 設(shè)計模式的模型或?qū)崿F(xiàn);他們對數(shù)據(jù)庫、AJAX 或持久數(shù)據(jù)一無所知。它們只是代表概念和圍繞該概念的業(yè)務(wù)規(guī)則。

        因此,如果我們博客引擎的用戶在評論有關(guān)暴力的文章時有年齡限制,我們會有一個 user.isMajor()方法,該方法將在 article.canBeCommentedBy(user)內(nèi)部調(diào)用,以某種方式將年齡分類規(guī)則保留在 user 對象內(nèi),并將年齡限制規(guī)則保留在 article 對象內(nèi)。AddCommentToArticle 用例是將用戶實例傳遞給 article.canBeCommentedBy,而用例則是在它們之間執(zhí)行 interaction 的地方。

        有一種方法可以識別代碼庫中某物是否為實體:如果一個對象代表一個 domain 概念并且它具有標識符屬性(例如,id 或文檔編號),則它是一個實體。此標識符的存在很重要,因為它是區(qū)分實體和值對象的原因。

        盡管實體具有標識符屬性,但值對象的身份由其所有屬性的值組合而成?;靵y?考慮一個顏色對象。當用對象表示顏色時,我們通常不給該對象一個 ID。我們給它提供紅色,綠色和藍色的值,這三個屬性結(jié)合在一起可以識別該對象?,F(xiàn)在,如果我們更改紅色屬性的值,我們可以說它代表了另一種顏色,但是用 id 標識的用戶卻不會發(fā)生同樣的情況。如果我們更改用戶的 name 屬性的值但保留相同的 ID,則表示它仍然是同一用戶,對嗎?

        在本節(jié)的開頭,我們說過在實體中使用方法以及給定實體的業(yè)務(wù)規(guī)則和行為是很普遍的。但是在前端,將業(yè)務(wù)規(guī)則作為實體對象的方法并不總是很好??紤]一下函數(shù)式編程:我們沒有實例方法,或者 this, 可變性--這是一種使用普通 JavaScript 對象而不是自定義類的實例的,很好兼容單向數(shù)據(jù)流的范例。那么在使用函數(shù)式編程時,實體中具有方法是否有意義?當然沒有。那么我們?nèi)绾蝿?chuàng)建具有此類限制的實體?我們采用函數(shù)式方式!

        我們將不使用帶有 user.isMajor() 實例方法的 User 類,而是使用一個名為 User 的模塊,該模塊導(dǎo)出 isMajor(user) 函數(shù),該函數(shù)會返回具有用戶屬性的對象,就像 User 類的 this。該參數(shù)不必是特定類的實例,只要它具有與用戶相同的屬性即可。這很重要:屬性(用戶實體的預(yù)期參數(shù))應(yīng)以某種方式形式化。你可以在具有工廠功能的純 JavaScript 中進行操作,也可以使用 Flow 或 TypeScript 更明確地進行操作。

        為了更容易理解,我們看下前后對比。

        使用類實現(xiàn)的實體
        1. // User.js


        2. exportdefaultclassUser{

        3. static LEGAL_AGE =21;


        4. constructor({ id, age }){

        5. this.id = id;

        6. this.age = age;

        7. }


        8. isMajor(){

        9. returnthis.age >=User.LEGAL_AGE;

        10. }

        11. }


        12. // usage

        13. importUserfrom'./User.js';


        14. const user =newUser({ id:42, age:21});

        15. user.isMajor();// true


        16. // if spread, loses the reference for the class

        17. const user2 ={...user, age:20};

        18. user2.isMajor();// Error: user2.isMajor is not a function

        使用函數(shù)實現(xiàn)的實體

        1. // User.js


        2. const LEGAL_AGE =21;


        3. exportconst isMajor = user =>{

        4. return user.age >= LEGAL_AGE;

        5. };


        6. // this is a user factory

        7. exportconst create = userAttributes =>({

        8. id: userAttributes.id,

        9. age: userAttributes.age

        10. });


        11. // usage

        12. import*asUserfrom'./User.js';


        13. const user =User.create({ id:42, age:21});

        14. User.isMajor(user);// true


        15. // no problem if it's spread

        16. const user2 ={...user, age:20};

        17. User.isMajor(user2);// false

        當與 Redux 之類的狀態(tài)管理器打交道時,越容易支持 immutable(不變性)就越好,因此無法展開對象來進行淺拷貝并不是一件好事。使用函數(shù)式方式會強制解耦,并且我們可以展開對象。

        所有這些規(guī)則都適用于值對象,但它們還有另一個重要性:它們有助于減少實體的膨脹。通常,實體中有很多彼此不直接相關(guān)的屬性,這可能表明我們可以提取其中一些屬性給值對象。舉例來說,假設(shè)我們有一個椅子實體,其屬性有 id,cushionType,cushionColor,legsCount,legsColor 和 legsMaterial。注意到 cushionType 和 cushionColor 與 legsCount,legsColor 和 legsMaterial 不相關(guān),因此在提取了一些值對象之后,我們的椅子將減少為三個屬性:id,cushion 和 legs?,F(xiàn)在,我們可以繼續(xù)為 cushion 和 legs 添加屬性,而不會使椅子變得更繁冗。

        a4886b657995cd207d476a249e5c7327.webp

        提取鍵值對之前


        468ea1d1c3a9539fc79597390abd990a.webp

        提取鍵值對之后

        但是,僅從實體中提取值對象并不總是足夠的。你會發(fā)現(xiàn),通常會有與次要實體相關(guān)聯(lián)的實體,其中主要概念由第一個實體表示,依賴于這些次要實體作為一個整體,而僅存在這些次要實體是沒有意義的?,F(xiàn)在你的腦海中肯定會有些混亂,所以讓我們清除一下。

        想一下購物車。購物車可以由購物車實體表示,該實體將由訂單項組成,而訂單項又是實體,因為它們具有自己的 ID。訂單項只能通過主要實體購物車對象進行交互。想知道特定產(chǎn)品是否在購物車內(nèi)?調(diào)用 cart.hasProduct(product) 方法,而不是像 cart.lineItems.find(...) 那樣直接訪問 lineItems 屬性。對象之間的這種關(guān)系稱為聚合,給定聚合的主要實體(在本例中為 cart 對象)稱為聚合根。代表聚合及其所有組件概念的實體只能通過購物車進行訪問,但聚合內(nèi)部的實體從外部引用對象是可以的。我們甚至可以說,在單個實體能夠代表整個概念的情況下,該實體也是由單個實體及其值對象(如果有)組成的聚合。因此,當我們說“聚合”時,從現(xiàn)在開始,你必須將其解釋為適當?shù)木酆虾蛦我粚嶓w聚合。

        d7c2ddd94cf18227142fa71d803cc010.webp

        外部無法訪問聚合的內(nèi)部實體,但是次要實體可以從聚合外部訪問事物,例如 products。

        在我們的代碼庫中具有明確定義的實體,集合和值對象,并以領(lǐng)域?qū)<胰绾我盟鼈儊砻赡芊浅S袃r值(無雙關(guān)語)。因此,在將代碼丟到其他地方之前,請始終注意是否可以使用它們來抽象一些東西。另外,請務(wù)必了解實體和聚合,因為它對下一種模式很有用!

        Repositories

        你是否注意到我們還沒有談?wù)摮志没??考慮這一點很重要,因為它會強制執(zhí)行我們從一開始就講過的話:持久化是實現(xiàn)細節(jié),是次要關(guān)注點。只要在軟件中將負責處理的部分合理地封裝并且不影響其余代碼,將內(nèi)容持久化到哪里就沒什么關(guān)系。在大多數(shù)基于分層的架構(gòu)中,這就是 repository 的職責,該 repository 位于 infrastructure 層內(nèi)。

        Repositories 是用于持久化和讀取實體的對象,因此它們應(yīng)實現(xiàn)使它們感覺像集合的方法。如果你有 article 對象并希望保留它,則可能有一個帶有 add(article) 方法的 ArticleRepository,該方法將文章作為參數(shù),將其保留在某個地方,然后返回帶有附加的僅保留屬性(如 id)的文章副本。

        我說過我們會有一個 ArticleRepository,但是我們?nèi)绾纬志没渌麑ο竽兀课覀兪欠駪?yīng)該使用其他 repository 來持久存儲用戶?我們應(yīng)該有多少個 repository,它們應(yīng)該有多少顆粒度?冷靜下來,規(guī)則并不難掌握。你還記得聚合嗎?那是我們切入的地方。根據(jù)經(jīng)驗一般是為代碼庫的每個聚合提供一個 repository。我們也可以為次要實體創(chuàng)建 repository,但僅在需要時才可以。

        好吧,好吧,這聽起來很像后端談話。那么,repository 在前端做什么?我們那里沒有數(shù)據(jù)庫!這就是要注意的問題:停止將 repository 與數(shù)據(jù)庫相關(guān)聯(lián)。repository 與整個持久性有關(guān),而不僅僅是數(shù)據(jù)庫。在前端,repository 處理數(shù)據(jù)源,例如 HTTP API,LocalStorage,IndexedDB 等。在上一個示例中,我們的 ArticleRepository.add 方法將 Article 實體作為輸入,將其轉(zhuǎn)換為 API 期望的 JSON 格式,對 API 進行 AJAX 調(diào)用,然后將 JSON 響應(yīng)映射回 Article 實體的實例。

        很高興注意到,例如,如果 API 仍在開發(fā)中,我們可以通過實現(xiàn)一個名為 LocalStorageArticleRepository 的 ArticleRepository 來模擬它,該 ArticleRepository 與 LocalStorage 而不是與 API 交互。當 API 準備就緒時,我們?nèi)缓髣?chuàng)建另一個稱為 AjaxArticleRepository 的實現(xiàn),從而替換 LocalStorage 實現(xiàn)--只要它們都共享相同的接口,并注入通用名稱即可,而不需要展示底層技術(shù),例如 articleRepository。

        我們在這里使用“接口”一詞來表示對象應(yīng)實現(xiàn)的一組方法和屬性,因此請不要將其與圖形用戶界面(也稱為 GUI)混淆。如果你使用的是純 JavaScript,則接口僅是概念性的;它們是虛構(gòu)的,因為該語言不支持接口的顯式聲明,但是如果你使用的是 TypeScript 或 Flow,則它們可以是顯性的。

        e7bf586e31d6b45fdeb07bbd9d8871ea.webp

        Services

        這是最后一種模式,不是偶然。正是在這里,因為它應(yīng)該被視為“最后的資源”。如果你無法將概念適用于上述任何一種模式,則只有在那時才考慮創(chuàng)建服務(wù)。在代碼庫中,任何可重用的代碼被拋出到所謂的“服務(wù)對象”中是很普遍的,它不過是一堆沒有封裝概念的可重用邏輯。始終要意識到這一點,不要讓這種情況在你的代碼庫中發(fā)生,并且要避免創(chuàng)建服務(wù)而不是用例的沖動,因為它們不是一回事。

        簡而言之:服務(wù)是一個對象,它實現(xiàn)了領(lǐng)域?qū)ο笾胁贿m合的過程。例如,支付網(wǎng)關(guān)。

        讓我們想象一下,我們正在建立一個電子商務(wù),并且需要與支付網(wǎng)關(guān)的外部 API 交互以獲取購買的授權(quán)令牌。付款網(wǎng)關(guān)不是一個領(lǐng)域概念,因此非常適合 PaymentService。向其中添加不會透露技術(shù)細節(jié)的方法,例如 API 響應(yīng)的格式,然后你將擁有一個通用對象,可以很好地封裝你的軟件和支付網(wǎng)關(guān)之間的交互。

        就是這樣,這里不是秘密。嘗試使你的領(lǐng)域概念適應(yīng)上述模式,如果它們不起作用,則僅考慮提供服務(wù)。它對代碼庫的所有層都很重要!

        文件組織

        許多開發(fā)人員誤解了架構(gòu)和文件組織之間的區(qū)別,認為后者定義了應(yīng)用程序的架構(gòu)。甚至擁有良好的文件組織,應(yīng)用程序就可以很好地擴展,這完全是一種誤導(dǎo)。即使是最完美的文件組織,你仍然可能在代碼庫中遇到性能和可維護性問題,因此這是本文的最后主題。讓我們揭開文件組織的神秘面紗,以及如何將其與架構(gòu)結(jié)合使用以實現(xiàn)可讀且可維護的項目結(jié)構(gòu)。

        基本上,文件組織是你從視覺上分離應(yīng)用程序各部分的方式,而架構(gòu)是從概念上分離應(yīng)用程序的方式。你可以很好地保持相同的架構(gòu),并且在文件組織方案時仍然可以有多個選擇。但是,最好是組織文件以反映架構(gòu)的各個層次,并幫助代碼庫的讀者,以便他們僅查看文件樹即可了解會發(fā)生什么。

        沒有完美的文件組織,因此請根據(jù)你的喜好和需求進行明智的選擇。但是,有兩種方法對突出本文討論的層特別有用。讓我們看看它們中的每一個。

        第一個是最簡單的,它包括將 src 文件夾的根分為幾層,然后是架構(gòu)的概念。例如:

        1. .

        2. |-- src

        3. ||-- app

        4. |||-- user

        5. ||||--CreateUser.js

        6. |||-- article

        7. ||||--GetArticle.js

        8. ||-- domain

        9. |||-- user

        10. ||||-- index.js

        11. ||-- infra

        12. |||-- common

        13. ||||-- httpService.js

        14. |||-- user

        15. ||||--UserRepository.js

        16. |||-- article

        17. ||||--ArticleRepository.js

        18. ||-- store

        19. |||-- index.js

        20. |||-- user

        21. ||||-- index.js

        22. ||-- view

        23. |||-- ui

        24. ||||--Button.js

        25. ||||--Input.js

        26. |||-- user

        27. ||||--CreateUserPage.js

        28. ||||--UserForm.js

        29. |||-- article

        30. ||||--ArticlePage.js

        31. ||||--Article.js

        當這種文件組織與 React 和 Redux 配合使用時,通常會看到諸如 components, containers, reducers, actions 等文件夾。我們傾向于更進一步,將相似的職責分組在同一文件夾中。例如,我們的 components 和 containers 都將在 view 文件夾中,而 actions 和 reducers 將在 store 文件夾中,因為它們遵循將出于相同原因而改變的事物收集在一起的規(guī)則。以下是該文件組織的立場:

        • 你不應(yīng)該通過文件夾來反映技術(shù)角色,例如“controllers”,“components”,“helpers”等;

        • 實體位于 domain/ 文件夾中,其中“ concept”是實體所在的集合的名稱,并通過 domain / /index.js 文件導(dǎo)出;

        • 只要不會引起耦合,就可以在同一層的概念之間導(dǎo)入文件。

        我們的第二個選擇包括按功能分隔 src 文件夾的根。假設(shè)我們正在處理文章和用戶;在這種情況下,我們將有兩個功能文件夾來組織它們,然后是第三個文件夾,用于處理諸如通用 Button 組件之類的常見事物,甚至是僅用于 UI 組件的功能文件夾:

        1. .

        2. |-- src

        3. ||-- common

        4. |||-- infra

        5. ||||-- httpService.js

        6. |||-- view

        7. ||||--Button.js

        8. ||||--Input.js

        9. ||-- article

        10. |||-- app

        11. ||||--GetArticle.js

        12. |||-- domain

        13. ||||--Article.js

        14. |||-- infra

        15. ||||--ArticleRepository.js

        16. |||-- store

        17. ||||-- index.js

        18. |||-- view

        19. ||||--ArticlePage.js

        20. ||||--ArticleForm.js

        21. ||-- user

        22. |||-- app

        23. ||||--CreateUser.js

        24. |||-- domain

        25. ||||--User.js

        26. |||-- infra

        27. ||||--UserRepository.js

        28. |||-- store

        29. ||||-- index.js

        30. |||-- view

        31. ||||--UserPage.js

        32. ||||--UserForm.js

        該組織的立場與第一個組織的立場基本相同。對于這兩種情況,你都應(yīng)將 dependencies container 保留在 src 文件夾的根目錄中。

        同樣,這些選項可能無法滿足你的需求,可能不是你理想的文件組織方式。因此,請花一些時間來移動文件和文件夾,直到獲得可以更輕松地找到所需工件為止。這是找出最適合你們團隊的最佳方法。請注意,僅將代碼分成文件夾不會使你的應(yīng)用程序更易于維護!你必須保持相同的心態(tài),同時在代碼中分離職責。

        接下來

        哇!很多內(nèi)容,對不對?沒關(guān)系,我們在這里談到了很多模式,所以不要一口氣讀懂所有這些內(nèi)容。隨時重新閱讀并檢查該系列的第一篇文章和我們的示例,直到你對體系結(jié)構(gòu)及其實現(xiàn)的輪廓感到更滿意為止。

        在下一篇文章中,我們還將討論實際示例,但將完全集中在狀態(tài)管理上。

        如果你想看到此架構(gòu)的實際實現(xiàn),請查看此示例博客引擎應(yīng)用程序的代碼,點擊查看。請記住,沒有什么是一成不變的,在以后的文章中,我們還會討論一些模式。

        推薦閱讀鏈接

        • Mark Seemann — Functional architecture — The pits of success

        • Scott Wlaschin — Functional Design Patterns

        分享前端好文,點亮?在看?7980de198dd5c65b2aadfcfc72e29224.webp

        瀏覽 54
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            青草视频网 | 婷婷色香五月综合激情 | 欧美操逼片 | 欧美激情乱人伦 | 啪啪啪免费网站在线 | 日日添日日鲁日日夜 | 国产伦精品一区二区三区视频1 | 成人女毛片免费观看 | 疯狂做爰小说高潮细节 | 林熙蕾胸被揉到高潮后不让我吃奶 |