尋找頭緒:編寫可維護的 JavaScript

十年蹤跡發表於2016-05-25

幾乎每個程式設計師都有接手維護別人遺留專案的經歷。或者,有可能一個老專案某一天又被重新啟動。通常情況下,接手老專案都會讓人恨不得拋棄掉整個程式碼庫從頭開始。老程式碼凌亂、文件缺失、需要研究很多天才能完全搞明白它。然而,通過合適的規劃、分解和好的工作流,專案程式碼可以變得乾淨、有組織和可擴充套件。

尋找頭緒:編寫可維護的 JavaScript

我曾經接手清理許多專案的程式碼,讓我不得不重頭開始的專案真心不多,不過我最近就遇到了一個。我從中學到了很多關於 JavaScript 程式碼組織的內容,以及最重要的是冷靜,不要為你的前任抓狂。在這篇文章裡,我想要讓你知道我是怎麼一步步處理專案程式碼的,告訴你我的經驗。

分析專案

第一步是先概覽整個專案,弄明白問題所在。如果這是一個網站,通過點選測試所有功能點:開啟模組、提交表單以及其他的。在做這件事的時候,開啟開發者工具,看看是否有任何報錯,看看控制檯有沒有日誌。如果這是一個Node.js專案,開啟命令列介面然後檢查各個API。最好的情況是專案通過統一的入口(例如:main.js, index.js, app.js, ……)來初始化所有的模組,最差的情況是整個業務邏輯散落於各處。

搞清楚專案採用了哪些工具。是 jQueryReact 或是 Express?將所有重要的資訊整理在一個清單裡。假設這個專案是用 Angular 2 寫的,你之前對 Angular 2 不熟悉,先去檢視文件,對它有一個初步的瞭解,並尋找最佳實踐的範例。

從更高層面上理解專案

瞭解技術點是一個好開端,但要進一步深入理解,我們需要看一看它的單元測試。單元測試通過測試程式碼的功能函式與方法來保證程式碼如預期的那樣執行。不同於僅僅閱讀程式碼,檢視和執行單元測試能讓你更加深入理解程式碼的期望執行結果。如果接手的專案沒寫單元測試,沒關係,我們可以自己來寫。

建立基線

這樣做是為了確立一致性。你已經瞭解了專案的工具鏈、程式碼結構、模組的邏輯關係,現在該為專案建立基線了。我推薦新增一個 .editorconfig 檔案讓程式碼風格在不同的編輯器、IED和不同的開發者之間保持一致。

一致的縮排

使用 tab 還是空格來縮排是一個老問題 ,常引發程式設計師爭論不休,不過沒關係,不管專案用的是空格還是 tab,繼續使用之前的就好了。除非程式碼庫既有空格縮排又有 tab 縮排的程式碼,那就只好在兩者中做出一個取捨。每個人都可以保持自己的觀點,但一個好的專案要保證所有的開發者都可以無爭議地協同工作。

為什麼這個很重要?因為每一個人都有自己使用編輯器或 IDE 的習慣。比如,我是個程式碼摺疊控,要是編輯器沒有正確的程式碼摺疊功能,我整個人都會迷失在檔案裡。如果程式碼的縮排不一致,就會影響到摺疊功能,因此每一次我開啟一個檔案,不得不先修復那些縮排然後才能開始工作,這十分浪費時間。

命名

確保專案中使用命名約定是值得推崇的做法。駝峰命名通常在 JavaScript 程式碼中使用,但是我看到過許多不一致的命名。例如:jQuery 專案經常會有程式碼在命名上混淆 jQuery 物件和其他物件。

程式碼檢查

之前所做的一切是在美化程式碼,主要是讓它變得更易於檢查。接下來我們介紹保證程式碼質量的通用最佳實踐。ESLintJSLint,還有 JSHint 是目前最受歡迎的三個 JavaScript 程式碼檢查工具。我個人用 JSHint 最多,但我現在最喜歡 ESLint,主要是因為它可以自定義規則,也較早地支援了 ES2015。

一旦你開始程式碼檢查,如果有很多錯誤資訊出現,立即修復它們。別跳過這些步驟,直到你的程式碼檢查工具對你的程式碼徹底滿意了。

升級依賴

升級依賴需要仔細些,如果你不注意依賴本身的升級帶來的一些變化,就很容易導致錯誤。一些專案可能只能依賴某些庫的固定版本(例如:v1.12.5),而另一些則使用版本萬用字元(例如:v1.12.x)。如果你要快速升級依賴,你需要知道依賴模組的版本號通常按如下規則建立:主版本.小版本.補丁。如果你對 semantic versioning 的方式不熟悉,我推薦你先閱讀 Tim Oxley 的這篇文章

升級依賴沒有通用方法。每個專案是不一樣的,需要區別對待。升級依賴的補丁版本通常不會出什麼問題,小版本一般也還OK。但如果你要升級依賴的主版本,你就需要仔細檢查版本升級帶來的改變。有可能 API 完全改變了,那樣你就得重寫你專案的一大堆程式碼。如果非必要,我一般避免將依賴升級到下一個主版本。

如果你的專案使用 npm 來管理依賴,你可以很方便地使用 npm outdated 命令來檢查你的依賴是否已經過時了。我用一個專案 FrontBook 來舉例說明,在這個專案裡,我經常升級所有的依賴:

Image of npm outdated

如你所見,我這個專案裡的依賴有很多主版本升級。我不會一次將他們全部升級,但是會一次升級一個。雖然這會耗費許多時間,但這是確保不會出問題的唯一辦法(尤其是如果這個專案沒有任何測試)。

下面該幹髒活了

我必須讓你知道的非常重要的一點是,清理程式碼並不意味著需要移除和重寫大量的程式碼片段。當然,有時候這可能是唯一的解決辦法,但是這不應該是你的首選方案。JavaScript 特別靈活,因此難以給出一般性的建議,通常情況下你必須對症下藥。

建立單元測試

單元測試能保證你理解程式碼是如何工作的,這樣避免一些意外導致錯誤。JavaScript 單元測試的內容足夠寫另一篇文章,所以我在這裡不能詳細介紹。目前被廣泛使用的單元測試框架有 KarmaJasmineMocha 以及 Ava。如果你還要測試你的使用者介面,Nightwatch.jsDalekJS 是適合瀏覽器自動化測試的工具。

單元測試和瀏覽器自動化測試的區別是,前者測試你的 JavaScript 程式碼本身,來確保你所有的模組和主要邏輯執行無誤。後者,測試使用者介面,確保介面元素在正確的位置,且如預期地工作。

在你開始動手重構程式碼之前,認真對待單元測試,那樣你的專案的穩定性將得到改善,而你甚至還沒有開始考慮可擴充套件性。單元測試帶來的另一個好處是你不再需要無時無刻擔心你的改動會無意中破壞原有功能。

Rebecca Murphey 寫了一篇很棒的文章關於如何為現有程式碼寫單元測試

架構

JavaScript 架構是另一個大話題。重構和清理架構歸結於你在這方面積累了多少經驗。我們可以選擇許多不同的設計模式,但不是所有的模式都適合於提升可擴充套件性。限於篇幅,我不能涵蓋所有模式,但我至少可以給你一些通用的建議。

首先,你需要找出哪些設計模式在你的專案中已經使用到了。閱讀有關這些模式的部分,確保它們在專案中使用上保持一致性。可擴充套件性的關鍵之一便是堅持一致的模式,避免混搭。當然,你可以針對專案中的不同目的採用不同的設計模式(例如,將單例模式用於資料結構和短名稱空間的輔助功能函式,以及將觀察者模式用於與模組),但是別對一個模組使用了一種設計模式,對另一個模組又用另一種不同的設計模式。

如果你的專案沒有任何架構(可能一切都堆在一個巨大的app.js檔案裡),從現在開始讓它有架構。不過別指望一口吃成胖子,需要一點一點來。再次強調,沒有對任何專案都適用的萬精油方案,每一個專案的情況都是不同的。根據規模和複雜度不同,專案檔案目錄結構各有不同。通常,最基本的原則是,目錄結構應當將第三方庫、模組、資料以及負責初始化模組與邏輯的入口檔案(比如:index.jsmain.js)分開來。

簡而言之就是模組化

將一切模組化?

模組化不是解決 JavaScript 擴充套件性問題的唯一選擇。模組化增加了一層 API,開發者不得不額外去熟悉它們。這雖然增加了麻煩,但是值得去做的。模組化的基本原則是將所有功能拆分為小模組。這麼做了以後,不僅讓你更容易解決程式碼裡的問題,也讓專案組的其他成員更容易協同工作。每個模組只做一件事,它們不用關心外部邏輯,可以被複用在不同的地方。

如何將一大堆功能拆分成許多邏輯關聯的小模組?讓我們來做做看:

上面的程式碼不是模組化的。所有的功能都耦合在一起。想象一下,如果這是更復雜的函式,由於出了一些錯誤你必須除錯它們,可能 API 沒返回,可能某些原因 JSON 內部的值被改變或者別的什麼問題。除錯這一大坨程式碼如同噩夢般,不是嗎?
讓我們將程式碼按不同的職責拆分開來:

好了,我們現在有了三個新模組,讓我們看看重構之後的 fetch 呼叫:

當然我們還可以進一步將 .then( ) 中的邏輯也抽出來形成模組,不過我想我已經充分表達了模組化的含義。
如果不想模組化呢?
如我前面提到的,將你的程式碼拆成小模組會增加額外的一層 API。如果你不想這麼做,但是又想要讓程式碼易於與其他開發者一起維護,不拆函式也沒問題,你依然可以將你的程式碼分解成更簡單的部分並把重點放在可測試的程式碼上。
為你的程式碼撰寫文件
文件化是一個老生常談的話題。程式設計師社群的一部分人提倡將一切文件化,而另一部分人認為好程式碼即是文件。我奉行中庸之道,我覺得程式碼的可讀和可擴充套件之間需要保持平衡。你可以使用 JSDoc 來幫助你實現文件化。
JSDoc 是一個 JavaScript 的 API 文件生成器。常用的編輯器和 IDE 都有支援它的外掛。我們看一個例子:

這個函式有兩個引數,遍歷一個物件,返回一個陣列。這也許不是一個過於複雜的方法,但是對於沒寫過這段程式碼的人來說,搞懂它還是有點費勁。此外,這個方法具體的作用也不是很明確。讓我們對它文件化:

我沒有改變任何程式碼,只是改了一下函式名,新增了一段簡短的註釋,就已經讓這段程式碼的可讀性變得好多了。

有條理的程式碼提交工作流

重構本身是一項艱鉅的任務。為了能隨時回滾你的修改(假如你破壞了一些原有功能,過了一會才意識到,你可能就需要回滾程式碼到之前版本),你的每一部分修改,比如重寫了一個方法、重新命名了一個名字空間,都應該及時提交到 git (或者 svn)。這麼做也許會讓你覺得麻煩,但這麼做有助於讓你的清理工作更有條理。

為你的重構工作開一個新的分支,千萬別總是在主線 (master) 上改。因為主線版本你有可能需要臨時做一些更新或者隨時釋出一些 bug fixes 到線上環境,而你又不能將你沒有經過測試的和未完成的重構一同釋出到線上,因此我建議你還是應該在不同的分支上工作。

在 GitHub 上有一份有趣的指導,是關於如何使用他們的版本控制流程的。

別失去理智

除了用技術解決問題之外,有一個很重要的點很少被人提及:別為你的前任抓狂。我無意指責任何人,但是我知道一些人經歷過這種情況。我花了很多年的時間去理解和克服心理上的不爽。我曾經對前任開發者們留下的程式碼、解決方案感到有些抓狂,他們做的一切在我眼裡看來都造成混亂。

結果,這些消極情緒沒帶給我任何好處。消極情緒會導致你過度重構,浪費你的時間,而且可能破壞一些原有功能,而這一切又導致你越來越惱怒。你可能會花費額外的時間去重寫一個本來毫無問題的模組,沒有人會因此感謝你,因為你在做無用功。先分析狀況,然後做有價值的重構。在任何時候,你隨時可以對一個模組做一些細節的改進。

一段程式碼為什麼寫成這樣往往是有歷史原因的,也許前任程式設計師沒有足夠的時間將程式碼寫得足夠好、或者不知道有更好的寫法,或者別的什麼原因。我們都是過來人。

整理一下

讓我們從頭回顧一下所有的步驟,為你的下一個專案建立一個 checklist:

  1. 分析專案
    • 先忘掉自己的開發者身份,以一個使用者的身份來看清它的全貌。
    • 瀏覽程式碼庫,列出專案使用的工具。
    • 閱讀專案相關工具的文件和最佳實踐。
    • 瀏覽單元測試,從更高層面上了解專案。
  2. 建立基線
    • 引入 .editorconfig 以保證在所有的編輯器和 IDE 下保持程式碼風格一致。
    • 使縮排風格一致,至於是用 tab 還是空格,無所謂。
    • 執行命名約定。
    • 如果程式碼檢查工具不存在, 新增一個,可以是 ESLintJSLint 或者 JSHint
    • 升級依賴,但是需要格外小心,弄清楚到底升級了什麼。
  3. 清理程式碼
    • 建立單元測試與瀏覽器自動化測試,可以使用一些工具,例如 KarmaJasmine、或者 Nightwatch.js
    • 確保架構和設計模式保持一致。
    • 不要混用 設計模式,堅持使用已經存在的設計模式。
    • 決定你是否需要將程式碼庫拆分成模組。每一個模組應當具有單一的目的,模組不用關心自身之外的其他邏輯。
    • 如果你不想拆分模組,把重點放在可測試的程式碼上,把它們分解成更簡單的程式碼塊。
    • 恰當地命名你的函式,為程式碼適當撰寫文件,保持可讀和可擴充套件的平衡。
    • 使用 JSDoc 來生成文件。
    • 定期提交程式碼,特別在有重要改變時。這樣如果有什麼改錯了,可以方便回滾。
  4. 別失去理智
    • 別為你的前任開發者抓狂。負面情緒只會導致過度重構而浪費時間。
    • 一段程式碼為什麼寫成這樣總是有原因的。要牢記我們都是過來人。

我非常希望這篇文章能幫到你。如果你正在為程式碼重構做這些努力,或者你有一些我沒有提到過的好建議,我希望你可以告訴我。

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

尋找頭緒:編寫可維護的 JavaScript 尋找頭緒:編寫可維護的 JavaScript

相關文章