发布于: 代码组织

代码组织概念

当你不再满足于仅使用 jQuery 为网站添加简单的增强功能,而开始开发成熟的客户端应用程序时,你需要考虑如何组织你的代码。在本章中,我们将介绍可以在 jQuery 应用中使用的各种代码组织模式,并探索 RequireJS 依赖管理和构建系统。

链接 核心概念

在深入研究代码组织模式之前,理解一些所有优秀的组织模式所共有的概念非常重要。

  • 你的代码应该被划分为功能单元——模块、服务等。不要经受不住诱惑而将所有代码都写在一个巨大的 $( document ).ready() 块中。这个概念通俗地被称为“封装”。
  • 不要重复自己(DRY)。找出各功能块之间的相似之处,并利用继承技术来避免重复代码。
  • 尽管 jQuery 具有以 DOM 为中心的特性,但 JavaScript 应用程序并不完全是关于 DOM 的。请记住,并非所有的功能单元都需要(或应该)有 DOM 表现形式。
  • 功能单元应该是松耦合的,也就是说,一个功能单元应该能够独立存在,而单元之间的通信应通过消息系统(如自定义事件或发布/订阅模式)来处理。尽可能避免功能单元之间的直接通信。

对于初次尝试开发复杂应用程序的开发者来说,松耦合的概念可能尤其令人困扰,因此在开始时请务必留意这一点。

链接 封装

代码组织的第一步是将应用程序划分为不同的部分;有时,仅仅这一项努力就足以改善代码结构及其可维护性。

链接 对象字面量

对象字面量(Object Literal)或许是封装相关代码最简单的方法。它不提供属性或方法的私密性,但对于消除代码中的匿名函数、集中配置选项以及简化重用和重构路径非常有用。

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
// An object literal
var myFeature = {
myProperty: "hello",
myMethod: function() {
console.log( myFeature.myProperty );
},
init: function( settings ) {
myFeature.settings = settings;
},
readSettings: function() {
console.log( myFeature.settings );
}
};
myFeature.myProperty === "hello"; // true
myFeature.myMethod(); // "hello"
myFeature.init({
foo: "bar"
});
myFeature.readSettings(); // { foo: "bar" }

上面的对象字面量只是一个赋值给变量的对象。该对象有一个属性和多个方法。所有的属性和方法都是公开的,因此应用程序的任何部分都可以查看属性并调用该对象的方法。虽然有一个 init 方法,但并没有规定在对象生效前必须调用它。

我们如何将这种模式应用于 jQuery 代码?假设我们有一段以传统 jQuery 风格编写的代码:

1
2
3
4
5
6
7
8
9
10
11
12
// Clicking on a list item loads some content using the
// list item's ID, and hides content in sibling list items
$( document ).ready(function() {
$( "#myFeature li" ).append( "<div>" ).click(function() {
var item = $( this );
var div = item.find( "div" );
div.load( "foo.php?item=" + item.attr( "id" ), function() {
div.show();
item.siblings().find( "div" ).hide();
});
});
});

如果这就是我们应用程序的全部内容,那么保持原样也是可以的。另一方面,如果这是更大规模应用程序的一部分,我们最好将此功能与无关功能分开。我们可能还想将 URL 移出代码并放入配置区域。最后,我们可能想打破链式调用,以便稍后更容易修改功能片段。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Using an object literal for a jQuery feature
var myFeature = {
init: function( settings ) {
myFeature.config = {
items: $( "#myFeature li" ),
container: $( "<div class='container'></div>" ),
urlBase: "/foo.php?item="
};
// Allow overriding the default config
$.extend( myFeature.config, settings );
myFeature.setup();
},
setup: function() {
myFeature.config.items
.each( myFeature.createContainer )
.click( myFeature.showItem );
},
createContainer: function() {
var item = $( this );
var container = myFeature.config.container
.clone()
.appendTo( item );
item.data( "container", container );
},
buildUrl: function() {
return myFeature.config.urlBase + myFeature.currentItem.attr( "id" );
},
showItem: function() {
myFeature.currentItem = $( this );
myFeature.getContent( myFeature.showContent );
},
getContent: function( callback ) {
var url = myFeature.buildUrl();
myFeature.currentItem.data( "container" ).load( url, callback );
},
showContent: function() {
myFeature.currentItem.data( "container" ).show();
myFeature.hideContent();
},
hideContent: function() {
myFeature.currentItem.siblings().each(function() {
$( this ).data( "container" ).hide();
});
}
};
$( document ).ready( myFeature.init );

你首先会注意到的是,这种方法显然比原始代码长得多——再次强调,如果这就是我们应用的全部,使用对象字面量可能大材小用了。但假设它不是应用的全部,我们已经获得了几点好处:

  • 我们将功能拆分成了微小的方法。未来,如果我们想改变内容的展示方式,很容易知道在哪里修改。在原始代码中,这一步很难定位。
  • 我们消除了匿名函数的使用。
  • 我们将配置选项从代码主体中移出,放在了一个中心位置。
  • 我们消除了链式调用的约束,使代码更容易重构、混合和重新排列。

对于非简单的功能,对象字面量明显优于塞在 $( document ).ready() 块中的长段代码,因为它们促使我们思考功能的组成部分。然而,它们并不比在 $( document ).ready() 块内部简单地声明一堆函数先进多少。

链接 模块模式

模块模式(Module Pattern)克服了对象字面量的一些局限性,它为变量和函数提供了私密性,同时可以根据需要公开公共 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
// The module pattern
var feature = (function() {
// Private variables and functions
var privateThing = "secret";
var publicThing = "not secret";
var changePrivateThing = function() {
privateThing = "super secret";
};
var sayPrivateThing = function() {
console.log( privateThing );
changePrivateThing();
};
// Public API
return {
publicThing: publicThing,
sayPrivateThing: sayPrivateThing
};
})();
feature.publicThing; // "not secret"
// Logs "secret" and changes the value of privateThing
feature.sayPrivateThing();

在上面的示例中,我们自执行一个返回对象的匿名函数。在函数内部,我们定义了一些变量。因为变量是在函数内部定义的,除非我们将它们放入返回的对象中,否则在函数外部无法访问它们。这意味着函数外部的代码无法访问 privateThing 变量或 changePrivateThing 函数。然而,sayPrivateThing 可以访问 privateThingchangePrivateThing,因为它们都定义在与 sayPrivateThing 相同的作用域内。

这种模式非常强大,因为正如你从变量名中可以看出的,它可以为你提供私有变量和函数,同时通过返回对象的属性和方法暴露有限的 API。

下面是前一个示例的修订版本,展示了我们如何使用模块模式创建相同的功能,同时只暴露模块的一个公共方法:showItemByIndex()

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Using the module pattern for a jQuery feature
$( document ).ready(function() {
var feature = (function() {
var items = $( "#myFeature li" );
var container = $( "<div class='container'></div>" );
var currentItem = null;
var urlBase = "/foo.php?item=";
var createContainer = function() {
var item = $( this );
var _container = container.clone().appendTo( item );
item.data( "container", _container );
};
var buildUrl = function() {
return urlBase + currentItem.attr( "id" );
};
var showItem = function() {
currentItem = $( this );
getContent( showContent );
};
var showItemByIndex = function( idx ) {
$.proxy( showItem, items.get( idx ) )();
};
var getContent = function( callback ) {
currentItem.data( "container" ).load( buildUrl(), callback );
};
var showContent = function() {
currentItem.data( "container" ).show();
hideContent();
};
var hideContent = function() {
currentItem.siblings().each(function() {
$( this ).data( "container" ).hide();
});
};
items.each( createContainer ).click( showItem );
return {
showItemByIndex: showItemByIndex
};
})();
feature.showItemByIndex( 0 );
});