◎ 本文轉載自 網站製作學習誌。
之前分享了初探 RequireJS 一文後,對 RequireJS 已經有一定的瞭解,但後來實際應用到 Backbone.js 程式上時,發現了一些要特別注意的事項。
以下便是我在整合兩者時的筆記。
在 初探 RequireJS 的範例中,我是這樣寫的:
js/main.js 1 require({
2 paths: {
3 "order": "../lib/requirejs/order",
4 "lib": "../lib"
5 }
6 });
7
8 require([
9 'app',
10 'order!lib/jquery/jquery-min',
11 'order!lib/underscore/underscore-min',
12 'order!lib/backbone/backbone-min'
13 ], function (App) {
14 App.initialize();
15 });
事實上這會影響我們在用 r.js
時的編譯,所以第一步我們先加上 baseUrl 選項:
1 require({
2 baseUrl: './',
3 paths: {
4 order: 'lib/requirejs/order',
5 }
6 });
7
8 require([
9 'js/old_app',
10 'order!lib/jquery/jquery-min',
11 'order!lib/underscore/underscore-min',
12 'order!lib/backbone/backbone-min'
13 ], function (App) {
14 App.initialize();
15 });
baseUrl 是指讓 RequireJS 搜尋模組路徑時的起始位置;如果是用 ./
的話,指的就是 index.html 所在的網址。
這樣一來我們就不用設定 lib
的別名了。
註:接下來的範例裡,除非必要,否則將不會特別提到選項的部份。
先前我們的 js/app.js
是這樣寫的:
1 define(function () {
2 return {
3 initialize: function () {
4 var Model = Backbone.Model.extend({
5 // ...
6 });
7 var View = Backbone.View.extend({
8 // ...
9 });
10 var model = new Model();
11 var view = new View({
12 model: model
13 })
14 }
15 }
16 });
在 initialize
方法裡我把所有的程式碼都塞在這裡,而當我想把 model/view 分離出來時,卻遇到了很大的難題。
首先我想把 Model 分離出來,所以加入一個 js/model/Model.js
如下:
1 define(function () {
2 return Backbone.Model.extend({
3 // ...
4 });
5 });
然後把 js/app.js
改寫成:
1 define([
2 'js/model/Model'
3 ], function (Model) {
4 return {
5 initialize: function () {
6 var View = Backbone.View.extend({
7 // ...
8 });
9 var model = new Model();
10 var view = new View({
11 model: model
12 })
13 }
14 }
15 });
重新執行,結果瀏覽器告訴我:Backbone is not defined
。
怎麼回事呢?
原因是 js 檔案的載入順序,以及 factory 執行的時機。我們來分析一下整個程式的載入流程:
載入 js/app.js 。
載入 js/model/Config.js 。
執行 js/model/Config.js 的工廠方法。
載入 jQuery 。
載入 underscore.js 。
載入 Backbone.js 。
執行 js/app.js 的工廠方法。
執行 js/app.js 的 initialize
方法。
還記得 define
API 會在相依的模組完載入後就執行工廠方法嗎?
這裡我們的 js/model/Config.js
因為沒有指定相依模組,所以載入後就會直接執行其工廠方法。但是因為 Backbone.js 等第三方套件還沒載入,所以在第三個步驟時,瀏覽器就噴錯誤給我們了。
怎麼解決呢?第一個方法是把 js/main.js
中的 'js/app'
移到第三方套件後載入,不過得再加入不必要的 namespace:
1 require([
2 'order!lib/jquery/jquery-min',
3 'order!lib/underscore/underscore-min',
4 'order!lib/backbone/backbone-min',
5 'order!js_1/app',
6 ], function (_jQuery, _Underscore, _Backbone, App) {
7 App.initialize();
8 });
註:要記得相依的 js 模組會與 callback 的參數從左至右一一對應。
這樣確實就可以讓模組依照我們相要的順序依序載入了。
不過那些用不到的 namespace 真的很礙眼,還好 JavaScript 可以讓我們不需要寫它們;我們直接改用 arguments
來將 App
載入:
1 require([
2 'order!lib/jquery/jquery-min',
3 'order!lib/underscore/underscore-min',
4 'order!lib/backbone/backbone-min',
5 'order!js/app',
6 ], function () {
7 App = _.last(arguments);
8 App.initialize();
9 });
這樣就看不到那些無用的 namespace 了。
仔細比較 js/app.js
與 js/model/Model.js
,一個是回傳純物件 {...}
,一個是回傳 Backbone.Model.extend({...})
,這兩者有什麼不同呢?
純物件的方式,我們可以直接使用它的方法:
1 App.initialize();
而 Backbone.Model.extend({...})
回傳的則是一個建構函式,我們要用 new
關鍵字來使用它:
1 define([
2 'js/model/Model'
3 ], function (Model) {
4 return {
5 initialize: function () {
6 var model = new Model();
7 }
8 }
9 });
另外千萬不要直接回傳 new 之後的 Model 或是 View,也就是:
js/model/Model.js 1 define(function () {
2 return new Backbone.Model.extend({
3 // ...
4 });
5 });
因為這樣一來回傳的是物件,而非建構函式,開發上就會造成問題。
RequireJS 的 order plugin 可以讓我們依序載入模組,但它卻還是有所限制。
假設我把前面的第三方套件放到 js/vendor.js
裡面:
1 define([
2 'order!lib/jquery/jquery-min',
3 'order!lib/underscore/underscore-min',
4 'order!lib/backbone/backbone-min',
5 ], function () {});
然後把 js/main.js
改成:
1 require([
2 'order!js/vendor',
3 'order!js/app',
4 ], function (Vendor, App) {
5 App.initialize();
這時我發現程式處於一種不穩定的狀態,也就是時好時壞。
這是因為我們使用的是非同步載入的方式,但 order plugin 只能確保同一個檔案裡的載入順序,當跨到不同檔案時,order plugin 就會失效了。
原本以為 r.js
能夠取用 js/main.js
裡的設定,但後來測試的結果是不行。所以在使用 r.js
編譯檔案時,如果有以下的設定:
1 require({
2 baseUrl: './',
3 paths: {
4 order: 'lib/requirejs/order',
5 text: 'lib/requirejs/text'
6 }
7 });
那麼在下 r.js
指令時,就要把它們都加入:
1 r.js -o \
2 name=js/main \
3 out=js/main-built.js \
4 baseUrl="./" \
5 paths.order="lib/requirejs/order" \
6 paths.text="lib/requirejs/text"
這樣一來,才能編出正確的單一壓縮 js 檔案。
不過每次要下這麼長的指令還真麻煩,還好 r.js
也提供了方便的用法,就是建立一個 build profile ;我們只需要在專案根目錄加上一個 build.js
:
1 ({
2 baseUrl: './',
3 name: 'js/main',
4 out: 'main-built.js',
5 paths: {
6 order: 'lib/requirejs/order',
7 text: 'lib/requirejs/text'
8 }
9 })
然後利用 build.js
來重新最佳化:
1 r.js -o build.js
效果就是一樣的了。
build profile 的其他選項請參考官方提供的 build.js 範例。
RequireJS 提供了一個很棒的非同步樣版載入模組,名為 text
;它可以幫我們把外部的 html 檔案載入,並當做字串使用。用法如下:
1 define([
2 'text!template/view_template.html'
3 ], function (viewTemplate) {
4 return Backbone.View.extend({
5 initialize: function () {
6 this.$el.html(viewTemplate);
7 },
8 });
9 });
註: text
是 lib/requirejs/text
的別名。
有趣的是,它在透過 r.js
編譯時,就會把樣版檔直接編譯為字串,而不再經外部載入。
假設 template/view_template.html
的內容為:
1 <p>This is template.</p>
那麼透過 r.js
編譯後的結果如下:(已經有重新格式化)
1 /* other modules */, define("text!template/view_template.html", [], function () {
2 return "<p>This is template.</p>"
3 }), define("js/view/View", ["text!template/view_template.html"], function (a) {
4 return Backbone.View.extend({
5 initialize: function () {
6 this.$el.html(a)
7 }
8 })
9 })
從上面的範例就可以看到 text plugin 把 template/view_template.html
變成一個會回傳字串的模組了。