[譯] 前端組件設計原則

原文地址:Front end component design principleshtml

原文做者:Andrew Dinihan前端

文中示例代碼:傳送門ios

限於我的能力,若有錯漏之處,煩請不吝賜教。 git

img

前言

我在最近的工做中開始使用 Vue 進行開發,可是我在上一家公司積累了三年以上 React 開發經驗。雖然在兩種不一樣的前端框架之間進行切換確實須要學習不少,可是兩者之間在不少基礎概念、設計思路上是相通的。其中之一就是組件設計,包括組件層次結構設計以及組件各自的職責劃分。github

組件是大多數現代前端框架的基本概念之一,在 React 和 Vue 以及 Ember 和 Mithril 等框架中均有所體現。組件一般是由標記語言、邏輯和樣式組成的集合。它們被建立的目的就是做爲可複用的模塊去構建咱們的應用程序。web

相似於傳統 OOP 語言中 class 的設計,在設計組件的時候須要考慮到不少方面,以便它們能夠很好的複用,組合,分離和低耦合,可是功能能夠比較穩定的實現,即便是在超出實際測試用例範圍的狀況下。這樣的設計提及來容易作起來卻很難,由於現實中咱們每每沒有足夠的時間按照最優的方式去作。axios

方法

在本文中,我想介紹一些組件相關的設計概念,在進行前端開發時應該考慮這些概念。我認爲最好的方法是給每一個概念一個簡潔精煉的名字,而後逐一解釋每一個概念是什麼以及爲何重要,對於比較抽象概念的會舉一些例子來幫助理解。後端

如下這個列表並非不全面也不完整,但我注意到的只有 8 件事情值得一提,對於那些已經能夠編寫基本組件但想要提升他們的技術設計技能的人來講。因此這是列表: 如下列舉的這個列表僅僅是是我注意到的 8 個方面,固然組件設計還有其餘一些方面。在此我只是列舉出來我認爲值得一提的。數組

對於已經掌握基本的組件設計而且想要提升自身的組件設計能力的開發者,我認爲如下 8  項是我認爲值得去注意的,固然這並非組件設計的所有。前端框架

  1. 層次結構和 UML 類圖
  2. 扁平化、面向數據的 state/props
  3. 更加純粹的 State 變化
  4. 低耦合
  5. 輔助代碼分離
  6. 提煉精華
  7. 及時模塊化
  8. 集中/統一的狀態管理

請注意,代碼示例可能有一些小問題或有點人爲設計。可是它們並不複雜,只是想經過這些例子來幫助更好的理解概念。

層次結構和類圖

應用內的組件共同造成組件樹, 而在設計過程當中將組件樹可視化展現能夠幫助你全面瞭解應用程序的佈局。一個比較好的展現這些的辦法就是組件圖。

UML 中有一個在 OOP 類設計中常用的類型,稱爲 UML 類圖。類圖中顯示了類屬性、方法、訪問修飾符、類與其餘類的關係等。雖然 OOP 類設計和前端組件設計差別很大,可是經過圖解輔助設計的方法值得參考。對於前端組件,該圖表能夠顯示:

  • State
  • Props
  • Methods
  • 與其餘組件的關係( Relationship to other components )

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

img

雖然這樣的事情可能看起來有點多,可是它具備許多優勢,而且在大型應用程序開發設計中所須要的。這樣會帶來的一個比較重要的問題是它會須要你在開始 codeing 以前就須要考慮到具體細節的實現,例如每一個組件須要什麼類型的數據,須要實現哪些方法,所需的狀態屬性等等。

一旦你對如何構建一個組件(或一組組件)的總體有大概的思路,就會很容易認爲當本身真正開始編碼實現時,它會如本身所指望的循序漸進的完成,但事實上每每會出現一些預料以外的事情, 固然你確定不但願所以去重構以前的某些部分,或者忍受初始設想中的缺點並所以擾亂你的代碼思路。而這些類圖的如下優勢能夠幫助你有效的規避以上問題,優勢以下:

  1. 一個易於理解的組件組成和關聯視圖
  2. 一個易於理解的應用程序 UI 層次結構的概述
  3. 一個結構數據層次及其流動方式的視圖
  4. 一個組件功能職責的快照
  5. 便於使用圖表軟件建立

順帶一提,上圖並非基於某些官方標準,好比 UML 類圖,它是我基本上建立的一套表達規則。例如,在 props 、方法的參數和返回值的數據類型定義聲明都是基於 Typescript 語法。我尚未找到書寫前端組件類圖的官方標準,多是因爲前端 Javascript 開發的相對較新且生態系統不夠完善所致,但若是有人知道主流標準,請在回覆中告訴我!

扁平的,面向數據的 state/props

在 state 和 props 頻繁被 watch 和 update 的狀況下,若是你有使用嵌套數據,那麼你的性能可能會受到影響,尤爲是在如下場景中,例如一些由於淺對於而觸發的從新渲染;在涉及 immutability 的庫中,好比 React,你必須建立狀態的副本而不是像在 Vue 中那樣直接更改它們,而且使用嵌套數據這樣作可能會建立笨拙,醜陋的代碼。

img

即便使用展開運算符,這種寫法也並不夠優雅。扁平 props 也能夠很好地清除組件正在使用的數據值。若是你傳給組件一個對象可是你並不能清楚的知道對象內部的屬性值,因此找出實際須要的數據值是來自組件具體的屬性值則是額外的工做。但若是 props 足夠扁平化,那麼起碼會方便使用和維護。

img

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

(此外,對於數據繁重的應用程序,數據規範化能夠帶來巨大的好處,除了扁平化以外,你可能還須要考慮一些別的優化方法)。

更加純粹的 State 變化

對 state 的更改一般應該響應某種事件,例如用戶單擊按鈕或 API 的響應。此外它們不該該由於別的 state 的變化而作出響應,由於 state 之間這種關聯可能會致使難以理解和維護的組件行爲。state 變化應該沒有反作用。

若是你濫用watch而不是有限考慮以上原則,那麼在 Vue 的使用中就可能由此引起的問題。咱們來看一個基本的 Vue 示例。我正在研究一個從 API 獲取一些數據並將其呈現給表的組件,其中排序,過濾等功能都是後端完成的,所以前端須要作的就是 watch 全部搜索參數,並在其變化時觸發 API 調用。其中一個須要 watch 的值是「zone」,這是一個過濾器。當更改時,咱們想要使用過濾後的值從新獲取服務端數據。watcher 以下:

img

你會發現一些奇怪的東西。若是他們超出告終果的第一頁,咱們重置頁碼而後結束?這彷佛不對,若是它們不在第一頁上,咱們應該重置分頁並觸發 API 調用,對吧?爲何咱們只在第 1 頁上從新獲取數據?實際上緣由是這樣,讓咱們來看下完整的 watch:

img

當分頁改變時,應用首先會經過 pagination 的處理函數從新獲取數據。所以,若是咱們改變了分頁,咱們並不須要去關注數據更新這段邏輯。

讓咱們一下來考慮如下流程:若是當前頁面超出了第 1 頁而且更改了 zone,而這個變化會觸發另外一個狀態(pagination)發生變化,進而觸發 pagination 的觀察者從新請求數據。這樣並非預料之中的行爲,並且產生的代碼也不夠直觀。

解決方案是改變頁碼這個行爲的事件處理函數(不是觀察者,用戶更改頁面的實際處理函數)應該更改頁面值觸發 API 調用請求數據。這也將消除對觀察者的需求。經過這樣的設置,直接從其餘地方改變分頁狀態也不會致使從新獲取數據的反作用。

雖然這個例子很是簡單,但不難看出將更復雜的狀態更改關聯在一塊兒會產生使人難以理解的代碼,這些代碼不只不可擴展而且是調試的噩夢。

鬆耦合

組件的核心思想是它們是可複用的,爲此要求它們必須具備功能性和完整性。「耦合」是指實體彼此依賴的術語。鬆散耦合的實體應該可以獨立運行,而不依賴於其餘模塊。就前端組件而言,耦合的主要部分是組件的功能依賴於其父級及其傳遞的 props 的多少,以及內部使用的子組件(固然還有引用的部分,如第三方模塊或用戶腳本)。

緊密耦合的組件每每更不容易被複用,當它們做爲特定父組件的子項時,就很難正常工做,當父組件的一個子組件或一系列子組件只能在該父組件纔可以正常發揮做用時,就會使得代碼寫的很冗餘。由於父子組件別過分的關聯在一塊兒了。

在設計組件時,你應該考慮到更加通用的使用場景,而不只僅只是爲了知足最開始某個特定場景的需求。雖然通常來講組件最初都是出於特定目的進行設計,但不要緊,若是在設計它們站在更高的角度去看待,那麼不少組件將具備更好的適用性。

讓咱們看一個簡單的 React 示例,你想在寫出一個帶有一個 logo 的連接列表,經過鏈接能夠訪問特定的網站。最開始的設計多是並無跟內容合理的進行解耦。下面是最初的版本:

img

雖然這這樣會知足預期的使用場景,但卻很難被複用。若是你想要更改連接地址該怎麼辦?你必須從新複製一份相同代碼,而且手動去替換連接地址。並且, 若是你要去實現一個用戶能夠更改鏈接的功能,那麼意味着不可能將代碼寫「死」,也不能指望用戶去手動修改代碼,那麼讓咱們來看一下複用性更高的組件應該如何設計:

img

在這裏咱們能夠看到,雖然它的原始連接和 logo 具備默認值,但咱們能夠經過 props 傳入的值去覆蓋掉默認值。讓咱們來看一下它在實際中的使用:

img

並不須要從新編寫新的組件!若是咱們解決上文中用戶能夠自定義連接的使用場景,能夠考慮動態構建連接數組。此外,雖然在這個具體的例子中沒有解決,但咱們仍然能夠注意到這個組件沒有與任何特定的父/子組件創建密切關聯。它能夠在任何須要的地方呈現。改進後的組件明顯比最第一版本具備更好的複用性。

若是不是要設計須要服務於特定的一次性場景的組件,那麼設計組件的最終目標是讓它與父組件鬆散耦合,呈現更好的複用性,而不是受限於特定的上下文環境。

輔助代碼分離

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

  • 配置代碼
  • 假數據
  • 大量非技術說明文檔

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

提煉精華

雖然這樣作起來可能具備挑戰性,但開發組件的一個好方法是使它們包含渲染它們所需的最小 Javascript。一些可有可無的東西,好比數據獲取,數據整理或事件處理邏輯,理想狀況下應該將通用的部分移入外部 js 或或者放在共同的祖先中。

單獨從組件分的「視圖」部分來看,即你看到的內容(html 和 樣式)。其中的 Javascript 僅用於幫助渲染視圖,可能還有一些針對特定組件的邏輯(例如在其餘地方使用時)。除此以外的任何事情,例如 API 調用,數值的格式化(例如貨幣或時間)或跨組件複用的數據,均可以移動外部的 js 文件中。讓咱們看一下 Vue 中的一個簡單示例,使用嵌套列表組件。咱們能夠先看下下面這個有問題的版本。

這是第一個層級:

img

這是嵌套列表組件:

img

在這裏咱們能夠看到此列表的兩個層級都具備外部依賴關係,最上層導引入外部 js 文件中的函數和 JSON 文件的數據,嵌套組件鏈接到 Vuex 存儲並使用 axios 發送請求。它們還具備僅適用於當前場景的嵌入功能(最上層中源數據處理和嵌套列表的中度 click 時間的特定響應功能)。

雖然這裏採用了一些很好的通用設計技術,例如將通用的 數據處理方法移動到外部腳本而不是直接將函數寫死,但這樣仍然不具有很高的複用性。若是咱們是從 API 的響應中獲取數據,可是這個數據跟咱們指望的數據結構或者類型不一樣的時候要怎麼辦?或者咱們指望單擊嵌套項時有不一樣的行爲?在遇到這些需求的場景下,這個組件沒法被別的組件直接引用並根據實際需求改變自身的特性。

讓咱們看看咱們是否能夠經過提高數據並將事件處理做爲 props 傳遞來解決這個問題,這樣組件就能夠簡單地呈現數據而不會封裝任何其餘邏輯。

這是改進後的第一級別:

img

而新的第二級:

img

使用這個新列表,咱們能夠得到想要的數據,並定義了嵌套列表的 onClick 處理函數,以便在父級中傳入任何咱們想要的操做,而後將它們做爲 props 傳遞給頂級組件。這樣,咱們能夠將導入和邏輯留給單個根組件,因此不須要爲了可以在新的場景下使用去從新再實現一個相似組件。

有關此主題的簡短文章能夠在這裏找到。它由 Redux 的做者 Dan Abramov 編寫,雖然是用 React 舉例說明。可是組件設計的思想是通用的。

及時模塊化

咱們在實際進行組件抽離工做的時候,須要考慮到不要過分的組件化,誠然將大塊代碼變成鬆散耦合且可用的部分是很好的實踐,可是並非全部的頁面結構(HTML 部分)都須要被抽離成組件,也不是全部的邏輯部分都須要被抽出到組件外部。

在決定是否將代碼分開時,不管是 Javascript 邏輯仍是抽離爲新的組件,都須要考慮如下幾點。一樣,這個列表並不完整,只是爲了讓你瞭解須要考慮的各類事項。(記住,僅僅由於它不知足一個條件並不意味着它不會知足其餘條件,因此在作出決定以前要考慮全部條件):

  1. 是否有足夠的頁面結構/邏輯來保證它? 若是它只是幾行代碼,那麼最終可能會建立更多的代碼來分隔它,而不只僅是將代碼放入其中。
  2. 代碼重複(或可能重複)? 若是某些東西只使用一次,而且服務於一個不太可能在其餘地方使用的特定用例,那麼將它嵌入其中可能會更好。若是須要,你能夠隨時將其分開(但不要在須要作這些工做的時候將此做爲偷懶的藉口)。
  3. 它會減小須要書寫的模板嗎? 例如,假設你想要一個帶有特定樣式的 div 屬性結構和一些靜態內容/功能的組件,其中一些可變內容嵌套在內部。經過建立可重用的包裝器(與 React 的 HOC 或 Vue 的 slot 同樣),你能夠在建立這些組件的多個實例時減小模板代碼,由於你不須要從新再寫外部的包裝代碼。
  4. 性能會收到影響嗎? 更改 state/props 會致使從新渲染,當發生這種狀況時,你須要的是 只是從新去渲染通過 diff 以後獲得的相關元素節點。在較大的、關聯很緊密的組件中,你可能會發現狀態更改會致使在不須要它的許多地方從新呈現,這時應用的性能就可能會開始受到影響。
  5. 你是否會在測試代碼的全部部分時遇到問題? 咱們老是但願可以進行充分的測試,好比對於一個組件,咱們會指望它的正常工做不依賴特定的用例(上下文),而且全部 Javascript 邏輯都按預期工做。當元素具備某個特定假設的上下文或者分別將一大堆邏輯嵌入到單個函數中時,這樣將會很難知足咱們的指望。若是測試的組件是具備比較大模板和樣式的單個巨型組件,那麼組件的渲染測試也會很難進行。
  6. 你是否有一個明確的理由? 在分割代碼時,你應該考慮它究竟實現了什麼。這是否容許更鬆散的耦合?我是否打破了一個邏輯上有意義的獨立實體?這個代碼是否真的可能在其餘地方被重複使用?若是你不能清楚地回答這個問題,那最好先不要進行組件抽離。由於這樣可能致使一些問題(好比拆解掉本來某些潛在的耦合關係)。
  7. 這些好處是否超過了成本? 分離代碼不可避免地須要時間和精力,其數量根據具體狀況而變化,而且在最終作出此決定時會有許多因素(例如此列表中列舉出來的一些)。通常來講,進行一些對抽象的成本和收益研究能夠幫助更快更準確去作出是否須要組件化的決策。最後,我提到了這一點,由於若是咱們過度關注優點,就很容易忘記達成目標所須要作的努力,因此在作出決定之前須要權衡這兩個方面。

集中/統一的狀態管理

許多大型應用程序使用 Redux 或 Vuex 等狀態管理工具(或者具備相似 React 中的 Context API 狀態共享設置)。這意味着他們從 store 得到 props 而不是經過父級傳遞。在考慮組件的可重用性時,你不只要考慮直接的父級中傳遞而來的 props,還要考慮 從 store 中獲取到的 props。若是你在另外一個項目中使用該組件,則須要在 store 中使用這些值。或許其餘項目根本不使用集中存儲工具,你必須將其轉換爲從父級中進行 props 傳遞 的形式。

因爲將組件掛接到 store(或上下文)很容易而且不管組件的層次結構位置如何均可以完成,所以很容易在 store 和 web 應用的組件之間快速建立大量緊密耦合(不關心組件所處的層級)。一般將組件與 store 進行關聯只需簡單幾行代碼。可是請注意一點,雖然這種鏈接(耦合)更方便,但它的含義並無什麼不一樣,你也須要考慮儘可能符合如同在使用父級傳遞方式時的要點。

最後

我想提醒你們的是:應該更注重以上這些組件設計的原則和你已知的一些最佳實踐在實際中的應用。雖然你應該盡力維護良好的設計,可是不要爲了包裝 JIRA ticket 或一個取消請求而有損代碼完整性,同時老是把理論置於現實世界結果之上的人也每每會讓他們的工做受到影響。大型軟件項目有許多活動部分,軟件工程的許多方面與編碼沒有特別的關係,但仍然是不可或缺的,例如遵照最後期限和處理非技術指望。

雖然充分的準備很重要,應該成爲任何專業軟件設計的一部分,但在現實世界中,切實的結果纔是最爲重要的。當你被僱用來實際創造一些東西時,若是在最後期限到來以前,你有的只是一個如何構建完美產品的驚人計劃,但卻沒有實際的成果,你的僱主可能不會過高興吧?此外,軟件工程中的東西不多徹底按計劃進行,所以過分具體的計劃每每會在時間使用方面獲得拔苗助長的效果。

此外,組件規劃和設計的概念也適用於組件重構。雖然用了 50 年的時間來計劃一切使人難以忍受的細節,而後從一開始就完美地編寫它就會很好,回到現實世界,咱們每每會遇到這種狀況,即爲了趕進度而不能使代碼達到完美的預期。然而,一旦咱們有了空閒時間,那麼一個推薦的作法就是回過頭來重構早期不夠理想的的代碼,這樣它就能夠做爲咱們向前發展的堅實基礎。

在一天結束時,雖然你的直接責任多是「編寫代碼」,但你不該忽視你的最終目標,即創建一些東西。建立產品。爲了產生一些你能夠引覺得豪的東西並幫助別人,即便它在技術上並不完美,永遠記得找到一個平衡點。不幸的是,在一週內天天 8 小時盯着眼前的代碼會使得眼界和角度變得更爲「狹窄」,這個時候你須要的你是退後一步,確保你不要爲了一顆樹而失去整個森林。

相關文章
相關標籤/搜索