编写高度可维护javascript代码的几点关键性原则
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的过程更加简单和快键。希望大家能够觉得我们的文章对你有帮助,你如果有任何问题,请在留言处留言,你的建议和评论将带给我们不同角度的见解和看法,并且鼓励我们带来更多更好的文章!