一直以來,我們都習慣使用
script
這個 HTML 標籤來載入 JavaScript 檔案。這種方式有兩種缺點:- 無法在 JavaScript 程式中直接管理相依性,必須在 HTML 中處理。
- 雖然目前新式瀏覽器已經能夠以非同步的方式來載入 js 檔案,但是舊型瀏覽器還是會有阻塞 (blocking) 問題。
目前已經有許多實作 AMD 規範的 JavaScript Library 了,而 RequireJS 則是目前討論最多,應用最廣的其中一個實作。
以下是我在研究 RequireJS 時的筆記,若有謬誤還請大家指正。
起手式
先來看看我們的程式目錄架構:
├── index.html
├── js
│ ├── app.js
│ └── main.js
└── lib
├── backbone
│ ├── backbone-min.js
│ └── wrapper.js
├── jquery
│ ├── jquery-min.js
│ └── wrapper.js
├── underscore
│ ├── underscore-min.js
│ └── wrapper.js
└── requirejs
├── order.js
└── require.js
其中 index.html 的內容如下:
index.html1 2 3 4 5 6 7 8 9 10 | <!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Beginning Require.JS</title> <script data-main="js/main" src="/lib/requirejs/require.js"></script> </head> <body> </body> </html> |
你會發現,我們只需要用一個 script 標籤來載入
lib/requirejs/require.js
即可,剩下的 js 檔案都可以讓 RequireJS 來幫我們載入。
可是 RequireJS 怎麼知道要載入哪些檔案呢?注意 script 標籤上的
js/main.jsdata-main
屬性,它指向了js/main.js
(可以將 js 副檔名省略) 。在 js/main.js 中,我們就可以指定我們要載入的模組:1 2 3 4 5 | require([ '../lib/jquery/jquery-min' ], function () { console.log($); }); |
所以一切都是從
js/main.js
開始執行。
在
js/main.js
裡,我們用到了 require
這個 RequireJS 中最主要的 API ,它的基本用法如下:1 |
|
其中
dependencies
的格式必須為陣列, callback
則為函式。dependencies
表示我們要載入的 js ,而其路徑則是相對於 js/main.js
,而且一樣不需要寫副檔名。
因此在
dependencies
中,我們就可以將所有會用到的 js 載入,然後在 callback
中撰寫我們真正要處理的程式邏輯。模組化
當然把所有的程式邏輯都寫在
js/main.js
的 callback
裡面是沒問題的,但那就沒辦法達到我們想要的模組化了。
而 RequireJS 也實作 AMD 所定義的
define
API 方法,所以我們就可以用它來實現程式的模組化。 define 的 API 如下:1 | define(id?, dependencies?, factory); |
其中
id
格式為字串,代表模組的名稱,可以不寫。如果要寫的話,就必須是相對於js/main.js
的檔案路徑,但不用加上 js
副檔名,例如 ../lib/foo
或 ./js/bar
。
而
dependencies
格式為陣列,作用與 require
中的 dependencies
相同。一般來說如果我們在 js/main.js
中定義好相依性後,這裡可以不需要特別指定。
最後的
factory
則為一個工廠方法,它必須回傳一個物件,也就是我們的模組。
接著我們把原來
js/app.jsrequire
API 中的 callback
改成模組,並將它放到 js/app.js
中:1 2 3 4 5 6 7 | define(function () { return { initialize: function () { console.log($); } } }); |
js/app.js
會回傳一個包含 initialize
方法的物件模組,而這個方法就是我們前面的callback
。注意這個例子裡並沒有設定模組的 id
。
接下來我們把
js/main.jsjs/app.js
加到 require
的第一個參數中,特別注意這裡的 app
是指js/app.js
,而不是模組名稱。1 2 3 4 5 6 | require([ 'app', '../lib/jquery/jquery-min' ], function (App) { App.initialize(); }); |
在
callback
的第一個參數 App
會對應到 js/app.js
中所回傳的物件,這意謂著我們可以為該物件指定新的 namespace 。
到這裡其實可以應付很多基本的應用了,不過如果當 library 間有相依性問題時,這樣的寫法就可能會出錯了。
順序問題
因為使用非同步的載入方式,所以用
require
載入套件時,是有可能會造成相依性上的問題。 所幸 RequireJS 提供了一個 order
plugin ,讓我們可以依序載入正確的套件。
以 Backbone 為例,我們需要依序載入 jQuery 、 underscore 及 Backbone 等三個套件,方法如下:
js/main.js1 2 3 4 5 6 7 8 | require([ 'app', '../lib/requirejs/order!../lib/jquery/jquery-min', '../lib/requirejs/order!../lib/underscore/underscore-min', '../lib/requirejs/order!../lib/backbone/backbone-min' ], function (App) { App.initialize(); }); |
如上面的範例所示,在每一行載入 js 的字串中,我們先載入 plugin ,然後利用
!
符號來將 library 的位置傳給 plugin 。
其他有用的 plugin ,可以在官方網站的 Plugins 頁找到。
路徑別名
不過每次都要輸入這麼長的路徑實在是很麻煩,還好 RequireJS 也提供了
js/main.jspaths
讓我們設定路徑的別名,就不需要輸入這麼多字:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | require({ paths: { "order": "../lib/requirejs/order", "lib": "../lib" } }); require([ 'app', 'order!lib/jquery/jquery-min', 'order!lib/underscore/underscore-min', 'order!lib/backbone/backbone-min' ], function (App) { App.initialize(); }); |
要特別注意的是,這裡設定的別名,也會影響到其他模組裡所使用的
define
API 。require
還有其他設定,請參考官方文件的 Configuration Options 。Namespace
前面提到
js/main.jsrequire
API 可以讓我們對載入的 js 檔案所回傳的模組物件做 namespace 對應,也就是上述例子的 App
。事實上我們可以針對每個模組都設定一個 namespace ,例如:1 2 3 4 5 6 7 8 9 10 11 | require([ '../lib/a', '../lib/b', '../lib/c', '../lib/d' ], function (moduleA, moduleB, moduleC) { moduleA.doSomething(); moduleB.doSomething(); moduleC.doSomething(); namespaceD.doSomething(); }); |
可以看到
'../lib/a'
這個模組對應到 moduleA
這個 namespace ,'../lib/b'
則對應到moduleB
,以此類推。
但是
namespaceD
並沒有在 require
方法的 callback
參數中,那為什麼我們可以取用呢?
回到一開始我們用
require
載入第三方套件的方式,其實可以看到我們是直接利用該套件定義好的 namespace ,例如 jQuery 的 $
符號,或是 underscore.js 的 _
符號。
而我們並沒有再為這些套件指定新的 namespace ,是因為這些 namespace 已經被綁在 global 變數裡了 (在瀏覽器環境下是指 window 變數) ,所以我們可以直接取用。
所以
lib/d.jsnamespaceD
其實就是 lib/d.js
裡已經定義好的,例如:1 2 3 4 5 6 7 8 | define(function () { var namespaceD = window.namespaceD = { doSomething: function () { console.log('namespaceD.doSomething()'); } }; return namespaceD; }); |
瞭解這個回到我們前面所提到的 Backbone.js 範例,有些文章的例子會教大家這麼用:
js/main.js1 2 3 4 5 6 7 8 9 10 11 | require([ 'order!lib/jquery/jquery-min', 'order!lib/underscore/underscore-min', 'order!lib/backbone/backbone-min', 'order!app' ], function ($, _, Backbone, App) { console.log($); console.log(_); console.log(Backbone); console.log(App); }); |
如果各位是使用 Underscore.js 及 Backbone.js 的官方版本時,這樣做是錯誤的,你會發現
callback
裡的 _
, Backbone
都會是 null 值。為什麼呢?主要是因為這兩個套件目前不支援 AMD 架構,所以無法正確回傳對應的 Underscore.js 及 Backbone.js 物件回來。所以很多人在透過 RequireJS 使用這兩個套件時,就會在這裡卡關。
最簡單的方式就是不要再為這些第三方套件設定一個 namespace ,也就是一開始為大家介紹的用法。
另一種方式就是直接使用 RequireJS 所 fork 出來的 Underscore.js 及 Backbone.js 的 AMD 版本。
還有一種方法是為這些套件的官方版本定義一個
lib/underscore/wrapper.jswrapper
,以 Underscore.js 為例:1 2 3 4 5 | define([ 'lib/underscore/underscore-min' ], function(){ return _.noConflict(); }); |
這樣在
js/main.jsjs/main.js
裡就可以重新使用 Underscore.js 的 namespace 了,例如:1 2 3 4 5 | require([ 'order!lib/underscore/wrapper' ], function (_) { console.log(_); }) |
不過因為非同步載入的關係,要使用
wrapper
方法處理套件相依性時,其流程就稍微複雜些了,大家可以參考 Organizing your application using Modules (require.js) 一文的介紹。編譯
當我們把 JavaScript 拆成這麼多模組檔案後,那麼不就會讓 HTTP Request 變多了嗎?有沒有什麼方法可以幫我們把這些檔案再組合成為一支檔案呢?
RequireJS 就提供了一個好用的工具,叫做
r.js
。它必須透過 node.js 的套件管理系統來安裝,也就是 npm
;安裝方法如下:npm -g install requirejs
若無錯誤的話,應該會出現以下畫面:
npm http GET https://registry.npmjs.org/requirejs
npm http 200 https://registry.npmjs.org/requirejs
npm http GET https://registry.npmjs.org/requirejs/-/requirejs-1.0.7.tgz
npm http 200 https://registry.npmjs.org/requirejs/-/requirejs-1.0.7.tgz
/usr/local/bin/r.js -> /usr/local/lib/node_modules/requirejs/bin/r.js
requirejs@1.0.7 /usr/local/lib/node_modules/requirejs
r.js
會幫我們處理以 require
或 define
所定義的模組,再參照其相依性把所有檔案合併為單一的 JavaScript 檔案。用法如下:r.js -o name=js/main out=js/main-built.js baseUrl=. paths.order="lib/requirejs/order"
其中
-o
為最佳化; name
則為要處理的 JavaScript 檔案; out
則是輸出的檔案名稱;baseUrl
為指定 r.js
在處理相依性時所要參考的相對路徑; paths.order
是路徑別名,但不相對於 js/main.js
,而是相對於 baseUrl
。
處理完成後,我們就可以直接改用以下方式載入:
index.html1 |
|
當然聰明如你,應該想到該怎麼讓開發和上線環境使用不同的 JavaScript 檔案了吧?
心得
以往在寫 JavaScript 時,雖然都會儘可能模組化,但變數的管理還有程式拆解不易的狀況,都是自己在維護 JavaScript 程式時很大的痛處。
在瞭解 RequireJS 的強大後,我相信以這樣的模組化方式再搭配 Backbone.js 的架構,一定可以讓系統在開發與維護上更為有組織性。
reference : http://www.openfoundry.org/index.php?option=com_content&task=view&id=8678&Itemid=4%3Bisletter%3D1