<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          可擴(kuò)展的前端 -- 常見模式

          共 9526字,需瀏覽 20分鐘

           ·

          2020-06-11 23:22

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

          者:唐江洪

          譯文: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ù)討論前端可擴(kuò)展性!在可擴(kuò)展的前端 -- 架構(gòu)基礎(chǔ)中,我們僅在概念上討論了前端應(yīng)用程序中的架構(gòu)基礎(chǔ)。現(xiàn)在,我們將動手操作實(shí)際代碼。

          常見模式

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

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

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

          用例(Use Case)

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

          用例通常也稱為 interactors,它們負(fù)責(zé)在其他層之間執(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 中包含更豐富的消息,以便用戶知道故障下出了什么問題。但是有一個重要的細(xì)節(jié):結(jié)果狀態(tài)的邏輯應(yīng)該在用例之內(nèi),而不是 view 層--因為知道這一點(diǎn)不是 view 層的責(zé)任。這意味著 view 層不應(yīng)從用例中接收通用錯誤對象,而應(yīng)使用 if 語句來找出失敗的原因--例如檢查 error.message 屬性或 instanceof 以查詢錯誤的類。

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

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

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

          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)進(jìn)行任何斷言;我們相信用例將為每個結(jié)果調(diào)用正確的回調(diào)。另外,即使 userData 對象中的值來自 HTML 輸入,用例對此也不了解。它僅接收提取的數(shù)據(jù)并將其轉(zhuǎn)發(fā)。

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

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

          實(shí)體是我們 domain 層的核心:它們代表了我們軟件所處理的概念。假設(shè)我們正在構(gòu)建博客引擎應(yīng)用程序,在這種情況下,如果我們的引擎允許,我們可能會有一個 User 實(shí)體,Article 實(shí)體,甚至還有 Comment 實(shí)體。因此,實(shí)體只是保存數(shù)據(jù)和這些概念的行為的對象,而不用考慮技術(shù)實(shí)現(xiàn)。實(shí)體不應(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 用例是將用戶實(shí)例傳遞給 article.canBeCommentedBy,而用例則是在它們之間執(zhí)行 interaction 的地方。

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

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

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

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

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

          使用類實(shí)現(xiàn)的實(shí)體
          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ù)實(shí)現(xiàn)的實(shí)體

          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

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

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

          a4886b657995cd207d476a249e5c7327.webp

          提取鍵值對之前


          468ea1d1c3a9539fc79597390abd990a.webp

          提取鍵值對之后

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

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

          d7c2ddd94cf18227142fa71d803cc010.webp

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

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

          Repositories

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

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

          我說過我們會有一個 ArticleRepository,但是我們?nèi)绾纬志没渌麑ο竽兀课覀兪欠駪?yīng)該使用其他 repository 來持久存儲用戶?我們應(yīng)該有多少個 repository,它們應(yīng)該有多少顆粒度?冷靜下來,規(guī)則并不難掌握。你還記得聚合嗎?那是我們切入的地方。根據(jù)經(jīng)驗一般是為代碼庫的每個聚合提供一個 repository。我們也可以為次要實(shí)體創(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 實(shí)體作為輸入,將其轉(zhuǎn)換為 API 期望的 JSON 格式,對 API 進(jìn)行 AJAX 調(diào)用,然后將 JSON 響應(yīng)映射回 Article 實(shí)體的實(shí)例。

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

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

          e7bf586e31d6b45fdeb07bbd9d8871ea.webp

          Services

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

          簡而言之:服務(wù)是一個對象,它實(shí)現(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ù)細(xì)節(jié)的方法,例如 API 響應(yīng)的格式,然后你將擁有一個通用對象,可以很好地封裝你的軟件和支付網(wǎng)關(guān)之間的交互。

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

          文件組織

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

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

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

          第一個是最簡單的,它包括將 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

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

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

          • 實(shí)體位于 domain/ 文件夾中,其中“ concept”是實(shí)體所在的集合的名稱,并通過 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 文件夾的根目錄中。

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

          接下來

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

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

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

          推薦閱讀鏈接

          • Mark Seemann — Functional architecture — The pits of success

          • Scott Wlaschin — Functional Design Patterns

          分享前端好文,點(diǎn)亮?在看?7980de198dd5c65b2aadfcfc72e29224.webp

          瀏覽 54
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日逼免费观看网站 | 我要操逼| 亚洲精品乱码久久久久久蜜桃图片 | 色夜av在线 | 97人妻一区二区三区 |