幾乎每個開發者都接手或維護過遺留專案,或者說是重啟一箇舊的專案。通常第一反應是拋棄原有的程式碼,從頭開始寫。這些程式碼會混亂不堪,沒有文件,並且別人可能要花費好幾天去讀懂程式碼。但是,如果結合正確的規劃、分析、和一個好的工作流程,那就有可能把一個義大利麵式的程式碼倉庫整理成一個整潔、有組織並易擴充套件的一份專案程式碼。
我曾經不得不接手並整理了很多的專案。從一開始就混亂不堪的也不是特別多。但實際上,最近就遇到了一個這樣的情況。我已經學會了關於JavaScript程式碼組織的很多知識,最重要的是,不要被之前的程式設計師逼瘋。在這篇文章中我想分享下一些我的步驟和我的經驗。
分析專案
開始的第一步是簡要看一下要做什麼。如果是個網站,點選網站所有的功能:開啟對話方塊、提交表單等等。做這些的時候,開啟你的開發者工具,看下是否有報錯或輸出日誌。如果是個node.js專案,開啟命令列介面檢查一下api。最好的情況是專案有一個入口(例如main.js,index.js,app.js),通過入口能將所有的模組初始化;如果是最壞的情況,也要找到每個業務邏輯的位置。
找出使用的工具。jquery? React? Express? 列出需要了解的一切重要的東西。如果所在專案使用 angular2 寫的,而你還沒有使用過,直接去看文件,最起碼有個基本的瞭解。總之尋找最佳實踐。
深入的瞭解專案
瞭解技術是一個好的開始,但是要得到真實的感覺和理解,需要研究一下 單元測試 。單元測試是用來測試程式碼的功能和方法是否按預期呼叫的一種方式。相比閱讀和執行程式碼,單元測試能更深入的幫你瞭解程式碼。如果在你的專案中還沒有單元測試,別急,我們接著往下看。
建立一個規範
這些都是關於程式碼一致性的內容。現在你已經瞭解了專案中使用的所有工具集,你知道了程式碼的結構和邏輯功能的位置,是時候建立一個規範。我建議新增一個 .editorconfig
檔案來保證程式碼在不同的編輯器、IDE 或不同的開發者之間的編寫風格一致。
正確的縮排
這是一個 飽受爭議 (跟戰爭一樣),程式碼中使用空格還是tab,其實這不重要。如果之前程式碼用的空格,那麼使用空格,如果使用tab,繼續用tab。只有當程式碼中都用到的時候才有必要決定使用其中的哪一個。討論的觀點是好的,但是一個好的專案必須保證所有的開發者能在一起和諧的工作。
為什麼這很重要。因為每個人都有使用自己編輯器或使用 IDE 的方式。舉例來說,我是 code folding 的追捧者。沒有這些特性,我幾乎會在檔案中迷失。如果縮排不一樣,那麼程式碼看起來會很亂。所以,每次開啟一個檔案,在我開始工作之前必須修復縮排的問題。這很浪費時間。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// While this is valid JavaScript, the block can't // be properly folded due to its mixed indentation. function foo (data) { let property = String(data); if (property === 'bar') { property = doSomething(property); } //... more logic. } // Correct indentation makes the code block foldable, // enabling a better experience and clean codebase. function foo (data) { let property = String(data); if (property === 'bar') { property = doSomething(property); } //... more logic. } |
命名
保證專案裡面使用到的命名規則是合理的。通常 JavaScript 使用駝峰式命名方式,但是我看到了很多混合式的命名方式。舉例來說,jQuery 專案常常含有 jQuery 變數和其他變數的混合命名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Inconsistent naming makes it harder // to scan and understand the code. It can also // lead to false expectations. const $element = $('.element'); function _privateMethod () { const self = $(this); const _internalElement = $('.internal-element'); let $data = element.data('foo'); //... more logic. } // This is much easier and faster to understand. const $element = $('.element'); function _privateMethod () { const $this = $(this); const $internalElement = $('.internal-element'); let elementData = $element.data('foo'); //... more logic. } |
儘可能使用 lint
前面的幾步會使我們的程式碼變得好看些,能夠幫助我們快速地瀏覽程式碼,在此我還要推薦保證程式碼整潔性的最佳實踐方案。 ESlint,JSlint , JSHint 是現在最流行的 JavaScript 格式工具。個人來說,之前使用 JSLint 比較多,現在開始又開始喜歡 ESlint 了,主要是它的一些自定義規則和最早支援 ES2015 語法很好用。
當你使用 lint 時,如果編輯器報了一堆錯誤,那麼修復它們。在此之前什麼也不要做。
更新依賴
更新依賴需要非常謹慎,如果你更換或更新了依賴很容易引發更多的錯誤,所以一些專案可能在某個版本(例如 v1.12.5)下面正常工作,然而萬用字元匹配到另個版本(例如 v1.22.x)就出問題了。這種情況下,你需要快速升級,版本號一般是這樣的: MAJOR.MINOR.PATCH 。如果你還對語義化版本不熟悉,建議先讀下 Tim Oxley 的這篇文章– Semver: A Primer 。
升級依賴沒有通用的處理規則。每個專案不同,必須區分對待。專案中升級補丁版本號一般都不是問題,也可以建立副本使用。只有當依賴中主版本號內容發生衝突錯誤時,那就應該看下具體是什麼發生了變化。可能 API 改變了,那樣你就要大面積重寫你專案中的程式碼。如果那覺得這樣代價太高,那麼我建議不要升級這個主版本號。
如果你使用 npm 來管理依賴(而且基本沒什麼其他好的方案了),你可以使用 npm outdated 命令在你的CLI裡來檢查哪些依賴版本是比較舊的。我舉一個我專案裡面叫 FrontBook 的例子,在這個專案中我經常更新依賴:
如你所見,我這裡有很多更新。但我一般不會馬上更新,而是一次更新一個。可以說,這樣花費了很多時間,然而這是保證不出問題的唯一方法(如果專案沒有任何測試用例的話)。
讓我們動起手來
這裡我主要要表達的是整理專案並不一定意味著移除或重寫大部分的程式碼。當然,有時候這是唯一的解決方案,但是這不是你一開始就應該考慮的問題。JavaScript 程式碼很可能成為一個奇怪的程式碼,後面去做一些調整通常是不可能的。我們通常需要根據特定的場景來給出一個改造方案。
建立單元測試
使用測試用例可以保證你的程式碼能進行正確的執行而不會出現意外的錯誤。JavaScript 單元測試直接可以寫出很多文章,所有我這裡沒辦法介紹太多。廣泛使用的框架有 karma、jasmine、macha 和 ava 等。如果你也想測試你的使用者介面,推薦使用 Nightwatch.js 和 Dalekjs 這類瀏覽器自動測試工具。
單元測試和瀏覽器自動化測試的區別是,前者測試 JavaScript 本身程式碼。它保證了所有的模組和通用邏輯能預期執行。瀏覽器自動化,另一方面來說是測試介面,也就是專案的使用者介面,保證頁面上的元素在預期正確的位置。
在重構任何事情之前先建立好單元測試。這樣專案的穩定性會提升,或許你沒有考慮過專案穩定性的東西。這樣做的好處是,不必擔心犯一些自己沒有意識到的錯誤。
Rebecca Murphey 寫過一篇非常不錯的文章 writing unit tests for existing JavaScript 。
架構
JavaScript 架構是另一個大的主題。重構和整理框架決定於你在這方面有多少經驗。我們在軟體開發中有很多的設計模式。但是並不是所有的都能適應穩定性的需求。很不幸,這篇文章中我不能給出所有的場景,但至少還是可以給一些通用性的建議。
首先,你要知道你的專案中使用到了那種設計模式。瞭解下這種模式,並保證它在整個專案是一致的。可擴充套件性的一個關鍵的地方是和設計模式相結合的,而不是混合的方法。當然,在你的專案裡可以使用不同的設計模式來達到不同的目的(例如使用 單例模式 來建立資料結構或者短命名的工具函式,或者在模組中使用 觀察者模式 ),但是絕對不要在一個模組中使用了一種設計模式,而在另外的模組中使用不同的模式。
如果在你的專案中個確實沒有使用到什麼架構(可能什麼程式碼都在一個巨大的 app.js 中 ),那麼是時候改變它了。但不要馬上做所有的改變,而是一點一點的來。同樣,這裡沒有萬能的方法,每個專案的設定也是不一樣的。專案目錄結構根據專案的規模和複雜度不同也不一樣。通常,對於最基本的層級,結構一般分為分為第三方內容、模組內容、資料和一個初始化所有模組和邏輯的入口(例如 index.js、main.js)。
這樣我們就需要模組化了。
所有的東西都模組化?
模組化至今也不是大規模可擴充套件 JavaScript 專案的解決方案。它需要開發者必須去熟悉另一層 API 。儘管這樣可能會帶來很多的困難,但它的原則是把你的功能劃分成小的模組。這樣,在團隊協作過程中解決問題就變的更簡單了。每個模組應該有個一個明確的目標功能點。一個模組應該是不知道你外面程式碼邏輯是什麼樣的,並且能在不同的地方和場景下複用。
那麼怎樣將大量關聯邏輯的程式碼拆分成模組呢?一起看下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// This example uses the Fetch API to request an API. Let's assume // that it returns a JSON file with some basic content. We then create a // new element, count all characters from some fictional content // and insert it somewhere in your UI. fetch('https://api.somewebsite.io/post/61454e0126ebb8a2e85d', { method: 'GET' }) .then(response => { if (response.status === 200) { return response.json(); } }) .then(json => { if (json) { Object.keys(json).forEach(key => { const item = json[key]; const count = item.content.trim().replace(/s+/gi, '').length; const el = ` <div class="foo-${item.className}"> <p>Total characters: ${count}</p> </div> `; const wrapper = document.querySelector('.info-element'); wrapper.innerHTML = el; }); } }) .catch(error => console.error(error)); |
這裡基本沒有模組化。所有的東西都是緊密結合的,並且相互依賴。想象一下在更大、更復雜的函式裡,如果出了問題你需要除錯 bug。可能API 不響應、JSON 裡面欄位改變了或者其他的問題。這簡直是噩夢。
讓我們把它按照不同職責分離開來:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// In the previous example we had a function that counted // the characters of a string. Let's turn that into a module. function countCharacters (text) { const removeWhitespace = /s+/gi; return text.trim().replace(removeWhitespace, '').length; } // The part where we had a string with some markup in it, // is also a proper module now. We use the DOM API to create // the HTML, instead of inserting it with a string. function createWrapperElement (cssClass, content) { const className = cssClass || 'default'; const wrapperElement = document.createElement('div'); const textElement = document.createElement('p'); const textNode = document.createTextNode(`Total characters: ${content}`); wrapperElement.classList.add(className); textElement.appendChild(textNode); wrapperElement.appendChild(textElement); return wrapperElement; } // The anonymous function from the .forEach() method, // should also be its own module. function appendCharacterCount (config) { const wordCount = countCharacters(config.content); const wrapperElement = createWrapperElement(config.className, wordCount); const infoElement = document.querySelector('.info-element'); infoElement.appendChild(wrapperElement); } |
很好,我們現在有三個模組了,我們來看下呼叫的情況:
1 2 3 4 5 6 7 8 9 10 11 12 |
fetch('https://api.somewebsite.io/post/61454e0126ebb8a2e85d', { method: 'GET' }) .then(response => { if (response.status === 200) { return response.json(); } }) .then(json => { if (json) { Object.keys(json).forEach(key => appendCharacterCount(json[key])) } }) .catch(error => console.error(error)); |
我們將 .then() 裡面的方法提取了出來,這裡我想我已經向大家演示了模組化的意思了。
如果不用模組化會怎麼樣
正如我所說的,將你的專案程式碼分成增加了 API 的小模組 。如果你不想這樣,又想保證程式碼在團隊協作中的整潔性,那絕對就是使用更大的函式。但是你仍然可以將你的程式碼拆分成多個簡單的部分並專注於可測試程式碼。
寫註釋
註釋是一個很沉重的討論話題。程式設計社群中一部分人主張為任何東西書寫註釋,而另一部分人認為 自帶必要註釋 的程式碼就夠了。就像生活中很多事情一樣,我想在兩者之間做一個平衡是最好的選擇。這裡推薦使用 JSDoc 來管理你的文件。
JSDoc 是一個 JavaScript 的 API 文件生成器。通常可以在 IDE 外掛裡面使用。例如
1 2 3 4 5 6 7 8 9 10 11 12 |
function properties (name, obj = {}) { if (!name) return; const arr = []; Object.keys(obj).forEach(key => { if (arr.indexOf(obj[key][name]) <= -1) { arr.push(obj[key][name]); } }); return arr; } |
這個函式接受兩個引數後遍歷一個物件,然後返回一個陣列。這段程式碼不是很複雜,但是對於沒有接觸過這段程式碼的人還是需要一點時間來弄明白髮生了什麼事情。此外,函式的功能不是很明確。所以註釋可以這樣寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * Iterates over an object, pushes all properties matching 'name' into * a new array, but only once per occurance. * @param {String} propertyName - Name of the property you want * @param {Object} obj - The object you want to iterate over * @return {Array} */ function getArrayOfProperties (propertyName, obj = {}) { if (!propertyName) return; const properties = []; Object.keys(obj).forEach(child => { if (properties.indexOf(obj[child][propertyName]) <= -1) { properties.push(obj[child][propertyName]); } }); return properties; } |
我沒有接觸太多程式碼本身。只是通過重新命名了函式並且新增了一個簡短的描述性註釋塊,這樣就提升了程式碼的可讀性。
擁有一個有組織的提交工作流程
重構本身是件巨大的工作。為了可以回滾更改(實際上你嚐嚐寫錯了但是後來才知道),我建議提交你的每一次修改。重寫了一個方法? git commit (或者 svn commit ,如果你用的是 SVN)。重新命名一個名字,檔名或者一些圖片? git commit
。你可能已經懂了,我強烈建議一些人使用,它確實能幫你做整理和組織化程式碼。
在一個新分支上重構,不要在主幹上直接修改。你可能需要快速修改主幹並提交 bug fixes 到正式環境,但是你又不想你的重構程式碼沒有測試或完成就上線。所以建議在另一個分支上開始。
假如你需要快速瞭解 git 的工作原理。這裡有一個 GitHub 上的版本控制的 介紹 。
怎樣不會瘋掉
除了保持程式碼整潔的技術步驟之外,還有重要的一步——我很少看到別的地方有提到(就是負面情緒):不要被之前的開發者逼瘋。當然,這並不是說每個人,但是我知道有些人經歷過。我花了數年時間才真正明白並克服它。我曾經幾乎被我前面的開發者逼瘋了,完全不明白他的程式碼、解決方案以及為什麼一切都是這麼亂套。
最後,這些負面情緒沒有改變什麼。沒有幫我重構程式碼,反而浪費了我的時間,讓我的程式碼出錯。這會讓你越來越鬱悶。因為你可能花掉幾個小時去重構一個功能,並且沒有人會感謝你重寫了一個已經存在的模組。這不值得。做需要的事情,然後分析處境。你可能經常需要重構一些模組裡面很小的部分。
程式碼為什麼是這樣寫總是有原因的。可能之前的程式設計師並沒有時間去思考正確的方法,或者因為其他原因。我們自己也是。
總結一下
讓我們再來梳理一下,為下一個專案整理一個目錄。
1、分析專案
- 不考慮你是開發者,把自己當成一個使用者去審視專案
- 瀏覽程式碼看一下用了哪些工具
- 閱讀工具的文件和最佳實踐
- 通過單元測試,從更高的角度上了解專案
2、建立規範
- 使用
.editorconfig
來保證不同編輯器之間的程式碼規範 - 確定好縮排方式。tab或空格都無所謂
- 保證命名規範
- 如果還沒使用,那麼推薦使用格式工具,例如 ESLint , JSLint,或 JSHint 等
- 更新依賴,但是要理性的慢慢升級
3、整理
- 使用 Karma、Jasmine 或 Nightwatch.js 來建立單元測試或瀏覽器自動化測試
- 保證架構和設計模式一致性
- 不要混用 design patterns,儘可能結合已有的設計模式
- 決定你是否想將專案分離成模組。每個模組只做明確的一個功能的並且和外面的程式碼解耦
- 如果不想做模組化,專注於分離成多個可測試的小型程式碼塊
- 為你的程式碼建立文件和合適的命名方式
- 使用 JSDoc 來自動生成註釋
- 提交每一個程式碼變更。如果出錯方便回滾
4、不要瘋掉
- 不要抱怨前面的程式設計師。負面情緒不能幫你重構,只會浪費你的時間
- 每行程式碼都有它存在的原因。記住我們寫的程式碼也是
真心希望這篇文章能幫助你,如果大家對那些步驟有爭議,或者有我沒想到的更好的建議,請告訴我。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式