React 組件設(shè)計(jì)指南
本文已獲得原作者的獨(dú)家授權(quán),有想轉(zhuǎn)載的朋友們可以在后臺(tái)聯(lián)系我申請(qǐng)開(kāi)白哦! PS:歡迎掘友們向我投稿哦,被采用的文章還可以送你掘金精美周邊!
前言
在我過(guò)往的經(jīng)歷里, 在面試與被面之間通常都會(huì)夾雜一些關(guān)于組件設(shè)計(jì)方面的問(wèn)題, 但通常面試官和候選人都只能通過(guò)一些實(shí)際的項(xiàng)目經(jīng)歷來(lái)就設(shè)計(jì)進(jìn)行討論, 相比服務(wù)端面試中可能還涉及一些設(shè)計(jì)原則和基本思路, 但是在前端的面試過(guò)程中, 設(shè)計(jì)似乎成了一種經(jīng)驗(yàn).
但設(shè)計(jì)真的只是一種經(jīng)驗(yàn)么?
顯然不是, 因?yàn)榻?jīng)驗(yàn)是對(duì)過(guò)去問(wèn)題的總結(jié), 并且經(jīng)驗(yàn)是沒(méi)有標(biāo)準(zhǔn)沒(méi)有約束的, 每個(gè)人經(jīng)歷的項(xiàng)目, 團(tuán)隊(duì), 業(yè)務(wù)形成了每個(gè)人屬于自己的獨(dú)特的研發(fā)經(jīng)驗(yàn). 而設(shè)計(jì)其實(shí)就是在這些經(jīng)驗(yàn)上不斷提煉加工總結(jié)出來(lái)了的研發(fā)標(biāo)準(zhǔn).
而前端組件研發(fā)在設(shè)計(jì)方面就目前來(lái)看是毫無(wú)標(biāo)準(zhǔn)可言的, 這種現(xiàn)狀導(dǎo)致前端組件研發(fā)成了一項(xiàng)經(jīng)驗(yàn)性技能, 是否能開(kāi)發(fā)出高度可擴(kuò)展, 結(jié)構(gòu)清晰易于使用的前端組件不是通過(guò)科學(xué)合理的設(shè)計(jì), 而是基于開(kāi)發(fā)者過(guò)往的經(jīng)驗(yàn). 這導(dǎo)致有經(jīng)驗(yàn)的前端組件庫(kù)維護(hù)者成了一種稀缺資源.
基于這樣的現(xiàn)狀, 我想試著從 React 組件設(shè)計(jì)的角度去展示過(guò)去幾年我從各種組件研發(fā)經(jīng)驗(yàn)中提煉出來(lái)的一些東西, 或許能給各位帶來(lái)一些靈感.
正文
前端組件發(fā)展簡(jiǎn)史
要聊前端組件設(shè)計(jì), 必然離不開(kāi)對(duì)前端組件發(fā)展歷史的探究, 關(guān)于這塊我不會(huì)長(zhǎng)篇大論, 畢竟這不是本文的核心內(nèi)容.
以我的經(jīng)驗(yàn)來(lái)看, 前端組件大致經(jīng)歷了這樣幾個(gè)階段.
早年門戶網(wǎng)站下, 以原生 JavaScript 為主的腳本化組件
我記得 8 年前我剛開(kāi)始寫前端的時(shí)候, 做的第一個(gè)組件就是一個(gè)懸浮的廣告組件, 如果你工作年限夠長(zhǎng), 應(yīng)該對(duì) 2010 年左右流行的大門戶網(wǎng)站的左右懸浮廣告頗有印象.
此時(shí)的前端組件通常就是一段的 JavaScript 腳本, 利用錨定特定的 DOM Id, 通過(guò) IIFE, 來(lái)構(gòu)建一個(gè)相對(duì)獨(dú)立的運(yùn)行環(huán)境.
這時(shí)候的前端組件符合當(dāng)時(shí)主流開(kāi)發(fā)對(duì) JavaScript 的印象 - 玩具語(yǔ)言.
2013 年其實(shí)是此類組件的末期, 因?yàn)?jQuery 崛起了.
隨著 jQuery 的崛起, 大幅提升了 DOM 操作的便捷性, 同時(shí) jQuery 內(nèi)生的插件機(jī)制將前端組件引入了插件化時(shí)代.
jQuery 帶來(lái)插件化組件
jQuery 插件能夠大幅提升當(dāng)時(shí)前端研發(fā)的效率, 彼時(shí) AJAX 雖然較為流行, 但前端的角色還是更貼視覺(jué)交互這一層, 類似 bootstrap 將常用的 jQuery 插件進(jìn)行整合, 提煉了在我看來(lái)是當(dāng)時(shí)第一代通用前端組件的雛形.
包含了最常見(jiàn)的輪播圖, 導(dǎo)航菜單, Tips 等.
那個(gè)年代, 是否能手寫輪播圖是考驗(yàn)一個(gè)前端工程師的黃金標(biāo)準(zhǔn)
一代神作 Angular 1.0
15 年以后加入這個(gè)行業(yè)的年輕人可能很難理解當(dāng)時(shí)我們對(duì) Angular 的那種癡迷. 可以說(shuō) Angular 很大程度上奠定了目前前端研發(fā)中的一些核心要素.
包括
模塊化 基于前端的單頁(yè)路由 ViewModel 數(shù)據(jù)驅(qū)動(dòng)
當(dāng)然最重要的一點(diǎn), Angular 帶來(lái)了有別于 jQuery 的前端組件研發(fā)思路, 通過(guò)內(nèi)置的模塊系統(tǒng)將前端組件拆解成了幾種不同的類型
數(shù)據(jù)型組件 操作 DOM 的指令 帶有路由的組件
可惜好景不長(zhǎng), Angular 大包大攬的路線最終沒(méi)能適應(yīng)時(shí)代發(fā)展的需求. 隨著 React 和 Vue 的快速崛起, 一家獨(dú)大變成了三足鼎立
虛擬 DOM 和 JSX
React 的走紅給前端組件研發(fā)帶來(lái)了兩個(gè)重量級(jí)的概念, 虛擬 DOM 和 JSX, 這兩個(gè)特性分別解決了在此之前前端組件研發(fā)的兩個(gè)問(wèn)題
手動(dòng)操作 DOM 的低效和不易維護(hù) Angular 帶來(lái)的前端組件設(shè)計(jì)上的分裂.
基于 React, 無(wú)論是數(shù)據(jù)型還是交互型, 在前端組件設(shè)計(jì)上都統(tǒng)一了. 都是標(biāo)準(zhǔn)的 React 組件.
Google 主導(dǎo)的 webcompoent 雖然紅極一時(shí), 但從現(xiàn)在的情況來(lái)開(kāi)有點(diǎn)無(wú)疾而終的意思. 不過(guò)我依然傾向于認(rèn)為前端組件的 web 化是未來(lái)的趨勢(shì), 至于是不是 webcompoent, 可以拭目以待
上述簡(jiǎn)史不包含各種有趣的細(xì)節(jié)和歷史的分支, 我只是大概羅列了下幾個(gè)相對(duì)重要的節(jié)點(diǎn), 如果你對(duì)這段歷史有興趣可以留言, 后續(xù)我會(huì)單獨(dú)開(kāi)一篇詳細(xì)講講
當(dāng)下 React 組件設(shè)計(jì)上存在的問(wèn)題
雖然 React 通過(guò)框架級(jí)的設(shè)計(jì)將前端組件統(tǒng)一成 React 給定的模型, 但在實(shí)際研發(fā)的過(guò)程中, 大統(tǒng)一的方式并不能解決組件類型的問(wèn)題, 并且隨著前端研發(fā)日趨復(fù)雜, 這一塊就我看來(lái)幾經(jīng)進(jìn)入了混沌領(lǐng)域.
讓我們換個(gè)角度來(lái)看 React 組件設(shè)計(jì), 比如提個(gè)問(wèn)題, 現(xiàn)在的 React 組件有設(shè)計(jì)可言么?
當(dāng)我們用 React 進(jìn)行開(kāi)發(fā)的時(shí)候, 我們?nèi)绾味x組件?
我想答案是千差萬(wàn)別的, 因?yàn)檫@成了一種經(jīng)驗(yàn).
通過(guò)不同的維度, 我們會(huì)發(fā)現(xiàn)現(xiàn)有的 React 組件設(shè)計(jì)中存在大量的不確定性
基于通信的角度, 我們將組件定義為父子組件, 一個(gè)組件既可以是父組件也可以是子組件. 所以父組件和子組件的標(biāo)準(zhǔn)是什么? 如果是平行通信又該叫什么? 兄弟組件? 那深層嵌套下, 難道叫爺孫, 爺爺爺爺孫孫孫孫組件么?
另外組件通信在實(shí)現(xiàn)上也缺乏標(biāo)準(zhǔn), 基于事件管道? 廣播, 單播, 基于路由? 還是通過(guò) context / props? 或者掛在 classComponent 的 this 上? 引入 redux mobx 或者其他的狀態(tài)管理庫(kù)? 用 hook 不用 hook 基于 React 框架的角度又可以定義各種不同類型的組件, 比如 classComponent, functionComponent, HOC, 受控和非受控組件, 自定義 hook 算組件么? 狀態(tài)什么時(shí)候用 useState, 什么時(shí)候用 useReducer, useContext 的使用標(biāo)準(zhǔn)是什么?
基于業(yè)務(wù)角度的定義, 用戶中心, 賬戶組件, 通知, 和公司特定業(yè)務(wù)掛鉤的組件
基于視覺(jué)角度的定義, 表格, 表單, 導(dǎo)航, 對(duì)話框
這些缺乏設(shè)計(jì)標(biāo)準(zhǔn)的不確定性給 React 組件設(shè)計(jì)帶來(lái)了非常大的困難, 可以說(shuō)正是因?yàn)檫@些不確定性, React 組件, 或者說(shuō)前端組件壓根就沒(méi)有設(shè)計(jì)可言.
類似 AntD 這樣的組件庫(kù)更多是從視覺(jué)和用戶體驗(yàn)的角度出來(lái)來(lái)定義前端組件, 但是正如我上面提到的, 前端組件包含的角度非常多, 單純通過(guò)一個(gè)角度去定義組件, 組件的擴(kuò)展性就會(huì)收到很大的影響, 就拿 AntD 的表單組件來(lái)看, 實(shí)際使用中要擴(kuò)展成符合自己公司業(yè)務(wù)的就很難, 你只有二次開(kāi)發(fā)這一條路可選.
如果用 React 和 Vue 做個(gè)對(duì)比, 我覺(jué)得從這個(gè)角度看, React 和 Vue 并不是同一種東西, React 是一個(gè)并不關(guān)心真實(shí)研發(fā)的 UI 庫(kù), 它的重點(diǎn)在于如何更高效的實(shí)現(xiàn) UI 渲染, 并在這種高效渲染中能夠完成和外部狀態(tài)的連接
可以看成 React 是一個(gè)渲染函數(shù)的管理引擎, 從設(shè)計(jì)上, 他們一直致力于提升渲染函數(shù)的執(zhí)行效率和性能, 同時(shí)讓注入?yún)?shù)不會(huì)對(duì)這種效率和性能產(chǎn)生影響, 這也是為什么會(huì)有 hook. 因?yàn)?classComponent 在性能和效率上有明顯的瓶頸
但是 Vue 不同, Vue 更像 Angular, 是一種包含了前端研發(fā)各方面訴求的研發(fā)框架, 雖然有些雜.
因此我認(rèn)為兩者并不等價(jià), 也不具備可比性.
扯遠(yuǎn)了, 讓我們回到本文的主題 React 組件設(shè)計(jì)
讓我們?cè)囍?React 組件設(shè)計(jì)中的不確定性.
假設(shè)我們基于以上的不確定性, 討論 React 組件的設(shè)計(jì)標(biāo)準(zhǔn), 那這個(gè)設(shè)計(jì)標(biāo)準(zhǔn)應(yīng)該具備
可實(shí)施, 可以轉(zhuǎn)化為某種框架 多角度, 不從單一角度去定義前端組件 更高的抽象, 在多個(gè)角度之上統(tǒng)一進(jìn)行組件抽象.
結(jié)合上述的不確定性, 我們可以將 React 組件抽象出一些特性
視覺(jué)性 交互性 數(shù)據(jù)性
考慮到 React 是狀態(tài)驅(qū)動(dòng)的, 其核心就是對(duì) State 的管控. 因此圍繞 React 組件的設(shè)計(jì)可以進(jìn)一步將特性擴(kuò)展為
特性性
狀態(tài) 操作函數(shù) 視覺(jué)性狀態(tài)和視覺(jué)性操作函數(shù)
交互性狀態(tài)和交互性操作函數(shù)
數(shù)據(jù)性狀態(tài)和數(shù)據(jù)性操作函數(shù)
在此基礎(chǔ)上, 根據(jù)不同特性處理的問(wèn)題我們可以進(jìn)一步給出定義
視覺(jué)性狀態(tài)
直接用于渲染的文本/數(shù)字... 樣式, 用于增強(qiáng)視覺(jué) 動(dòng)效, 在樣式的基礎(chǔ)上加入繪制過(guò)程 視覺(jué)性狀態(tài)操作函數(shù), 將輸入狀態(tài)轉(zhuǎn)換為視覺(jué)性狀態(tài)的函數(shù), 例如 transform
交互性狀態(tài)
描述交互動(dòng)作的標(biāo)志, 例如 open, close... 描述頁(yè)面間的狀態(tài), 例如 url 上的 query 組件運(yùn)行環(huán)境變化帶來(lái)的狀態(tài), 例如瀏覽器下的 onLoading 交互性狀態(tài)操作函數(shù), 觸發(fā)用戶和組件交互的行為控制函數(shù), 例如 controller
數(shù)據(jù)性狀態(tài)
來(lái)自外部輸入的數(shù)據(jù), 比如接口 本地緩存的數(shù)據(jù), localStorage, 磁盤, 文件 數(shù)據(jù)性狀態(tài)操作函數(shù), 用于和外部數(shù)據(jù)進(jìn)行互通的控制函數(shù), 例如調(diào)用 api 的 service
根據(jù)這些標(biāo)準(zhǔn), 在 React 組件設(shè)計(jì)中可以酌情去判斷一些設(shè)計(jì)的合理性.
例如
狀態(tài)的定義是否不符合標(biāo)準(zhǔn), 常見(jiàn)的將服務(wù)端傳回來(lái)的數(shù)據(jù)直接渲染在界面上. 對(duì)于組件來(lái)說(shuō), 這回導(dǎo)致視覺(jué)性和數(shù)據(jù)性混淆, 當(dāng)接口變更時(shí)產(chǎn)生連帶的影響. 也不利于組件的可復(fù)用性.
在 onClick 又寫交互又寫樣式控制, 將視覺(jué)性和交互性混淆, 這種往往會(huì)導(dǎo)致一些性能問(wèn)題, 尤其是在編寫一些動(dòng)畫效果的時(shí)候, 同時(shí)影響代碼的復(fù)用性.
除了這些還有 交互性和數(shù)據(jù)性混淆, 數(shù)據(jù)性操作函數(shù)直接操作視覺(jué)性和交互性狀態(tài)等等.
事實(shí)上在實(shí)際操作中, 即使明確了標(biāo)準(zhǔn)也很難嚴(yán)格的執(zhí)行, 這和開(kāi)發(fā)團(tuán)隊(duì)本身的能力有很大關(guān)系, 但就像建筑行業(yè), 有高水平的施工團(tuán)隊(duì), 牛逼的設(shè)計(jì)事務(wù)所, 也有門口擺攤的包工頭, 全憑經(jīng)驗(yàn)的小施工隊(duì), 一個(gè)成熟的行業(yè)是有其包容性的, 但不能全是小施工隊(duì).
要將這些標(biāo)準(zhǔn)實(shí)施, 將設(shè)計(jì)融入到日常的組件開(kāi)發(fā)里, 僅僅靠約定是遠(yuǎn)遠(yuǎn)不夠的, 所以我提到了標(biāo)準(zhǔn)要具備可實(shí)施可轉(zhuǎn)換為框架的可能性.
我們團(tuán)隊(duì)目前就在嘗試研發(fā)此類框架, 之前在幾篇文章中也有提到, 有興趣的可以看看.
git 地址: https://github.com/kinop112365362/structured-react-hook
事實(shí)上對(duì)于這塊內(nèi)容我們一直在實(shí)踐和調(diào)整. 目前來(lái)看核心目標(biāo)是希望能在 React 組件研發(fā)上形成可實(shí)施的設(shè)計(jì)標(biāo)準(zhǔn), 同時(shí)提供研發(fā)配套.
至于更長(zhǎng)遠(yuǎn)的目標(biāo), 應(yīng)該是推動(dòng)前端組件的 web 化吧.
后話
一個(gè)沒(méi)有標(biāo)準(zhǔn)的行業(yè)是不成熟的, 一個(gè)憑經(jīng)驗(yàn)辦事的行業(yè)是低效的, 前端面對(duì)的問(wèn)題, 促使前端天然就對(duì)組件化有很強(qiáng)的訴求, 前端研發(fā)的過(guò)程也是組件研發(fā), 裝配, 調(diào)試, 運(yùn)行的過(guò)程, 前端組件不應(yīng)該僅僅是幾個(gè)組件庫(kù)那么簡(jiǎn)單, 而是應(yīng)該結(jié)合科學(xué)合理的設(shè)計(jì)標(biāo)準(zhǔn), 在日常的研發(fā)工作中也可以被很好的應(yīng)用. 只有融入設(shè)計(jì), 前端"工程師" 才名副其實(shí), 雖然這條路看起來(lái)還很長(zhǎng).
愛(ài)心三連擊
1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的在看是我創(chuàng)作的動(dòng)力。
2.關(guān)注公眾號(hào)腦洞前端,獲取更多前端硬核文章!加個(gè)星標(biāo),不錯(cuò)過(guò)每一條成長(zhǎng)的機(jī)會(huì)。
3.如果你覺(jué)得本文的內(nèi)容對(duì)你有幫助,就幫我轉(zhuǎn)發(fā)一下吧。
