React 歷史項目維護與優化實踐

本文介紹了做者接手維護一箇中型 React 歷史項目時的一系列改進實踐,包括模塊結構拆分、業務邏輯梳理、Webpack 打包優化等。react

背景

這是一個 PC 的管理後臺類項目,沒有引入 react-router 和 redux。待維護的頁面全部模板和邏輯所有在一個千行級的 JSX 中實現,包括調用組件庫、發送 fetch 請求、切換子頁面狀態等。而且,該項目實際上並非單頁應用,而是經過 Webpack 區分多個 entry 的方式實現了多入口頁面。webpack

模塊拆分

在開始實現新增需求前,首先要作的是瞭解代碼,整理其結構並適當地以拆分模塊的形式逐步重構之。在這一步中,並不涉及最使人畏懼的【重構業務邏輯】,而更多地是【更高級的代碼美化】,在完整保留原有代碼邏輯和調用方式的前提下,利用一些 JS 的技巧,按照單一職責原則拆分不一樣的業務邏輯代碼到不一樣的模塊中,以提升【麪條代碼】的模塊化程度。這一步處理要解決的主要問題是:web

  • 歷史代碼中混雜了 JSX 模板結構、數據處理、異步控制、狀態管理的各類邏輯。npm

  • 代碼中如菜單名稱結構、表單字段名等的各類硬編碼配置分散在各處。redux

  • 幾乎所有的業務邏輯均在一個扁平的組件中實現。瀏覽器

解決上述問題,並不涉及到具體業務邏輯的重寫,而是經過將同類功能提取爲獨立模塊,經過一些簡單的語法糖來保證僅更改儘可能少的業務代碼,就能實現初步的模塊拆分。緩存

針對上述的幾個問題,初步的模塊拆分包括:性能優化

  1. 包含大多數 React 組件方法的主頁面組件。babel

  2. 包含異步請求的 action 模塊。react-router

  3. 包含各類硬編碼配置的 consts 模塊。

  4. 包含調用組件庫中表單等組件的配置文件 model 模塊。

而後就能夠一步步將代碼邏輯遷移到新模塊中,在保證頁面的功能不受影響的前提下逐步實現初步的模塊拆分了。這個過程當中屢次用到的技巧包括:

將執行異步請求的組件方法拆分至模塊中,再在構造器中 bind 回組件。如一個典型的查詢邏輯:

// main.js
class Demo extends Component {
  fetchData () {
    fetch('...').then(data => {
      // 此處一般有冗長的業務邏輯
      this.setState({ data })
    })
  }
}

可將其先拆分至 action.js 模塊中,形如:

// action.js
// 業務邏輯徹底保留,只是添加了 export function 前綴
export function fetchData () {
  fetch('...').then(data => {
    this.setState({ data })
  })
}

而後在原組件中加載並 bind 該函數,從而實現模塊拆分:

import { fetchData } from './actions'
 
class Demo extends Component {
  constructor() {
    // 在此 bind 便可
    this.fetchData = fetchData.bind(this)
  }
}

以及,將一些加載時引用了 this 的配置對象封裝至新模塊的工廠函數中:

render() {
  // 包含冗長表單配置的配置變量
  const demo = {
    // 直接將其提取至新模塊在此會報錯
    value: this.state.xxx
  }
}

新建一個返回 demo 的工廠函數:

// model.js
export const getDemo  = () => ({
  // 在此的業務代碼一樣可原封不動地移動
  value: this.state.xxx
})

修改原有位置的調用邏輯:

import { getDemo } from './model'

render() {
  // 在調用工廠函數時綁定上下文,便可使模塊中 this 指向正確
  const demo = getDemo.call(this)
}

實踐中在這一步完成後,其實已經實現【將千行級代碼拆分至若干個百行級的模塊,每一個模塊均僅包含相似的邏輯功能】了。

業務梳理

在初步整理模塊後,對代碼結構也有了初步的瞭解,此時能夠開始添加一些新的業務需求了。這時,對於與新需求相關的原有代碼,能夠在理解基礎上進行梳理與局部的重構,以實現新功能(注意這時重構是爲了實現新功能,而非重寫原有代碼以實現相同功能)。

這一步主要須要解決的問題是:

  1. 原代碼中有較多晦澀的 if-else 控制流邏輯,包含對某些狀態的組合判斷,這對新加入業務代碼會有必定的障礙。

  2. 在 JSX 中大量【嵌套的三目表達式】長度很長且不易讀(這其實是 JSX 相對模板天生的問題),這也形成了必定的困擾。

因爲業務邏輯的複用價值較低,這裏較難經過代碼的形式給出【最佳實踐】的代碼,但通用的處理模式可總結以下:

  1. 經過一些簡單的 log 來判斷一個事件觸發流程中,基本的代碼調用和執行順序。

  2. 對執行過程當中遇到的組件狀態,在 React 開發工具中確認 state / props 執行先後的變化,肯定【某段業務邏輯所依賴的組件狀態,及其觸發先後的組件狀態】

  3. 以【編寫輸入新需求下輸入狀態,輸出新需求下輸出狀態】爲目標,維護並編寫新業務邏輯代碼。

  4. 新邏輯完成後,逐步註釋並最終替換掉老代碼,漸進地實現業務需求。

在這一步達到較高的完善程度後,能夠從新審視新增的代碼段作局部重構,或提取一些可複用的邏輯到上一步中的相應模塊中。到這一步爲止,便可基本上將老項目像我的起手的項目同樣作到較爲輕車熟路的開發維護了。

Webpack 優化

在業務需求按時完成的前提下,纔有必要進行這一步的優化。對一個配置文件多達數百行的穩按期項目,切換當時的 Webpack 1 到 Webpack 2 難度較大,但相應的意義卻並不大。所以,在構建方向上的優化策略最後以這幾條爲主:

  1. 分析多頁面的公共依賴配置,優化公共依賴提取,去除冗餘依賴。

  2. 修復已知問題。

  3. 優化構建速度。

首先,在優化公共依賴方面,難點並非【如何更改公共依賴】,而是如何獲知【有哪些依賴須要被提取爲公共依賴】。在這方面,須要的是一個查看各 Bundle 內容及尺寸的可視化工具,可使用 webpack-bundle-analyzer 這一 Webpack 插件來實現。使用該插件的方式也很簡單,直接將其添加在 Webpack 的 plugins 配置中,從新執行打包命令便可。打包成功後,會彈出瀏覽器窗口展現各 Bundle 的公共依賴,以下圖是優化前的公共依賴配置:

bundle-before

能夠發現原始的依賴配置中,位於圖中角落的 common 包僅包括了原始的 React,而組件庫、lodash、moment 等依賴在每一個頁面包中都重複出現了。所以,在 Webpack 的 entry 配置字段中,爲 common 包添加 ['babel-polyfill', 'lodash', 'moment'] 等依賴名後,便可實現公共依賴的提取。

實際上,提取公共依賴並不能減小每一個頁面最終的打包輸出體積。只有去除冗餘依賴,才能直接影響頁面最終的包大小。那麼這樣的冗餘依賴是否存在呢?答案是確定的。在排查過程當中發現,導入 moment 這一很是經常使用的時間庫時,會默認導入其對應的多語言依賴 locale 包,而這對當前項目是徹底無用的。對於這種【依賴自己依賴了冗餘依賴】的情形,Webpack 一樣提供了優化方案。在 Plugins 中添加以下的一行便可:

new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)

這一行代碼可以直接減小開發環境 300K 的包大小!在進行了依賴優化後,獲得的包體積可視化爲下圖:

bundle-after

能夠發現,common 的大小獲得了大幅增長,而各個頁面的業務包體積則減小了 2/3 以上。不過,在這個優化方向上並無作到極致。因爲 Webpack 1 不支持原生的 Tree Shaking 功能,致使了 UI 組件庫即使經過 import { xxx } 語法引入,最終仍是會將整個組件庫導入公共依賴包中,沒有作到按需加載。而相應的 import 插件又存在配置上的不便,其結果是最終沒有在這個項目中實現 UI 組件庫的按需加載。固然,隨着 Webpack 2 的普及,新項目中這應當不會成爲問題。

接下來,在修復已知問題方面,優化過程當中修復了兩個較爲常見的問題:common 包隨業務包變動而變動的問題;hash 值每次全量變動的問題。

在直接經過 CommonsChunkPlugin 拆分 common 包的配置方式下,每一個頁面最終使用的包都是 common 包和業務包兩個。這時,在頁面 A 中修改業務邏輯,會形成 common 包的細微變更,致使新的打包文件中,common 包雖然沒有源碼變動,卻隨着業務包的變動而變動了。這會致使每次版本更新時包括 common 在內的全部包都會被全量更新,沒有實現按需的更新。

解決方案是,在 CommonsChunkPlugin 的配置中,將 name 字段改成 names 字段,提供 ['common', 'manifest'] 兩個公共依賴入口。這樣,在業務包變更時,只有 manifest 會隨之變更,而 common 的內容不會受到影響,這也就實現了真正意義上的按需更新,更大限度地利用瀏覽器緩存。雖然這一實踐其實是 Webpack 2 文檔中官方的推薦作法,但 Webpack 1 也徹底支持。

另外一個問題是,每次打包的產物文件中雖然都附帶了一個 hash 值,但對全部打包文件,該值都是同樣的。這一樣會致使僅有某個 bundle 變動時,全量的生產包名稱變動,形成緩存的失效。相應的解決方案也很簡單:將 output 配置字段中的 [hash] 改成 [chunkhash],便可爲每一個包添加不一樣的 hash 值。

最後,在提高面向開發者的打包體驗方面,本次優化中主要實現的是 lint 與 Webpack 的解耦。在使用 IDE 開發時,lint 的引入較爲繁瑣,所以當時採用的是將 lint 做爲 Webpack 的 loader 形式引入,在每次增量打包後執行 lint,對存在不符合風格指南的代碼在終端報錯並不予編譯經過的策略。這個模式兼容性繞過了編輯器和 IDE 的配置,於是更加通用,但問題在於:

  1. 每次打包都須要重複的 lint 過程,下降了打包速度。

  2. lint 規則較嚴格時,調試過程受到了較大的限制。如 class 方法必須存在對 this 的引用、函數參數必須所有被使用、不容許 return 後存在業務邏輯等 lint 策略,它們雖然確實能提升代碼質量,但在調試過程當中局部存在這樣的代碼很是常見,禁止編譯這些不存在語法問題的代碼,對開發效率存在較大的影響。

於是,在優化中果斷去除了 Webpack 的 lint 配置,轉而經過 VSCode 等編輯器的 lint 插件實現開發過程當中的動態 lint 提示和自動美化。另外,對 Webpack 每次打包的輸出格式也進行了優化,去除了較多冗餘的包信息 log 內容,僅保留每次打包的 hash 信息便可。最後的開發體驗與新 Webpack 2 項目相近,實現了必定的開發效率提高。

總結

在維護過程當中,首先仍是理解已有業務代碼,而後按部就班地走改良路線,而不該以【老代碼好亂】爲理由貿然重寫,這會存在很大的風險。雖然 React 自己設計較爲鬆散,使得開發者更容易產出較無序的代碼,但 JS 目前的模塊和 OO 機制爲無需重寫的填坑提供了很大的幫助,實踐中最後本質上重寫的也只有新需求相關的部分,已有的邏輯獲得了儘量的保留和複用。而性能優化則屬於錦上添花的【折騰向】內容,優先級較低,能夠在時間相對寬鬆的時候處理,優化方式上也有較多的工具和插件支持,相對須要實際編碼的業務而言,難度較低。

但願以上實踐經驗對於更多開發者的踩坑 / 填坑路可以有所幫助。

相關文章
相關標籤/搜索