發(fā)布于:2021-02-05 14:50:20
0
307
0
我們從長(zhǎng)期維護(hù)大型JavaScript應(yīng)用程序中學(xué)到的經(jīng)驗(yàn)教訓(xùn)。
在我們公司,客戶項(xiàng)目通常持續(xù)幾個(gè)月。從第一次與客戶接觸到設(shè)計(jì)階段,再到實(shí)施和初始啟動(dòng),一個(gè)項(xiàng)目大約需要半年的時(shí)間。但有時(shí)我們?cè)趲啄甑臅r(shí)間里開發(fā)和維護(hù)一個(gè)特定的軟件。
例如,我們?cè)?012年為基金會(huì)啟動(dòng)了GED-VIZ,2013年發(fā)布了GED-VIZ,并每隔幾年添加新的功能和數(shù)據(jù)。2016年,我們將核心可視化轉(zhuǎn)變?yōu)榭芍赜脦?kù),對(duì)其進(jìn)行了重大重構(gòu)。如今,央行仍在使用流量數(shù)據(jù)可視化引擎。另一個(gè)長(zhǎng)期項(xiàng)目是OECD數(shù)據(jù)門戶前端:我們于2014年開始實(shí)施,目前仍在擴(kuò)展代碼庫(kù)。
在主開發(fā)階段之后,我們將應(yīng)用修復(fù)程序并添加新特性。通常情況下,沒(méi)有預(yù)算進(jìn)行重大的重構(gòu)甚至重寫。因此,在一些項(xiàng)目中,我堅(jiān)持使用4-6年前編寫的代碼和當(dāng)時(shí)流行的庫(kù)堆棧。
小的改進(jìn)而不是大的重寫
上面提到的兩個(gè)項(xiàng)目都是相當(dāng)大的客戶端JavaScript應(yīng)用程序?,F(xiàn)在,你只能找到幾篇關(guān)于多年來(lái)維護(hù)現(xiàn)有JavaScript代碼庫(kù)的博客文章。你會(huì)發(fā)現(xiàn)很多關(guān)于用現(xiàn)在流行的JavaScript框架重寫前端的帖子。
遷移到一組新的庫(kù)和工具是一項(xiàng)巨大的投資,可能很快就會(huì)有回報(bào)。這樣可以方便維修。它可以降低變革的成本。它允許更快地迭代并更快地實(shí)現(xiàn)新特性。它可以減少誤差,提高魯棒性和性能。最終,這種投資可能會(huì)降低總體擁有成本。
但是當(dāng)一個(gè)客戶無(wú)法進(jìn)行這種投資時(shí),我們會(huì)尋找方法來(lái)逐步改進(jìn)現(xiàn)有的代碼庫(kù)。
從長(zhǎng)期項(xiàng)目中學(xué)習(xí)
對(duì)于一些web開發(fā)人員來(lái)說(shuō),使用現(xiàn)有的代碼庫(kù)是一場(chǎng)噩夢(mèng)。他們以貶義的方式使用“遺留”一詞來(lái)表示他們最近沒(méi)有編寫的代碼。
對(duì)我來(lái)說(shuō),恰恰相反。在幾年的時(shí)間里維護(hù)一個(gè)項(xiàng)目的代碼讓我學(xué)到了更多關(guān)于軟件開發(fā)的知識(shí),而不是多個(gè)短命的、一勞永逸的項(xiàng)目。
最重要的是,它讓我不得不面對(duì)多年前編寫的代碼。我?guī)啄昵白龅臎Q定對(duì)今天的整個(gè)系統(tǒng)都有影響。我今天所做的決定決定了這個(gè)系統(tǒng)長(zhǎng)期的命運(yùn)。
我常常在想:今天我會(huì)做什么不同的事?需要改進(jìn)什么?像每一個(gè)開發(fā)人員一樣,我有時(shí)也會(huì)有一種沖動(dòng),那就是銷毀一切,從頭開始構(gòu)建。
但大多數(shù)時(shí)候,我在現(xiàn)有代碼中遇到的問(wèn)題更加微妙:今天,我將用不同的結(jié)構(gòu)編寫相同的邏輯。讓我向您展示我在JavaScript代碼中發(fā)現(xiàn)的主要結(jié)構(gòu)問(wèn)題。
避免復(fù)雜結(jié)構(gòu)
我所說(shuō)的“復(fù)雜”不僅僅是指大。每個(gè)不平凡的項(xiàng)目都有很多邏輯。有很多案例需要考慮和測(cè)試。要處理的數(shù)據(jù)不同。
復(fù)雜性來(lái)自不同關(guān)注點(diǎn)的交織。人們無(wú)法完全避免這一點(diǎn),但我已經(jīng)學(xué)會(huì)了先分離關(guān)注點(diǎn),然后以可控的方式將它們帶回來(lái)。
讓我們看看JavaScript中的簡(jiǎn)單和復(fù)雜結(jié)構(gòu)。
功能
最簡(jiǎn)單的可重用JavaScript代碼是函數(shù)。特別是一個(gè)純函數(shù),它獲取一些輸入并生成一個(gè)結(jié)果(返回值)。函數(shù)以參數(shù)形式顯式獲取所有必需的數(shù)據(jù)。它不會(huì)更改輸入數(shù)據(jù)或其他上下文數(shù)據(jù)。這樣一個(gè)函數(shù)易于編寫、易于測(cè)試、易于記錄和推理。
編寫好的JavaScript并不一定需要高級(jí)設(shè)計(jì)模式。首先也是最重要的,它需要以一種聰明和有益的方式使用最基本的技術(shù):用做一件事正確的函數(shù)構(gòu)造程序。然后將低級(jí)函數(shù)組合成高級(jí)函數(shù)。
JavaScript中的函數(shù)是成熟的值,也稱為一級(jí)對(duì)象。作為一種多范例語(yǔ)言,JavaScript允許強(qiáng)大的函數(shù)式編程模式。在我的職業(yè)生涯中,我只接觸過(guò)JavaScript函數(shù)式編程的皮毛,但了解基礎(chǔ)知識(shí)已經(jīng)有助于編寫更簡(jiǎn)單的程序。
對(duì)象
下一個(gè)復(fù)雜結(jié)構(gòu)是對(duì)象。在其最簡(jiǎn)單的形式中,對(duì)象將字符串映射為任意值,而沒(méi)有邏輯。但它也可以包含邏輯:函數(shù)在附加到對(duì)象時(shí)成為方法。
const cat = {
name: 'Maru',
meow() {
window.alert(`${this.name} says MEOW`);
}
};
cat.meow();
JavaScript中的對(duì)象無(wú)處不在,用途廣泛。一個(gè)對(duì)象可以作為一個(gè)附加了多個(gè)處理函數(shù)的參數(shù)包。對(duì)象可以對(duì)相關(guān)值進(jìn)行分組,但也可以構(gòu)造程序。例如,您可以在一個(gè)對(duì)象上放置幾個(gè)類似的函數(shù),并讓它們對(duì)相同的數(shù)據(jù)進(jìn)行操作。
類
JavaScript中最復(fù)雜的結(jié)構(gòu)是類。它是對(duì)象的藍(lán)圖,同時(shí)也是這些對(duì)象的工廠。它將原型繼承與對(duì)象的創(chuàng)建相結(jié)合。它將邏輯(函數(shù))與數(shù)據(jù)(實(shí)例屬性)交織在一起。有時(shí)構(gòu)造函數(shù)上有一些屬性,稱為“靜態(tài)”屬性。像“singleton”這樣的模式用更多的邏輯重載了一個(gè)類。
類是面向?qū)ο笳Z(yǔ)言中的常見工具,但它們需要設(shè)計(jì)模式的知識(shí)和對(duì)象建模的經(jīng)驗(yàn)。尤其是在JavaScript中,它們很難管理:構(gòu)建繼承鏈、對(duì)象組合、應(yīng)用mixin、超級(jí)調(diào)用、處理實(shí)例屬性、getter和setter、方法綁定、封裝,ECMAScript既沒(méi)有為常見的OOP概念提供標(biāo)準(zhǔn)的解決方案,也沒(méi)有為社區(qū)提供關(guān)于類使用的最佳實(shí)踐。
如果類有一個(gè)明確的目的,那么它們是合適的。我學(xué)會(huì)了避免在課堂上增加更多的關(guān)注點(diǎn)。例如,有狀態(tài)的React組件通常被聲明為類。這對(duì)于特定的問(wèn)題域是有意義的。它們有一個(gè)明確的目的:將道具、狀態(tài)和對(duì)兩者都起作用的幾個(gè)函數(shù)分組。類的中心是render
函數(shù)。
我不再用更多松散相關(guān)的邏輯來(lái)豐富這些類。值得注意的是,React團(tuán)隊(duì)正在慢慢地從類轉(zhuǎn)向有狀態(tài)的功能組件。
同樣,Angular中的組件類是幾個(gè)關(guān)注點(diǎn)的交叉點(diǎn):使用@Component()
裝飾器應(yīng)用的元數(shù)據(jù)字段。基于構(gòu)造函數(shù)的依賴注入。狀態(tài)為實(shí)例屬性(輸入、輸出以及自定義公共和私有屬性)。這類課程根本不是簡(jiǎn)單或單一的目的。它們是可管理的,只要它們只包含所需的特定于角度的邏輯。
選擇結(jié)構(gòu)
多年來(lái),我一直遵循以下準(zhǔn)則:
使用最直接、最靈活、最通用的結(jié)構(gòu):函數(shù)。如果可能,讓它成為純函數(shù)。
如果可能,避免在對(duì)象中混合數(shù)據(jù)和邏輯。
如果可能,避免使用類。如果你使用它們,讓它們做一件事。
大多數(shù)JavaScript框架都有自己的代碼結(jié)構(gòu)。在基于組件的UI框架(如React和Angular)中,組件通常是對(duì)象或類。選擇組合而不是繼承很容易:只需創(chuàng)建一個(gè)新的輕量級(jí)組件類來(lái)分離關(guān)注點(diǎn)。
這并不意味著需要堅(jiān)持這些結(jié)構(gòu)來(lái)建模業(yè)務(wù)邏輯。最好將這個(gè)邏輯放到函數(shù)中,并將它們從UI框架中分離出來(lái)。這允許分別開發(fā)框架代碼和業(yè)務(wù)邏輯。
模塊
管理JavaScript文件和外部庫(kù)之間的依賴關(guān)系過(guò)去是一團(tuán)糟。在9elements,我們是CommonJS或AMD模塊的早期采用者。后來(lái),社區(qū)決定使用標(biāo)準(zhǔn)的ECMAScript 6模塊。
模塊成為JavaScript中的一種基本代碼結(jié)構(gòu)。這取決于它們的用法是簡(jiǎn)單還是復(fù)雜。
隨著時(shí)間的推移,我對(duì)模塊的使用也發(fā)生了變化。我以前用多次導(dǎo)出創(chuàng)建相當(dāng)大的文件?;蛘撸瑔蝹€(gè)導(dǎo)出是一個(gè)巨大的對(duì)象,它將一組常量和函數(shù)分組。今天我嘗試用一個(gè)導(dǎo)出或幾個(gè)導(dǎo)出來(lái)創(chuàng)建小型的、扁平的模塊。這將導(dǎo)致每個(gè)函數(shù)一個(gè)文件,每個(gè)類一個(gè)文件,以此類推。文件foo.js
如下所示:
export default function foo(…) {…}
如果您更喜歡命名導(dǎo)出而不是默認(rèn)導(dǎo)出:
export function foo(…) {…}
這使得單個(gè)函數(shù)更易于引用和重用。以我的經(jīng)驗(yàn),很多小文件不會(huì)帶來(lái)很大的成本。它們?cè)试S更容易地在代碼中導(dǎo)航。此外,特定代碼段的依賴關(guān)系也可以更有效地聲明。
避免創(chuàng)建非類型化對(duì)象
JavaScript最好的特性之一是對(duì)象文本。它允許您快速創(chuàng)建具有任意屬性的對(duì)象。我們已經(jīng)看到了上面的一個(gè)例子:
const cat = {
name: 'Maru',
meow() {
window.alert(`${this.name} says MEOW`);
}
};
JavaScript對(duì)象表示法是如此簡(jiǎn)單和富有表現(xiàn)力,以至于它被轉(zhuǎn)換成了一種如今無(wú)處不在的獨(dú)立數(shù)據(jù)格式:JSON。但是在ECMAScript版本的過(guò)程中,對(duì)象文本獲得了越來(lái)越多超出其最初用途的特性。新的ECMAScript特性,比如objectrest/Spread,允許更自由地創(chuàng)建和混合對(duì)象。
在一個(gè)小的代碼庫(kù)中,動(dòng)態(tài)創(chuàng)建對(duì)象是一個(gè)高效的特性。不過(guò),在大型代碼庫(kù)中,對(duì)象文字成為一種負(fù)擔(dān)。在我看來(lái),具有任意屬性的對(duì)象不應(yīng)該存在于這樣的項(xiàng)目中。
問(wèn)題不在于對(duì)象本身。問(wèn)題是不符合中心類型定義的對(duì)象。它們通常是運(yùn)行時(shí)錯(cuò)誤的來(lái)源:屬性可能存在或不存在,可能有某種類型或沒(méi)有。對(duì)象可以具有所有必需的屬性,但也可以具有更多屬性。通過(guò)閱讀代碼,您無(wú)法判斷對(duì)象在運(yùn)行時(shí)將具有哪些屬性。
JavaScript沒(méi)有類型定義,但是有幾種方法可以以更受控制的方式創(chuàng)建對(duì)象。例如,一個(gè)函數(shù)可以用來(lái)創(chuàng)建所有看起來(lái)相似的對(duì)象。該函數(shù)確保所需的屬性存在且有效,或者具有默認(rèn)值。另一種方法是使用創(chuàng)建死簡(jiǎn)單值對(duì)象的類。
同樣,函數(shù)可以在運(yùn)行時(shí)檢查參數(shù)是否可用。它可以使用typeof
、instanceof
、Number.isNaN
等顯式檢查類型,也可以隱式使用duck類型。
一個(gè)更徹底的解決方案是用類型定義來(lái)豐富JavaScript,比如TypeScript或Flow。例如,在TypeScript中,首先定義重要數(shù)據(jù)模型的接口。函數(shù)聲明其參數(shù)的類型和返回值。TypeScript編譯器確保只傳遞允許的類型,因?yàn)榫幾g器可以訪問(wèn)所有調(diào)用。
作者介紹
熱門博客推薦