解開面條代碼: 怎樣書寫可維護JavaScript

DaleneO68 8年前發布 | 32K 次閱讀 測試技術 JavaScript開發 JavaScript

[譯者注]:這篇文章結合作者自己的經歷確實寫的很到位,大部分內容感同身受。同時作者很有條理的告訴我們應該怎樣去思考解決問題。推薦給大家~

幾乎每個開發者都有接手過維護遺留項目的經歷,或者說是一個舊的項目想繼續維護起來。通常第一反應是拋開它們代碼規范基礎,按自己的意思去寫。這樣代碼會很亂,不可理解,并且別人可能要花費好幾天去讀懂代碼。但是,如果結合正確的規劃、分析、和一個好的工作流,那就有可能把一個面條式的代碼倉庫整理成一個整潔、有組織并易擴展的一份項目代碼。

我曾經不得不接手并整理很多的項目。但還沒有很多開始就很亂的。但實際上,最近就遇到了一個這樣的情況。雖然我已經學會了關于JavaScript代碼組織的很多知識,最重要的是,在前一個項目開發維護中幾乎瘋掉。在這篇文章中我想分享下一些我的步驟和我的經驗。

一、分析項目

最開始的一步是看一下到底怎么回事。如果是個網站,點擊網站所有的功能:打開對話框、發送表單等等。做這些的同時,打開開發者工具,看下是否報錯或輸出日志。如果是個nodejs項目,打開命令行接口過一下api,最好的情況是項目有一個入口(例如main.js,index.js,app.js),通過入口能將所有的模塊初始化;或者最壞的情況下,也要找到每個業務邏輯的位置。

找出使用的工具。jquery?React?Express?列出需要了解一些一切重要的東西。如果所在項目使用angular2寫的,而你還沒有使用過,直接去看文檔先有個基本的了解。總之需要找下最好的開始方案。

在更高的層次上看項目

要知道技術是一個好的開始,但是為了有一個真實的感覺和理解,是時候研究下單元測試了。單元測試是用來測試功能和代碼的方法是否按預期調用的一種方式。相比閱讀代碼和運行代碼,單元測試能更深入的幫你了解代碼。如果在你的項目中還沒有單元測試,別急,我們接著往下看。

二、創建一個基準

這些都是關于代碼一致性的內容。現在你已經了解了項目中使用的所有工具集,你知道了代碼的結構和邏輯功能的位置,是時候建立一個基準了。我建議添加一個 editorconfig 文件來保證代碼在不同的編輯器、ide或不同的開發者之間的編寫風格一致。

正確的縮進

這是一個飽受爭議的問題(跟戰爭一樣),代碼中使用空格還是tab,其實這不重要。如果之前代碼用的空格,那么使用空格,如果使用tab,繼續使用。如果代碼中都使用到了,那么是時候決定使用哪個了。討論的觀點是好的,但是一個好的項目目的是必須保證所有的開發者能在一起和諧的工作。

為什么這很重要。因為每個人都有使用自己編輯器或使用ide的方式。舉例來說,我是code folding的追捧者。沒有這些特性,我幾乎在一個文件里面迷失。如果縮進不是一樣的,那么代碼會看起來很亂。所以,們每次我打開一個文件,在我開始工作之前必須修復縮進的問題。這很浪費時間。

// 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變量和其他變量的混合命名。

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里來檢查哪些依賴版本是比較舊的。讓我舉個我項目里面一個叫fontBook的例子,所以我這個項目里面經常更新:

如你所見,我這里做了很多更新。但我一般不是馬上所有的做更新,而是一次更新一個。可以說,這樣花費了很多時間,然而這是唯一保證不出問題的方法(如果項目沒有任何測試用例的話)。

三、讓我們干點事情

這里我主要要表達的是整理項目并不一定意味著移除或重寫大部分的代碼。當然,有時候這時唯一的解決方案,但是這不是你一開始就應該考慮的問題。JavaScript代碼很可能成為一個奇怪的代碼,后面去做一些調整通常是不可能的。我們通常需要根據特定的場景來給出一個改造方案。

建立測試用例

使用測試用例可以保證你的代碼能進行正確的運行而不會出現意外的錯誤。JavaScript單元測試直接可以寫出很多文章,所有我這里沒辦法介紹太多。廣泛使用的框架有karma、jasmine、macha和ava等。如果你也想測試你的用戶界面,推薦使用Nightwatch.js和Dalekjs這類瀏覽器自動測試工具。

單元測試和瀏覽器自動化測試的區別是,前者測試JavaScript本身代碼。它保證了所有的模塊和通用邏輯能預期運行。瀏覽器自動化,另一方面來說是測試界面,也就是項目的用戶界面,保證頁面上的元素在預期正確的位置。

在重構任何事情之前先建立好測試用例。這樣項目的穩定性會提升,或許你甚至壓根也考慮過項目穩定性的東西。一個很大的好處是,一旦出了一些自己沒有意識到的錯誤時,你不用太著急。Rebecca Murphey寫過一篇非常不錯的文章可以看下 writing unit tests for existing JavaScript

架構

JavaScript架構是另一個大的主題。重構和整理框架決定于你在這方面有多少經驗。我們在軟件開發中有很多的設計模式。但是并不是所有的都能適應穩定性的需求。不幸的是,這篇文章中我不能給出所有的場景,但至少還是可以給一些通用性的建議。

首先,你要知道你的項目中使用到了那種設計模式。了解下這種模式,并保證它在整個項目是一致的。穩定性一個關鍵的地方是和設計模式緊密結合的,而不是混合的方法技術。當然,在你的項目里可以使用不同的設計模式來達到不同的目的(例如使用單例來建立數據結構或者短命名的工具函數,或者在模塊中使用觀察者模式),但是絕對不要一個模塊使用一種設計模式,另一個模塊使用另一個模式。

如果在你的項目中個確實沒有使用到什么架構(可能什么東西都是一個巨大的huge.js),那么是時候改變它了。但不要馬上做所有的改變,而是一點一點的來。同樣,這里沒有通用的方法,每個項目的設置也是不一樣的。項目目錄結構根據項目的規模和復雜度不同也不一樣。通常,對于最基本的層級,結構一般分為分為第三方內容、模塊內容、數據和一個初始化所有模塊和邏輯的入口(例如index.js、main.js)。這樣我們就需要模塊化了。

所有的東西都模塊化?

模塊化至今也不是大規模可擴展JavaScript項目的解決方案。它需要開發者必須去熟悉另一層api。盡管這樣可能會帶來很多的困難,但它的原則是把你的功能劃分成小的模塊。這樣,在團隊協作過程中解決問題就變的更簡單了。每個模塊應該有個一個明確的目標功能點。一個模塊應該是不知道你外面代碼邏輯是什么樣的,并且能在不同的地方和場景下復用。

那么怎樣將大量關聯邏輯的代碼拆分成模塊呢?一起看下。

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));

這里基本沒有模塊化。所有的東西都是緊密結合的,并且相互依賴。想象一下在更大、更復雜的函數里,如果出了問題你要來debug。可能api不響應、json里面字段改變了或者其他的。這簡直瘋掉。

// 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);
}

&ems; 很好,我們現在有三個模塊了,我們來看下調用的情況:

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插件里面使用。例如

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;
}

這個函數接受兩個參數后迭代一個對象,然后返回一個數組。這段代碼不是很復雜,但是對于沒有接觸過這段代碼的人還是需要一點時間來弄明白發生了什么事情。另外,函數做了什么事情不是很明確,所以文檔可以這樣寫。

/**
 * 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 svn 。你可能已經懂了,我強烈建議一些人使用,它確實能幫你做整理和組織化代碼。

在一個新分支上重構,不要在主干上直接修改。你可能需要快速修改主干并提交bug fix到正式環境,但是你又不想你的重構代碼沒有測試或完成就上線。所以建議在另一個分支上開始。

假如你需要快速了解git的工作原理。這里有一個 介紹

四、怎樣不會瘋掉

為了不被之前的開發者逼瘋,除了這些的技術整理步驟之外,還有重要的一步是我很少看到的(就是負面情緒)。當然,著并不是說的每個人,但是我知道有些人經歷過。它確實花費我很多時間來冷靜下來。我曾經幾乎被我前面的開發者逼瘋掉了,完全不明白他的代碼、解決方案以及為什么一切都是這么亂套。

最后,這些負面情緒沒有改變什么。沒有幫我重構代碼,反而浪費了我的時間,讓我的代碼出錯。這會讓你越來越郁悶。因為你可能花掉幾個小時去重構一個功能,并且沒有人會感謝你重寫了一個已經存在的模塊。這不值得。做重要的事情,然后分析處境。你可能經常需要重構一些模塊里面很小的部分。

另外,你的代碼總有你這樣寫的原因。可能之前的程序員可能并沒有時間去思考怎樣正確的那樣做,或者因為其他原因。我們自己也是。

五、總結一下

讓我們再來梳理一下,為下一個項目整理一個目錄。

1、分析項目

  • 不考慮你是開發者,把自己當成一個用戶看下你的項目是什么東西
  • 瀏覽下代碼看下用了哪些工具
  • 看些文檔找下好的工具實踐
  • 過下單元測試,從更高的角度上看下項目

2、建立基準

  • 使用 .editorconfig 來保證不同編輯器之間的代碼規范
  • 確定好縮進方式。tab或空格都無所謂
  • 保證命名規范
  • 如果還沒使用,那么推薦使用格式工具,例如 ESlint, JSlint ,或 JSHint 等
  • 更新依賴,但是要理性的去慢慢升級

3、整理

  • 使用 Karma、Jasmine或Nightwatch.js來建立單元測試或瀏覽器自動化測試
  • 保證架構和設計模式一致性
  • 不要混用設計模式,盡可能結合已有的設計模式
  • 決定你是否想將項目分離成模塊。每個模塊只做明確的一個功能的并且和外面的代碼解耦
  • 如果不想做模塊化,專注于分離成多個可測試的小型代碼塊
  • 為你的代碼建立文檔和合適的命名方式
  • 使用JSDoc來自動生成注釋
  • 提交每一個代碼變更。如果出錯方便回滾

4、不要瘋掉

  • 不要抱怨前面的程序員。負面情緒不能幫你重構,只會浪費你的時間
  • 每行代碼都有它存在的原因。記住我們寫的代碼也是

真心希望這篇文章能幫助你,或者大家覺得有什么沒提到的地方或好的建議也可以告訴我。

原文作者:Moritz Kr?ger

原譯:ouven

原文地址: https://www.sitepoint.com/write-maintainable-javascript/

 

來自: http://ouvens.github.io/article-translation/2016/05/20/how-to-write-maintainable-js.html

 

 本文由用戶 DaleneO68 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!