Slack使用React重寫了Web客戶端。在這篇文章中,他們以重寫Emoji選擇器為例,展示了React在性能和代碼可維護(hù)性上給他們帶來的巨大好處,以及給用戶帶來的體驗(yàn)升級(jí)。查看英文原文: Rebuilding Slack’s Emoji Picker in React。
Slack正在將Web客戶端遷移到React。在最開始,我們的前端使用了jQuery和Handlebars。后來,社區(qū)開發(fā)出更好的方案用于創(chuàng)建可伸縮的、基于數(shù)據(jù)驅(qū)動(dòng)的用戶界面。jQuery的“渲染后修改”模式直截了當(dāng),但無法與底層的模型保持同步。不同的是,React的“渲染后再渲染”模式可以保證渲染和模型的一致性。Slack也緊跟業(yè)界的步伐,不斷改進(jìn)前端的性能和可靠性。
我們認(rèn)為,要引入React,最好的辦法就是先用React重寫現(xiàn)有產(chǎn)品里的某個(gè)特性——這樣我們就可以比較出新的開發(fā)流程和結(jié)果與原先的有什么不同。我們需要重寫一個(gè)組件,這個(gè)組件必須具備交互性,能夠自包含,并能夠體現(xiàn)React在性能方面的優(yōu)勢。我們很快就找到了一個(gè)絕佳的組件——被重度使用且極度復(fù)雜的Emoji選擇器。
Virtual DOM的優(yōu)勢
閱讀這篇文章要求對React有一定的了解,如果你不熟悉React,建議先閱讀一下React的官方文檔。簡單地說,React是一個(gè)JavaScript代碼庫,可以用它方便地開發(fā)聲明式的、基于數(shù)據(jù)驅(qū)動(dòng)的用戶界面。它的API很簡單,主要由一個(gè)組件類組成,這個(gè)類包含了一些生命周期方法。組件本身不會(huì)生成HTML,相反,它們會(huì)生成類似DOM的樹,叫作Virtual DOM。React會(huì)比較兩個(gè)Virtual DOM,并使用最少的操作將其中的一棵樹轉(zhuǎn)換成另一棵樹。例如,你可以告訴React基于新的模型數(shù)據(jù)重新渲染整個(gè)視圖,它就會(huì)以最快的速度幫你更新文本節(jié)點(diǎn),就好像它有一個(gè)精靈軍團(tuán)在幫你完成DOM的更新操作一樣。
React擅長于將組件在單個(gè)模板上的各種行為合并在一起。舉個(gè)例子,假設(shè)一個(gè)Slack頻道變?yōu)槲醋x狀態(tài)時(shí),你通過JavaScript來更新頻道的邊欄:
找出這個(gè)頻道的ID在DOM里查找這個(gè)頻道對應(yīng)的節(jié)點(diǎn)將節(jié)點(diǎn)狀態(tài)切換成未讀(應(yīng)用CSS類)這個(gè)過程很簡單,不過你還得為其他事件編寫不同的處理邏輯,比如“create”、“join”、“leave”和“rename”。相反,React把這5中情況合并在一起統(tǒng)一處理:
使用新的模型數(shù)據(jù)重新渲染頻道邊欄。我們不需要為每一種DOM操作編寫代碼,而是重新渲染整個(gè)組件,React會(huì)為我們完成這個(gè)過程。React通過讓代碼變得更通用(一刀切的模板)來簡化開發(fā)。
Emoji選擇器
Emoji是Slack UI的一個(gè)組成部分,是最理想的React組件。它動(dòng)態(tài)、離散,只需要少量的輸入——一組emoji、默認(rèn)皮膚和用戶的emoji使用歷史。剛好現(xiàn)有的Emoji選擇器需要進(jìn)行性能調(diào)優(yōu),因?yàn)楝F(xiàn)在不管emoji會(huì)不會(huì)出現(xiàn)在視圖里都需要進(jìn)行渲染。在查找emoji時(shí)需要切換每個(gè)emoji的可見性,在重度使用時(shí)性能很成問題。新的Slack團(tuán)隊(duì)準(zhǔn)備了1374個(gè)默認(rèn)emoji,這還不包括自定義emoji(在寫這篇文章的時(shí)候,Slack團(tuán)隊(duì)總共有3126個(gè)emoji,有些團(tuán)隊(duì)甚至更多)。重寫Emoji選擇器將會(huì)對Slack的日常使用產(chǎn)生重大影響。
我們選擇在Storybook里開發(fā)新的組件,Storybook自稱是一個(gè)“會(huì)讓你喜歡上它的UI開發(fā)環(huán)境”。它不要求你改變開發(fā)方式,但會(huì)讓開發(fā)、測試和代碼審查變得更有趣。你可以在Storybook里通過指定不同的屬性來定義不同版本的組件。我們?yōu)镋moji選擇器增加了一個(gè)新皮膚和幾種emoji查找方式。
組件布局
React Emoji選擇器的根組件是有狀態(tài)的,而子組件則是無狀態(tài)的。我們按照慣例把每個(gè)組件導(dǎo)出到單獨(dú)的文件里。結(jié)構(gòu)如下所示:
Header
分類選項(xiàng)卡:列出了emoji的類別,每個(gè)類別都有一個(gè)“jump to”鏈接。搜索框:通過emoji的名稱或別名過濾emoji。Body
固定的頭部:顯示當(dāng)前類別選項(xiàng)卡的名稱。emoji列表:所有類別的emoji虛擬列表。Footer
emoji預(yù)覽:當(dāng)前選擇的emoji大圖預(yù)覽。皮膚選擇器:顯示當(dāng)前的皮膚,并可以切換到其他皮膚。快捷動(dòng)作(可選的):emoji的子集,用于快速回復(fù)消息。React為編寫無狀態(tài)組件提供了兩種方式:PureComponent類和function。function更為簡單一些,不過它們在每次合并時(shí)都會(huì)進(jìn)行渲染,會(huì)影響性能。React團(tuán)隊(duì)計(jì)劃對function進(jìn)行優(yōu)化,不過目前最好還是避免使用它們。于是我們選擇了PureComponent,它預(yù)定以了shouldComponentUpdate方法,這個(gè)方法可以防止在遇到相同屬性時(shí)進(jìn)行更新操作。
React是一個(gè)視圖層,把它與自己開發(fā)的應(yīng)用集成要比把它與標(biāo)準(zhǔn)的框架集成直截了當(dāng)?shù)枚唷N覀儾粦?yīng)該破壞Emoji選擇器的封裝性,這樣才能很好地與Slack現(xiàn)有的模式集成在一起——我們希望這個(gè)組件就像是從一個(gè)端到端的React應(yīng)用里拿出來的一樣。為了保持選擇器的純凈,我們在現(xiàn)有的模塊系統(tǒng)里創(chuàng)建了一個(gè)輕量級(jí)的適配器。適配器掛載選擇器組件,抽取模型數(shù)據(jù),并監(jiān)聽來自外部的信號(hào)。采用這種模式,我們可以在開發(fā)新功能的同時(shí)逐步地遷移代碼庫。
新的開發(fā)流程
雖然使用React進(jìn)行開發(fā)是一件很愉悅的事情,但將它集成到我們已有的開發(fā)流程里卻不是那么一回事——至少在一開始不是那么令人愉快。在那個(gè)時(shí)候,Slack使用的是自己開發(fā)的前端構(gòu)建管道,沒有所謂的導(dǎo)入、依賴或者復(fù)雜的轉(zhuǎn)換(比如transpilation)。我們決定采用JSX語法和ES2015+,并使用Babel和webpack在本地構(gòu)建Emoji選擇器的資源。
我們預(yù)期簽入本地編譯的代碼會(huì)很痛苦,但我們低估了接連發(fā)生的合并沖突和依賴管理問題是多么令人抓狂。最后,我們嘗試將webpack集成到我們的開發(fā)和staging環(huán)境里,目標(biāo)是無縫地替代已有的工作流。為此,我們做了如下的工作。
基于webpack-dev-server開發(fā)了一個(gè)服務(wù),當(dāng)相關(guān)資源和依賴發(fā)生變更時(shí),自動(dòng)編譯本地開發(fā)服務(wù)器上的資源。支持將webpack資源加載到單元測試?yán)铮ㄟ@樣就有可能為React組件編寫測試用例)。重構(gòu)生產(chǎn)環(huán)境的構(gòu)建流程,將webpack資源推送到我們的CDN。通過重寫Emoji選擇器,迫使我們反思我們的構(gòu)建管道如何能夠以一種更健壯、更具伸縮性的方式打包資源。
性能
我們在少量的團(tuán)隊(duì)里部署了新的組件,并觀察結(jié)果。我們觀察了Emoji選擇器在用戶使用不同的5種交互方式下的渲染速度,對于大部分的操作,React表現(xiàn)出了顯著的速度提升。以下列出了選擇器在正常規(guī)模團(tuán)隊(duì)里的不同渲染時(shí)間。
第一次掛載:-270毫秒(減少了85%)第二次掛載:-158毫秒(減少了91.3%)搜索(多個(gè)結(jié)果):+27毫秒(增加了259%)搜索(一個(gè)結(jié)果):-25毫秒(減少了53.2%)重置搜索:-68毫秒(減少了70.1%)最大的改進(jìn)來自“第一次掛載”,從318毫秒到48毫秒,減少了270毫秒,也就是85%。這要極力歸功于react-virtualized——一個(gè)虛擬列表代碼庫——減少重新渲染emoji的數(shù)量。在默認(rèn)視圖上,React Emoji選擇器比DOM少渲染了85%。
或許最讓人感到吃驚的變化來自“搜索(多個(gè)結(jié)果)”,時(shí)間從17毫秒增加到了44毫秒,增加了27毫秒。舊選擇器只是把不匹配的emoji隱藏起來,也就是說,當(dāng)匹配到大部分emoji時(shí)會(huì)相對較快。但它的缺點(diǎn)也是顯而易見的,“搜索(一個(gè)結(jié)果)”和“重置搜索”就讓它的缺點(diǎn)原形畢露,因?yàn)榇藭r(shí)它需要隱藏更多的emoji。
未來
使用React重寫Emoji選擇器加快了渲染速度,同時(shí)簡化了代碼,讓代碼更容易維護(hù)。我們正在使用React重寫剩余的代碼。我們還有很多工作要做,這次重寫將為用戶的日常體驗(yàn)帶來積極的影響,為此我們感到非常興奮。與此同時(shí),我們積累了React的實(shí)踐經(jīng)驗(yàn),可以幫助平臺(tái)更進(jìn)一步。