<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>

          前端組件設(shè)計原則

          共 10832字,需瀏覽 22分鐘

           ·

          2020-08-31 01:37


          譯者:@沒有好名字了
          譯文:https://github.com/lightningminers/article/issues/36,https://juejin.im/post/5c49cff56fb9a049bd42a90f
          作者:@Andrew Dinihan
          原文:https://engineering.carsguide.com.au/front-end-component-design-principles-55c5963998c9

          前言

          我在最近的工作中開始使用 Vue 進行開發(fā),但是我在上一家公司積累了三年以上 React 開發(fā)經(jīng)驗。雖然在兩種不同的前端框架之間進行切換確實需要學習很多,但是二者之間在很多基礎(chǔ)概念、設(shè)計思路上是相通的。其中之一就是組件設(shè)計,包括組件層次結(jié)構(gòu)設(shè)計以及組件各自的職責劃分。

          組件是大多數(shù)現(xiàn)代前端框架的基本概念之一,在 React 和 Vue 以及 Ember 和 Mithril 等框架中均有所體現(xiàn)。組件通常是由標記語言、邏輯和樣式組成的集合。它們被創(chuàng)建的目的就是作為可復用的模塊去構(gòu)建我們的應用程序。

          類似于傳統(tǒng) OOP 語言中 class 的設(shè)計,在設(shè)計組件的時候需要考慮到很多方面,以便它們可以很好的復用,組合,分離和低耦合,但是功能可以比較穩(wěn)定的實現(xiàn),即使是在超出實際測試用例范圍的情況下。這樣的設(shè)計說起來容易做起來卻很難,因為現(xiàn)實中我們往往沒有足夠的時間按照最優(yōu)的方式去做。

          方法

          在本文中,我想介紹一些組件相關(guān)的設(shè)計概念,在進行前端開發(fā)時應該考慮這些概念。我認為最好的方法是給每個概念一個簡潔精煉的名字,然后逐一解釋每個概念是什么以及為什么重要,對于比較抽象概念的會舉一些例子來幫助理解。

          以下這個列表并不是不全面也不完整,但我注意到的只有 8 件事情值得一提,對于那些已經(jīng)可以編寫基本組件但想要提高他們的技術(shù)設(shè)計技能的人來說。所以這是列表:
          以下列舉的這個列表僅僅是是我注意到的 8 個方面,當然組件設(shè)計還有其他一些方面。在此我只是列舉出來我認為值得一提的。

          對于已經(jīng)掌握基本的組件設(shè)計并且想要提高自身的組件設(shè)計能力的開發(fā)者,我認為以下 8 項是我認為值得去注意的,當然這并不是組件設(shè)計的全部。

          • 層次結(jié)構(gòu)和 UML 類圖

          • 扁平化、面向數(shù)據(jù)的 state/props

          • 更加純粹的 State 變化

          • 低耦合

          • 輔助代碼分離

          • 提煉精華

          • 及時模塊化

          • 集中/統(tǒng)一的狀態(tài)管理

          請注意,代碼示例可能有一些小問題或有點人為設(shè)計。但是它們并不復雜,只是想通過這些例子來幫助更好的理解概念。

          層次結(jié)構(gòu)和類圖

          應用內(nèi)的組件共同形成組件樹, 而在設(shè)計過程中將組件樹可視化展示可以幫助你全面了解應用程序的布局。一個比較好的展示這些的辦法就是組件圖。

          UML 中有一個在 OOP 類設(shè)計中經(jīng)常使用的類型,稱為 UML 類圖。類圖中顯示了類屬性、方法、訪問修飾符、類與其他類的關(guān)系等。雖然 OOP 類設(shè)計和前端組件設(shè)計差異很大,但是通過圖解輔助設(shè)計的方法值得參考。對于前端組件,該圖表可以顯示:

          • State

          • Props

          • Methods

          • 與其他組件的關(guān)系( Relationship to other components )

          因此,讓我們看一下下面這個基礎(chǔ)表組件的組件層次圖,該組件的渲染對象是一個數(shù)組。該組件的功能包括顯示總行數(shù)、標題行和一些數(shù)據(jù)行,以及在單擊其單元格標題格時對該列進行排序。在它的 props 中,它將傳遞列列表(具有屬性名稱和該屬性的人類可讀版本),然后傳遞數(shù)據(jù)數(shù)組。我們可以添加一個可選的’on row click’功能來進行測試。

          雖然這樣的事情可能看起來有點多,但是它具有許多優(yōu)點,并且在大型應用程序開發(fā)設(shè)計中所需要的。這樣會帶來的一個比較重要的問題是它會需要你在開始 codeing 之前就需要考慮到具體細節(jié)的實現(xiàn),例如每個組件需要什么類型的數(shù)據(jù),需要實現(xiàn)哪些方法,所需的狀態(tài)屬性等等。

          一旦你對如何構(gòu)建一個組件(或一組組件)的整體有大概的思路,就會很容易認為當自己真正開始編碼實現(xiàn)時,它會如自己所期望的按部就班的完成,但事實上往往會出現(xiàn)一些預料之外的事情, 當然你肯定不希望因此去重構(gòu)之前的某些部分,或者忍受初始設(shè)想中的缺點并因此擾亂你的代碼思路。而這些類圖的以下優(yōu)點可以幫助你有效的規(guī)避以上問題,優(yōu)點如下:

          • 一個易于理解的組件組成和關(guān)聯(lián)視圖

          • 一個易于理解的應用程序 UI 層次結(jié)構(gòu)的概述

          • 一個結(jié)構(gòu)數(shù)據(jù)層次及其流動方式的視圖

          • 一個組件功能職責的快照

          • 便于使用圖表軟件創(chuàng)建

          順帶一提,上圖并不是基于某些官方標準,比如 UML 類圖,它是我基本上創(chuàng)建的一套表達規(guī)則。例如,在 props 、方法的參數(shù)和返回值的數(shù)據(jù)類型定義聲明都是基于 Typescript 語法。我還沒有找到書寫前端組件類圖的官方標準,可能是由于前端 Javascript 開發(fā)的相對較新且生態(tài)系統(tǒng)不夠完善所致,但如果有人知道主流標準,請在回復中告訴我!

          扁平的,面向數(shù)據(jù)的 state/props

          在 state 和 props 頻繁被 watch 和 update 的情況下,如果你有使用嵌套數(shù)據(jù),那么你的性能可能會受到影響,尤其是在以下場景中,例如一些因為淺對于而觸發(fā)的重新渲染;在涉及 immutability 的庫中,比如 React,你必須創(chuàng)建狀態(tài)的副本而不是像在 Vue 中那樣直接更改它們,并且使用嵌套數(shù)據(jù)這樣做可能會創(chuàng)建笨拙,丑陋的代碼。

          //Flat,?data-oriented?state/props
          const?state?=?{
          ??clients:?{
          ????allClients,
          ????firstClient,
          ????lastClient:?{
          ??????name:?'John',
          ??????phone:?'Doe',
          ??????address:?{
          ????????number:?5,
          ????????street:?'Estin',
          ????????suburb:?'Parrama',
          ????????city:?'Sydney'
          ??????}
          ????}
          ??}
          }

          //?倘若我們需要去修改 address number時需要怎么辦?
          const?test?=?{
          ??clients:?{
          ????...state.clients,
          ????lastClient:?{
          ??????...state.clients.lastClient,
          ??????address:?{
          ????????...state.clients.lastClient.address,
          ????????number:?10
          ??????}
          ????}
          ??}
          }


          即使使用展開運算符,這種寫法也并不夠優(yōu)雅。扁平 props 也可以很好地清除組件正在使用的數(shù)據(jù)值。如果你傳給組件一個對象但是你并不能清楚的知道對象內(nèi)部的屬性值,所以找出實際需要的數(shù)據(jù)值是來自組件具體的屬性值則是額外的工作。但如果 props 足夠扁平化,那么起碼會方便使用和維護。

          //?我們無法得知?customer?這個對象里面擁有什么屬性
          //?這個組件需要使用這個對象所有的屬性值或者只是需要其中的一部分?
          //?如果我想要將這個組件在別處使用,我應該傳入什么樣的對象

          <listItem?customer={customer}/>

          //?下面的這個組件接收的屬性就一目了然

          <listItem?phone={customer.phone}?name={customer.name}?iNumber={customer.iNumber}??/>


          state / props 還應該只包含組件渲染所需的數(shù)據(jù)。You shouldn’t store entire components in the state/props and render straight from there.

          (此外,對于數(shù)據(jù)繁重的應用程序,數(shù)據(jù)規(guī)范化可以帶來巨大的好處,除了扁平化之外,你可能還需要考慮一些別的優(yōu)化方法)。

          更加純粹的 State 變化

          對 state 的更改通常應該響應某種事件,例如用戶單擊按鈕或 API 的響應。此外它們不應該因為別的 state 的變化而做出響應,因為 state 之間這種關(guān)聯(lián)可能會導致難以理解和維護的組件行為。state 變化應該沒有副作用。

          如果你濫用watch而不是有限考慮以上原則,那么在 Vue 的使用中就可能由此引發(fā)的問題。我們來看一個基本的 Vue 示例。我正在研究一個從 API 獲取一些數(shù)據(jù)并將其呈現(xiàn)給表的組件,其中排序,過濾等功能都是后端完成的,因此前端需要做的就是 watch 所有搜索參數(shù),并在其變化時觸發(fā) API 調(diào)用。其中一個需要 watch 的值是“zone”,這是一個過濾器。當更改時,我們想要使用過濾后的值重新獲取服務(wù)端數(shù)據(jù)。watcher 如下:

          //State?change?purity
          zone:{
          ??handler()?{
          ????//?重置頁碼
          ????if(this.pagination.page?>?1){
          ????????this.pagination.page?=?1
          ????????return;
          ????}
          ????this.getDataFromApi()
          ??}
          }


          你會發(fā)現(xiàn)一些奇怪的東西。如果他們超出了結(jié)果的第一頁,我們重置頁碼然后結(jié)束?這似乎不對,如果它們不在第一頁上,我們應該重置分頁并觸發(fā) API 調(diào)用,對吧?為什么我們只在第 1 頁上重新獲取數(shù)據(jù)?實際上原因是這樣,讓我們來看下完整的 watch:

          watch:?{
          ??pagination()?{
          ????this.getDataFromApi()
          ??}
          },
          zone:?{
          ??handler()?{
          ????//?重置頁碼
          ????if(this.pagination.page?>?1)?{
          ????????this.pagination.page?=?1
          ????????return;
          ????}
          ????this.getDataFromApi()
          ??}
          }


          當分頁改變時,應用首先會通過 pagination 的處理函數(shù)重新獲取數(shù)據(jù)。因此,如果我們改變了分頁,我們并不需要去關(guān)注數(shù)據(jù)更新這段邏輯。

          讓我們一下來考慮以下流程:如果當前頁面超出了第 1 頁并且更改了 zone,而這個變化會觸發(fā)另一個狀態(tài)(pagination)發(fā)生變化,進而觸發(fā) pagination 的觀察者重新請求數(shù)據(jù)。這樣并不是預料之中的行為,而且產(chǎn)生的代碼也不夠直觀。

          解決方案是改變頁碼這個行為的事件處理函數(shù)(不是觀察者,用戶更改頁面的實際處理函數(shù))應該更改頁面值并觸發(fā) API 調(diào)用請求數(shù)據(jù)。這也將消除對觀察者的需求。通過這樣的設(shè)置,直接從其他地方改變分頁狀態(tài)也不會導致重新獲取數(shù)據(jù)的副作用。

          雖然這個例子非常簡單,但不難看出將更復雜的狀態(tài)更改關(guān)聯(lián)在一起會產(chǎn)生令人難以理解的代碼,這些代碼不僅不可擴展并且是調(diào)試的噩夢。

          松耦合

          組件的核心思想是它們是可復用的,為此要求它們必須具有功能性和完整性。“耦合”是指實體彼此依賴的術(shù)語。松散耦合的實體應該能夠獨立運行,而不依賴于其他模塊。就前端組件而言,耦合的主要部分是組件的功能依賴于其父級及其傳遞的 props 的多少,以及內(nèi)部使用的子組件(當然還有引用的部分,如第三方模塊或用戶腳本)。

          緊密耦合的組件往往更不容易被復用,當它們作為特定父組件的子項時,就很難正常工作,當父組件的一個子組件或一系列子組件只能在該父組件才能夠正常發(fā)揮作用時,就會使得代碼寫的很冗余。因為父子組件別過度的關(guān)聯(lián)在一起了。

          在設(shè)計組件時,你應該考慮到更加通用的使用場景,而不僅僅只是為了滿足最開始某個特定場景的需求。雖然一般來說組件最初都是出于特定目的進行設(shè)計,但沒關(guān)系,如果在設(shè)計它們站在更高的角度去看待,那么很多組件將具有更好的適用性。

          讓我們看一個簡單的 React 示例,你想在寫出一個帶有一個 logo 的鏈接列表,通過連接可以訪問特定的網(wǎng)站。最開始的設(shè)計可能是并沒有跟內(nèi)容合理的進行解耦。下面是最初的版本:

          const?Links?=?()=>(
          ??<div?className="links-container">
          ????<div?class="links-list">
          ??????<a?href="/">
          ????????Home
          ??????a>

          ??????<a?href="/shop">
          ????????Products
          ??????a>
          ??????<a?href="/help">
          ????????Help
          ??????a>
          ????div>
          ????<div?className="links-logo">
          ??????<img?src="/default/logo.png"/>
          ????div>
          ??div>
          )


          雖然這這樣會滿足預期的使用場景,但卻很難被復用。如果你想要更改鏈接地址該怎么辦?你必須重新復制一份相同代碼,并且手動去替換鏈接地址。而且, 如果你要去實現(xiàn)一個用戶可以更改連接的功能,那么意味著不可能將代碼寫“死”,也不能期望用戶去手動修改代碼,那么讓我們來看一下復用性更高的組件應該如何設(shè)計:

          const?DEFAULT_LINKS?=?[
          ??{route:?"/",?text:?"Home"},
          ??{route:?"/shop",?text:?"Products"},
          ??{route:?"/help",?text:?"Help"}
          ]
          const?DEFAULT_LOGO?=?"/default/logo.png"

          const?Links?=?({links?=?DEFAULT_LINKS,logoPath?=?DEFAULT_LOGO?})?=>?(
          ??<div?className="links-container">
          ????<div?class="links-list">
          ???????//?將數(shù)組依次渲染為超鏈接
          ???????links.map((link)?=>?<a?href={link.route}>?{link.text}a>
          )
          ????div>
          ????<div?className="links-logo">
          ??????<img?src={logoPath}/>
          ????div>
          ??div>
          )


          在這里我們可以看到,雖然它的原始鏈接和 logo 具有默認值,但我們可以通過 props 傳入的值去覆蓋掉默認值。讓我們來看一下它在實際中的使用:

          const?adminLinks?=?{
          ??links:?[
          ????{route:?"/",?text:?"Home"},
          ????{route:?"/metrics",?text:?"Site?metrics"},
          ????{route:?"/admin",?text:?"Admin?panel"}
          ??],
          ??logoPath:?"/admin/logo.png"
          }


          并不需要重新編寫新的組件!如果我們解決上文中用戶可以自定義鏈接的使用場景,可以考慮動態(tài)構(gòu)建鏈接數(shù)組。此外,雖然在這個具體的例子中沒有解決,但我們?nèi)匀豢梢宰⒁獾竭@個組件沒有與任何特定的父/子組件建立密切關(guān)聯(lián)。它可以在任何需要的地方呈現(xiàn)。改進后的組件明顯比最初版本具有更好的復用性。

          如果不是要設(shè)計需要服務(wù)于特定的一次性場景的組件,那么設(shè)計組件的最終目標是讓它與父組件松散耦合,呈現(xiàn)更好的復用性,而不是受限于特定的上下文環(huán)境。

          輔助代碼分離

          這個可能不那么的偏理論,但我仍然認為這很重要。與你的代碼庫打交道是軟件工程的一部分,有時一些基本的組織原則可以使事情變得更加順暢。在長時間與代碼相處的過程中,即使改變一個很小的習慣也可以產(chǎn)生很大的不同。其中一個有效的原則就是將輔助代碼分離出來放在特定的地方,這樣你在處理組件時就不必考慮這些。以下列舉一些方面:

          • 配置代碼

          • 假數(shù)據(jù)

          • 大量非技術(shù)說明文檔

          因為在嘗試處理組件的核心代碼時,你不希望看到與技術(shù)無關(guān)的一些說明(因為會多滾動幾下鼠標滾輪甚至打斷思路)。在處理組件時,你希望它們盡可能通用且可重用。查看與組件當前上下文相關(guān)的特定信息可能會使得設(shè)計出來的組件不易與具體業(yè)務(wù)解耦。

          提煉精華

          雖然這樣做起來可能具有挑戰(zhàn)性,但開發(fā)組件的一個好方法是使它們包含渲染它們所需的最小 Javascript。一些無關(guān)緊要的東西,比如數(shù)據(jù)獲取,數(shù)據(jù)整理或事件處理邏輯,理想情況下應該將通用的部分移入外部 js 或或者放在共同的祖先中。

          單獨從組件分的“視圖”部分來看,即你看到的內(nèi)容(html 和 樣式)。其中的 Javascript 僅用于幫助渲染視圖,可能還有一些針對特定組件的邏輯(例如在其他地方使用時)。除此之外的任何事情,例如 API 調(diào)用,數(shù)值的格式化(例如貨幣或時間)或跨組件復用的數(shù)據(jù),都可以移動外部的 js 文件中。讓我們看一下 Vue 中的一個簡單示例,使用嵌套列表組件。我們可以先看下下面這個有問題的版本。

          這是第一個層級:

          //?組件父級