Login  |  繁體中文
感謝您對「自由軟體鑄造場」的支持與愛護,十多年來「自由軟體鑄造場」受中央研究院支持,並在資訊科學研究所以及資訊科技創新研究中心執行,現已完成階段性的任務。 原網站預計持續維運至 2021年底,網站內容基本上不會再更動。本網站由 Denny Huang 備份封存。
也紀念我們永遠的朋友 李士傑先生(Shih-Chieh Ilya Li)。
Tech Column 初探 RequireJS

初探 RequireJS

一直以來,我們都習慣使用 script 這個 HTML 標籤來載入 JavaScript 檔案。這種方式有兩種缺點:

  • 無法在 JavaScript 程式中直接管理相依性,必須在 HTML 中處理。

  • 雖然目前新式瀏覽器已經能夠以非同步的方式來載入 js 檔案,但是舊型瀏覽器還是會有阻塞 (blocking) 問題。

終於 CommonJS 提出了 AMD 這個 API 規範,用以讓我們的 JavaScript 程式可以模組化,並同時解決 js 檔案載入時的阻塞問題。

目前已經有許多實作 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.html
1
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 標籤上的 data-main 屬性,它指向了 js/main.js (可以將 js 副檔名省略) 。在 js/main.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
require(dependencies, callback); 

其中 dependencies 的格式必須為陣列, callback 則為函式。

dependencies 表示我們要載入的 js ,而其路徑則是相對於 js/main.js ,而且一樣不需要寫副檔名。

因此在 dependencies 中,我們就可以將所有會用到的 js 載入,然後在 callback 中撰寫我們真正要處理的程式邏輯。

模組化

當然把所有的程式邏輯都寫在 js/main.jscallback 裡面是沒問題的,但那就沒辦法達到我們想要的模組化了。

而 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 則為一個工廠方法,它必須回傳一個物件,也就是我們的模組。

接著我們把原來 require API 中的 callback 改成模組,並將它放到 js/app.js 中:

js/app.js
1
2
3
4
5
6
7
define(function () {
  return {
    initialize: function () {
      console.log($);
    }
  }
});

js/app.js 會回傳一個包含 initialize 方法的物件模組,而這個方法就是我們前面的 callback 。注意這個例子裡並沒有設定模組的 id

接下來我們把 js/app.js 加到 require 的第一個參數中,特別注意這裡的 app 是指 js/app.js ,而不是模組名稱。

js/main.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.js
1
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 也提供了 paths 讓我們設定路徑的別名,就不需要輸入這麼多字:

js/main.js
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

前面提到 require API 可以讓我們對載入的 js 檔案所回傳的模組物件做 namespace 對應,也就是上述例子的 App 。事實上我們可以針對每個模組都設定一個 namespace ,例如:

js/main.js
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 變數) ,所以我們可以直接取用。

所以 namespaceD 其實就是 lib/d.js 裡已經定義好的,例如:

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.js
1
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.jsBackbone.js 的 AMD 版本。

還有一種方法是為這些套件的官方版本定義一個 wrapper ,以 Underscore.js 為例:

lib/underscore/wrapper.js
1
2
3
4
5
define([
  'lib/underscore/underscore-min'
], function(){
  return _.noConflict();
});

這樣在 js/main.js 裡就可以重新使用 Underscore.js 的 namespace 了,例如:

js/main.js
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 會幫我們處理以 requiredefine 所定義的模組,再參照其相依性把所有檔案合併為單一的 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.html
1
<script data-main="js/main-built" src="lib/requirejs/require.js"></script>

當然聰明如你,應該想到該怎麼讓開發和上線環境使用不同的 JavaScript 檔案了吧?

心得

以往在寫 JavaScript 時,雖然都會儘可能模組化,但變數的管理還有程式拆解不易的狀況,都是自己在維護 JavaScript 程式時很大的痛處。

在瞭解 RequireJS 的強大後,我相信以這樣的模組化方式再搭配 Backbone.js 的架構,一定可以讓系統在開發與維護上更為有組織性。

參考




OSSF Newsletter : 第 194 期 初探 RequireJS
Tags: RequireJS,  
Category: Tech Column



Comments 

 
+1 #1 淡无横 2012-04-28 17:51
通过r.js合并的代码,修改完路径之后 再次刷新页面,这段代码被引入了 ,但是由于文件内有requir e,所以插件再次被调用,请教如 何使得開發和上線環境使用不同的 JavaScript
 
 
0 #2 chan 2012-08-07 19:57
請教一下,他已經把 order.js 這個功能以及 plugin 拿掉了,我該如何讓他可以依照相 依性載入呢?
 
 
0 #3 jaceju 2012-08-08 09:57
@淡无横:

如果我沒有誤會你的意思的話,解 法應該是在用 r.js 時,不要將 require.js 包含進來。

@chan:

請先參考此篇作法: github.com/.../...
 
 
0 #4 chan 2012-08-08 10:33
感謝鐵神的回答,但不知道是否我 理解有問題,跟您說明一下我的狀

我的 main.js 要 load 三個 js 檔案
jquery.js
jquery.blockUI.js
functions.js

functions.js 裡面有一個 function 需要用到 jquery.blockUI 的 api

所以直接照下方這樣 load 會出現 has no method blockUI

requirejs([
'jquery-1.7.2.min',
'jquery.blockUI',
'functions'
],
function() {
$(function() {
showBlock();
});
});

我加上了下面的相依性設定

requirejs.config({
shim: {
'functions': ['jquery.blockUI']
}
});

不知道是否理解錯誤,但 has no method 'blockUI' 還是存在
 
 
+1 #5 chan 2012-08-08 10:35
排版都跑掉了,所以縮排沒有出現 ,請鐵神見諒
 
 
+1 #6 jaceju 2012-08-08 12:01
@chan:
我的方法你參考看看,檔案結構如 下:
Code:

js/libs/jquery.blockUI.js

js/app.js

js/main.js



// index.html

...

<script data-main="js/main" src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.0.2/require.min.js"></script>

</head>

...



// main.js

require.config({

paths: {

jquery: '//cdnjs.cloudflare.com/ajax/libs/jquery/1.7.2/jquery.min',

blockui: 'libs/jquery.blockUI' // 定義 js/libs/jquery.blockUI.js 的別名

},



shim: {

'jquery': {

exports: '$'

},

'blockui': ['jquery'] // blockui 相依於 jquery

}

});



define([

'app' // 載入 app.js

], function(App) {

App.initialize();

});



// app.js

define([

'blockui' // 這時候才載入 jquery 及 blockui ,解決了 RequireJS 1.0 的順序問題

], function () {

var showBlock = function () {

$.blockUI({ message: 'Hello World' });

}

return {

initialize: function () {

showBlock();

}

}

});