前端元件化思想
1.開篇
先說說為什麼要寫這篇文章吧:不知從什麼時候開始,大家相信前端摩爾定律:“每18個月,前端難度會增加一倍”。我並不完全認可這個數字的可靠性,但是這句話的本意我還是非常肯定的。
是的,前端越來越簡單了,但也越來越複雜了—簡單到你可以用一個Github
的starter
搭建一個框架,整合所有的全家桶,涵蓋單元測試和功能測試,包括部署以及釋出,甚至你開發時使用的UI庫都讓你寫不了幾行css
;可又複雜到如此多的框架和庫層出不窮,你還沒來得及學會官網的doc呢,就已經有新的替代品了,那就更別提靜下心去學習其中的原始碼或推敲原理了,跟不上腳步強行搬磚自然略顯疲憊。
正是前端飛速的發展使得前端看似簡單,但若想深入卻實屬不易。順便提一句,去年6月底,ES8
ES5
->ES6
這種跨度了)。
所以,我近期覺得使用的框架有些多了,得靜下心來沉澱沉澱—為什麼要說寫元件化思想呢?因為我覺得它是伴隨著前端發展的一個不可或缺的設計思想,目前幾大流行框架也都非常好的實現了元件化,比如React
,Vue
。React
之前用得算是比較多了,所以本篇我決定以Vue
作為基礎,去談一談前端模組化,元件化,可維護化的設計細想。
2.什麼是元件化
元件化並不是前端所特有的,一些其他的語言或者桌面程式等,都具有元件化的先例。確切的說,只要有UI層的展示,就必定有可以元件化的地方。簡單來說,元件就是將一段UI樣式和其對應的功能作為獨立的整體去看待,無論這個整體放在哪裡去使用,它都具有一樣的功能和樣式,從而實現複用,這種整體化的細想就是元件化。不難看出,元件化設計就是為了增加複用性,靈活性,提高系統設計,從而提高開發效率。
3.元件化的演變
如果你對JS的理解還停留在jQuery
的話(jQuery
本身是一個非常優秀的庫),那麼請跳過此文(開個玩笑)。在那個時候,大部分的前端開發應該都是十分過程式的開發:操作DOM
,發起ajax
請求,重新整理資料,區域性更新頁面。這樣的動作反反覆覆,甚至在同一個專案裡同樣的流程也許還要重複,其實jQuery
本身也有有自己模組化的設計,有時我們也會用到類似jQuery UI
等不錯的庫來減少工作量,但請注意,這裡我只認為它是模組化的。
頻繁操作DOM,過程式的開發方式的確不怎麼樣。這時開始流行MV*
,比如MVC
,前端開始學習後端的思想,講業務邏輯,UI,功能,可以按照不同的檔案去劃分,結構清晰,設計明瞭,開發起來也不錯。在這個基礎上,又有了更加不錯的MVVM
ViewModel
,即你看到的view
就是真實的資料,並且實現了雙向繫結,只要UI改變,UI所對應的資料也改變,反之亦然。這的確很方便,但大部分的MVVM
框架,並沒有實現元件化,或者說沒有很好的實現元件化,因為MVVM
最大的問題就是:
1.執行效率,只要資料改變,它下面所有監測資料上繫結的UI一般都會去更新,效率很低,如果你操作頻繁,很可能調了幾十萬遍(有可能層次太深或者監測了太多的資料變化)。
2.由於
MVVM
一般需要嚴格的ViewModel
的作用域,因此大部分情況不支援多次繫結,或者只允許繫結一個根節點做為頂層DOM渲染,這就給元件化帶來了困難(不能獨立的去繫結部分UI)。
而後,在此基礎上,一些新的前端框架“取其精華,去其糟粕”,開始大力推廣前端元件化的開發方式,從這一點來說,React
和Vue
是類似的。
但從框架本身來說,React
和Vue
是完全不同的,前者是單向資料流管理設計的先驅,如果非讓我做一個不恰當的比較的話,我覺得React+Redux
是將MVC
做到了極致(action->request, reducer->controller
);而後者則是後起之秀,既吸取了React
的資料流管理方式(Vue
本身也可以用類似React
的方式去開發,但難度比較大而已,不是很Vue
)的設計理念,也實現了MVVM
的雙向繫結和資料監控(這應該是Vue
的核心了),所以Vue
是比較靈活的,可以按需擴充套件,它才敢稱自己是漸進式框架。
PS1: 並非討論孰好孰壞,兩大框架我都很喜歡,因為都非常好的實現了元件化。
PS2: 上面有提到模組化,個人覺得如果更廣義的來講,模組化和元件化並不在一個維度上,模組化往往是程式碼的設計和專案結構的設計;但很多時候在狹義的場景中,比如一個很通用的功能,也完全能夠將其元件化或模組化,這兩者此時十分相似,最大的區別就是元件必定是模組化的,並且往往需要例項化,也應當賦有生命週期,而模組化往往是直接引用。
4.如何實現元件化
我就以搜房網為例(最近房價居高不下,各個大佬還在吹各種牛x說房價不久後將白菜價,我順便mark下看以後打誰的臉)進行demo分析。隨手截圖如下:
4.1分析頁面佈局
從大體上來看,可以分為頂部搜尋,中間內容展示。而中間內容又分為part1,2,3三種類型。由於篇幅問題,本文只分析part1,2,3
每一個part中又可以分為header(title + link)和content(每個part不一樣)
4.2初步開發
如果沒有經過任何設計,也許會出現下面的程式碼:
<template>
<div id="app">
<div class="nav-search">...</div>
<div class="panel">
<div class="part1 left">
<div>
<span>萬科城潤園樓盤動態</span>
<a href="">更多動態>></a>
</div>
<div>這裡是每個part裡面的具體內容</div>
</div>
<div class="part2 right">
<div>
<span>樓盤故事</span>
<a href="">更多>></a>
</div>
<div>這裡是每個part裡面的具體內容</div>
</div>
<div class="part3">
<div>
<span>萬科城潤園戶型</span>
<a href="">二居(1)</a>
<a href="">三居(4)</a>
<a href="">四居(3)</a>
<a href="">更多>></a>
</div>
<div>這裡是每個part裡面的具體內容</div>
</div>
</div>
</div>
</template>
其中我省略了大部分的細節實現,實際程式碼量應該是這裡的數倍。
這段程式碼有幾個問題:
1.part1,2,3的結構很類似,有些許重複
2.實際的程式碼量將會很多,很難快速定位問題,維護難度較大
4.3化繁為簡
首先我們可以將part1,2,3進行分離,這樣就獨立出來三個檔案,那麼結構上將會非常清晰
<template>
<div id="app">
<div class="nav-search">...</div>
<div class="panel">
<part1 />
<part2 />
<part3 />
</div>
</template>
這有些類似將一個大函式逐步拆解成幾部分的過程,不難想象part1,2,3中的程式碼,必然是適用性很差,確切的說只有這裡能夠引用。(但我看過很多專案的程式碼,就是這麼幹的,認為自己做了元件化,抽象還不錯(@[email protected])
)
4.4元件抽象
仔細觀察part1,2,3,正如我上面所說,它們其實是很相似的:都具有相同的外層border並附有shadow,都具有擡頭和顯示更多,各自內容部分暫不細說的話,這三個完全就是一模一樣。
如此,我們將具有高度相似的業務資料進行抽離,實現元件的抽象。
part.vue
<template>
<div class="part">
<div class="hearder">
<span>{{ title }}</span>
<a :href="linkForMore">{{ showMore || '更多>>' }}</a>
</div>
<slot name="content" />
</div>
</template>
我們將part內可以抽象的資料都做成了props,包括利用slot去做模版,同時showMore || '更多>>'
也考慮到了part1的link名字和其他幾個part不一致的情況。
這樣一來app.vue就更加清晰化
<template>
<div id="app">
<div class="nav-search">...</div>
<div class="panel">
<part
title="萬科城潤園樓盤動態"
linkForMore="#1"
showMore="更多動態>>"
>
<div slot="content">這裡是part1裡面的具體內容</div>
</part>
<part
title="樓盤故事"
linkForMore="#2"
>
<div slot="content">這裡是part2裡面的具體內容</div>
</part>
<part
title="萬科城潤園戶型"
linkForMore="#3"
>
<div slot="content">這裡是part3裡面的具體內容</div>
</part>
</div>
</template>
這裡有幾點需要說明一下:
- 1.三個part中部分UI差異應該在哪裡定義?
比如三個part的寬度都不一樣,並且part1和part2可能要需要進行浮動。
必須要記住,這種差異並不是元件本身的,<part />
的設計本身應該是無浮動並且寬度佔100%的,至於佔誰的100%,那就取決於誰引用它,至於向左還是向右浮動,同樣也取決於引用它的container需要自己去定義,在上面的程式碼中,app.vue
就應該是<part />
的container,app想要的是一個左浮動且寬度為80%的part(part1),右浮動且寬度為20%的part(part2)和一個寬度為100%的part(part3),但它們都是part,所以應該由app來設定這些差異。
記住這一點,將給你的抽象和擴充套件但來事半功倍的效果。
- 2.三個part中的資料差異應該在哪裡定義?
比如part3中,其他的part只有一個類似更多>>
的link,但是它卻有多個(一居,二居...
)。
這裡我推薦將這種差異體現在元件內部,設計方法也很多:
比如可以將link陣列化為links;
比如可以將更多>>
看作是一個default的link,而多餘的部分則是使用者自定義的特殊link,這兩者合併組成了links。使用者自定義的預設是沒有的,需要引用元件時進行傳入。
總之,只要有資料差異化,就應該結合元件本身和業務上下文將差異合理的消除在內部。
- 3.注意元件內資料的命名方式
一個通用的,可擴充套件性高的元件,必然是有非常合理的命名的,比如觀察一些元件庫的命名,總會出現類似list,data,content,name,key,callback,className
等名詞,絕對不會出現我們系統中的類似iterationList, projectName
等業務名詞,這些名詞和任一產品和應用都無關,它與自身抽象的元件有關,只表明元件內部的資料含義,偶爾也會代表其結構,所以只有這樣,才能讓使用者通用。
我們在元件化時,也需要遵循這種設計原則,但庫往往是想讓廣大開發者通用,而我們可以降低scope,做到在整個app內通用即可。所以從這個角度來說,好的元件化必然有好的BA和UX,這是大實話
5.寫在最後
你也許會認為這樣抽象沒有太大的必要性,畢竟它只是一段靜態UI(pure component
),但任何的設計都是基於一定的複雜度才衍生出來的,其實大部分情況下這種設計都是需要將功能邏輯程式碼也納入其中的,並不光只是UI(如antd, element-ui等),我這裡舉的例子也相對比較簡單,並不想有太多的程式碼。
個人認為在一個大型前端專案中,這種元件化的抽象設計是很重要的,不僅增加了複用性提高了工作效率,從某種程度上來說也反應了程式設計師對業務和產品設計的理解,一旦有問題或者需要功能擴充套件時,你就會發現之前的設計是多麼的make sense
(畢竟需求總是在變哪)。