React window 與 IntersectionObserver API 實現無限捲動 Dcard 文章閱讀器之心得紀錄
本篇文章不會說明所有程式碼的細節,而將重點聚焦在實作時的思維與心路歷程。
這次的挑戰是用 React 實作無限滾動的 Dcard 文章列表,首先來看看基本需求與加分條件:
看到這個題目的當下我先整理出了幾個實作的重點:
- Infinite scroll
- Data pagination
- Virtualized list
- Modal
當然這些 feature 都有相應的第三方套件可以使用,但如果能自己實作一些功能當然是對成長更有幫助的,然而在完成期限只有一週的條件下,所有功能都自己實作未免有點吃力不討好,畢竟可以的話我還希望可以在專案架構或是效能上做點優化,或是實作更多新功能,因此最終我選擇使用 react-window 來實作 virtualized list、react-modal 來實作 modal 功能、至於 Infinite scroll 與 Data pagination 則是我想自己實作邏輯而不依賴第三方套件的兩個功能。儘管存在像 react-window-infinite-loader 這樣能跟 react-window 無痛搭配的無限滾動第三方套件,我最終還是選擇嘗試前陣子看到的瀏覽器 API IntersectionObserver 來實作無限滾動功能。
由於時間限制,我選擇使用 create-react-app 的 TypeScript template 來快速建立專案環境,省去設置 webpack 、babel 等工具的煩雜程序。儘管這個挑戰看似是個小專案,我認為在開發的時候,依然要帶著這個專案未來可能會需要擴展或新增功能的想法,儘可能寫出可維護、可擴展且易懂的程式碼與架構。
架構方面
component 架構
我將每個 component 抽成一個獨立的 folder,正常狀況下裡面會包含三個檔案:
- tsx 檔 — component 的 source code
- index.ts — 負責 import component 再 export 出去,目的為讓引入 component 的行為一致(引入時不需要輸入檔名)
3. style.ts 檔 — 使用 styled component 建構 styled 元件,並用一個名為 S 的物件包覆 styled 元件,讓開發者在使用元件時可以得知它是經過 styled-component 建構的 styled 元件。
關於 import
我習慣將第三方套件與需要寫相對路徑的檔案用一行分開來,方便管理與查找。
獨立出可複用邏輯
我想大部分的開發者都聽過 DRY (Don’t repeat yourself) 的概念 ,而將專案其他地方會共同使用到的程式邏輯抽出去寫成一個可複用的 function 就是一個基本的方法。
React hooks 提出了 custom hook 的概念,其實就是一種把邏輯抽出去的方式,在這個專案中我將抓取 API 的行為抽成了一個 custom hook,在這個 custom hook 中連帶處理了相關狀態的 state,例如 loading, error, hasMore 來讓引入 custom hook 的 comopnent 可以輕易管控 API 抓取的狀態。
善加利用變數
將會在專案中多個地方使用到的值命名為一個變數可以避免要改變數值時需要改動非常多地方,另外也較可以避免打錯字的錯誤發生。
Virtualized List
當要渲染非常長的列表時,效能就變成一個需要考量的點。這時需要的就是名為 “windowing” 的技術。它代表頁面只渲染出現在畫面上的 item ,當滑出可視區的 item 就進行回收,藉此優化應用的效能。
在使用第三方套件以前,我還是先去了解了不依賴套件要如何實現 virtualized list,畢竟知道底層原理再去使用工具才不會過度依賴 denpency 而在技術思維上沒有成長。查詢了一下 React 的 virtualized list 套件,發現了 react-virtualized 是很多人在使用的套件,然而該套件的作者卻推薦他開發的另一個套件 react-window,最大的因素是 react-window 的 bundle size 相較於 react-virtualized 小了許多,如果只需要使用到基本功能的話,使用 react-window 無疑是較佳的選擇。
如果只是單純要渲染出具 windowing 功能的 List,使用 react-window 並沒有什麼困難的地方,使用方式大概是 render props 加上類似 map 的寫法,簡單的寫法卻在背後幫我們實現了 windowing ,只渲染出現在可視區的項目,並將滑出可視區的項目進行回收,提升了不少效能與使用者體驗。
Infinite Scroll
其實一開始選擇使用 react-window 時有看到 react-window-infinite-loader 這個套件,可以輕易的與 react-window 結合實現無限滾動功能。但是前陣子看到了 IntersectionObserver 這個 web API,發現除了 IE 以外的瀏覽器都有支援了(自己只用 chrome 跟 safari),決定來使用它試試看。
IntersectionObserver 從名稱來看可以知道它是一個 observer,可以觀察某一個 element,為了實現無限滾動,這邊要做的很簡單,就是去觀察當前列表的最後一個 item ,當它與 viewport 交界時就去抓新的資料回來。
不過在看程式碼前得先來了解一下 dcard 的 API:
既然要實現 pagination,勢必要用到第二個 before 的 query,也就是去紀錄當前列表最後一個 item 的 id ,在需要載入更多文章時,將 id 帶上,才能取到該篇文章以後的文章。
我使用 ref 去抓取列表的最後一個元素,並呼叫 IntersectionObserver 觀察它,當它與 viewport 交界時表示需要載入更多資料了,這邊去改變 lastId 這個 state ,因為在 useFetchPost 這個 custom hook 中有設定 lastId 為重新呼叫的 dependency,因此改變 lastId state 也會觸發 API 的呼叫,藉此達成 Infinite scroll 與 Data pagination 的功能。
至於 Modal 的部分因為只是使用套件做最基本的操作,我就不花篇幅講解囉。
功能實作完了,下一步能做什麼?
效能優化
效能優化在專案初期絕非是重點,畢竟先求有,才能進一步求好。不過既然功能都實作完成了,也就可以大概看看應用程式有沒有可以優化的地方。就程式碼而言,React 提供了許多效能優化的方法,例如 React.memo、useMemo、useCallback、React.lazy,但前提是得用在對的地方與對的時機,沒有經過慎重考慮就胡亂使用的結果可能導致效能不升反降。
善用各種檢測工具
React Profiler 可以檢測頁面的 render 狀況
Google Lighthouse 可以檢測網頁的各種指標,也會提示要怎麼做可以提升指標的分數。自己覺得很有趣的是 PWA 的檢測功能,PWA 很有可能成為未來網頁的趨勢,Lighthouse 中的 PWA 指標會告訴開發者需要實作哪些步驟(如最基本的 serviceWorker 與 manifest.json)才能達到 PWA 的指標,這也是我未來會想去深入研究的技術。
透過 Developer tool 的 FPS meter,我們可以檢測頁面的 Frame rate 與 memory 使用量。來看看 MDN 官方怎麼描述 Frame rate 吧
幀速率是一個網站的響應的量度。低或不一致的幀速率可以使一個網站出現反應遲鈍或janky,鬧了不好的用戶體驗。
60fps的幀頻是平穩的性能目標,給你所有需要響應某些事件更新的16.7毫秒時間預算。
所幸 Dcard Reader 這個作品在滑動時 fps 確實可以保持在 60fps 左右,在列表滑動時都可以保持非常的順暢。
關於使用者體驗
一個好的應用(這邊先看前端部分)除了要有精美或獨特的 User Interface,使用者體驗更是決定成敗的關鍵。
舉 Dcard Reader 一個簡單的例子:當列表滑到底要載入更多文章時,我選擇擺放一個 Placeholder,讓使用者感受到當下正在載入新文章,而不是看著一片空白發呆,我在專案中還故意延遲抓取 API 的時間,讓使用者去感受抓取資料的過程。這個機制有使用社群媒體的人應該都很清楚,當你在一個網路連線不佳的狀況下使用臉書,你會看到灰色的區塊,而不是完全的空白,會加強使用者等待的耐心。
因此在開發的同時也該時時刻刻以一個使用者的角度去看整個應用,盡可能避免影響使用者體驗的流程與行為。
Server-Side-Rendering
為了提升 SEO 與效能,將專案轉換成 SSR 也可能是我未來會想實作的功能。
結語
學習的確是成長的一個方法,然而實際動手執行則是驗證所學的好時機,一個簡單的 project 也可以迫使你去思考、去嘗試,過程中將會得到單方面接收知識所無法獲取的經驗與成就感。
完整程式碼 Github Repo: