RequireJS是一個非常小巧的JavaScript模塊載入框架,是AMD規范最好的實現者之一。最新版本的RequireJS壓縮后只有14K,堪稱非常輕量。它還同時可以和其他的框架協同工作,使用RequireJS必將使您的前端代碼質量得以提升。
一、AMD 介紹
前端開發在近一兩年發展的非常快,JavaScript作為主流的開發語言得到了前所未有的熱捧。大量的前端框架出現了,這些框架都在嘗試着解決一 些前端開發中的共性問題,但是實現又不盡相同。在這個背景下,CommonJS社區誕生了,為了讓前端框架發展的更加成熟,CommonJS鼓勵開發人員 一起在社區里為一些完成特定功能的框架制定規范。AMD(Asynchronous Module Definition)就是其中的一個規范。
傳統JavaScript代碼的問題
讓我們來看看一般情況下JavaScript代碼是如何開發的:通過<script>標簽來載入JavaScript文件,用全局變量 來區分不同的功能代碼,全局變量之間的依賴關系需要顯式的通過指定其加載順序來解決,發布應用時要通過工具來壓縮所有的JavaScript代碼到一個文 件。當Web項目變得非常龐大,前端模塊非常多的時候,手動管理這些全局變量間的依賴關系就變得很困難,這種做法顯得非常的低效。
AMD(Asynchronous Module Definition)的引入
從名稱上看便知它是適合script tag的。也可以說AMD是專門為瀏覽器中JavaScript環境設計的規范。它吸取了CommonJS的一些優點,但又不照搬它的格式。開始AMD作為CommonJS的transport format 存在,因無法與CommonJS開發者達成一致而獨立出來。它有自己的wiki 和討論組 。
AMD提出了一種基於模塊的異步加載JavaScript代碼的機制,它推薦開發人員將JavaScript代碼封裝進一個個模塊,對全局對象的依 賴變成了對其他模塊的依賴,無須再聲明一大堆的全局變量。通過延遲和按需加載來解決各個模塊的依賴關系。模塊化的JavaScript代碼好處很明顯,各 個功能組件的松耦合性可以極大的提升代碼的復用性、可維護性。這種非阻塞式的並發式快速加載JavaScript代碼,使Web頁面上其他不依賴 JavaScript代碼的UI元素,如圖片、CSS以及其他DOM節點得以先加載完畢,Web頁面加載速度更快,用戶也得到更好的體驗。
CommonJS的AMD規范中只定義了一個全局的方法,如清單1所示。
define(id?, dependencies?, factory);
該方法用來定義一個JavaScript模塊,開發人員可以用這個方法來將部分功能模塊封裝在這個define方法體內。
id表示該模塊的標識,為可選參數。
dependencies是一個字符串Array,表示該模塊依賴的其他所有模塊標識,模塊依賴必須在真正執行具體的factory方法前解決,這 些依賴對象加載執行以后的返回值,可以以默認的順序作為factory方法的參數。dependencies也是可選參數,當用戶不提供該參數時,實現 AMD的框架應提供默認值為[“require”,”exports”,“module”]。
factory是一個用於執行改模塊的方法,它可以使用前面dependencies里聲明的其他依賴模塊的返回值作為參數,若該方法有返回值,當該模塊被其他模塊依賴時,返回值就是該模塊的輸出。
CommonJS在規范中並沒有詳細規定其他的方法,一些主要的AMD框架如RequireJS、curl、bdload等都實現了define方法,同時各個框架都有自己的補充使得其API更實用。
AMD設計出一個簡潔的寫模塊API:
define(id?, dependencies?, factory);
其中:
- id: 模塊標識,可以省略。
- dependencies: 所依賴的模塊,可以省略。
- factory: 模塊的實現,或者一個JavaScript對象。
define(function() { return { mix: function(source, target) { } }; });
define(['base'], function(base) { return { show: function() { // todo with module base } } });
page.js
define(['data', 'ui'], function(data, ui) { // init here });
define({
users: [],
members: []
});
define('index', ['data','base'], function(data, base) { // todo });
define(function(require, exports, module) { var base = require('base'); exports.show = function() { // todo with module base } });
二、RequireJS
RequireJS會讓你以不同於往常的方式去寫JavaScript。你將不再使用script標簽在HTML中引入JS文件,以及不用通過script標簽順序去管理依賴關系。
1、簡單示例
當然也不會有阻塞(blocking)的情況發生。好,以一個簡單示例開始。
<!doctype html> <html> <head> <title>requirejs入門(一)</title> <meta charset="utf-8"> <!--引入require.js(實際上除了require.js,其它文件模塊都不再使用script標簽引入)---> <script data-main="main" src="require.js"></script> </head> <body> ... </body> </html>
main.js
require.config({ paths: { jquery: 'jquery-1.7.2' } }); require(['jquery'], function($) { alert($().jquery); });
main.js中就兩個函數調用require.config和require。
- require.config用來配置一些參數,它將影響到requirejs庫的一些行為。require.config的參數是一個JS對象,常用的配置有baseUrl,paths等。
- 這里配置了paths參數,使用模塊名“jquery”,其實際文件路徑jquery-1.7.2.js(后綴.js可以省略)。
這里require函數的第一個參數是數組,數組中存放的是模塊名(字符串類型),數組中的模塊與回調函數的參數一一對應。這里的例子則只有一個模塊“jquery”。
- 我們知道jQuery從1.7后開始支持AMD規范,即如果jQuery作為一個AMD模塊運行時,它的模塊名是“jquery”。注意“jquery”是固定的,不能寫“jQuery”或其它。
- 如果文件名“jquery-1.7.2.js”改為“jquery.js”就不必配置paths參數了。
- require.config中config可以省略
jQuery中的支持AMD代碼如下
if ( typeof define === "function" && define.amd && define.amd.jQuery ) { define( "jquery", [], function () { return jQuery; } ); }
我們知道jQuery最終向外暴露的是全局的jQuery和 $。如下
// Expose jQuery to the global object window.jQuery = window.$ = jQuery;
如果將jQuery應用在模塊化開發時,其實可以不使用全局的,即可以不暴露出來。需要用到jQuery時使用require函數即可,
把目錄r1放到apache或其它web服務器上,訪問index.html。
網絡請求如下
我們看到除了require.js外main.js和jquery-1.7.2.js也請求下來了。而它們正是通過requirejs請求的。
頁面上會彈出jQuery的版本
這是一個很簡單的示例,使用requirejs動態加載jquery。
2、寫一個自己的模塊:選擇器
為演示方便這里僅實現常用的三種選擇器id,className,attribute。RequireJS使用define來定義模塊。
本例目的:
- 1、使用baseUrl來配置模塊根目錄,baseUrl可以是絕對路徑也可以是相對路徑。
- 2、使用define定義一個函數類型模塊,RequireJS的模塊可以是JS對象,函數或其它任何類型(CommonJS/SeaJS則只能是JS對象)
<!doctype html> <html> <head> <title>requirejs入門(二)</title> <meta charset="utf-8"> <style type="text/css"> .wrapper { width: 200px; height: 200px; background: gray; } </style> </head> <body> <div class="wrapper"></div> <script data-main="js/main" src="require.js"></script> </body> </html>
注意:
- 把script標簽放到了div的后面,因為要用選擇器去獲取頁面dom元素,而這要等到dom ready后。
- 因為把main.js放到js目錄中,這里data-main的值須改為“js/main”(說明:data-main貌似可以省略????)
selector.js代碼
define(function() { function query(selector,context) { var s = selector, doc = document, regId = /^#[\w\-]+/, regCls = /^([\w\-]+)?\.([\w\-]+)/, regTag = /^([\w\*]+)$/, regNodeAttr = /^([\w\-]+)?\[([\w]+)(=(\w+))?\]/; var context = context == undefined ? document : typeof context == 'string' ? doc.getElementById(context.substr(1,context.length)) : context; if(regId.test(s)) { return doc.getElementById(s.substr(1,s.length)); } // 略... } return query; });
define的參數為一個匿名函數,該匿名函數執行后返回query,query為函數類型。query就是選擇器的實現函數。
main.js 如下
require.config({ baseUrl: 'js' }); require(['selector'], function(query) { var els = query('.wrapper'); console.log(els) });
require.config方法執行配置了baseUrl為“js”,baseUrl指的模塊文件的根目錄,可以是絕對路徑或相對路徑。這里用的是相對路徑。相對路徑指引入require.js的頁面為參考點,一般是index.html。
把目錄r2放到apache或其它web服務器上,訪問index.html。
網絡請求如下
main.js和selector.js都請求下來了。
selector.js下載后使用query獲取頁面class為“.wrapper”的元素,控制台輸出了該元素。如下
3、寫一個具有依賴的事件模塊
具有依賴的事件模塊event提供三個方法bind、unbind、trigger來管理DOM元素事件。
event依賴於cache模塊,cache模塊類似於jQuery的$.data方法。提供了set、get、remove等方法用來管理存放在DOM元素上的數據。
示例實現功能: 為頁面上所有的段落P元素添加一個點擊事件,響應函數會彈出P元素的innerHTML。
為了獲取元素,用到了上一例寫的selector.js。不在重復貼其代碼
<!doctype html> <html> <head> <title>requirejs入門(三)</title> <meta charset="utf-8"> <style type="text/css"> p { width: 200px; background: gray; } </style> </head> <body> <p>p1</p><p>p2</p><p>p3</p><p>p4</p><p>p5</p> <script data-main="js/main" src="require.js"></script> </body> </html>
cache.js
define(function() { var idSeed = 0, cache = {}, id = '_ guid _'; // @private function guid(el) { return el[id] || (el[id] = ++idSeed); } return { set: function(el, key, val) { if (!el) { throw new Error('setting failed, invalid element'); } var id = guid(el), c = cache[id] || (cache[id] = {}); if (key) c[key] = val; return c; }, // 略去... }; });
cache模塊的寫法沒啥特殊的,與selector不同的是返回的是一個JS對象。
event.js 如下
define(['cache'], function(cache) { var doc = window.document, w3c = !!doc.addEventListener, expando = 'snandy' + (''+Math.random()).replace(/\D/g, ''), triggered, addListener = w3c ? function(el, type, fn) { el.addEventListener(type, fn, false); } : function(el, type, fn) { el.attachEvent('on' + type, fn); }, removeListener = w3c ? function(el, type, fn) { el.removeEventListener(type, fn, false); } : function(el, type, fn) { el.detachEvent('on' + type, fn); }; // 略去... return { bind : bind, unbind : unbind, trigger : trigger }; });
event依賴於cache,定義時第一個參數數組中放入“cache”即可。第二個參數是為函數類型,它的參數就是cache模塊對象。
這樣定義后,當require事件模塊時,requirejs會自動將event依賴的cache.js也下載下來。
main.js 如下
require.config({ baseUrl: 'js' }); require(['selector', 'event'], function($, E) { var els = $('p'); for (var i=0; i<els.length; i++) { E.bind(els[i], 'click', function() { alert(this.innerHTML); }); } });
依然先配置了下模塊的根目錄js,然后使用require獲取selector和event模塊。
回調函數中使用選擇器$(別名)和事件管理對象E(別名)給頁面上的所有P元素添加點擊事件。
注意:require的第一個參數數組內的模塊名必須和回調函數的形參一一對應。
把目錄r3放到apache或其它web服務器上,訪問index.html。網絡請求如下
我們看到當selector.js和event.js下載后,event.js依賴的cache.js也被自動下載了。這時點擊頁面上各個P元素,會彈出對應的innerHTML。如下
總結:
當一個模塊依賴(a)於另一個模塊(b)時,定義該模塊時的第一個參數為數組,數組中的模塊名(字符串類型)就是它所依賴的模塊。
當有多個依賴模時,須注意回調函數的形參順序得和數組元素一一對應。此時requirejs會自動識別依賴,且把它們都下載下來后再進行回調。
說明和其他問題:
1、路徑與后綴名
在 require 一個 js 文件的時候,一般不需要加上后綴名。如果加上后綴名,會按照絕對路徑加載。沒有后綴名,是按照下面的路徑加載:
<script data-main=
"js/main"
src=
"js/require-jquery.js"
></script
>
也就是默認加載 data-main 指定的目錄,即 js/main.js 文件所在的目錄。當然,你可以通過配置文件修改。
2、define 定義模塊方法只能用在獨立的js文件中,不能在頁面中直接使用。
否則會報 Mismatched anonymous define() module 錯誤。
3、和其他第三方js類庫是否沖突?
不會沖突。一般比較規范的類庫,都會給自己的js加上命名空間。比如 wojilu 舊有的 wojilu.common.js ,其實就是放在 wojilu 命名空間中(當然是通過更原始的方式實現命名空間的)。
在通過 RequireJS 加載這些第三方的 js 的時候,完全不要有任何擔憂。
當然,如果第三方類庫能夠使用 RequireJS 的方式進行改造,那是最好。比如 wojilu 中大多數js 都按照 RequireJS 的方式進行了改造。但是,如果你不改造,也是完全不要緊的。
4、在代碼中 require 一個文件多次,是否會導致瀏覽器反復加載?
不會,這是 RequrieJS 的優點,即使你反復 require 它,它只加載一次。
參考: