文/劉先寧
隨著SPA,前後端分離的技術架構在業界越來越流行,前端(注:本文中的前端泛指所有的使用者可接觸的介面,包括桌面,移動端)需要管理的內容,承擔的職責也越來越多。再加上移動網際網路的火爆,及其帶動的Mobile First風潮,各大公司也開始在前端投入更多的資源。
這一切,使得業界對前端開發方案的思考上多了很多,以React框架為代表推動的元件化開發方案就是目前業界比較認可的方案,本文將和大家一起探討一下元件化開發方案能給我們帶來什麼,以及如何在React Native專案的運用元件化開發方案。
一、為什麼要採用元件化開發方案?
在講怎麼做之前,需要先看看為什麼前端要採用元件化開發方案,作為一名程式設計師和諮詢師,我清楚地知道凡是拋開問題談方案都是耍流氓。那麼在面對隨著業務規模的增加,更多的業務功能推向前端,以及隨之而來的開發團隊擴張時,前端開發會遇到些什麼樣的問題呢?
1. 前端開發面臨的問題
- 資源冗餘:頁面變得越來越多,頁面的互動變得越來越複雜。在這種情況下,有些團隊成員會根據功能寫自己的CSS、JS,這會產生大量的新的CSS或JS檔案,而這些檔案中可能出現大量的重複邏輯;有些團隊成員則會重用別人的邏輯,但是由於邏輯拆分的粒度差異,可能會為了依賴某個JS中的一個函式,需要載入整個模組,或者為了使用某個CSS中的部分樣式依賴整個CSS檔案,這導致了大量的資源冗餘。
- 依賴關係不直觀:當修改一個JS函式,或者某個CSS屬性時,很多時候只能靠人力全域性搜尋來判斷影響範圍,這種做法不但慢,而且很容易出錯。
- 專案的靈活性和可維護性差:因為專案中的交叉依賴太多,當出現技術方案變化時,無法做到漸進式的、有節奏地替換掉老的程式碼,只能一次性替換掉所有老程式碼,這極大地提升了技術方案升級的成本和風險。
- 新人進組上手難度大:新人進入專案後,需要了解整個專案的背景、技術棧等,才能或者說才敢開始工作。這在小專案中也許不是問題,但是在大型專案中,尤其是人員流動比較頻繁的專案,則會對專案進度產生非常大的影響。
- 團隊協同度不高:使用者流程上頁面間的依賴(比方說一個頁面強依賴前一個頁面的工作結果),以及技術方案上的一些相互依賴(比方說某個檔案只能由某個團隊修改)會導致無法發揮一個團隊的全部效能,部分成員會出現等待空窗期,浪費團隊效率。
- 測試難度大:整個專案中的邏輯拆分不清晰,過多且雜亂的相互依賴都顯著拉昇了自動化測試的難度。
- 溝通反饋慢:業務的要求,UX的設計都需要等到開發人員寫完程式碼,整個專案編譯部署後才能看到實際的效果,這個反饋週期太長,並且未來的任何一個小修改又需要重複這一整個流程。
2.元件化開發帶來的好處
元件化開發的核心是“業務的歸業務,元件的歸元件”。即元件是一個個獨立存在的模組,它需要具備如下的特徵:
(圖片來自:http://serenity.bh/wp-content/)
- 職責單一而清晰:開發人員可以很容易瞭解該元件提供的能力。
- 資源高內聚: 元件資源內部高內聚,元件資源完全由自身載入控制。
- 作用域獨立: 內部結構密封,不與全域性或其他元件產生影響。
- 介面規範化: 元件介面有統一規範。
- 可相互組合: 組裝整合成複雜元件,高階元件等。
- 獨立清晰的生命週期管理:元件的載入、渲染、更新必須有清晰的、可控的路徑。
而業務就是通過組合這一堆元件完成User Journey。下一節中,會詳細描述採用元件化開發方案的團隊是如何運作的。
在專案中分清楚元件和業務的關係,把系統的構建架構在元件化思想上可以:
- 降低整個系統的耦合度:在保持介面不變的情況下,我們可以把當前元件替換成不同的元件實現業務功能升級,比如把一個搜尋框,換成一個日曆元件。
- 提高可維護性:由於每個元件的職責單一,在系統中更容易被複用,所以對某個職責的修改只需要修改一處,就可獲得系統的整體升級。獨立的,小的元件程式碼的更易理解,維護起來也更容易。
- 降低上手難度:新成員只需要理解介面和職責即可開發元件程式碼,在不斷的開發過程中再進一步理解和學習專案知識。另外,由於程式碼的影響範圍僅限於元件內部,對專案的風險控制也非常有幫助,不會因為一次修改導致雪崩效應,影響整個團隊的工作。
- 提升團隊協同開發效率:通過對元件的拆分粒度控制來合理分配團隊成員任務,讓團隊中每個人都能發揮所長,維護對應的元件,最大化團隊開發效率。
- 便於自動化測試:由於元件除了介面外,完全是自治王國,甚至概念上,可以把元件當成一個函式,輸入對應著輸出,這讓自動化測試變得簡單。
- 更容易的自文件化:在元件之上,可以採用Living Style Guide的方式為專案的所有UI元件建立一個‘活’的文件,這個文件還可以成為業務,開發,UX之間的溝通橋樑。這是對‘程式碼即文件’的另一種詮釋,巧妙的解決了程式設計師不愛寫文件的問題。
- 方便除錯:由於整個系統是通過元件組合起來的,在出現問題的時候,可以用排除法直接移除元件,或者根據報錯的元件快速定位問題。另外,Living Style Guide除了作為溝通工具,還可以作為除錯工具,幫助開發者除錯UI元件。
二、元件化開發方案下,團隊如何運作?
前面大致講了下元件化開發可以給專案帶來的好處,接下來聊一聊採用元件化開發方案的團隊是應該如何運作?
在ThoughtWorks,我們把一個專案的生命週期分為如下幾個階段:
元件化開發方案主要關注的是在迭代開發階段的對團隊效率的提升。 它主要從以下幾個方面提升了開發效率:
1. 以架構層的元件複用降低工作量
在大型應用的後端開發中,為了分工、複用和可維護性,在架構層面將應用抽象為多個相對獨立的模組的思想和方法都已經非常成熟和深入人心了。
但是在前端開發中,模組化的思想還是比較傳統,開發者還是隻有在需考慮複用時才會將某一部分做成元件,再加上當開發人員專注在不同介面開發上時,對於該介面上哪些部分可以重用缺乏關注,導致在多個介面上重複開發相同的UI功能,這不僅拉昇了整個專案的工作量,還增加了專案後續的修改和維護成本。
在元件化開發方案下,團隊在交付開始階段就需要從架構層面對應用的UI進行模組化,團隊會一起把需求分析階段產生的原型中的每一個UI頁面抽象為一顆元件樹,UI頁面自己本身上也是一個元件。如下圖:
通過上面的抽象之後,我們會發現大量的元件可以在多個UI介面上覆用,而考慮到在前端專案中,構建各個UI介面佔了80%以上的工作量,這樣的抽象顯著降低了專案的工作量,同時對後續的修改和維護也會大有裨益。
在這樣的架構模式下,團隊的運作方式就需要相應的發生改變:
- 工程化方面的支援,從目錄結構的劃分上對開發人員進行元件化思維的強調,區分基礎元件,業務元件,頁面元件的位置,職責,以及相互之間的依賴關係。
- 工作優先順序的安排,在敏捷團隊中,我們強調的是交付業務價值。而業務是由頁面元件串聯而成,在元件化的架構模式下,必然是先完成元件開發,再串聯業務。所以在做迭代計劃時,需要對團隊開發元件的任務和串聯業務的任務做一個清晰的優先順序安排,以保證團隊對業務價值的交付節奏。
2.以元件的規範性保障專案設計的統一性
在前端開發中,因為CSS的靈活性,對於相同的UI要求(比如:佈局上的靠右邊框5個畫素),就可能有上十種的CSS寫法,開發人員的背景,經歷的不同,很可能會選擇不同的實現方法;甚至還有一些不成熟的專案,存在需求方直接給一個PDF檔案的使用者流程圖介面,不給PSD的情況,所有的設計元素需要開發人員從圖片中抓取,這更是會使得專案的樣式寫的五花八門。因為同樣的UI設計在專案中存在多種寫法,會導致很多問題,第一就是設計上可能存在不一致的情況;第二是UI設計發生修改時,出現需要多種修改方案的成本,甚至出現漏改某個樣式導致bug的問題。
在元件化開發方案下,專案設計的統一性被上拉到元件層,由元件的統一性來保障。其實本來所有的業務UI設計就是元件為單位的,設計師不會說我要“黃色”,他們說得是我要“黃色的按鈕……”。是開發者在實現過程中把UI設計下放到CSS樣式上的,相比一個個,一組組的CSS屬性,元件的整體性和可理解性都會更高。再加上元件的資源高內聚特性,在元件上對樣式進行調整也會變得容易,其影響範圍也更可控。
在元件化開發方案下,為了保證UI設計的一致性,團隊的運作需要:
- 定義基礎設計元素,包括色號、字型、字號等,由UX決定所有的基礎設計元素。
- 所有具體的UI元件設計必須通過這些基礎設計元素組合而成,如果當前的基礎設計元素不能滿足需求,則需要和UX一起討論增加基礎設計元素。
- UI元件的驗收需要UX參與。
3. 以元件的獨立性和自治性提升團隊協同效率
在前端開發時,存在一個典型的場景就是某個功能介面,距離啟動介面有多個層級,按照傳統開發方式,需要按照頁面一頁一頁的開發,當前一個頁面開發未完成時,無法開始下一個頁面的開發,導致團隊工作的併發度不夠。另外,在團隊中,開發人員的能力各有所長,而頁面依賴降低了整個專案在任務安排上的靈活性,讓我們無法按照團隊成員的經驗,強項來合理安排工作。這兩項對團隊協同度的影響最終會拉低團隊的整體效率。
在元件化開發方案下,強調業務任務和元件任務的分離和協同。元件任務具有很強的獨立性和自治性,即在介面定義清楚的情況下,完全可以拋開上下文進行開發。這類任務對外無任何依賴,再加上元件的職責單一性,其功能也很容易被開發者理解。
所以在安排任務上,元件任務可以非常靈活。而業務任務只需關注自己依賴的元件是否已經完成,一旦完成就馬上進入Ready For Dev狀態,以最高優先順序等待下一位開發人員選取。
在元件化開發方案下,為了提升團隊協同效率,團隊的運作需要:
- 把業務任務和元件任務拆開,元件的歸元件,業務的歸業務。
- 使用Jira,Mingle等團隊管理工具管理好業務任務對元件任務的依賴,讓團隊可以容易地瞭解到每個業務價值的實現需要的完成的任務。
- Tech Lead需要加深對團隊每個成員的瞭解,清楚的知道他們各自的強項,作為安排任務時的參考。
- 業務優先原則,一旦業務任務依賴的所有元件任務完成,業務任務馬上進入最高優先順序,團隊以交付業務價值為最高優先順序。
- 元件任務先於業務任務完成,未納入業務流程前,團隊需要Living Style Guide之類的工具幫助驗收元件任務。
4.以元件的Living Style Guide平臺降低團隊溝通成本
在前端開發時,經常存在這樣的溝通場景:
- 開發人員和UX驗證頁面設計時,因為一些細微的差異對UI進行反覆的小修改。
- 開發人員和業務人員驗證介面流程時,因為一些特別的需求對UI進行反覆的小修改。
- 開發人員想複用另一個元件,尋找該元件的開發人員瞭解該元件的設計和職責
- 開發人員和QA一起驗證某個公用元件改動對多個介面上的影響
當這樣的溝通出現在上一小節的提到的場景,即元件出現在距離啟動介面有多個層級的介面時,按照傳統開發方式,UX和開發需要多次點選,有時甚至需要輸入一些資料,最後才能到達想要的功能介面。沒有或者無法搭建一個直觀的平臺滿足這些需求,就會導致每一次的溝通改動就伴隨著一次重複走的,很長的路徑。使得團隊的溝通成本激增,極大的降低了開發效率。
在元件化開發方案下, 因為元件的獨立性,構建Living Style Guide平臺變得非常簡單,目前社群已經有了很多工具支援構建Living Style Guide平臺(比如getstorybook)。
開發人員把元件以Demo的形式新增到Living Style Guide平臺就行了,然後所有與UI元件的相關的溝通都以該平臺為中心進行,因為開發對元件的修改會馬上體現在平臺上,再加上平臺對元件的組織形式讓所有人都可以很直接的訪問到任何需要的元件,這樣,UX和業務人員有任何要求,開發人員都可以快速修改,共同在平臺上驗證,這種“所見即所得”的溝通方式節省去了大量的溝通成本。
此外,該平臺自帶元件文件功能,團隊成員可以從該平臺上看到所有元件的UI,介面,降低了人員變動導致的元件上下文知識缺失,同時也降低了開發者之間對於元件的溝通需求。
想要獲得這些好處,團隊的運作需要:
- 專案初期就搭建好Living Style Guide平臺。
- 開發人員在完成元件之後必須新增Demo到平臺,甚至根據該元件需要適應的場景,多新增幾個Demo。這樣一眼就可以看出不同場景下,該元件的樣子。
- UX,業務人員通過平臺驗收元件,甚至可以在平臺通過修改元件Props,探索性的測試在一些極端場景下元件的反應。
5. 對需求分析階段的訴求和產品演進階段的幫助
雖然需求分析階段和產品演進階段不是元件化開發關注的重點,但是元件化開發的實施效果卻和這兩個階段有關係,元件化方案需要需求分析階段能夠給出清晰的Domain資料結構,基礎設計元素和介面原型,它們是元件化開發的基礎。而對於產品演進階段,元件化開發提供的兩個重要特性則大大降低了產品演進的風險:
- 低耦合的架構,讓開發者清楚的知道自己的修改影響範圍,降低演進風險。開發團隊只需要根據新需求完成新的元件,或者替換掉已有元件就可以完成產品演進。
- Living Style Guide的自文件能力,讓你能夠很容易的獲得現有元件程式碼的資訊,降低人員流動產生的上下文缺失對產品演進的風險。
三、元件化開發方案在React Native專案中的實施
前面已經詳細討論了為什麼和如何做元件化開發方案,接下來,就以一個React Native專案為例,從程式碼級別看看元件化方案的實施。
1. 定義基礎設計元素
在前面我們已經提到過,需求分析階段需要產出基本的設計元素,在前端開發人員開始寫程式碼之前需要把這部分基礎設計元素新增到程式碼中。在React Native中,所有的CSS屬性都被封裝到了JS程式碼中,所以在React Native專案開發中,不再需要LESS,SCSS之類的動態樣式語言,而且你可以使用JS語言的一切特性來幫助你組合樣式,所以我們可以建立一個theme.js存放所有的基礎設計元素,如果基礎設計元素很多,也可以拆分位多個檔案存放。
1 2 3 4 5 6 7 8 |
import { StyleSheet } from 'react-native'; module.exports = StyleSheet.create({ colors: {...}, fonts: {...}, layouts: {...}, borders: {...}, container: {...}, }); |
然後,在寫具體UI元件的styles,只需要引入該檔案,按照JS的規則複用這些樣式屬性即可。
2.拆分元件樹之Component,Page,Scene
在實現業務流程前,需要對專案的原型UI進行分解和分類,在React Native專案中,我把UI元件分為了四種型別:
- Shared Component: 基礎元件,Button,Label之類的大部分其它元件都會用到的基礎元件
- Feature Component: 業務元件,對應到某個業務流程的子元件,但其不對應路由, 他們通過各種組合形成了Pag元件。
- Page: 與路由對應的Container元件,主要功能就是組合子元件,所有Page元件最好名字都以Page結尾,便於區分。
- Scene: 應用狀態和UI之間的聯結器,嚴格意義上它不算UI元件,主要作用就是把應用的狀態和Page元件繫結上,所有的Scene元件以Scene字尾結尾。
Component和Page元件都是Pure Component,只接收props,然後展示UI,響應事件。Component的Props由Page元件傳遞給它,Page元件的Props則是由Scene元件繫結過去。下面我們就以如下的這個頁面為例來看看這幾類元件各自的職責範圍:
(1)searchResultRowItem.js
1 2 3 4 5 6 7 8 9 10 |
export default function (rowData) { const {title, price_formatted, img_url, rowID, onPress} = rowData; const price = price_formatted.split(' ')[0]; return ( onPress(rowID)} testID={'property-' + rowID} underlayColor='#dddddd'> {price}{title} );} |
(2)SearchResultsPage.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import SearchResultRowItem from '../components/searchResultRowItem'; export default class SearchResultsPage extends Component { constructor(props) { super(props); const dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1.guid !== r2.guid}); this.state = { dataSource: dataSource.cloneWithRows(this.props.properties), onRowPress: this.props.rowPressed, }; } renderRow(rowProps, sectionID, rowID) { return ; } render() { return ( ); }} |
(3)SearchResultsScene.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import SearchResults from '../components/searchResultsPage'; function mapStateToProps(state) { const {propertyReducer} = state; const {searchReducer:{properties}} = propertyReducer; return { properties, }; } function mapDispatchToProps(dispatch) { return { rowPressed: (propertyIndex) => { dispatch(PropertyActions.selectProperty(propertyIndex)); RouterActions.PropertyDetails(); } }; } module.exports = connect( mapStateToProps, mapDispatchToProps,)(SearchResults); |
3.Living Style Guide
目前社群上,最好的支援React Native的Living Style Guide工具是getstorybook,關於如何使用getstorybook搭建React Native的Living Style Guide平臺可以參見官方文件或者我的部落格。
搭建好Living Style Guide平臺後,就可以看到如下的介面:
接下來的工作就是不斷在往該平臺新增UI元件的Demo。向storybook中新增Demo非常簡單,下面就是一個關於SearchPage的Demo:
1 2 3 4 5 6 7 |
import React from 'react'; import {storiesOf, action} from '@kadira/react-native-storybook'; import SearchPage from '../../../../src/property/components/searchPage'; storiesOf('Property', module) .add('SearchPage', () => ( )); |
從上面的程式碼可以看出,只需要簡單的三步就可以完成一個UI元件的Demo:
- import要做Demo的UI元件。
- storiesOf定義了一個元件目錄。
- add新增Demo。
在構建專案的storybook時,一些可以幫助我們更有效的開發Demo小Tips:
- 儘可能的把目錄結構與原始碼結構保持一致。
- 一個UI元件對應一個Demo檔案,保持Demo程式碼的獨立性和靈活性,可以為一個元件新增多個Demo,這樣一眼就可以看到多個場景下的Demo狀態。
- Demo命名以UI元件名加上Demo綴。
- 在元件引數複雜的場景下,可以單獨提供一個fakeData的目錄用於存放重用的UI元件Props資料。
4.一個完整的業務開發流程
在完成了上面三個步驟後,一個完整的React Native業務開發流程可簡單分為如下幾步:
- 使用基礎設計元素構建基礎元件,通過Living Style Guide驗收。
- 使用基礎元件組合業務元件,通過Living Style Guide驗收。
- 使用業務元件組合Page元件,通過Living Style Guide驗收。
- 使用Scene把Page元件的和應用的狀態關聯起來。
- 使用Router把多個Scene串聯起來,完成業務流程。
四、總結
隨著前後端分離架構成為主流,越來越多的業務邏輯被推向前端,再加上使用者對於體驗的更高要求,前端的複雜性在一步一步的拔高。對前端複雜性的管理就顯得越來越重要了。經過前端的各種框架,工具的推動,在前端工程化實踐方面我們已經邁進了很多。而元件化開發就是筆者覺得其中比較好的一個方向,因為它不僅關注了當前的專案交付,還指導了團隊的運作,幫助了後期的演進,甚至在程式設計師最討厭的寫文件的方面也給出了一個巧妙的解法。希望對該方法感興趣的同學一起研究,改進。