[译] 前端组件设计原则
原文地址:Front end component design principles
原文作者:Andrew Dinihan
文中示例代碼:傳送門
限于個人能力,如有錯漏之處,煩請不吝賜教。
前言
我在最近的工作中開始使用 Vue 進行開發,但是我在上一家公司積累了三年以上 React 開發經驗。雖然在兩種不同的前端框架之間進行切換確實需要學習很多,但是二者之間在很多基礎概念、設計思路上是相通的。其中之一就是組件設計,包括組件層次結構設計以及組件各自的職責劃分。
組件是大多數現代前端框架的基本概念之一,在 React 和 Vue 以及 Ember 和 Mithril 等框架中均有所體現。組件通常是由標記語言、邏輯和樣式組成的集合。它們被創建的目的就是作為可復用的模塊去構建我們的應用程序。
類似于傳統 OOP 語言中 class 的設計,在設計組件的時候需要考慮到很多方面,以便它們可以很好的復用,組合,分離和低耦合,但是功能可以比較穩定的實現,即使是在超出實際測試用例范圍的情況下。這樣的設計說起來容易做起來卻很難,因為現實中我們往往沒有足夠的時間按照最優的方式去做。
方法
在本文中,我想介紹一些組件相關的設計概念,在進行前端開發時應該考慮這些概念。我認為最好的方法是給每個概念一個簡潔精煉的名字,然后逐一解釋每個概念是什么以及為什么重要,對于比較抽象概念的會舉一些例子來幫助理解。
以下這個列表并不是不全面也不完整,但我注意到的只有 8 件事情值得一提,對于那些已經可以編寫基本組件但想要提高他們的技術設計技能的人來說。所以這是列表: 以下列舉的這個列表僅僅是是我注意到的 8 個方面,當然組件設計還有其他一些方面。在此我只是列舉出來我認為值得一提的。
對于已經掌握基本的組件設計并且想要提高自身的組件設計能力的開發者,我認為以下 8 項是我認為值得去注意的,當然這并不是組件設計的全部。
請注意,代碼示例可能有一些小問題或有點人為設計。但是它們并不復雜,只是想通過這些例子來幫助更好的理解概念。
層次結構和類圖
應用內的組件共同形成組件樹, 而在設計過程中將組件樹可視化展示可以幫助你全面了解應用程序的布局。一個比較好的展示這些的辦法就是組件圖。
UML 中有一個在 OOP 類設計中經常使用的類型,稱為 UML 類圖。類圖中顯示了類屬性、方法、訪問修飾符、類與其他類的關系等。雖然 OOP 類設計和前端組件設計差異很大,但是通過圖解輔助設計的方法值得參考。對于前端組件,該圖表可以顯示:
- State
- Props
- Methods
- 與其他組件的關系( Relationship to other components )
因此,讓我們看一下下面這個基礎表組件的組件層次圖,該組件的渲染對象是一個數組。該組件的功能包括顯示總行數、標題行和一些數據行,以及在單擊其單元格標題格時對該列進行排序。在它的 props 中,它將傳遞列列表(具有屬性名稱和該屬性的人類可讀版本),然后傳遞數據數組。我們可以添加一個可選的'on row click'功能來進行測試。
雖然這樣的事情可能看起來有點多,但是它具有許多優點,并且在大型應用程序開發設計中所需要的。這樣會帶來的一個比較重要的問題是它會需要你在開始 codeing 之前就需要考慮到具體細節的實現,例如每個組件需要什么類型的數據,需要實現哪些方法,所需的狀態屬性等等。
一旦你對如何構建一個組件(或一組組件)的整體有大概的思路,就會很容易認為當自己真正開始編碼實現時,它會如自己所期望的按部就班的完成,但事實上往往會出現一些預料之外的事情, 當然你肯定不希望因此去重構之前的某些部分,或者忍受初始設想中的缺點并因此擾亂你的代碼思路。而這些類圖的以下優點可以幫助你有效的規避以上問題,優點如下:
順帶一提,上圖并不是基于某些官方標準,比如 UML 類圖,它是我基本上創建的一套表達規則。例如,在 props 、方法的參數和返回值的數據類型定義聲明都是基于 Typescript 語法。我還沒有找到書寫前端組件類圖的官方標準,可能是由于前端 Javascript 開發的相對較新且生態系統不夠完善所致,但如果有人知道主流標準,請在回復中告訴我!
扁平的,面向數據的 state/props
在 state 和 props 頻繁被 watch 和 update 的情況下,如果你有使用嵌套數據,那么你的性能可能會受到影響,尤其是在以下場景中,例如一些因為淺對于而觸發的重新渲染;在涉及 immutability 的庫中,比如 React,你必須創建狀態的副本而不是像在 Vue 中那樣直接更改它們,并且使用嵌套數據這樣做可能會創建笨拙,丑陋的代碼。
即使使用展開運算符,這種寫法也并不夠優雅。扁平 props 也可以很好地清除組件正在使用的數據值。如果你傳給組件一個對象但是你并不能清楚的知道對象內部的屬性值,所以找出實際需要的數據值是來自組件具體的屬性值則是額外的工作。但如果 props 足夠扁平化,那么起碼會方便使用和維護。
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 如下:
你會發現一些奇怪的東西。如果他們超出了結果的第一頁,我們重置頁碼然后結束?這似乎不對,如果它們不在第一頁上,我們應該重置分頁并觸發 API 調用,對吧?為什么我們只在第 1 頁上重新獲取數據?實際上原因是這樣,讓我們來看下完整的 watch:
當分頁改變時,應用首先會通過 pagination 的處理函數重新獲取數據。因此,如果我們改變了分頁,我們并不需要去關注數據更新這段邏輯。
讓我們一下來考慮以下流程:如果當前頁面超出了第 1 頁并且更改了 zone,而這個變化會觸發另一個狀態(pagination)發生變化,進而觸發 pagination 的觀察者重新請求數據。這樣并不是預料之中的行為,而且產生的代碼也不夠直觀。
解決方案是改變頁碼這個行為的事件處理函數(不是觀察者,用戶更改頁面的實際處理函數)應該更改頁面值并觸發 API 調用請求數據。這也將消除對觀察者的需求。通過這樣的設置,直接從其他地方改變分頁狀態也不會導致重新獲取數據的副作用。
雖然這個例子非常簡單,但不難看出將更復雜的狀態更改關聯在一起會產生令人難以理解的代碼,這些代碼不僅不可擴展并且是調試的噩夢。
松耦合
組件的核心思想是它們是可復用的,為此要求它們必須具有功能性和完整性。“耦合”是指實體彼此依賴的術語。松散耦合的實體應該能夠獨立運行,而不依賴于其他模塊。就前端組件而言,耦合的主要部分是組件的功能依賴于其父級及其傳遞的 props 的多少,以及內部使用的子組件(當然還有引用的部分,如第三方模塊或用戶腳本)。
緊密耦合的組件往往更不容易被復用,當它們作為特定父組件的子項時,就很難正常工作,當父組件的一個子組件或一系列子組件只能在該父組件才能夠正常發揮作用時,就會使得代碼寫的很冗余。因為父子組件別過度的關聯在一起了。
在設計組件時,你應該考慮到更加通用的使用場景,而不僅僅只是為了滿足最開始某個特定場景的需求。雖然一般來說組件最初都是出于特定目的進行設計,但沒關系,如果在設計它們站在更高的角度去看待,那么很多組件將具有更好的適用性。
讓我們看一個簡單的 React 示例,你想在寫出一個帶有一個 logo 的鏈接列表,通過連接可以訪問特定的網站。最開始的設計可能是并沒有跟內容合理的進行解耦。下面是最初的版本:
雖然這這樣會滿足預期的使用場景,但卻很難被復用。如果你想要更改鏈接地址該怎么辦?你必須重新復制一份相同代碼,并且手動去替換鏈接地址。而且, 如果你要去實現一個用戶可以更改連接的功能,那么意味著不可能將代碼寫“死”,也不能期望用戶去手動修改代碼,那么讓我們來看一下復用性更高的組件應該如何設計:
在這里我們可以看到,雖然它的原始鏈接和 logo 具有默認值,但我們可以通過 props 傳入的值去覆蓋掉默認值。讓我們來看一下它在實際中的使用:
并不需要重新編寫新的組件!如果我們解決上文中用戶可以自定義鏈接的使用場景,可以考慮動態構建鏈接數組。此外,雖然在這個具體的例子中沒有解決,但我們仍然可以注意到這個組件沒有與任何特定的父/子組件建立密切關聯。它可以在任何需要的地方呈現。改進后的組件明顯比最初版本具有更好的復用性。
如果不是要設計需要服務于特定的一次性場景的組件,那么設計組件的最終目標是讓它與父組件松散耦合,呈現更好的復用性,而不是受限于特定的上下文環境。
輔助代碼分離
這個可能不那么的偏理論,但我仍然認為這很重要。與你的代碼庫打交道是軟件工程的一部分,有時一些基本的組織原則可以使事情變得更加順暢。在長時間與代碼相處的過程中,即使改變一個很小的習慣也可以產生很大的不同。其中一個有效的原則就是將輔助代碼分離出來放在特定的地方,這樣你在處理組件時就不必考慮這些。以下列舉一些方面:
- 配置代碼
- 假數據
- 大量非技術說明文檔
因為在嘗試處理組件的核心代碼時,你不希望看到與技術無關的一些說明(因為會多滾動幾下鼠標滾輪甚至打斷思路)。在處理組件時,你希望它們盡可能通用且可重用。查看與組件當前上下文相關的特定信息可能會使得設計出來的組件不易與具體業務解耦。
提煉精華
雖然這樣做起來可能具有挑戰性,但開發組件的一個好方法是使它們包含渲染它們所需的最小 Javascript。一些無關緊要的東西,比如數據獲取,數據整理或事件處理邏輯,理想情況下應該將通用的部分移入外部 js 或或者放在共同的祖先中。
單獨從組件分的“視圖”部分來看,即你看到的內容(html 和 樣式)。其中的 Javascript 僅用于幫助渲染視圖,可能還有一些針對特定組件的邏輯(例如在其他地方使用時)。除此之外的任何事情,例如 API 調用,數值的格式化(例如貨幣或時間)或跨組件復用的數據,都可以移動外部的 js 文件中。讓我們看一下 Vue 中的一個簡單示例,使用嵌套列表組件。我們可以先看下下面這個有問題的版本。
這是第一個層級:
這是嵌套列表組件:
在這里我們可以看到此列表的兩個層級都具有外部依賴關系,最上層導引入外部 js 文件中的函數和 JSON 文件的數據,嵌套組件連接到 Vuex 存儲并使用 axios 發送請求。它們還具有僅適用于當前場景的嵌入功能(最上層中源數據處理和嵌套列表的中度 click 時間的特定響應功能)。
雖然這里采用了一些很好的通用設計技術,例如將通用的 數據處理方法移動到外部腳本而不是直接將函數寫死,但這樣仍然不具備很高的復用性。如果我們是從 API 的響應中獲取數據,但是這個數據跟我們期望的數據結構或者類型不同的時候要怎么辦?或者我們期望單擊嵌套項時有不同的行為?在遇到這些需求的場景下,這個組件無法被別的組件直接引用并根據實際需求改變自身的特性。
讓我們看看我們是否可以通過提升數據并將事件處理作為 props 傳遞來解決這個問題,這樣組件就可以簡單地呈現數據而不會封裝任何其他邏輯。
這是改進后的第一級別:
而新的第二級:
使用這個新列表,我們可以獲得想要的數據,并定義了嵌套列表的 onClick 處理函數,以便在父級中傳入任何我們想要的操作,然后將它們作為 props 傳遞給頂級組件。這樣,我們可以將導入和邏輯留給單個根組件,所以不需要為了能夠在新的場景下使用去重新再實現一個類似組件。
有關此主題的簡短文章可以在這里找到。它由 Redux 的作者 Dan Abramov 編寫,雖然是用 React 舉例說明。但是組件設計的思想是通用的。
及時模塊化
我們在實際進行組件抽離工作的時候,需要考慮到不要過度的組件化,誠然將大塊代碼變成松散耦合且可用的部分是很好的實踐,但是并不是所有的頁面結構(HTML 部分)都需要被抽離成組件,也不是所有的邏輯部分都需要被抽出到組件外部。
在決定是否將代碼分開時,無論是 Javascript 邏輯還是抽離為新的組件,都需要考慮以下幾點。同樣,這個列表并不完整,只是為了讓你了解需要考慮的各種事項。(記住,僅僅因為它不滿足一個條件并不意味著它不會滿足其他條件,所以在做出決定之前要考慮所有條件):
集中/統一的狀態管理
許多大型應用程序使用 Redux 或 Vuex 等狀態管理工具(或者具有類似 React 中的 Context API 狀態共享設置)。這意味著他們從 store 獲得 props 而不是通過父級傳遞。在考慮組件的可重用性時,你不僅要考慮直接的父級中傳遞而來的 props,還要考慮 從 store 中獲取到的 props。如果你在另一個項目中使用該組件,則需要在 store 中使用這些值。或許其他項目根本不使用集中存儲工具,你必須將其轉換為從父級中進行 props 傳遞 的形式。
由于將組件掛接到 store(或上下文)很容易并且無論組件的層次結構位置如何都可以完成,因此很容易在 store 和 web 應用的組件之間快速創建大量緊密耦合(不關心組件所處的層級)。通常將組件與 store 進行關聯只需簡單幾行代碼。但是請注意一點,雖然這種連接(耦合)更方便,但它的含義并沒有什么不同,你也需要考慮盡量符合如同在使用父級傳遞方式時的要點。
最后
我想提醒大家的是:應該更注重以上這些組件設計的原則和你已知的一些最佳實踐在實際中的應用。雖然你應該盡力維護良好的設計,但是不要為了包裝 JIRA ticket 或一個取消請求而有損代碼完整性,同時總是把理論置于現實世界結果之上的人也往往會讓他們的工作受到影響。大型軟件項目有許多活動部分,軟件工程的許多方面與編碼沒有特別的關系,但仍然是不可或缺的,例如遵守最后期限和處理非技術期望。
雖然充分的準備很重要,應該成為任何專業軟件設計的一部分,但在現實世界中,切實的結果才是最為重要的。當你被雇用來實際創造一些東西時,如果在最后期限到來之前,你有的只是一個如何構建完美產品的驚人計劃,但卻沒有實際的成果,你的雇主可能不會太高興吧?此外,軟件工程中的東西很少完全按計劃進行,因此過度具體的計劃往往會在時間使用方面得到適得其反的效果。
此外,組件規劃和設計的概念也適用于組件重構。雖然用了 50 年的時間來計劃一切令人難以忍受的細節,然后從一開始就完美地編寫它就會很好,回到現實世界,我們往往會遇到這種情況,即為了趕進度而不能使代碼達到完美的預期。然而,一旦我們有了空閑時間,那么一個推薦的做法就是回過頭來重構早期不夠理想的的代碼,這樣它就可以作為我們向前發展的堅實基礎。
在一天結束時,雖然你的直接責任可能是“編寫代碼”,但你不應忽視你的最終目標,即建立一些東西。創建產品。為了產生一些你可以引以為豪的東西并幫助別人,即使它在技術上并不完美,永遠記得找到一個平衡點。不幸的是,在一周內每天 8 小時盯著眼前的代碼會使得眼界和角度變得更為“狹窄”,這個時候你需要的你是退后一步,確保你不要為了一顆樹而失去整個森林。
轉載于:https://juejin.im/post/5c49cff56fb9a049bd42a90f
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的[译] 前端组件设计原则的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【大数据24小时】“天智一号”卫星将在太
- 下一篇: 什么是ie浏览器_?IE 浏览器为什么不