掌控前端數據流,響應式編程使你看得更遠

在計算機領域,響應式編程是一種異步編程範式,關注數據流和變化的傳播。這意味着可以通過所使用的編程語言輕鬆地表達靜態或動態數據流。並且,在關聯執行模型中,存在可推斷的依賴關係,這有助於自動傳播與數據流相關的更改。

響應式編程可以加深代碼的抽象程度,使開發人員更專注於業務邏輯,與此同時,還能使代碼更加簡潔、易用。目前,響應式編程在前端開發、Android 開發中運用廣泛,此外,其非阻塞異步編程模型以及對消息流的處理模式在後端也得到了越來越多的應用。然而,響應式編程不是銀彈,它的邊界在哪?主流框架又受到其何種影響?前端開發還可以從哪些方面進行創新?

帶着這些困惑,InfoQ 採訪到近期參與 JSConf China 2019 的演講嘉賓——豆瓣閱讀前端負責人馬申彥,就響應式編程以及豆瓣閱讀的前端實踐進行了探討,在 JSConf 上,他也對響應式編程進行了演講分享。以下爲採訪全文整理,希望對正在從事前端開發的你有所幫助。

InfoQ:您在本次 JSConf 的演講主題是響應式編程,爲什麼會選擇這一話題?將響應式編程的範式應用在前端開發可以解決哪些問題?傳統開發模式與響應式開發模式有何區別?

馬申彥:響應式編程其實已經擁有了很長的歷史,可以追溯到上世紀九十年代,ReactiveX 系列函數庫也是早在 2012 年就由微軟發佈開源,Angular 在從 1.x 升級到 2.0 的時候果斷選擇了 RxJS 作爲其數據處理的工具,想必是經歷了一番思索的。Angular 因開發週期過長,期間 React + Vue 雙雄崛起,一舉拿下了國內大部分市場,但相當一部分企業級應用依然堅持使用 Angular 來保證代碼的健壯性和可維護性。豆瓣閱讀站內當前採用 React 作爲主要 UI 框架,但同時吸收了 Angular 採用的 TypeScript 和 RxJS 來控制代碼質量和數據流,取得了不錯的成果,這才萌生了向廣大前端 er 繼續科普 RxJS 及響應式編程的想法。RxJS 本身就是一個基於 TypeScript 開發的框架,所以其代碼健壯性很高,而且從 v5 到 v6 通過 pipe 方式載入運算符以後大大縮減了打包後的體積,爲前端項目後續優化提供了豐富且寶貴的經驗。

在三大框架大行其道的當下,大部分複雜的 DOM 操作都被框架接管了,現在的開發人員主要是基於框架做業務層的開發,但業務層往往數據密集且多變,而且又是與用戶直接面對面,需要時刻處理用戶的輸入。一個比較常見的場景是用戶的點擊事件觸發了一個網絡請求繼而發生了 DOM 上的變更,這種簡單的變動一般一個函數就可以完成,但是遇到複雜的多輸入問題,往往會形成函數調用函數難以追溯事件源頭的情境。我們前端組在維護歷史遺留的 Backbone 代碼過程中這種情況時常發生,後來把一系列異步流程用 Promise 來進行管理,確實達到了一定的效果,但我們不禁反思,這種複雜異步操作究竟如何才能做到在多重依賴和併發下的優雅實現,後來我們找到了它,就是響應式編程。

傳統開發模式大部分是命令式的,也就是函數的操作方式。響應式和命令式最大的區別在於,命令式是 pull,就是我去調一個函數傳遞一個參數,最後得到一個值。而響應式的思維方式是完全相反的,它是一個 push 操作,就是我去訂閱一個數據源,看它什麼時候給我發消息,我會按約定向下繼續傳遞。就像工廠流水線一樣,從上游發出原材料經過一層層加工後傳遞到下游,最後組裝成一個東西。具體來說,響應式編程容易讓事件的發生和轉化過程變得清晰有序,因爲它的惰性求值可以做到按需計算,省去傳統開發模式中很多無用的中間變量,又因爲它幾乎是純函數的,所以也方便構成組件複用於不同的項目。這樣我們就着眼於數據的流動而不是時時刻刻思考着用戶做了操作之後該執行哪一步,因爲這些都是響應式編程的範式內部所解決的問題。

InfoQ:目前,主流前端框架或多或少受響應式編程範式的影響。如何利用好這種範式,寫出更優雅易讀的代碼呢?

馬申彥:因爲我們組對 React 比較熟悉,就應用 React 做下說明,我們可以從源頭說起,大家知道一個 React 組件有 props 和 state 兩種儲存變量的方式,如何判斷究竟是 props 還是 state 呢?有三個問題:它是不是從父組件傳遞下來?它會隨時間變化嗎?它可以由其他 props 或者 state 計算而來嗎?按照這種思路得到的 state 本身就是一個獨立的、隨時間不斷吐出值的數據流。我們會在設計 state 時有意識地讓它們變得正交,也就是彼此獨立不相干。但這時問題來了,存在着部分 state 需要跟隨 props 的變化進行更新,所以有了 componentWillReceiveProps 這類 lifecycle method,可是其調用方式非常詭譎,初學者使用時不小心就容易形成死循環。

爲了解決這些問題,React hooks 應運而生,它就是一個很棒的響應式編程的實踐,比如說 useEffect 這個 hooks,它可以觀察一組輸入,包括 props 或是 state 等,當其中任一輸入值發生改變時做一些操作。這組輸入就是 single source of truth,真理的唯一源頭。而 useEffect 要做的就是處理這些輸入轉換成輸出的過程,這是一個純函數,就保證了流經它的數據流是被約束的。

可以進一步思考這組輸入,通常情況下無外乎網絡請求得到的異步數據或是用戶輸入等,這些都可以稱作 inputs,我們可以得到一個公式:

newState = oldState + props + inputs

這就和 Redux 的數據模型很像了。

newState = oldState + action

React 和 Redux 本質上都是把一組輸入通過一系列變化,同步或者異步地轉化成一個 state 的過程,僅僅保證數據的獨立其實是不夠的,還需要保證轉化過程的獨立性,這時我們發現了 rxjs-hooks 這個庫,它很好地結合了 React hooks 對數據依賴的抽象,以及運用了 RxJS 來處理複雜的異步數據流。特別是 useEventCallback 這個 API,可以把它理解成通常寫的 handleClick 函數,只不過它完美地封裝了對 props 的依賴和初始 state,然後返回一個 [eventCallback, value] 的數組,只需要將 eventCallback 連接到組件的 event listener 上就可以獲取,或者說是訂閱了這樣一個隨着 event 的發生、props 的變動而實時更新的 value,書寫 React 異步操作從未如此簡單。

InfoQ:響應式編程不是銀彈,也有不擅長的領域。您覺得這個邊界在哪?如何在開發過程中進行判斷?

馬申彥:具體來說,響應式編程主要應用在那些隨着時間推移會變化的數據上,如果只是一次性的同步計算,那響應式編程將毫無用武之處,當然還是可以用一些運算符來進行規約,畢竟同步編程可以看作是是異步編程的一種特殊解,例如著名的 MapReduce 就是通過並行計算來提升性能,web 上也有 Service Worker,簡單來說就是同步轉異步,用併發來換取計算性能的提升。還有就是極其簡單的異步操作,比如一次簡單的 fetch 那就沒有必要用上 RxJS 這種重型武器,直接用 Promise 上的方法就很方便了,但如果說需要一個帶自動重試功能同時還要處理併發請求的 fetch 操作,這時候就可能需要按需引入 RxJS 來管理這些請求了。

雖然 RxJS 號稱 Lodash for events,但需要注意的是,RxJS 整體的學習曲線比較陡峭,但上手之後你肯定會愛上它的,而且其豐富的操作符集合也有助於控制並保持團隊內開發者對於異步事件處理的代碼風格,畢竟輪子還是別人造的香且好用。事實上,最難的部分莫過於在學習 RxJS 過程中放棄命令式編程相關的想法:事件在某種層面上與我們每天正在用的集合(例如 Array)不同。Lodash 基於同步數據的集合,而 RxJS 基於異步數據的事件流。

InfoQ:隨着移動應用在用戶交互方面的演進,前端可以從哪些方面進行創新?

馬申彥:用戶交互與前端關係密切,影響用戶體驗的要素很多,簡單來說分爲頁面加載與後續用戶交互兩部分。先談談頁面加載,移動端網頁可以分 App 內嵌頁面和瀏覽器打開兩種,比如豆瓣 App 內書影音這個 tab 裏就是 App 內嵌的頁面,上面一排包括電影、電視、讀書等頁面內 tab 是原生實現的 tab,下面的內容部分是基於豆瓣自研的 rexxar 框架開發,實現了包括熱部署、緩存、網絡請求和頁面喚起系統 API 等功能,這樣打包在 App 內的頁面首次加載只需載入數據速度很快,相對來講用戶體驗是極好的,而且應用了 React 作爲 Javascript 這邊的開發框架顯著降低了新頁面的開發難度。針對瀏覽器打開的頁面,豆瓣閱讀 mobile 版是一個縮略版的書店 SPA 應用,混合了 Backbone 和 React 的開發模式,最大限度地複用 pc 端的組件,在保證頁面跳轉無全頁刷新的基礎上利用分包異步加載等技術降低用戶等待時間從而提升使用體驗。

從用戶在頁面上操作的交互上講,可以分爲用戶輸入過程和輸入反饋兩類。輸入過程主要有表單和按鈕等,表單是前端老生常談的問題了,例如使用 input text=‘tel’ 來喚起系統數字鍵盤方便用戶輸入電話號碼等也是人盡皆知的手法,最近項目裏應用了第三方的級聯省市區選擇器,能夠根據屏幕尺寸和展開位置自動響應級聯菜單的位置,方便用戶選擇。同時移動端表單也會考慮屏幕的侷限性,提示語和輸入框會盡量採用上下式而不是左右式佈局,有時就需要考慮輸入框組是否過長的問題,會考慮將表單分段方便用戶理解和操作。輸入反饋是用戶直觀看到的部分,例如表單的提交成功會給予反饋,比如有全局的模態框和 toast 等,用戶操作路徑上比較重要的提示或選擇就會使用模態框,而一般的操作反饋就會通過 toast 告知用戶操作結果。在交互細節上,比如控制按鈕可點擊區域的大小適中符合人手指尺寸,定義 scroll-snap-type 以獲得連貫流程的滾動體驗,使用 css 內聯 svg 以避免出現元素空白等。移動端現在常用 feed 流形式展示數據,這時就需要考慮滾動加載的時機和實現形式了。

總的來說,可以從網絡請求、數據加載、用戶在頁面上的操作、視覺UI方面去切合移動端的需求。天下難事,必作於易;天下大事,必作於細。通過開發手段提升用戶體驗的要素有很多,最關鍵的是一顆主動體驗使用自己開發的產品並認真負責每一處細節的心。

InfoQ:我之前看到過一篇豆瓣前端工程師寫給前端新人的一封信,裏面提到前端要有預見性地學習新技術,擁抱新標準,前端工程師是所有工程師中最需要“工匠精神”的。您可以談談豆瓣目前主要的技術重點是什麼?在新技術上有哪些嘗試?

馬申彥:不只是前端工程師,所有工程師都非常需要“工匠精神”,豆瓣向來擁抱新技術的嘗試與落地,前端組也會不定期組織組內分享各產品線或個人項目運用的新技術。2016 年底小程序剛出來時,豆瓣就組織了以小程序開發爲主題的 Happy Day 活動,其中用小程序開發的“阿爾法城重生”項目觸動了在場工程師們的集體懷舊記憶。豆瓣現在正處在快速增長時期,平臺和業務方面都遇到了很多挑戰,但同時也是機遇,如何應對階段性的高併發請求,如何降低服務器開銷,如何提升用戶訪問頁面的速度等,都是我們所面臨的問題。

豆瓣閱讀早期使用 jQuery 和 Backbone 來構建 web 應用,在 React 出來以後迅速地被其聲明式的寫法所吸引,現今站內包括管理後臺和編輯器在內的中大型 web 應用都優選使用 React 構建,應用了包括 TypeScript、GraphQL、RxJS、Storybook 和 Jest 在內的多種技術完善保證項目的開發與維護。現在閱讀站內主要針對原創作品寫作者和讀者進行了多項功能優化和調整。爲了開發維護方便,我們果斷拋棄了使用人數非常少的 IE10 以下的瀏覽器,這樣我們代碼裏省去了很多爲特定瀏覽器做的 css prefix 或是 hack。在產品上針對 Draft.js 進行二次開發的編輯器以及 web 閱讀器,在保證正常閱讀編輯體驗的同時,不斷優化代碼結構,保證像素級體驗。

InfoQ:在今年柏林的JSConf EU上,npm公開了一組關於“逃離 JavaScript”的數據,其中提到兩個趨勢,分別是:TypeScript的使用者已經從去年的46%增長到63%;WebAssembly的出現。您如何看待JavaScript未來的發展趨勢?

馬申彥:TypeScript 已是大中型前端項目的標準配置,而且它是 JavaScript 的超集,這其實並不能說是逃離 JavaScript 的趨勢,可以說是應用了更強類型規約的 JavaScript 來保證我們的代碼的健壯性。WebAssembly 主要是解決 JavaScript 計算性能不足的問題,重點用在依賴圖形渲染或者高速計算的項目,WebAssembly 的另一個意義是可以有更多的語言參與到瀏覽器這塊戰場,這是好事,不應該將其他語言視爲敵人,而應該擁抱這種變化,正如我們 iOS App 內的排版引擎由 C++ 編寫,但界面依然是用 Objective-C 和 Swift 構建,所謂術業有專攻。軟件項目開發與技術人員對業務邏輯的熟悉度和實現難度關係更大,只要能夠帶來生產效率的提升,那麼任何新技術都是值得探討和學習的。

回到 JavaScript 的發展趨勢,應該還是漸進式增強,完善面向對象編程(主要是 class)的部分,標準類型借鑑現有流行類庫的最佳實踐,更友好的 DOM 操作方法,以及更優雅的語法糖等等,同樣值得注意的還是與設備端的聯合,比如傳感器等 API,這也是移動優先的發展方向。

InfoQ:您目前還在關注哪些新的技術問題?

馬申彥:我們團隊去年在推動 GraphQL 的落地與實現,嘗試建立從 GraphQL 的 schema 到 TypeScript 類型系統的直接映射,希望能優化開發流程;同時研究 CSS in JS 和 CSS Modules 相關技術以優化項目內對組件樣式的開發;從工程化的角度來說,還關注前端的微服務化,但這方面目前看到的最佳實踐還比較少。

專家介紹:
馬申彥,豆瓣閱讀前端負責人。現負責豆瓣閱讀 web 前端的開發與維護,包括完整的 web 版電子書店、App 內嵌頁面、原創作品的寫作發佈及後臺審稿相關功能,日常運營相關活動與比賽等。目前,豆瓣閱讀技術團隊由十多名各具匠心且熱心前沿技術方向的工程師組成,依託豆瓣的技術平臺與基礎設施進行獨立開發,繼承了豆瓣優良的技術氛圍同時具有自主創新的產品方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章