Javascript模塊化編程學習小結


擁抱模塊化的javascript

隨着越來越多的后端邏輯轉移到前端,網頁中的js代碼量也是與日俱增,只是靠函數式的方式來組織代碼已經漸漸變得力不從心,這時類的方式出現了,Prototype、YUI、Ext等框架漸漸流行開來,以滿足大量javascript代碼的開發。

而緊接着nodejs橫空出世,這個服務端的javascript采用模塊化的寫法很快就在前端領域產生了巨大影響,大牛們紛紛效仿,各種模塊化的規范也是浮出水面。CommonJS想統一前后端的模塊寫法,但AMD卻被認為是最適合瀏覽器的,無論如何,模塊化編程已經在javascript中流行了起來。

但是javascript本身並不是一種模塊化編程語言,它不支持類(class),更別說模塊(module)了,雖然ECMAScript第六版已經開始支持類和模塊,但目前來說,了解javascript模塊化編程依然是很有必要的。Javascript社區做了很多努力,在現有運行環境中,實現了模塊的效果。


一、模塊基本寫法-信息隱藏

模塊應設計的使其所包含的信息(過程和數據)對於那些不需要用到它的其他模塊不可見。每個模塊只完成一個獨立工程,然后提供該功能的接口。模塊間通過接口訪問。Javascript中可以用函數去隱藏,封裝,然后返回接口對象。比如:

var math = (function(){
var count = 0;
var add = function(){
//...
};
var sub = function(){
//...
};
return {
add: add;
sub: sub
};
})();
這是javascript模塊的基本寫法,此時外部無法訪問也無法修改count變量。


二、模塊的升級版寫法

如果一個模塊很大,必須分成很多部分,每個部分可能新增某些功能,或者模塊之間需要有繼承關系,這時就需要用這種“放大模式”的寫法:

var module = (function(mod){
mod.m1 = function(){
//...
};
return mod;

})(module);
在瀏覽器環境中,模塊的各個部分通常都是網上獲取的,有時無法知道哪個部分先加載。如果采用上面的方法可能會導致加載一個不存在的空對象,這時就需要采用下面這種“寬放大模式”。

var module = (function(mod){
//...

return mod;
})(window.module || {});
與“放大模式”相比,“寬放大模式”就是立即執行函數的參數可以是空對象。


三、輸入全局變量

獨立性是模塊的重要特點,模塊內部最好不與程序的其他部分直接交互。

為了在模塊內部調用全局變量,必須顯式地將其他變量輸入模塊。

var module = (function($,YAHOO){

//...

})(jQuery,YAHOO);


四、模塊規范之CommonJS

2009年,美國程序員Ryan Dahlchu創造了node.js項目,將javascript語言用於服務器端編程。



node.js的模塊系統,就是參照CommonJS規范實現的。在CommonJS中,有一個全局性方法require(),用於加載模塊。假設有一個模塊math.js,就可以像下面這樣加載並調用其方法。

var math = require("math");
math.add(2,3); //5
這個主要針對服務端編程,客戶端怎么辦呢,最好的當然是兩者都能兼容,一個模塊不用修改,在客戶端和服務端都能運行。

但有一個重大局限,采用這種方式加載模塊是同步完成的,也就是必須等某一個模塊加載完成后才能運行下面的語句,如果加載時間很長,整個應用都會卡在那里等。

這對服務端不是問題,因為所有模塊都存放在本地硬盤,可以同步加載完成,等待時間就是硬盤的讀取時間。但這顯然並不適合瀏覽器,因此,瀏覽器端的模塊,不能采用“同步加載”,只能采用“異步加載”,於是AMD誕生了。


五、模塊規范之AMD

AMD是“Asynchronous Module Definition”的縮寫,意思是“異步模塊定義”。所有以來這個模塊的語句,都定義在一個回調函數中。

AMD也采用require()語句加載模塊,但它要求兩個參數:

若將前面的代碼改成AMD形式,就是下面這樣:

require(['math'], function(math){
math.add(2,3);
});


六、require.js

為什么要用require.js?

下面這段代碼,相信很多人都見過。

<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<script src="4.js"></script>
<script src="5.js"></script>
<script src="6.js"></script>
<script src="7.js"></script>
這段代碼依次加載多個js文件,這樣的寫法有很大缺點。首先,加載的時候沒瀏覽器會停止網頁渲染,加載文件越多,網頁失去響應的時間越長;其次,由於js文件之間往往存在依賴關系,因此必須嚴格保證加載順序,當一來關系很復雜時候,代碼的編寫和維護都會變得困難。

於是,require.js誕生了。


(1)實現js文件的異步加載,避免網頁失去響應;

(2)管理模塊之間的依賴性,便於代碼的編寫和維護。

require.js的加載

先去官網下載最新版本,下載后,根據放置目錄就可以加載了。

<script src="js/require-2.2.0.js" data-main="main"></script>
你可能會覺得加載這個文件,也可能造成網頁失去響應。解決辦法有兩個,一是將它放在網頁底部加載,另一個是這樣:

<script src="js/require-2.2.0.js" defer async="true"></script>
async表明異步加載,而IE不支持這個屬性,只支持defer,所以defer最好也加上。

現在可以進一步加載自己的代碼了,假設我們的代碼文件是main,js,也在js目錄下:

<script src="js/require-2.2.0.js" data-main="js/main"></script>
data-main屬性的作用是指定網頁程序的主模塊。該文件會被require.js第一個加載。由於require.js默認的文件后綴名是js,所以可以把main.js簡寫成main。


七、主模塊的寫法

主模塊一般會依賴於其他模塊,這時就要使用AMD規范定義的require()函數。

//main.js
require(['moduleA','moduleB','moduleC'], function(moduleA,moduleB,moduleC){

//...

});
第一個參數數組內的模塊加載后會以參數的形式傳入該函數。

實際例子:

//main.js
require(['jquery','underscore','backbone'], function($, _, Backbone){

//...

});

八、模塊的加載

上節的寫法是默認了moduleA、moduleB、moduleC與main.js在同一目錄下,而實際情況當然要復雜一些,因此我們可以采用require.config()方法,對模塊的加載行為進行自定義。

//main.js
require.config({
paths:{
"jquery": "lib/jquery.min",
"underscore": "lib/underscore.min",
"backbone": "lib/backbone.min"

}
})
或者直接更改 基目錄(baseUrl)。

//main.js
require.config({
baseUrl: "js/lib",
paths:{
"jquery": "jquery.min",
"underscore": "underscore.min",
"backbone": "backbone.min"
}
})
或者指定遠程網址:

require.config({
paths:{
"jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
}
})


九、AMD模塊的寫法

具體來說,就是模塊必須采用特定的額define()函數來定義。如果一個模塊不依賴其他模塊,則直接定義在define()函數中。

// math.js
define(function (){
var add = function (x,y){
return x+y;
};
return {
add: add
};
});
如果這個模塊依賴其他模塊,那么應該這樣寫:

// math.js
define(['myLib'], function(myLib){
function foo(){
myLib.doSomething();
}
return {
foo : foo
};
});
當require()函數加載上面這個模塊的時候,就會先加載myLib.js文件。


十、requireJS路勁解析問題

baseUrl:基礎中的基礎

首先需要說明,在requirejs的模塊路勁解析里,baseUrl是非常基礎的概念,所有的路勁都是requirejs基於baseUrl指定的路勁來尋找。

舉個栗子,如下:

<script src="js/require.js" data-main="js/main.js"></script>

在demo.html里加載了require.js,同時在require.js所在的script上申明了data-main屬性,那么,requirejs加載下來后,它會做兩件事:

1、加載js/main.js

2、將baseUrl設置為data-main指定的文件所在的路勁,這里是js/


那么,下面依賴的lib模塊實際路勁為js/lib.js

require(['lib'], function(Lib){
// do sth
});
如果沒有通過data-main屬性指定baseUrl,也米有通過config顯式申明,那么baseUrl默認為加載requirejs的那個頁面所在路勁

baseUrl + path:讓依賴更簡潔、靈活

原始代碼:

//main.js
requirejs.config({
baseUrl: 'js'
});

// 加載一堆水果
require(['common/fruits/apple', 'common/fruits/orange', 'common/fruits/grape', 'common/fruits/pears'], function(Apple, Orange, Grape, Pears){
// do sth
});
改進后代碼:

//main.js
requirejs.config({
baseUrl: 'js',
paths: {
fruits: 'common/fruits'
}
});

// 加載一堆水果
require(['fruits/apple', 'fruits/orange', 'fruits/grape', 'fruits/pears'], function(Apple, Orange, Grape, Pears){
// do sth
});
對於改進后的好處不需要多說。

paths:簡單但需要記住的要點

看看下面三種情況:

//main.js
requirejs.config({
baseUrl: 'js',
paths: {
common: 'common/fruits'
}
});

<strong>// 從左到右,加載的路徑依次為 js/apple.js、 js/common/fruits/apple.js、common/apple.js</strong>
require(['apple', 'common/apple', '../common/apple'], function(){
// do something
});

關於define()內部依賴模塊路勁解析問題

首先來個例子:

//main.js
requirejs.config({
baseUrl: 'js'
});
// 依賴lib.js,實際加載的路徑是 js/common/lib.js,而lib模塊又依賴於util模塊('./util'),解析后的實際路徑為 js/common/util.js
require(['common/lib'], function(Lib){
Lib.say('hello');
});
// lib.jsdefine(['./util'], function(Util){    //...});
解析后的實際路徑為 js/common/util.js


下面改個寫法:

//main.js
requirejs.config({
baseUrl: 'js',
paths: {
lib: 'common/lib'
}
});

// 實際加載的路徑是 js/common/lib.js
require(['lib'], function(Lib){
Lib.say('hello');
});
// lib.jsdefine(['./util'], function(Util){    //...});
解析后的實際路徑為 js/util.js

為什么會出現這個問題?這個對於初識requirejs並且沒看過源碼的人確實難懂,你只需要記住以下幾點:

1、對於baseUrl的解析需要注意,當滿足以下條件,將不會相對baseUrl

(1)以“/”開頭

(2)以“.js”結尾

(3)包含各種協議

2、最終lib.js中解析的依賴模塊路勁跟main.js中require()函數中的依賴模塊路勁的parent有關。什么是parent?舉個栗子:

// main.js
require(['common/lib'], function(Lib){
//...
});

// lib.js
define(['./util'], function(Util){
//...
});//解析路勁為common/util
// main.jsrequire(['lib'], function(Lib){//...});// lib.jsdefine(['./util'], function(Util){    //...});//解析路勁為util
// main.jsrequire(['a/c/d/lib'], function(Lib){//...});// lib.jsdefine(['./util'], function(Util){    //...});//解析路勁為a/c/d/util


關於其背后的深入原因,有興趣的同學可以看看葉小釵大神寫的這篇博客requireJS路勁加載



注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com