Javascript是一個非常神奇的語言。非常容易書寫,但是難於維護。希望大家在完成這篇文章閱讀之后,能夠將你書寫的js代碼變成真正可維護可閱讀的代碼!
為什么這么困難?
記住在書寫js之前,你需要知道它是一個動態語言。這意味着有很多方式來書寫代碼。你不需要處理強類型,或者類似C#和java的復雜語言特性 。
最難的部分可以在如下圖片中很清楚的認識到:
上面左邊的超薄書本是來自於Douglas crokford的JavaScript:The Good Parts(影印版),另外一本厚的來自於David Flanagan的JavaScript權威指南(第6版)
。
兩本書都是超棒的閱讀本。前一本描述了雖然javascript擁有很多的特性,但是最好的部分可以用一個非常薄的書本來介紹。如果你尋找一個好的,快速閱讀的方式,那么這本書非常適合你。
你可以在這里閱讀javascript的歷史。但是主要要點在於Brandon Eich,在1995年的時候受雇於netscape公司來設計一個新的語言。他后來設計的語言就是我們現在使用的弱類型的javascript。很多年過去了,現在它成為了標准的腳本語言,但是由於瀏覽器戰爭,很多瀏覽器執行的特性不太一樣。這很自然的導致了我們這些開發人員的無眠之夜。這個問題,連同javascript的圖片和驗證處理功能,使得javascript成為一中可怕的語言。
現在呢?我們需要解決這個問題。雖然這里有很多javascript的問題,如果你能正確的使用它,它能夠成為一個神奇的語言!
讓Javascript做的更好
名字空間(Namespaces)
其中一個javascript實現不好的地方在於基於一個全局(Global)的對象來操作。在瀏覽器中,這就是window對象,因此,任何時候如下代碼都可以出現在頁面上:
function doStuff(){
alert('welcome to gbin1.com!');
}
function doMoreStuff(){
var images = document.images.length;
console.log("There are " + images + "on this page");
}
doStuff();
doMoreStuff();
以上代碼中doStuff和doMoreStuff方法立刻就對於window對象有效。
這意味着任何人嘗試書寫一個方法,如果也叫做doStuff的話,就是出現沖突。所有的script標簽都接受代碼,並且基於HTML參考在window對象上來運行。很自然,第二個doStuff方法會覆蓋第一個。
一般性的技巧來解決這個問題可以使用自執行的匿名方法或者名字空間。面對對象的開發人員人閱讀到這里會感覺非常熟悉,但是基本的想法是將一組功能針對不同區域來實現重用。
var NS = NS || {}; // "如果NS沒有定義,那么設置它為一個空對象"
NS.Utils = NS.Utils || {}; //定義一個工具類
NS.Models = NS.Models || {}; //定義一個模型類
NS.Views = NS.Views || {}; //定義一個視圖類
以上代碼會放置全局空間被破壞,並且幫助你提高代碼可閱讀性。現在你可以針對分開的名字空間定義不同的方法。一個經常被定義的名字空間是app,用來代表管理一個應用的其它部分。
設計模式和最佳實踐
在每一種語言中,都會存在一系列的設計模式。 Addy Osmani說過:
設計模式是軟件設計中可以重用的解決方法用以處理重復出現的問題。
這里有很多設計模式,在你正確使用的時候能夠幫助你很好的提供應用的可維護性。Addy寫過一個非常棒的設計模式書籍,叫做Essential Design Pattern,絕對值得一讀!
另外一個常用的模式是Revealing Module Pattern.
NS.App = (function () {
// 初始化應用
var init = function () {
NS.Utils.log('GBin1 Application initialized...');
};
// 返回應用的公共方法
return {
init: init
};
}());
NS.App.init();
在以上代碼中我們在NS對象中定義了一個App方法。在這個方法中,一個函數變量init被定義,並且返回一個匿名的對象。注意,在最后,這里有一個多余的括號組合}());。這里強制NS.App函數自動執行並且返回。現在你可以調用NS.App.init()方法來初始化你的應用。
匿名方法是javascript的一個最佳實踐,同時也叫做自執行的匿名方法。因為javascript的方法擁有自己的執行區域,例如,方法中定義的變量不能在外部使用,這使得匿名方法非常有用。
// 將你的代碼封裝到一個自執行的方法中
(function (global) {
// 所以這里定義的變量對於外部來說都是不可見的
var somethingPrivate = '無法取得我的值!';
global.somethingPublic = '但是可以得到我的值!';
}(window));
console.log(window.somethingPublic); // 這個可以正常運行
console.log(somethingPrivate); // 報錯
在以上代碼中,因為這個方法自執行,你可以傳遞window到這個自執行方法中作為參數。這樣global就可以正常被訪問。這個方法可以限制window對象上的全局變量,防止名字空間沖突。
現在你可以在其它的應用中使用自執行匿名方法,使得代碼更加的模塊化。允許你重用。
(function ($) {
var welcomeMessage = 'Welcome to gbin1.com application!'
NS.Views.WelcomeScreen = function () {
this.welcome = $('#welcome');
};
NS.Views.WelcomeScreen.prototype = {
showWelcome: function () {
this.welcome.html(welcomeMessage)
.show();
}
};
}(jQuery));
$(function () {
NS.App.init();
});
// 修改以上App.init
var init = function () {
NS.Utils.log('GBin1 應用初始化...');
this.welcome = new NS.Views.WelcomeScreen();
this.welcome.showWelcome();
};
大家可以看到,這里有些不同,首先,jQuery被作為參數傳遞到方法。保證$在匿名方法中是jQuery。
接下來,這里有一個私有變量,叫做welcomeMessage,一個方法被賦值到NS.Views.WelcomeScreen。在這個方法中,this.welcome被賦值給jQuery DOM選擇器。這里在welcomeScreen中緩存選擇器,這樣jQuery不會多次的查詢DOM。
DOM操作非常費內存,所以請保證你盡可能的多做緩存
接下來,我們封裝應用的init到$(function(){});,這和$(document).ready()方法一樣。
最后我們添加一些代碼到應用的初始模塊。這保證你的代碼更好並且更分離,更便於你以后修改。更具有可維護性!
觀察者模式(Observer pattern)
另外一個非常好的模式就是觀察者模式,有時候被叫做“Pubsub”(收發模式)。這個模式允許我們訂閱DOM事件,例如,click和mouseover。一方面,我們監聽這些事件,另外一方面,有東西會發布這些事件。例如,瀏覽器發布有人點擊特定元素。這里有很多的pubsub類型的類庫,因為代碼精悍。執行一個google搜索,你可以看到很多的選擇。其中一個是AmplifyJS的實現:
// 一個收集信息的數據模型
NS.Models.News = (function () {
var newsUrl = '/gbin1/news/'
//收集信息
var getNews = function () {
$.ajax({
url: newsUrl
type: 'get',
success: newsRetrieved
});
};
var newsRetrieved = function (news) {
// 發布收集的信息
amplify.publish('news-retrieved', news);
};
return {
getNews: getNews
};
}());
這段代碼定義了一個數據模型用來獲取某類服務。一旦新聞被AJAX取得,newsRetrieved方法就被觸發,傳遞獲取的新聞導Amplify,然后被發布到“news-retrieved"標題上。
(function () {
// 創建一個新聞視圖
NS.Views.News = function () {
this.news = $('#news');
// 訂閱新聞收集事件
amplify.subscribe('news-retrieved', $.proxy(this.showNews));
};
// 當新聞來到展示新聞
NS.Views.Login.prototype.showNews = function (news) {
var self = this;
$.each(news, function (article) {
self.append(article);
});
};
}());
在上面代碼中是一個顯示收集新聞的視圖。在News構建器中,Amplify收集news-retrieved標題。當標題被發布,showNews功能將會觸發,然后新聞被添加到DOM。
// 修改App.init中的this
var init = function () {
NS.Utils.log('GBin1 應用初始化...');
this.welcome = new NS.Views.WelcomeScreen();
this.welcome.showWelcome();
this.news = new NS.Views.News();
// 得到新聞
NS.Models.News.getNews();
};
再一次,我們修改init方法來添加新聞收集,完成!現在這里有分開的應用代碼片段,每一個負責一個操作。這就是Single Responsibility Principle.
文檔和文件壓縮
最關鍵原則之一在於管理代碼 - 不僅僅JS,還有文檔和注釋。注釋對於新的開發人員來說意義不大,他們需要先了解代碼。一個非常有用的工具是Docco。這個工具就是用來生成Backbone.js網站的工具。基本上,它會處理代碼注釋,將他們並排放到代碼中。
這里也有別的工具JSdoc ,用來生成API樣式的文檔,描述你的代碼中的所有類。
另外一件事,可能會對於開始一個新項目比較難,就是如何有效的組織你的代碼。一個方式是分開不同功能到不同的目錄。例如:
- /app.js
- /libs/jquery.js
- /libs/jquery-ui.js
- /users/user.js
- /views/home.js
這個結構幫助你保證功能性的分離。當然,這里有幾種方式組織代碼,但是主要是決定結構。接下來是編譯和壓縮工具:
- Grunt
- Google Closure
- JSMin
- YUI Compressor
這些工具幫助你刪除空格和注釋,整合所有文件到一個js中。這樣可以減少文件體積和HTTP請求次數。最重要的,你可以在開發環境中將js放置於不同的位置,但是產品環境中使用一個js文件。
異步模塊定義 - Asynchronous Module Definitin(AMD)
異步模塊定義是另外一種不同的書寫javascript的方法
異步模塊定義(AMD)將代碼分拆成不同的模塊。AMD創建一個標准的模式來書寫這些模塊並且異步加載。
使用script標簽將會阻塞頁面,因為它會在DOM准備好了以后才加載。然而,使用類似AMD的機制允許DOM持續加載,並且腳本也在加載。必要地,每一個模塊都被分開到自己的文件中,並且這里有一個文件開始這個流程。最流行的AMD實現是RequireJS。
// main.js
require(['libs/jquery','app.js'], function ($, app) {
$(function () {
app.init();
});
});
// app.js
define(['libs/jquery', 'views/home'], function ($, home) {
home.showWelcome();
});
// home.js
define(['libs/jquery'], function ($) {
var home = function () {
this.home = $('#home');
};
home.prototype.showWelcome = function () {
this.home.html('Welcome!');
};
return new home();
});
在以上代碼片段中,這里有main.js代碼文件,用來開始這個流程。require方法的第一個參數是相關的數組。這些相關的文件是app.js需要的。當它們完成加載,無論模塊返回任何內容都會被當做參數傳遞到右邊的callback方法中。
然后,這里的app.js需要jQuery,同樣view也需要。接下來,視圖home.js將只需要jQuery。它包含了一個home方法,返回了一個自己本身的實例。在你的應用中,這些模塊都保存在分離的文件中,使得你的應用非常易於維護。
總結
保持你的應用代碼易於維護對於開發來說非常重要。它幫助你減少bugs,使得修補bug的過程更加簡單和快鍵。希望大家能夠覺得我們的文章對你有幫助,你如果有任何問題,請在留言處留言,你的建議和評論將帶給我們不同角度的見解和看法,並且鼓勵我們帶來更多更好的文章!