摘要:本文介紹了作者接手維護(hù)一個(gè)中型歷史項(xiàng)目時(shí)的一系列改進(jìn)實(shí)踐,包括模塊結(jié)構(gòu)拆分業(yè)務(wù)邏輯梳理打包優(yōu)化等。代碼中如菜單名稱(chēng)結(jié)構(gòu)表單字段名等的各種硬編碼配置分散在各處。最后,在提升面向開(kāi)發(fā)者的打包體驗(yàn)方面,本次優(yōu)化中主要實(shí)現(xiàn)的是與的解耦。
本文介紹了作者接手維護(hù)一個(gè)中型 React 歷史項(xiàng)目時(shí)的一系列改進(jìn)實(shí)踐,包括模塊結(jié)構(gòu)拆分、業(yè)務(wù)邏輯梳理、Webpack 打包優(yōu)化等。
背景這是一個(gè) PC 的管理后臺(tái)類(lèi)項(xiàng)目,沒(méi)有引入 react-router 和 redux。待維護(hù)的頁(yè)面所有模板和邏輯全部在一個(gè)千行級(jí)的 JSX 中實(shí)現(xiàn),包括調(diào)用組件庫(kù)、發(fā)送 fetch 請(qǐng)求、切換子頁(yè)面狀態(tài)等。并且,該項(xiàng)目實(shí)際上并不是單頁(yè)應(yīng)用,而是通過(guò) Webpack 區(qū)分多個(gè) entry 的方式實(shí)現(xiàn)了多入口頁(yè)面。
模塊拆分在開(kāi)始實(shí)現(xiàn)新增需求前,首先要做的是了解代碼,整理其結(jié)構(gòu)并適當(dāng)?shù)匾圆鸱帜K的形式逐步重構(gòu)之。在這一步中,并不涉及最令人畏懼的【重構(gòu)業(yè)務(wù)邏輯】,而更多地是【更高級(jí)的代碼美化】,在完整保留原有代碼邏輯和調(diào)用方式的前提下,利用一些 JS 的技巧,按照單一職責(zé)原則拆分不同的業(yè)務(wù)邏輯代碼到不同的模塊中,以提高【面條代碼】的模塊化程度。這一步處理要解決的主要問(wèn)題是:
歷史代碼中混雜了 JSX 模板結(jié)構(gòu)、數(shù)據(jù)處理、異步控制、狀態(tài)管理的各種邏輯。
代碼中如菜單名稱(chēng)結(jié)構(gòu)、表單字段名等的各種硬編碼配置分散在各處。
幾乎全部的業(yè)務(wù)邏輯均在一個(gè)扁平的組件中實(shí)現(xiàn)。
解決上述問(wèn)題,并不涉及到具體業(yè)務(wù)邏輯的重寫(xiě),而是通過(guò)將同類(lèi)功能提取為獨(dú)立模塊,通過(guò)一些簡(jiǎn)單的語(yǔ)法糖來(lái)保證僅更改盡量少的業(yè)務(wù)代碼,就能實(shí)現(xiàn)初步的模塊拆分。
針對(duì)上述的幾個(gè)問(wèn)題,初步的模塊拆分包括:
包含大多數(shù) React 組件方法的主頁(yè)面組件。
包含異步請(qǐng)求的 action 模塊。
包含各種硬編碼配置的 consts 模塊。
包含調(diào)用組件庫(kù)中表單等組件的配置文件 model 模塊。
然后就可以一步步將代碼邏輯遷移到新模塊中,在保證頁(yè)面的功能不受影響的前提下逐步實(shí)現(xiàn)初步的模塊拆分了。這個(gè)過(guò)程中多次用到的技巧包括:
將執(zhí)行異步請(qǐng)求的組件方法拆分至模塊中,再在構(gòu)造器中 bind 回組件。如一個(gè)典型的查詢(xún)邏輯:
// main.js class Demo extends Component { fetchData () { fetch("...").then(data => { // 此處通常有冗長(zhǎng)的業(yè)務(wù)邏輯 this.setState({ data }) }) } }
可將其先拆分至 action.js 模塊中,形如:
// action.js // 業(yè)務(wù)邏輯完全保留,只是添加了 export function 前綴 export function fetchData () { fetch("...").then(data => { this.setState({ data }) }) }
然后在原組件中加載并 bind 該函數(shù),從而實(shí)現(xiàn)模塊拆分:
import { fetchData } from "./actions" class Demo extends Component { constructor() { // 在此 bind 即可 this.fetchData = fetchData.bind(this) } }
以及,將一些加載時(shí)引用了 this 的配置對(duì)象封裝至新模塊的工廠函數(shù)中:
render() { // 包含冗長(zhǎng)表單配置的配置變量 const demo = { // 直接將其提取至新模塊在此會(huì)報(bào)錯(cuò) value: this.state.xxx } }
新建一個(gè)返回 demo 的工廠函數(shù):
// model.js export const getDemo = () => ({ // 在此的業(yè)務(wù)代碼同樣可原封不動(dòng)地移動(dòng) value: this.state.xxx })
修改原有位置的調(diào)用邏輯:
import { getDemo } from "./model" render() { // 在調(diào)用工廠函數(shù)時(shí)綁定上下文,即可使模塊中 this 指向正確 const demo = getDemo.call(this) }
實(shí)踐中在這一步完成后,其實(shí)已經(jīng)實(shí)現(xiàn)【將千行級(jí)代碼拆分至若干個(gè)百行級(jí)的模塊,每個(gè)模塊均僅包含類(lèi)似的邏輯功能】了。
業(yè)務(wù)梳理在初步整理模塊后,對(duì)代碼結(jié)構(gòu)也有了初步的了解,此時(shí)可以開(kāi)始添加一些新的業(yè)務(wù)需求了。這時(shí),對(duì)于與新需求相關(guān)的原有代碼,可以在理解基礎(chǔ)上進(jìn)行梳理與局部的重構(gòu),以實(shí)現(xiàn)新功能(注意這時(shí)重構(gòu)是為了實(shí)現(xiàn)新功能,而非重寫(xiě)原有代碼以實(shí)現(xiàn)相同功能)。
這一步主要需要解決的問(wèn)題是:
原代碼中有較多晦澀的 if-else 控制流邏輯,包含對(duì)某些狀態(tài)的組合判斷,這對(duì)新加入業(yè)務(wù)代碼會(huì)有一定的障礙。
在 JSX 中大量【嵌套的三目表達(dá)式】長(zhǎng)度很長(zhǎng)且不易讀(這實(shí)際上是 JSX 相對(duì)模板天生的問(wèn)題),這也造成了一定的困擾。
由于業(yè)務(wù)邏輯的復(fù)用價(jià)值較低,這里較難通過(guò)代碼的形式給出【最佳實(shí)踐】的代碼,但通用的處理模式可總結(jié)如下:
通過(guò)一些簡(jiǎn)單的 log 來(lái)判斷一個(gè)事件觸發(fā)流程中,基本的代碼調(diào)用和執(zhí)行順序。
對(duì)執(zhí)行過(guò)程中遇到的組件狀態(tài),在 React 開(kāi)發(fā)工具中確認(rèn) state / props 執(zhí)行前后的變化,確定【某段業(yè)務(wù)邏輯所依賴(lài)的組件狀態(tài),及其觸發(fā)前后的組件狀態(tài)】
以【編寫(xiě)輸入新需求下輸入狀態(tài),輸出新需求下輸出狀態(tài)】為目標(biāo),維護(hù)并編寫(xiě)新業(yè)務(wù)邏輯代碼。
新邏輯完成后,逐步注釋并最終替換掉老代碼,漸進(jìn)地實(shí)現(xiàn)業(yè)務(wù)需求。
在這一步達(dá)到較高的完善程度后,可以重新審視新增的代碼段做局部重構(gòu),或提取一些可復(fù)用的邏輯到上一步中的相應(yīng)模塊中。到這一步為止,即可基本上將老項(xiàng)目像個(gè)人起手的項(xiàng)目一樣做到較為輕車(chē)熟路的開(kāi)發(fā)維護(hù)了。
Webpack 優(yōu)化在業(yè)務(wù)需求按時(shí)完成的前提下,才有必要進(jìn)行這一步的優(yōu)化。對(duì)一個(gè)配置文件多達(dá)數(shù)百行的穩(wěn)定期項(xiàng)目,切換當(dāng)時(shí)的 Webpack 1 到 Webpack 2 難度較大,但相應(yīng)的意義卻并不大。因此,在構(gòu)建方向上的優(yōu)化策略最后以這幾條為主:
分析多頁(yè)面的公共依賴(lài)配置,優(yōu)化公共依賴(lài)提取,去除冗余依賴(lài)。
修復(fù)已知問(wèn)題。
優(yōu)化構(gòu)建速度。
首先,在優(yōu)化公共依賴(lài)方面,難點(diǎn)并不是【如何更改公共依賴(lài)】,而是如何獲知【有哪些依賴(lài)需要被提取為公共依賴(lài)】。在這方面,需要的是一個(gè)查看各 Bundle 內(nèi)容及尺寸的可視化工具,可以使用 webpack-bundle-analyzer 這一 Webpack 插件來(lái)實(shí)現(xiàn)。使用該插件的方式也很簡(jiǎn)單,直接將其添加在 Webpack 的 plugins 配置中,重新執(zhí)行打包命令即可。打包成功后,會(huì)彈出瀏覽器窗口展示各 Bundle 的公共依賴(lài),如下圖是優(yōu)化前的公共依賴(lài)配置:
可以發(fā)現(xiàn)原始的依賴(lài)配置中,位于圖中角落的 common 包僅包括了原始的 React,而組件庫(kù)、lodash、moment 等依賴(lài)在每個(gè)頁(yè)面包中都重復(fù)出現(xiàn)了。因此,在 Webpack 的 entry 配置字段中,為 common 包添加 ["babel-polyfill", "lodash", "moment"] 等依賴(lài)名后,即可實(shí)現(xiàn)公共依賴(lài)的提取。
實(shí)際上,提取公共依賴(lài)并不能減少每個(gè)頁(yè)面最終的打包輸出體積。只有去除冗余依賴(lài),才能直接影響頁(yè)面最終的包大小。那么這樣的冗余依賴(lài)是否存在呢?答案是肯定的。在排查過(guò)程中發(fā)現(xiàn),導(dǎo)入 moment 這一非常常用的時(shí)間庫(kù)時(shí),會(huì)默認(rèn)導(dǎo)入其對(duì)應(yīng)的多語(yǔ)言依賴(lài) locale 包,而這對(duì)當(dāng)前項(xiàng)目是完全無(wú)用的。對(duì)于這種【依賴(lài)本身依賴(lài)了冗余依賴(lài)】的情形,Webpack 同樣提供了優(yōu)化方案。在 Plugins 中添加如下的一行即可:
new webpack.IgnorePlugin(/^./locale$/, /moment$/)
這一行代碼能夠直接減少開(kāi)發(fā)環(huán)境 300K 的包大??!在進(jìn)行了依賴(lài)優(yōu)化后,得到的包體積可視化為下圖:
可以發(fā)現(xiàn),common 的大小得到了大幅增加,而各個(gè)頁(yè)面的業(yè)務(wù)包體積則減少了 2/3 以上。不過(guò),在這個(gè)優(yōu)化方向上并沒(méi)有做到極致。由于 Webpack 1 不支持原生的 Tree Shaking 功能,導(dǎo)致了 UI 組件庫(kù)即便通過(guò) import { xxx } 語(yǔ)法引入,最終還是會(huì)將整個(gè)組件庫(kù)導(dǎo)入公共依賴(lài)包中,沒(méi)有做到按需加載。而相應(yīng)的 import 插件又存在配置上的不便,其結(jié)果是最終沒(méi)有在這個(gè)項(xiàng)目中實(shí)現(xiàn) UI 組件庫(kù)的按需加載。當(dāng)然,隨著 Webpack 2 的普及,新項(xiàng)目中這應(yīng)當(dāng)不會(huì)成為問(wèn)題。
接下來(lái),在修復(fù)已知問(wèn)題方面,優(yōu)化過(guò)程中修復(fù)了兩個(gè)較為常見(jiàn)的問(wèn)題:common 包隨業(yè)務(wù)包變更而變更的問(wèn)題;hash 值每次全量變更的問(wèn)題。
在直接通過(guò) CommonsChunkPlugin 拆分 common 包的配置方式下,每個(gè)頁(yè)面最終使用的包都是 common 包和業(yè)務(wù)包兩個(gè)。這時(shí),在頁(yè)面 A 中修改業(yè)務(wù)邏輯,會(huì)造成 common 包的細(xì)微變動(dòng),導(dǎo)致新的打包文件中,common 包雖然沒(méi)有源碼變更,卻隨著業(yè)務(wù)包的變更而變更了。這會(huì)導(dǎo)致每次版本更新時(shí)包括 common 在內(nèi)的所有包都會(huì)被全量更新,沒(méi)有實(shí)現(xiàn)按需的更新。
解決方案是,在 CommonsChunkPlugin 的配置中,將 name 字段改為 names 字段,提供 ["common", "manifest"] 兩個(gè)公共依賴(lài)入口。這樣,在業(yè)務(wù)包變動(dòng)時(shí),只有 manifest 會(huì)隨之變動(dòng),而 common 的內(nèi)容不會(huì)受到影響,這也就實(shí)現(xiàn)了真正意義上的按需更新,更大限度地利用瀏覽器緩存。雖然這一實(shí)踐實(shí)際上是 Webpack 2 文檔中官方的推薦做法,但 Webpack 1 也完全支持。
另一個(gè)問(wèn)題是,每次打包的產(chǎn)物文件中雖然都附帶了一個(gè) hash 值,但對(duì)所有打包文件,該值都是一樣的。這同樣會(huì)導(dǎo)致僅有某個(gè) bundle 變更時(shí),全量的生產(chǎn)包名稱(chēng)變更,造成緩存的失效。相應(yīng)的解決方案也很簡(jiǎn)單:將 output 配置字段中的 [hash] 改為 [chunkhash],即可為每個(gè)包添加不同的 hash 值。
最后,在提升面向開(kāi)發(fā)者的打包體驗(yàn)方面,本次優(yōu)化中主要實(shí)現(xiàn)的是 lint 與 Webpack 的解耦。在使用 IDE 開(kāi)發(fā)時(shí),lint 的引入較為繁瑣,因此當(dāng)時(shí)采用的是將 lint 作為 Webpack 的 loader 形式引入,在每次增量打包后執(zhí)行 lint,對(duì)存在不符合風(fēng)格指南的代碼在終端報(bào)錯(cuò)并不予編譯通過(guò)的策略。這個(gè)模式兼容性繞過(guò)了編輯器和 IDE 的配置,因而更加通用,但問(wèn)題在于:
每次打包都需要重復(fù)的 lint 過(guò)程,降低了打包速度。
lint 規(guī)則較嚴(yán)格時(shí),調(diào)試過(guò)程受到了較大的限制。如 class 方法必須存在對(duì) this 的引用、函數(shù)參數(shù)必須全部被使用、不允許 return 后存在業(yè)務(wù)邏輯等 lint 策略,它們雖然確實(shí)能提高代碼質(zhì)量,但在調(diào)試過(guò)程中局部存在這樣的代碼非常常見(jiàn),禁止編譯這些不存在語(yǔ)法問(wèn)題的代碼,對(duì)開(kāi)發(fā)效率存在較大的影響。
因而,在優(yōu)化中果斷去除了 Webpack 的 lint 配置,轉(zhuǎn)而通過(guò) VSCode 等編輯器的 lint 插件實(shí)現(xiàn)開(kāi)發(fā)過(guò)程中的動(dòng)態(tài) lint 提示和自動(dòng)美化。另外,對(duì) Webpack 每次打包的輸出格式也進(jìn)行了優(yōu)化,去除了較多冗余的包信息 log 內(nèi)容,僅保留每次打包的 hash 信息即可。最后的開(kāi)發(fā)體驗(yàn)與新 Webpack 2 項(xiàng)目相近,實(shí)現(xiàn)了一定的開(kāi)發(fā)效率提升。
總結(jié)在維護(hù)過(guò)程中,首先還是理解已有業(yè)務(wù)代碼,然后循序漸進(jìn)地走改良路線,而不應(yīng)以【老代碼好亂】為理由貿(mào)然重寫(xiě),這會(huì)存在很大的風(fēng)險(xiǎn)。雖然 React 本身設(shè)計(jì)較為松散,使得開(kāi)發(fā)者更容易產(chǎn)出較無(wú)序的代碼,但 JS 目前的模塊和 OO 機(jī)制為無(wú)需重寫(xiě)的填坑提供了很大的幫助,實(shí)踐中最后本質(zhì)上重寫(xiě)的也只有新需求相關(guān)的部分,已有的邏輯得到了盡可能的保留和復(fù)用。而性能優(yōu)化則屬于錦上添花的【折騰向】?jī)?nèi)容,優(yōu)先級(jí)較低,可以在時(shí)間相對(duì)寬松的時(shí)候處理,優(yōu)化方式上也有較多的工具和插件支持,相對(duì)需要實(shí)際編碼的業(yè)務(wù)而言,難度較低。
希望以上實(shí)踐經(jīng)驗(yàn)對(duì)于更多開(kāi)發(fā)者的踩坑 / 填坑路能夠有所幫助。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/83198.html
摘要:盡管等待了多年,但是最終還是發(fā)布了正式版本與上一個(gè)版本相比未有重大變化,主要著眼于部分錯(cuò)誤修復(fù)與提升。能夠?qū)惒胶瘮?shù)移入獨(dú)立線程中,可以看做函數(shù)的單函數(shù)簡(jiǎn)化版。不過(guò)需要注意的是,僅支持純函數(shù),其會(huì)在獨(dú)立的作用域中運(yùn)行這些函數(shù)。 showImg(https://segmentfault.com/img/remote/1460000013038757); 前端每周清單專(zhuān)注前端領(lǐng)域內(nèi)容,以對(duì)...
摘要:中簡(jiǎn)單搞定接口訪問(wèn),以及簡(jiǎn)析掘金最近總結(jié)的一些經(jīng)驗(yàn),對(duì)于或中使用接口的一些心得。這里,本文將數(shù)據(jù)結(jié)構(gòu)之學(xué)習(xí)總結(jié)掘金前言前面介紹了的數(shù)據(jù)結(jié)構(gòu),今天抽空學(xué)習(xí)總結(jié)一下另一種數(shù)據(jù)結(jié)構(gòu)。淺析事件傳遞掘金中的事件傳遞主要涉及三個(gè)方法和。 Android 系統(tǒng)中,那些能大幅提高工作效率的 API 匯總(持續(xù)更新中...) - 掘金前言 條條大路通羅馬。工作中,實(shí)現(xiàn)某個(gè)需求的方式往往不是唯一的,這些不...
摘要:在該版本發(fā)布之后,開(kāi)發(fā)團(tuán)隊(duì)并不會(huì)繼續(xù)發(fā)布新的特性,而會(huì)著眼于進(jìn)行重大的錯(cuò)誤修復(fù)。發(fā)布每六個(gè)星期,團(tuán)隊(duì)就會(huì)創(chuàng)建新的分支作為發(fā)布通道,本文即是對(duì)新近發(fā)布的版本進(jìn)行簡(jiǎn)要介紹。 showImg(https://segmentfault.com/img/remote/1460000013229009); 前端每周清單專(zhuān)注前端領(lǐng)域內(nèi)容,以對(duì)外文資料的搜集為主,幫助開(kāi)發(fā)者了解一周前端熱點(diǎn);分為新聞熱...
摘要:純前端開(kāi)發(fā)主要是針對(duì)靜態(tài)頁(yè)面。自主權(quán)最大,正常是使用進(jìn)行輔助開(kāi)發(fā),上線等。大致原因使用是為了和端的保持同步。四總結(jié)對(duì)于比較正式的項(xiàng)目,前端技術(shù)選型策略一定是產(chǎn)品收益最大化,用戶在首位。 對(duì)于前端團(tuán)隊(duì),可以實(shí)現(xiàn)企業(yè)受益最大化要點(diǎn)。 一、技術(shù)選型的策略 1、保證產(chǎn)品質(zhì)量 (1)功能穩(wěn)健:網(wǎng)頁(yè)不白屏,不錯(cuò)位,不卡死;操作正常;數(shù)據(jù)精準(zhǔn)。 (2)體驗(yàn)優(yōu)秀:加載體驗(yàn),交互體驗(yàn),視覺(jué)體驗(yàn),無(wú)障礙訪...
閱讀 3270·2021-11-23 09:51
閱讀 3729·2021-09-22 15:35
閱讀 3717·2021-09-22 10:02
閱讀 3030·2021-08-30 09:49
閱讀 586·2021-08-05 10:01
閱讀 3470·2019-08-30 15:54
閱讀 1727·2019-08-30 15:53
閱讀 3615·2019-08-29 16:27