发布在:代码组织

代码组织概念

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

链接 关键概念

在深入了解代码组织模式之前,了解一些所有良好的代码组织模式共有的概念非常重要。

  • 你的代码应分成功能单元——模块、服务等。避免将所有代码都放在一个巨大的 $( document ).ready() 块中。这个概念通常称为封装。
  • 不要重复自己。识别功能块之间的相似性,并使用继承技术来避免重复的代码。
  • 尽管 jQuery 具有以 DOM 为中心的特点,但 JavaScript 应用程序并不完全是关于 DOM 的。请记住,并非所有功能块都需要——或应该——具有 DOM 表示。
  • 功能单元应松散耦合,也就是说,功能单元应能够独立存在,并且单元之间的通信应通过消息系统(例如自定义事件或发布/订阅)进行处理。尽可能避免功能单元之间的直接通信。

松散耦合的概念对于首次涉足复杂应用程序的开发人员来说可能特别麻烦,因此在开始时请注意这一点。

link 封装

代码组织的第一步是将应用程序的各个部分分隔成不同的部分;有时,即使只是这种努力也足以改善代码的结构及其可维护性。

link 对象字面量

对象字面量可能是封装相关代码的最简单方法。它不为属性或方法提供任何私有性,但它可用于从代码中消除匿名函数,集中配置选项,并简化重用和重构的路径。

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() 块中拥有一堆函数声明更高级。

链接 模块模式

模块模式克服了对象字面量的一些限制,在需要时为变量和函数提供私有性,同时公开公共 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 );
});