淺談HTML5單頁面架構(二)——backbone + requirejs + zepto + underscore


上一篇《淺談HTML5單頁面架構(一)——requirejs + angular + angular-route》探討了angular+requirejs的一個簡單架構,這一篇繼續來看看backbone如何跟requirejs結合。

相同地,項目架構好與壞不是說用了多少牛逼的框架,而是怎么合理利用框架,讓項目開發更流暢,代碼更容易管理。那么帶着這個目的,我們來繼續探討backbone。

 

首先,來看看整個項目結構。

跟上一篇angular類似,libs里多了underscore和zepto。三個根目錄文件:

  • index.html:唯一的html
  • main.js:requirejs的配置,程序的入口
  • router.js:整個app或網站的單頁面路由配置

 

第一步,還是建立單頁面唯一的HTML

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>Backbone & Requirejs</title>
</head>
<body>
<div id="container"></div>
<script data-baseurl="./" data-main="main.js" src="libs/require.js" id="main"></script>
</body>
</html>

backbone沒有在dom屬性上做文章,我們還是按原生的或者說熟悉的方法寫東西。這里定義了一個container div作為backbone的視圖。

然后引入requirejs,data-main表示主程序入口。

 

 

第二步,配置main.js

(function (win) {
    //配置baseUrl
    var baseUrl = document.getElementById('main').getAttribute('data-baseurl');

    /*
     * 文件依賴
     */
    var config = {
        baseUrl: baseUrl,           //依賴相對路徑
        paths: {                    //如果某個前綴的依賴不是按照baseUrl拼接這么簡單,就需要在這里指出
            zepto: 'libs/zepto.min',
            jquery: 'libs/zepto.min',
            underscore: 'libs/underscore',
            backbone: 'libs/backbone',
            text: 'libs/text'             //用於requirejs導入html類型的依賴
        },
        shim: {                     //引入沒有使用requirejs模塊寫法的類庫。backbone依賴underscore
            'underscore': {
                exports: '_'
            },
            'jquery': {
                exports: '$'
            },
            'zepto': {
                exports: '$'
            },
            'backbone': {
                deps: ['underscore', 'jquery'],
                exports: 'Backbone'
            }
        }
    };

    require.config(config);

    //Backbone會把自己加到全局變量中
    require(['backbone', 'underscore', 'router'], function(){
        Backbone.history.start();   //開始監控url變化
    });

})(window);

關於requirejs的語法,還是不多說,大家自己去官網看吧。這個是基礎。

使用backbone,不得不強調requirejs的shim配置。backbone這個土匪,要的東西多了,要給他“鞋”(underscore),還要給他美金$(jquery)。

由於終端使用jquery就太龐大了,所以這里做了個小把戲,用zepto充當jquery,騙了土匪一把。用幾張越南盾,戲稱是美金,沒想到土老冒也信了。

有個地方需要注意的是,

無論在哪里用requirejs引入backbone后,就會多了Backbone和$這兩個全局變量,所以后續再使用backbone就不需要拘束於requirejs的AMD寫法了。適當放松透透氣也是好的。

配置好依賴關系后,就可以引入router,並調用關鍵的

Backbone.history.start

開始路由監控。

 

第三步,配置router,路由表

define(['backbone'], function () {

    var Router = Backbone.Router.extend({

        routes: {
            'module1': 'module1',
            'module2(/:name)': 'module2',
            '*actions': 'defaultAction'
        },

        //路由初始化可以做一些事
        initialize: function () {
        },

        module1: function() {
            var url = 'module1/controller1.js';
            //這里不能用模塊依賴的寫法,而改為url的寫法,是為了grunt requirejs打包的時候斷開依賴鏈,分開多個文件
            require([url], function (controller) {
                controller();
            });
        },

        //name跟路由配置里邊的:name一致
        module2: function(name) {
            var url = 'module2/controller2.js';
            require([url], function (controller) {
                controller(name);
            });
        },

        defaultAction: function () {
            console.log('404');
            location.hash = 'module2';
        }

    });

    var router = new Router();
    router.on('route', function (route, params) {
        console.log('hash change', arguments);  //這里route是路由對應的方法名
    });

    return router;    //這里必須的,讓路由表執行
});

Backbone.Router.extend這個語法,相信就不必多說了,說多了也說不清楚,大家去官網才是王道:http://backbonejs.org

backbone的路由寫法跟angular類似,但對於可選參數的寫法是不一樣的。angular使用:param?的方式,而backbone使用(:param),哪個方式好,見仁見智吧。

這里定義了一個默認路由,和兩個業務路由。

原理很簡單,就是遇到module1的哈希(hash)就執行后邊這個字符串對應的函數

估計大家早就知道這個玩意。而上述代碼中,關鍵不同點是,這里利用了requirejs做了模塊化,路由跳轉后做的所有邏輯都在另外的js中定義。

關鍵的關鍵,這里使用了url,而且是獨立變量的方式配置模塊的js,而不是

            require(['module1/controller1'], function (controller) {
                controller();
            });

目的是grunt做requirejs打包時,能切斷兩側的js,不要合並在一個大js中。

 

再另外,大家可以善用一下router.on('route', function)這個接口,及時做一下事件解綁和一些清理工作。

 

第四步,寫一個簡單模塊

controller1.js

define(['module1/view1'], function (View) {

    var controller = function () {
        var view = new View();
        view.render('kenko');
    };
    return controller;
});

view1.js

define(['text!module1/tpl.html'], function (tpl) {

    var View1 = Backbone.View.extend({
        el: '#container',

        initialize: function () {
        },

        render: function (name) {
            this.$el.html(_.template(tpl, {name: name}));
        }
    });

    return View1;
});

tpl.html

<div>
    Here is module 1. My name: <%=name %><br>
    <a href="#module2">turn to module 2</a>
</div>

 模版的寫法跟angular不一樣,采用的是更普遍的方式,jquery、underscore都是這個模式。簡單說就是<%%>包括js代碼,用=等號可以直接輸出變量值。

這里做了最簡單的MVC,M只是一個值name,C就是controller了,V就是view1。

View1的寫法需要遵循Backbone的語法,不然這里用Backbone就沒意義了。el指向對應的視圖dom元素,用的是css選擇器,在View中可以使用this.$el獲取到這個jquery風格變量。render是自定義的函數。

到這里,運行程序,就能看到module1的效果了。

 

第五步,再寫第二個模塊,嚴格MVC的

 model2.js

define([], function () {
    var Model2 = Backbone.Model.extend({

        //模型默認的數據
        defaults: function () {
            return {
                name: "noname"
            };
        },

        // 定義一些方法
        fetch: function () {
            var o = this;
            //可以做一些http請求
            setTimeout(function(){
                o.set({name:'vivi'});
                o.trigger('nameEvent');     //向view觸發事件
            }, 1000);
        }

    });

    return Model2;
});

Model的語法也是遵循Backbone要求了,defaults是默認屬性值。讀寫這些屬性,需要通過model.get/set接口,否則就是用toJSON返回整個對象,再不然就解剖式的使用model.attributes.xxx。

fetch是自定義方法,模擬http請求,這是很常規的做法了,不過這個例子沒使用backbone的rest化接口。

數據返回后,使用backbone內建的trigger觸發事件,通知監聽者,也就是view層了。backbone跟angular最大區別就是,backbone不關注view層的組件化,更關注的是model和事件機制,而angular則不重點提事件機制,采用雙向綁定把數據更新的破事隱藏起來。各有各的好處,見仁見智吧。

 

view2.js

define(['text!module2/tpl.html'], function (tpl) {

    var View2 = Backbone.View.extend({
        el: '#container',

        events: {
            'click button': 'clickSpan'     //使用代理監聽交互,好處是界面即使重新rander了,事件還能觸發,不需要重新綁定。如果使用zepto手工逐個元素綁定,當元素刷新后,事件綁定就無效了
        },

        initialize: function () {
            this.model.on('nameEvent', this.render, this);      //監聽事件
        },

        render: function () {
            this.$el.html(_.template(tpl, {name: this.model.get('name')}));     //類似java的DAO思想,一切通過get set操作
        },

        clickSpan: function (e) {
            alert('you clicked the button');
        }
    });

    return View2;
});

接着,我們看看backbone一個典型視圖怎么玩。先看initialize方法,這個是new View2()時先執行的初始化邏輯。

我們在這里監聽nameEvent這個消息,也就是model2拋出的事件。收到這個通知,就更新界面。邏輯很簡單。

這里有一個比較好用的events,交互事件代理機制。

我們不需要單獨的寫zepto on對dom分別綁定事件,只需要在這里配置一個events映射表即可。

click button等同於zepto的

$('button').on('click', function)

這里綁定的就是clickSpan事件。

這個事件代理機制,好處是,在路由切換的時候,可以輕松移除事件監聽。

view.undelegateEvents()

 

tpl.html

<div>
    Here is module 2. My name: <%=name %><br>
    <button>click me!</button>
    <a href="#module1">turn to module 1</a>
</div>

 

controller2.js

define(['module2/model2', 'module2/view2'], function (Model, View) {

    var controller = function (name) {
        var model = new Model();
        name && model.set({
            name:name               //設置默認的屬性值
        });
        var view = new View({model:model});
        view.render();      //利用Model定義的默認屬性初始化界面
        model.fetch();          //拉取cgi等等,獲取數據,再觸發事件,界面收到消息做相應的動作
    };

    return controller;
});

controller負責的做的事就是揉合數據,放到view中。先讓view用默認數據渲染,再讓model去拉取最新數據,最后通過事件機制更新界面。

當然,這個controller並不是backbone規范,大家可以盡情發揮。

最后回到路由表中,當hash變成module2時,就執行:

        module2: function(name) {
            var url = 'module2/controller2.js';
            require([url], function (controller) {
                controller(name);
            });
        },

至此,簡單的requirejs+backbone框架已經完成了。除了router的耦合度很高外,每個模塊邏輯代碼都已經獨立,app可以輕松實現按需加載。

那么追求機制的騷年,要停下來嗎?按這個方案,在實際開發中,多人經常會在router這個節骨眼上打架,這里的配置化還不夠完美。

 

 

第六步,優化router,徹底配置化

現有方案的問題是,router中除了寫路由配置外,還需要添加相應的function,這樣既冗余又容易沖突,那么能否監聽route事件,做一個統一的路由處理器?一個處理函數,處理全部路由響應。

define(['backbone'], function () {

    var routesMap = {
        'module1': 'module1/controller1.js',            //原來應該是一個方法名,這里取巧改為模塊路徑
        'module2(/:name)': 'module2/controller2.js',
        '*actions': 'defaultAction'
    };

    var Router = Backbone.Router.extend({

        routes: routesMap,

        defaultAction: function () {
            console.log('404');
            location.hash = 'module2';
        }

    });

    var router = new Router();
    //徹底用on route接管路由的邏輯,這里route是路由對應的value
    router.on('route', function (route, params) {
        require([route], function (controller) {
            if(router.currentController && router.currentController !== controller){
                router.currentController.onRouteChange && router.currentController.onRouteChange();
            }
            router.currentController = controller;
            controller.apply(null, params);     //每個模塊約定都返回controller
        });
    });

    return router;
});

上述代碼,把路由表抽離,目的是可以放到index.html中,可以在服務器做直出,保持0緩存,輕松實現對外網版本的控制。

另外Router中,沒有了每個路由對應的函數,而路由表中的key/value改為真正意義的一個字符串——模塊路徑。

感謝backbone的健壯,我開始還以為這樣肯定會報錯,結果backbone沒找到對應函數就停止執行了,不錯,贊一個。

沒有了一個個的相應函數,取而代之的是route事件處理器。

處理器中,利用了配置表的value,拉取對應的模塊,並調用相應的controller。有了這個小把戲,大家可以自由發揮了,配置成各種字符串,多個controller集合在一個requirejs模塊中等等。。。

另外,這里約定controller中有onRouteChange的接口,用於接收路由切換的通知,好做一些銷毀工作。

來看看新的controller代碼:

define(['module2/model2', 'module2/view2'], function (Model, View) {

    var controller = function (name) {
        var model = new Model();
        name && model.set({
            name:name               //設置默認的屬性值
        });
        var view = new View({model:model});
        view.render();      //利用Model定義的默認屬性初始化界面
        model.fetch();          //拉取cgi等等,獲取數據,再觸發事件,界面收到消息做相應的動作

        controller.onRouteChange = function () {
            console.log('change');  //可以做一些銷毀工作,例如view.undelegateEvents()
            view.undelegateEvents();
        };
    };

    return controller;
});

至此,大功告成,多人開發中,需要修改路由,只需要修改一個配置,不在這里寫任何邏輯,利用svn合並功能,輕松完成協同開發。

 

 

本文代碼:https://github.com/kenkozheng/HTML5_research/tree/master/BackboneRequireJS

下一篇:淺談HTML5單頁面架構(三)—— 回歸本真:自定義路由 + requirejs + zepto + underscore

上一篇:淺談HTML5單頁面架構(一)——requirejs + angular + angular-route

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM