require.js使用心得 -- 高階使用
高階使用§ 4
從包中載入模組§ 4.1
RequireJS支援從CommonJS包結構中載入模組,但需要一些額外的配置。具體地,支援如下的CommonJS包特性:
- 一個包可以關聯一個模組名/字首。
- package config可為特定的包指定下述屬性:
- name: 包名(用於模組名/字首對映)
- location: 磁碟上的位置。位置是相對於配置中的baseUrl值,除非它們包含協議或以“/”開頭
- main: 當以“包名”發起require呼叫後,所應用的一個包內的模組。預設為“main”,除非在此處做了另外設定。該值是相對於包目錄的。
重要事項
- 雖然包可以有CommonJS的目錄結構,但模組本身應為RequireJS可理解的模組格式。例外是:如果你在用r.js Node介面卡,模組可以是傳統的CommonJS模組格式。你可以使用CommonJS轉換工具來將傳統的CommonJS模組轉換為RequireJS所用的非同步模組格式。
- 一個專案上下文中僅能使用包的一個版本。你可以使用RequireJS的多版本支援來載入兩個不同的模組上下文;但若你想在同一個上下文中使用依賴了不同版本的包C的包A和B,就會有問題。未來可能會解決此問題。
如果你使用了類似於入門指導中的專案佈局,你的web專案應大致以如下的佈局開始(基於Node/Rhino的專案也是類似的,只不過使用scripts目錄中的內容作為專案的頂層目錄):
- project-directory/
- project.html
- scripts/
- require.js
而下面的示例中使用了兩個包,cart及store:
- project-directory/
- project.html
- scripts/
- cart/
- main.js
- store/
- main.js
- util.js
- main.js
- require.js
- cart/
project.html 會有如下的一個script標籤:
<script data-main="scripts/main" src="scripts/require.js"></script>
這會指示require.js去載入scripts/main.js。main.js使用“packages”配置項來設定相對於require.js的各個包,此例中是原始碼包“cart”及“store”:
//main.js contents
//Pass a config object to require
require.config({
"packages": ["cart", "store"]
});
require(["cart", "store", "store/util"],
function (cart, store, util) {
//use the modules as usual.
});
對“cart”的依賴請求會從scripts/cart/main.js中載入,因為“main”是RequireJS預設的包主模組。對“store/util”的依賴請求會從scripts/store/util.js載入。
如果“store”包不採用“main.js”約定,如下面的結構:
- project-directory/
- project.html
- scripts/
- cart/
- main.js
- store/
- store.js
- util.js
- main.js
- package.json
- require.js
- cart/
則RequireJS的配置應如下:
require.config({
packages: [
"cart",
{
name: "store",
main: "store"
}
]
});
減少麻煩期間,強烈建議包結構遵從“main.js”約定。
多版本支援§ 4.2
如配置項一節中所述,可以在同一頁面上以不同的“上下文”配置項載入同一模組的不同版本。require.config()返回了一個使用該上下文配置的require函式。下面是一個載入不同版本(alpha及beta)模組的示例(取自test檔案中):
<script src="../require.js"></script>
<script>
var reqOne = require.config({
context: "version1",
baseUrl: "version1"
});
reqOne(["require", "alpha", "beta",],
function(require, alpha, beta) {
log("alpha version is: " + alpha.version); //prints 1
log("beta version is: " + beta.version); //prints 1
setTimeout(function() {
require(["omega"],
function(omega) {
log("version1 omega loaded with version: " +
omega.version); //prints 1
}
);
}, 100);
});
var reqTwo = require.config({
context: "version2",
baseUrl: "version2"
});
reqTwo(["require", "alpha", "beta"],
function(require, alpha, beta) {
log("alpha version is: " + alpha.version); //prints 2
log("beta version is: " + beta.version); //prints 2
setTimeout(function() {
require(["omega"],
function(omega) {
log("version2 omega loaded with version: " +
omega.version); //prints 2
}
);
}, 100);
});
</script>
注意“require”被指定為模組的一個依賴,這就允許傳遞給函式回撥的require()使用正確的上下文來載入多版本的模組。如果“require”沒有指定為一個依賴,則很可能會出現錯誤。
在頁面載入之後載入程式碼§ 4.3
上述多版本示例中也展示瞭如何在巢狀的require()中遲後加載程式碼。
Web Worker 支援§ 4.4
從版本0.12開始,RequireJS可在Web Worker中執行。可以通過在web worker中呼叫importScripts()來載入require.js(或包含require()定義的JS檔案),然後呼叫require就好了。
你可能需要設定baseUrl配置項來確保require()可找到待載入指令碼。
你可以在unit test使用的一個檔案中找到一個例子。
Rhino 支援§ 4.5
RequireJS可通過r.js介面卡用在Rhino中。參見r.js的README。
處理錯誤§ 4.6
通常的錯誤都是404(未找到)錯誤,網路超時或載入的指令碼含有錯誤。RequireJS有些工具來處理它們:require特定的錯誤回撥(errback),一個“paths”陣列配置,以及一個全域性的requirejs.onError事件。
傳入errback及requirejs.onError中的error object通常包含兩個定製的屬性:
- requireType: 含有類別資訊的字串值,如“timeout”,“nodefine”, “scripterror”
- requireModules: 超時的模組名/URL陣列。
如果你得到了requireModules錯,可能意味著依賴於requireModules陣列中的模組的其他模組未定義。
在IE中捕獲載入錯§ 4.6.1
Internet Explorer有一系列問題導致檢測errbacks/paths fallbacks中的載入錯 比較困難:
- IE 6-8中的script.onerror無效。沒有辦法判斷是否載入一個指令碼會導致404錯;更甚地,在404中依然會觸發state為complete的onreadystatechange事件。
- IE 9+中script.onerror有效,但有一個bug:在執行指令碼之後它並不觸發script.onload事件控制代碼。因此它無法支援匿名AMD模組的標準方法。所以script.onreadystatechange事件仍被使用。但是,state為complete的onreadystatechange事件會在script.onerror函式觸發之前觸發。
因此IE環境下很難兩全其美:匿名AMD(AMD模組機制的核心優勢)和可靠的錯誤檢測。
但如果你的專案裡使用了define()來定義所有模組,或者為其他非define()的指令碼使用shim配置指定了匯出字串,則如果你將enforceDefine配置項設為true,loader就可以通過檢查define()呼叫或shim全域性匯出值來確認指令碼的載入無誤。
因此如果你打算支援Internet Explorer,捕獲載入錯,並使用了define()或shim,則記得將enforceDefine設定為true。參見下節的示例。
注意: 如果你設定了enforceDefine: true,而且你使用data-main=""來載入你的主JS模組,則該主JS模組必須呼叫define()而不是require()來載入其所需的程式碼。主JS模組仍然可呼叫require/requirejs來設定config值,但對於模組載入必須使用define()。
如果你使用了almond而不是require.js來build你的程式碼,記得在build配置項中使用insertRequire來在主模組中插入一個require呼叫 —— 這跟data-main的初始化require()呼叫起到相同的目的。
require([]) errbacks§ 4.6.2
當與requirejs.undef()一同使用errback時,允許你檢測模組的一個載入錯,然後undefine該模組,並重置配置到另一個地址來進行重試。
一個常見的應用場景是先用庫的一個CDN版本,如果其加載出錯,則切換到本地版本:
requirejs.config({
enforceDefine: true,
paths: {
jquery: 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min'
}
});
//Later
require(['jquery'], function ($) {
//Do something with $ here
}, function (err) {
//The errback, error callback
//The error has a list of modules that failed
var failedId = err.requireModules && err.requireModules[0];
if (failedId === 'jquery') {
//undef is function only on the global requirejs object.
//Use it to clear internal knowledge of jQuery. Any modules
//that were dependent on jQuery and in the middle of loading
//will not be loaded yet, they will wait until a valid jQuery
//does load.
requirejs.undef(failedId);
//Set the path to jQuery to local path
requirejs.config({
paths: {
jquery: 'local/jquery'
}
});
//Try again. Note that the above require callback
//with the "Do something with $ here" comment will
//be called if this new attempt to load jQuery succeeds.
require(['jquery'], function () {});
} else {
//Some other error. Maybe show message to the user.
}
});
使用“requirejs.undef()”,如果你配置到不同的位置並重新嘗試載入同一模組,則loader會將依賴於該模組的那些模組記錄下來並在該模組重新載入成功後去載入它們。
注意: errback僅適用於回撥風格的require呼叫,而不是define()呼叫。define()僅用於宣告模組。
paths備錯配置§ 4.6.3
上述模式(檢錯,undef()模組,修改paths,重載入)是一個常見的需求,因此有一個快捷設定方式。paths配置項允許陣列值:
requirejs.config({
//To get timely, correct error triggers in IE, force a define/shim exports check.
enforceDefine: true,
paths: {
jquery: [
'http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min',
//If the CDN location fails, load from this location
'lib/jquery'
]
}
});
//Later
require(['jquery'], function ($) {
});
上述程式碼先嚐試載入CDN版本,如果出錯,則退回到本地的lib/jquery.js。
注意: paths備錯僅在模組ID精確匹配時工作。這不同於常規的paths配置,常規配置可匹配模組ID的任意字首部分。備錯主要用於非常的錯誤恢復,而不是常規的path查詢解析,因為那在瀏覽器中是低效的。
全域性 requirejs.onError§ 4.6.4
為了捕獲在局域的errback中未捕獲的異常,你可以過載requirejs.onError():
requirejs.onError = function (err) {
console.log(err.requireType);
if (err.requireType === 'timeout') {
console.log('modules: ' + err.requireModules);
}
throw err;
};
載入外掛§ 5
RequireJS支援載入器外掛。使用它們能夠載入一些對於指令碼正常工作很重要的非JS檔案。RequireJS的wiki有一個外掛的列表。本節討論一些由RequireJS一併維護的特定外掛:
指定文字檔案依賴§ 5.1
如果都能用HTML標籤而不是基於指令碼操作DOM來構建HTML,是很不錯的。但沒有好的辦法在JavaScript檔案中嵌入HTML。所能做的僅是在js中使用HTML字串,但這一般很難維護,特別是多行HTML的情況下。.
RequireJS有個text.js外掛可以幫助解決這個問題。如果一個依賴使用了text!字首,它就會被自動載入。參見text.js的README檔案。
頁面載入事件及DOM Ready§ 5.2
RequireJS載入模組速度很快,很有可能在頁面DOM Ready之前指令碼已經載入完畢。需要與DOM互動的工作應等待DOM Ready。現代的瀏覽器通過DOMContentLoaded事件來知會。
但是,不是所有的瀏覽器都支援DOMContentLoaded。domReady模組實現了一個跨瀏覽器的方法來判定何時DOM已經ready。下載並在你的專案中如此用它:
require(['domReady'], function (domReady) {
domReady(function () {
//This function is called once the DOM is ready.
//It will be safe to query the DOM and manipulate
//DOM nodes in this function.
});
});
基於DOM Ready是個常規需求,像上述API中的巢狀呼叫方式,理想情況下應避免。domReady模組也實現了Loader Plugin API,因此你可以使用loader plugin語法(注意domReady依賴的!字首)來強制require()回撥函式在執行之前等待DOM Ready。當用作loader plugin時,domReady會返回當前的document:
require(['domReady!'], function (doc) {
//This function is called once the DOM is ready,
//notice the value for 'domReady!' is the current
//document.
});
注意: 如果document需要一段時間來載入(也許是因為頁面較大,或載入了較大的js指令碼阻塞了DOM計算),使用domReady作為loader plugin可能會導致RequireJS“超時”錯。如果這是個問題,則考慮增加waitSeconds配置項的值,或在require()使用domReady()呼叫(將其當做是一個模組)。
Define an I18N Bundle§ 5.3
一旦你的web app達到一定的規模和流行度,提供本地化的介面和資訊是十分有用的,但實現一個擴充套件良好的本地化方案又是很繁贅的。RequireJS允許你先僅配置一個含有本地化資訊的基本模組,而不需要將所有的本地化資訊都預先建立起來。後面可以將這些本地化相關的變化以值對的形式慢慢加入到本地化檔案中。
i18n.js外掛提供i18n bundle支援。在模組或依賴使用了i18n!字首的形式(詳見下)時它會自動載入。下載該外掛並將其放置於你app主JS檔案的同目錄下。
將一個檔案放置於一個名叫“nls”的目錄內來定義一個bundle——i18n外掛當看到一個模組名字含有“nls”時會認為它是一個i18n bundle。名稱中的“nls”標記告訴i18n外掛本地化目錄(它們應當是nls目錄的直接子目錄)的查詢位置。如果你想要為你的“my”模組集提供顏色名的bundle,應像下面這樣建立目錄結構:
- my/nls/colors.js
該檔案的內容應該是:
//my/nls/colors.js contents:
define({
"root": {
"red": "red",
"blue": "blue",
"green": "green"
}
});
以一個含有“root”屬性的object直接量來定義該模組。這就是為日後啟用本地化所需的全部工作。你可以在另一個模組中,如my/lamps.js中使用上述模組:
//Contents of my/lamps.js
define(["i18n!my/nls/colors"], function(colors) {
return {
testMessage: "The name for red in this locale is: " + colors.red
}
});
my/lamps模組具備一個“testMessage”屬性,它使用了colors.red來顯示紅色的本地化值。
日後,當你想要為檔案再增加一個特定的翻譯,如fr-fr,可以改變my/nls/colors內容如下:
//Contents of my/nls/colors.js
define({
"root": {
"red": "red",
"blue": "blue",
"green": "green"
},
"fr-fr": true
});
然後再定義一個my/nls/fr-fr/colors.js檔案,含有如下內容:
//Contents of my/nls/fr-fr/colors.js
define({
"red": "rouge",
"blue": "bleu",
"green": "vert"
});
RequireJS會使用瀏覽器的navigator.language或navigator.userLanguage屬性來判定my/nls/colors的本地化值,因此你的app不需要更改。如果你想指定一個本地化方式,你可使用模組配置將該方式傳遞給外掛:
requirejs.config({
config: {
//Set the config for the i18n
//module ID
i18n: {
locale: 'fr-fr'
}
}
});
注意 RequireJS總是使用小寫版本的locale值來避免大小寫問題,因此磁碟上i18n的所有目錄和檔案都應使用小寫的本地化值。 RequireJS有足夠智慧去選取合適的本地化bundle,使其儘量接近my/nls/colors提供的那一個。例如,如果locale值時“en-us”,則會使用“root” bundle。如果locale值是“fr-fr-paris”,則會使用“fr-fr” bundle
RequireJS也會將bundle合理組合,例如,若french bundle如下定義(忽略red的值):
//Contents of my/nls/fr-fr/colors.js
define({
"blue": "bleu",
"green": "vert"
});
則會應用“root”下的red值。所有的locale元件是如此。如果如下的所有bundle都已定義,則RequireJS會按照如下的優先順序順序(最頂的最優先)應用值:
- my/nls/fr-fr-paris/colors.js
- my/nls/fr-fr/colors.js
- my/nls/fr/colors.js
- my/nls/colors.js
如果你不在模組的頂層中包含root bundle,你可像一個常規的locale bundle那樣定義它。這種情形下頂層模組應如下:
//my/nls/colors.js contents:
define({
"root": true,
"fr-fr": true,
"fr-fr-paris": true
});
root bundle應看起來如下:
//Contents of my/nls/root/colors.js
define({
"red": "red",
"blue": "blue",
"green": "green"
});