我對Backbone.js的一些認識

backbone.js已經不是當前最流行的前端框架了,可是對於我而言,依然具備比較好的學習價值。雖然目前來講,react,vue等mvvm框架很是火熱,可是感受自身還不到去使用這種框架的層次。這些技術雖好,可是對我的的挑戰也是比較大:首先是在編程基礎這個部分,包括數據結構,算法,面向對象編程,設計模式,設計原則等等,我以爲在這些方面積累地還不夠;其次是工做方法層面,對比angualr,react以及vue,跟傳統的用原生js或者jquery寫的代碼,包括html,css和js三個方面,你會發現這兩種技術直接致使的咱們在工做結果上的巨大差異,這對於已經習慣了傳統開發的我來講,要挑戰的不只僅是新技術的學習跟研究,更多的是工做思路,工做方法甚至是跟其它同事配合協做方式的改變,這個難度也很大。再加上我本人是一個還比較喜歡去鑽研細節的人,因此從去年開始作前端開發到如今,一直都沒有大膽地學習不少的新東西,相反,我把更多的精力花在了編程思惟的鍛鍊以及基礎知識的鞏固上,這件事情看起來很小,可是對個人提升頗有幫助。css

我還記得去年我剛到上家單位的時候,是頂着比較大的壓力去作前端的,由於工做第一年,我在武漢作的是VB.NET的開發工做,技術鍛鍊的不多,邏輯思惟方面跟sql方面鍛鍊地多,由於公司作ERP系統,有封裝很好的技術平臺,套着用就好了,因此工做都花在寫業務邏輯和數據邏輯上;工做第二年我來了北京,在用友作軟件實施,折騰了大半年,最後仍是以爲在北京作技術最掙錢,就回到了這個本行;到上家單位的時候,狀況是:公司當時沒有前端,也沒有封裝前端任何的東西,我也沒有專門作過前端,之前作的開發仍是VB.NET的後臺開發,因此當時我也比較擔憂怕完成不了當時的工做任務。幸運的是,當時正好遇上一個緊急的項目,公司給機會讓我搭一套前端的架子,其實他們要求也不高,能把各類插件套上就好了。我當時想的是,既然要作前端開發的話,有這個機會,還不如本身動手好好模仿之前公司的開發平臺寫一套東西出來。當時項目很是着急,咱們一夥人連續20天的下班時間都在凌晨1-3點之間,我做爲惟一的前端,可以作的就是在後臺的同事須要某個東西以前,就提早把它開發出來。我就是在那段時間憋出了本身的第一套能夠當作開發平臺用的東西,雖然這套東西,我到如今都以爲拿不出手,可是它對我起到的做用是,讓我開始對代碼的重構和設計產生興趣,我纔開始去注意封裝思路,以及設計模式和設計原則在編程中的實際使用。曾經寫的那一套很粗糙的東西,後來我重構了3次,第一次重構是爲了優化API的使用方式,讓它更好用;第二次重構是改寫各個組件內部實現的方式,並提供各個組件的詳細使用文檔,以便其它不是很擅長js的同事也能快速使用;第三次重構是使用requirejs作模塊化,而且徹底跟後端分離,原來有些組件仍是藉助jsp來搞的。。。雖然我走了,曾經的同事還在使用我寫的東西繼續開發,並且會用的人都以爲用起來還挺簡單的;更有意思的是,同在上家單位的另一個很好的朋友,5年的java開發經驗,幾乎不怎麼作前端,連ajax都不怎麼會,在他前段時間去另一個單位的時候,用我跟他搭的這一套先後臺的東西,居然本身一我的搞定了一個內部管理系統的全部前端功能。html

上家單位的工做經歷,讓我開始重視本身的代碼質量,關注設計模式與設計原則,日常都有意去看相關的博客和書籍,目前來看,成效也很明顯:首先是很習慣性地在代碼中融入職責分離與開閉原則的思考,我之前寫的幾篇博客都有相關的說起;其次是部分設計模式在代碼中的實際運用,包括單例模式,單例模式,適配器模式,狀態模式,觀察者模式等,用多了,對它們的理解也就更深入了。在過去的一年,我還關注的東西有前端工程化構建,js模塊化,瀏覽器緩存管理,移動端頁面開發包括適配及優化等,這些都不是很高大上的一些技術名詞,僅僅是前端基礎知識的範疇,我去了解它們的目的,是以爲這是把這個崗位的工做作好的準備。正是這些對編程思路和基礎知識的學習,我如今看到一些新的功能,都能很快地造成工做思路,若是是要寫代碼的,很快就能想好要寫幾個類, 看清哪一個地方又得拆分紅多個類才能讓它們之間不會有強耦合,以及跨項目的複用等問題;在工做中,我在如今崗位上可以一我的獨立地完成整個前端的工程化管理以及公司產品從後端到PC端以及移動端的全部開發工做,可以總體把控項目的全部代碼,而在一年以前,我仍是一個剛從軟件實施轉回軟件行業的前端小白。因此我今年也仍是不着急去追逐其它火熱的技術,繼續搞本身的編程思惟跟鞏固基礎知識,前端技術變化那麼大,學那麼多用不上也不見得是好事,相反把一些工做中也許會用到的好好琢磨透,也許會來的更有意義。好比說,我我的對動效和svg不是很擅長,那麼過一段時間,我就會去專門研究這方面的內容,直到本身也能作出一些別人能比較承認的效果出來才行,這樣的好處是,在你工做須要的技術範圍內,你方方面面都能作地很好。前端

回到我這篇文章要介紹的backbonejs上來,我爲何在這段時間會去琢磨一個如今不是很火熱的前端框架,並且還認爲它有比較好的學習價值。由於基於backbone的Model與View的開發方式,或者說它提供的面向對象的代碼組織方式,跟我目前慣用的思路仍是比較像的,儘管我已有的代碼都是jquery搞的。可是它又要優於我如今的編程方式,由於它裏面有一個數據驅動UI的思想在裏面,並且還有一個很好的內置的事件管理機制,使得它在一些封裝層面的東西,比我寫的更要嚴謹,清晰一點。只要是可以提升代碼質量的東西,我都認爲是編程基礎的一部分,這正是我目前仍然想花時間去鑽研的東西,因此我想學習。即便不用它作任何的項目,只要把它的思想,可以滲透到我如今的思惟中便可,這個我感受也不太容易,因此我得花一小段時間,才能掌握好它裏面的一些機制。我學習它的主要方法是閱讀官方文檔和寫東西實踐,閱讀文檔過程當中對於本身有疑問的api,必須寫一些簡單的代碼才能知道它的詳細做用。好在它官方文檔組織地還不錯,因此在學習過程當中,須要去測試的api並非不少。而後爲了瞭解如何在實際工做中運用backbone編寫代碼以及它與我如今的編程方式上的區別,我分別用jquery跟backbone寫了一個todo app。簡單起見,jquery實現的版本與官網的功能徹底一致,固然代碼是不一樣的;backbone實現的版本在官網的基礎上,考慮了異步回調處理以及操做的交互還有批量請求的處理,使得這個簡單的app看起來更符合實際的產品需求。最後我發現,雖然這兩個版本實現方式不一樣,可是思路層面卻有類似性,這個去看一下兩份代碼中定義的類名就清楚了。下面是兩個版本的demo地址:vue

jquery版本的預覽地址java

backbone版本的預覽地址react

源碼在:https://github.com/liuyunzhuge/blog/tree/master/todosjquery

本文將從總體思想,數據驅動,事件機制,存在的問題以及實例分析等多個方面來介紹我對backbone的一些認識,裏面有不少我的觀點,受技術水平和經驗的限制,不必定絕對合理,歡迎批評與指正。git

1. backbone的總體思想

js的做用從大範圍上講,主要包含兩個內容,第一是將瀏覽器的數據與服務器的數據在適當的時候進行相互同步;第二是在用戶與瀏覽器,瀏覽器與服務器之間的交互過程當中,控制頁面的變化來反饋不一樣的功能效果。從技術上來講,前者主要體現爲異步請求的處理,後者主要體現爲對DOM的操做。你們都知道,這兩個方面的東西雖然不難,可是很繁瑣,backbone也好,後來的angular react也好,它們誕生都有一個目的就是爲了簡化這兩個方面的工做。不過本文的重點確定只有backbone了,要說明backbone的做用,得先看下咱們不採用backbone的時候是如何處理一個極其簡單的編輯頁面的,而後再來看backbone的解決方法,經過對比就能看出backbone的一些思想以及給咱們工做帶來的好處。github

這是要演練的編輯頁面的原型:ajax

image

需求以下:

1)這個編輯頁面用來處理某個類型的數據,這個類型的數據有兩個字段分別是id和name,表示它的惟一標識和名稱;
2)當用戶選擇新增模式打開這個編輯頁面的時候,頁面初始化狀態如原型所示,當用戶在文本框中輸入非空的文本再點擊保存時,頁面會把新增的文本傳遞給服務端,由服務端保存後再返回這個數據的id,同時在文本框下方的文本處顯示剛剛保存好的數據的名稱;
3)當用戶選擇編輯模式打開這個頁面時,頁面會傳遞一個id以便查出要顯示的數據條目,當正確查出了數據的名稱以後,把它顯示到文本框以及文本框下方的文本中。
4)在用戶新增完數據以後,以及從編輯模式打開這個頁面時,均可以再次編輯文本框中的內容,經過保存按鈕,將最新的名稱同步至服務器,成功以後,再在文本框下方顯示剛錄入的名稱。

不採用backbone,咱們可能會這樣去實現這個頁面的功能(提供大概的代碼,非完整的):

var $input = $('#new_input'),//輸入框
    $save = $('#btn_save'),//保存按鈕
    $name_text = $('#name_text');//數據名稱

//表示頁面要編輯的數據的惟一標識,新增時爲空,編輯時纔有值
var id = (function () {
    //獲取頁面id,詳細實現略
})();

if(id) {
    //編輯時先異步查詢數據,再作頁面初始化
    $.ajax({
        url: '/api/data/query',
        data: {
            id: id
        }
    }).done(function(res){
        if(res.code == 200) {
            var name = res.data.name;
            $input.val(name);
            $name_text.text(name);
        }
    })
}

$save.on('click', function () {
    var name = $.trim($input.val());
    if (!name) return;

    var params = {name: name};
    //編輯時再綁定一個id參數,以便服務器作更新操做
    id && (params.id = id);

    $.ajax({
        url: '/api/data/save',
        data: params
    }).done(function (res) {
        if (res.code == 200) {
            !id && (id = ~~res.data.id);
            $name_text.text(name);
        }
    })
});

這種實現的主要問題在於:

1)在數據變化的時候,必須手工更新DOM,看那兩個ajax請求的回調就知道。對這種簡單頁面可能還好說,要是頁面裏面包含幾十個不一樣類型的表單控件時,這些頁面的更新操做就會變得很是繁雜,並且還容易出錯;

2)缺少封裝,沒有體現數據的管理,功能都是直接靠請求與DOM操做實現的,實際上按照面向對象的思路以及職責分離的原則,應該把數據的同步和數據的管理功能單獨封裝起來,把界面變化的功能也單獨封裝起來,兩部分的內容經過接口或者事件來交互。

若是咱們把它換成backbone的寫法,就會變成:

//建立一個Data類,來表示一個實體類型
var Data = Backbone.Model.extend({
    //定義每一個Data類實例的默認值
    defaults: function () {
        return {
            name: ''
        }
    },
    //解析異步請求返回的結果,fetch方法與save方法都會調用它
    parse: function (res) {
        return res.data;
    }
});

//建立一個AppView類,來完成這個頁面的全部UI功能
var AppView = Backbone.View.extend({
    //指定這個AppView的實例關聯的DOM元素
    el: 'body',
    //指定這個AppView實例在作DOM更新時要採用的html模板
    template: _.template(document.body.innerHTML),
    //定義這個AppView內部要註冊的一些事件,相似jquery的委託方式註冊
    events: {
        'click #btn_save': 'save'
    },
    initialize: function () {
        //監聽關聯的model實例的change事件,只要model實例的屬性發生變化,都會調用自身的render方法
        this.listenTo(this.model,'change', this.render);

        this.$input = $('#new_input');
    },
    render: function () {
        //根據model實例的內容從新渲染html
        this.$el.html(this.template(this.model.attributes));
        return this;
    },
    save: function(){
        var name = $.trim($input.val());
        if (!name) return;

        //直接調用model的save方法來與服務器進行同步
        this.model.save({name: name});
    }
});

//建立一個Data實例
var model = new Data();

//建立一個AppView的實例,並把它關聯的model屬性指定爲上一步建立的Data實例
new AppView({
    model: model
});

//表示頁面要編輯的數據的惟一標識,新增時爲空,編輯時纔有值
var id = (function () {
    //獲取頁面id,詳細實現略
})();

if (id) {
    //編輯模式下設置id
    model.set('id', id);
    //經過fetch自動發送請求與後臺同步
    model.fetch();
}

對比前面這兩份代碼,你會發現,backbone的實現:

1)沒有了對ajax請求的直接調用

2)沒有了對$new_input以及$name_text這兩個DOM元素的直接操做

3)引入了html模板,以便可以快速地更新DOM

4)多了不少封裝,建立了Data和AppView類,最重要的是這個Data類,它的性質就表明着咱們的頁面在真實世界或者是數據庫中的一個業務實體類型,它的做用一方面是將數據管理的邏輯與界面邏輯進行解耦,同時把數據同步的邏輯包含在自身內部,這也是爲啥咱們沒有看到ajax直接調用的緣由,使得數據的邏輯嚴密性更強,也就是所謂的高內聚。

採用backbone以後,即便未來頁面增長了幾十個文本控件,有可能咱們只須要調整save方法便可,利用jq的serializeArray方法咱們能一次性的快速收集整個表單的數據,因此總體上代碼也不會增長不少。另外,從代碼之道的角度來講,backbone以後的代碼因爲更強的封裝性,使得代碼的可閱讀性也更強。因此從結果上來講,backbone可以對咱們的工做起到的做用仍是很明顯的。

那麼它是如何作到這些的呢?正如你在代碼中所看到的:Backbone.Model,Backbone.View,這兩個東西就是它實現這些漂亮代碼的關鍵。Model跟View屬於Backbone提供的兩個核心模塊,簡單來講,Model這個模塊可讓咱們用來定義一些純數據管理的類,大部分狀況下,這些類就是咱們所要開發的功能對應的業務實體,好比一個學生選課系統中,學生,課程,選課記錄這三個都是咱們所要開發的功能的業務實體;用Model定義的類可以爲咱們提供直接修改和獲取業務實體數據屬性的功能,也可以經過簡單明瞭的api直接與服務器進行同步,好比前面用到的fetch,save,還有沒用到的destroy等;View這個模塊可讓咱們來封裝頁面中某個獨立完整部分的UI功能,它每每會與Model模塊進行關聯,而且配合模板一塊兒完成UI的更新,經過監聽Model實例的變化來從新渲染html,經過自身註冊的事件將UI的變化同步到Model實例,它就像一個控制器,同步數據與界面UI的實時變化。除了Model跟View以外,Backbone的底層還有一個sync模塊,封裝了數據同步時異步請求管理的功能,雖然它是底層的實現,但卻不是一個特別好用的東西,在後面的內容中我會說明一些它的不合理的問題,如今只要知道它是用來管理異步請求的便可。Backbone官方文檔裏面,描述這三個模塊之間的關係,用到了一張很是清晰明瞭的示意圖:

image

但願這個圖加上前面的舉例和描述,可以讓你明白這三個模塊之間的關係以及做用。在下一部分我還會進一步的去說明這些模塊之間是如何互相影響的問題,尤爲是Model與View之間的交互。

以上的內容,都是跟前面舉例引入的那個編輯頁面有關,都是爲了說明backbone在簡化編輯頁面開發的時候,是如何實現的以及它背後的核心內容,可是在實際工做中,咱們一樣遇到不少的頁面功能,並非只處理單條數據的邏輯,而是以列表的形式展示多條數據,甚至還會有直接在列表上編輯單條數據等更復雜的功能出現,這個時候若是咱們仍是採用傳統方法來實現,確定還會遇到咱們在開發編輯頁面時遇到的那些問題,並且哪怕是最簡單的列表功能也會比前面的那個編輯頁面要複雜很多,因此這種方法也是須要考慮去改進的。backbone爲了解決這個問題,使用了另外的一個模塊Collection,這個模塊你能夠把它定義出來的東西當作是一個數組,可是它比數組的功能更豐富,由於它能夠指定存儲某種Model的實例,表明Model實例的一個集合,也提供有簡單的api好比fetch,create方法來直接同步服務器的數據;若是說Model跟View的關係,是把數據與UI進行解耦,那麼Collection跟View,就是把數據列表與UI進行解耦,它們的內涵跟機制都是差很少的,只不過Model實例僅僅是做用於單條數據的功能,而Collection實例能夠做用於多條數據的功能;就跟Model能夠被直接關聯到View同樣,Collection實例也能直接經過collection屬性,在建立View實例的時候,傳遞給View;在Collection內的model發生增刪改的時候,通知View實例去從新渲染html;在用戶與View實例發生交互的時候,View主動去調整Collection裏面的內容;View層仍是充當控制器的做用,實時同步UI與Collection之間的變化。關於Collection這個模塊的具體使用,這裏就再也不提供了,第一,前面給出的todos地址,就是一個很好的例子能對比說明全部模塊的做用和關係;第二是,Collection的做用,確實跟Model的做用差很少,理解它的方法,徹底能夠類比Model。這是Backbone官方文檔裏面,提供的描述Collection Model View sync這四個模塊之間關係的示意圖,但願對理解這個模塊的做用能有所幫助:

image

以上就是我認爲的Backbone總體思想的核心內容。不過看過或者用過的人,確定知道backbone還有另外幾個模塊:Events,History和Router,在我看來:

1)Events很重要,是backbone全部機制的核心基礎,但它並非這個框架的思想所在,backbone只是須要它來完成本身想作的事情,你在其它框架裏面也能看到這樣的基礎模塊,因此它不屬於思想的核心;並且用法簡單,沒有太多介紹的必要;

2)History跟Router只能算是backbone提供的工具,要是以爲它們很差用,或者不想作單頁的應用,徹底能夠不用它們,至少我如今不會用,因此我也不打算花時間去研究,沒有它們,咱們依然可使用backbone構建封裝性很強的應用程序。

還有一點就是,雖然backbone爲咱們提供了sync這個模塊,前面我說過它很差用,由於它是強restful風格的api形式,這個得徹底看項目團隊內的現有狀況去考慮是否要這麼幹,並且它要求必須用http 200來表示成功的請求,這對於那些本身去捕獲後臺異常,而後對http response自定義code的後臺服務來講,顯然是有問題的。好在backbone的api還不錯,即便咱們不直接使用那些sync相關的方法,咱們也能夠經過手工管理請求的方式來管理數據,那麼此時backbone爲咱們起到的做用,就真的只是讓代碼更漂亮,讓DOM操做更簡單了。固然更好地辦法就是去重寫一個backbone的sync模塊了。

下一部分說明前面這兩張圖裏面,這些模塊之間互相影響的內部機制。

2. 數據驅動及背後的事件機制

在沒有將數據管理從頁面邏輯中分離出來以前,可以致使DOM發生變化的大部分是2種狀況:第一,是用戶的輸入,包括鍵鼠的操做,好比表單輸入,窗口滾動及調整,鼠標點擊拖拽等;第二,是頁面中js代碼執行致使的變化。拿第二種狀況來講,在一個有大量表單控件的頁面中,若是咱們想收集表單數據同步到服務器,就必須找到合適的DOM元素而後得到它們的值,再一併組織好經過異步請求發送至後端;若是咱們從服務器獲取到了表單的初始數據,須要把這些數據一一回填到表單上的各個控件時,咱們就必須一一找到各個數據屬性對應的DOM元素而後設置它們的值。表單收集相對而言,仍是比較容易,可是表單回填就比較麻煩了,尤爲是表單組件不少,且有複雜邏輯的時候。依靠手工的方式一一設置value,顯得很是重複而繁瑣,還容易出問題。對於列表功能,這個問題也會一樣存在,好比某一個更新操做,在選中列表中的一行數據後,更新了該數據的內容,當結束編輯的時候,就顯然要在該列表中顯示該數據條目的最新狀態,若是採起手工的方式更新列表中顯示的內容,顯然就要找到該數據所關聯的DOM元素,而後根據數據屬性及其位置一一替換。

當backbone把界面邏輯拆分紅Model,View和Collection三個模塊以後,因爲數據的變化,致使UI變化的邏輯,處理起來就特別容易。咱們不用再去一一手工按數據屬性找到相應的元素去作替換了,只要View層實例,監聽到關聯的Model實例或者Collection實例有變化(增刪改)的時候,去調用View層實例的render方法,從新將界面渲染一遍便可,而這個render方法由於有模板引擎的幫助,渲染的步驟僅用一句話就能完成:

image

這種方式就能夠看做是數據驅動式的DOM更新方式,相比原來手工的方法,它顯然要省事很多。如此以來,咱們在編碼過程當中對DOM操做的工做,就能簡化很多。而在數據驅動的背後,依賴backbone提供的Events這個基礎模塊的功能,Model,View以及Collection三個模塊都繼承了Events,使得它們的實例都擁有直接進行事件管理的能力,均可以直接使用on once trigger off等方法來註冊和移除事件監聽,另外backbone爲了方便起見,還提供了一種主動式的事件綁定方式,相關的api是listenTo stopListenTo listenToOnce,名字都起得很明瞭,看到的話,很是好理解。這個Events模塊,除了讓Model等模塊的實例擁有強大的自定義事件管理,同時它還提供了一套內置的事件體系,這套事件體系其實就是前面數據驅動的關鍵:

image

注意紅框的這些事件,有些是隻有Model實例纔會觸發的,有的是隻有Collection實例纔會派發的,有的是都會觸發的,只要當View層的實例,與之相關聯的Model實例和Collection實例觸發了這些事件,均可以直接通知View層實例執行相應的事件回調,咱們所要作的只須要在View層實例初始化的時候,註冊好跟它關聯的Model實例或Collection實例的事件監聽便可。如:

image(關聯Model實例時)

image(關聯Collection實例時)

前面這些可以解釋爲何當Model或Collection發生變化的時候,爲何可以引發View層的變化。可是還有一個方面沒有說清楚,就是因爲用戶與瀏覽器交互致使的View層的變化,如何同步到數據。這個方法,backbone給出的實現,其實跟日常使用jquery綁定各種鍵鼠事件,而後在事件監聽裏面直接去更新關聯的Model或Collection實例沒有區別,事實上,它自己也是這麼作的。好比todos這個小東西里面的TodoView,用events註冊瞭如下事件:

image

相應的回調都設置成了View層的實例方法:

image

結合這兩段代碼,就能發現backbone的寫法,就是直接在事件回調裏面,去調用Model實例的方法,好在Model類要用的方法均可以實現封裝好,因此在調用的時候並不會很麻煩,若是碰到有表單數據收集的場景,也能夠考慮寫個簡單的方法作批量收集,畢竟只要找到表單元素便可完成這個事情。在另一個View組件AppView裏面,你依然能夠找到相似的代碼,這裏就再也不重複了。

不過這種從View層到Model層同步的方式,從我我的的角度,有一個別扭的問題,就是當用戶的鍵鼠操做,已經改變了界面上的內容時,因爲這些回調內對Model實例的同步操做,會致使Model實例觸發change等事件,而後又會致使View層跟Model的change事件綁定的方法(一般是render方法)被再次調用,也就是說頁面內容會作一次無心義的從新渲染。爲啥無心義,由於自己用戶的鍵鼠操做就已經改變完了界面內容 阿。不過看在DOM操做被簡化的份上,這個事情也就算了。可是這個得注意防止事件循環的狀況。什麼意思呢?就是在render過程當中,觸發了某些元素的事件,恰巧這些事件,你加了一些不合適的監聽,在這個監聽裏面又作了model層的同步,致使change事件被再次調用,render方法又被觸發,某些元素的事件也被觸發,而後就事件死循環了。

前面的這些內容,再結合第一部分的那張圖,就已經能把backbone的機制說的比較清楚了,最重要的東西無非就是Model,View,Collection對界面的解耦,事件機制,以及模板引擎而已。

3. 細說sync模塊的問題

看過官方文檔就知道backbone的Model模塊提供的fetch, save, destroy方法,和Collection模塊的fetch方法,都會發送異步請求,與後端服務進行交互。而後全部這些有異步請求操做的方法,都依賴於sync這個模塊,來完成請求頭和請求數據的封裝,以及請求回調的處理。sync這個模塊,採用的是比較原始的異步請求的調用方式,好比它的成功或失敗回調仍是經過jQuery.ajax調用時傳遞的success和error這兩個option來傳遞的,這個模塊徹底遵循rest接口的規範來封裝請求信息和請求數據,好比HTTP請求的METHOD會根據調用的方法,選用GET POST DELETE PUT PATCH中的一種;請求數據的傳遞方式,不是經過form data的形式,而是經過application\json的形式。這種方式從系統接口規範管理來講,確定是很是有好處的,由於語義化很強,接口的地址跟接口所使用的HTTP METHOD,簡單明瞭的表達了何種資源的何種操做。可是從我我的角度而言,我的對異步請求的使用習慣,以及團隊後臺同事對rest接口設計的支持程度,都是我本身不想採用這種請求接口的緣由,具體來講,有如下幾點:

1)我我的習慣仍是喜歡只用傳統的GET跟POST方法,而不是非得根據對資源操做的語義而後選用DELETE PUT PATCH之類的方法。在異步請求的處理過程當中,我認爲有兩點比較重要,第一是接口的地址,要友好要有語義,方便你們看懂;第二是請求數據和返回數據的封裝形式,要便於先後端快速解析,提升工做效率;而具體用什麼HTTP METHOD對工做或者對代碼影響都很小,只是從總體上感知系統的設計水平不同而已。我喜歡作一些區別很大的一些改進,而不是爲了一些規範而去強加約束。

2)因爲這種強rest接口的請求形式,致使請求成功與否徹底取決於HTTP請求狀態碼是否等於200。也就是說,只有在異步請求狀態碼爲200的時候,異步請求的success回調纔會被調用,不然都會調用error回調。看過fetch等方法的源碼就知道,backbone的Model模塊以及Collection模塊,在請求成功以後,對實例自己內部管理調用的那些東西,好比set方法,sync事件的觸發,都是在success回調裏面作的。這個對於那些作了自定義HTTP響應封裝的後端服務來講就會存在問題,好比一個HTTP請求,若是成功,HTTP狀態碼是200,而後後臺會返回這樣的一個格式的數據:

{code: 200, data: {...}}

若是失敗,HTTP狀態碼仍是200,只不過返回的響應就是下面的一個格式的數據:

{code: 500, data: {...}}

到時候前端在回調裏面,根據後端自定義的這個後端來判斷請求是否成功,這個在如今的一些軟件設計裏面也很常見,這是後端的設計習慣,並且這種封裝形式也挺好的,總比直接拋出HTTP 500要來的友好一些。可是問題就來了,由於無論HTTP請求,從業務角度而言是成功仍是失敗,HTTP狀態碼都是200,致使Backbone裏面success的回調會始終被觸發,而從業務角度而已,顯然有一部分狀況下被觸發的話就是錯誤的邏輯。

這個問題是我我的以爲在使用rest接口時最不靈活的一個問題。

3)這種rest接口形式把系統的複雜性想的太簡單了,每一個系統,不是由對資源的簡單的增刪該查的四種操做就完成了的,作過大型一點的管理系統就知道,一個複雜的功能,可能涉及到某個實體數據的查詢,就有可能劃分十多個查詢的場景,每種查詢場景下要使用的參數或者條件都不相同,我在開發這種功能的時候,一般會採用追加一些額外的請求參數來輔助後端進行判斷處理,而sync這個模塊,並無提供一個很好的方式來追加自定義的任意的請求參數。在官方文檔中,我見到的惟一的一個能夠傳遞額外參數的說明,就是Collection模塊的fetch方法,若是用於一些分頁查詢的場景,能夠經過下面的形式來傳遞分頁參數:

Documents.fetch({data: {page: 3}})

但是要是能更靈活一些就行了 阿。

以上就是我以爲在使用backbone作異步請求的時候,從我的角度發現的一些問題,我一再說明是我的角度,由於這些觀點都跟個人經驗能力習慣有關係,每一個人只要堅持本身的方式就好。儘管如此,我仍是很喜歡backbone默認的url的生成機制,由於根據model,collection生成的url,比較友好,對其它人看懂系統的請求路徑有幫助,因此我會嘗試一下徹底在工做中使用它的這種restful的url。

有了這些問題以後,我就在想如何去解決它們,只有這樣,我才能將backbone徹底應用到項目中來。這兩天稍微看了一下源碼,若是想去改變sync模塊內部對請求回調的處理,就必須去改動源碼,才能去改變它回調的處理方式,後來看到了它最後對請求調用的一個處理,就發現了一個更好地方法,能夠不用去改backbone的源碼,只用在外部去覆蓋就能夠了,這個方法就是去重寫Backbone.ajax這個模塊,默認狀況下它是對jQuery.ajax的代理,咱們徹底能夠重寫這個模塊,在請求真正發送前,對請求的option作一些小小的改動,就能解決我前面說的那三個問題,把異步請求形式換成傳統的方式,同時還能保證Model,Collection模塊中跟異步請求相關api的正確使用。

我把這一塊的代碼寫在了另一個位置,感興趣地能夠去了解一下:

https://github.com/liuyunzhuge/blog/tree/master/backbone_ajax

另外也針對這一個內容,提供了一份測試地址,請參考:

http://liuyunzhuge.github.io/blog/backbone_ajax/index.html

不過這個地址在預覽,執行裏面的代碼的時候,看不到我指望你看到的結果,由於這個頁面是發佈在gh-pages上的,github不容許這些靜態頁面發送post請求,因此最好的預覽方法,是clone到本地區查看:)

4. 增刪改注意事項

這裏要介紹的內容,若是隻看官方文檔中給出的todos,確定發現不了,可是當你把todos這個應用考慮成一個真實的軟件時,你就不難發現官方的todos在增刪改時實際上是不夠的。

先來講增:

想一想若是把todos這個app,作成一個直接保存到數據庫的應用,咱們在增長一個todo的時候的邏輯是怎麼樣的?在官方給出的代碼中,它的新增邏輯是,只要新的todo一保存就當即顯示到todo列表裏面去,根本都不判斷是否有保存成功(固然一個localStorage的應用,也沒有失敗的狀況吧)。若是考慮成一個直接保存的數據庫應用,我想大部分人習慣的邏輯應該是這樣的,先建立一個todo,而後調用異步請求持久化的數據庫,而且給異步請求添加回調,只有根據響應判斷請求成功以後,才往todo列表裏面添加一個新的,具體代碼就是這個樣子:

createTodo: function (e) {
    var $new_input = this.$new_input,
        value = $.trim($new_input.val());

    if (e.which == 13 && value) {

        //建立todo
        var td = new Todo({
            text: value
        }, {
            //必須經過collection指定todo屬於的集合,不然後面的save方法會報錯
            collection: this.todos
        });

        //異步保存
        //此處加wait: true也是爲了保證後端請求與前端UI展示一致,只有後端保存成功了,咱們纔會在前端新增一個TodoView
        var _async = td.save({}, {wait: true}), that = this;

        _async && _async.done(function () {
            //保存成功後與用戶交互
            TipView.create({info: '新增成功', type: 'success'}).show();

            //添加到todos,以便觸發todos的add事件
            that.todos.add(td);

            $new_input.focus().val('');
        });
    }
}

另外Collection有提供一個create方法,至關於一步完成save和add的操做,這個方法很差,由於它的返回值不是一個xhr對象,很差添加回調,固然使用option.success是能夠的,不過這種回調方式已通過時了,不符合如今的習慣了,因此我寧願拆開來用,先save,再add,代碼更清晰。

再來講刪:

一樣考慮保存到數據庫時的刪除場景,一般的邏輯應該是先發起刪除的異步請求,等請求成功並判斷刪除成功後,再從界面上清除相關的dom內容。因此正確的作法應該是這樣的:

clear: function (e) {
    //1. 此處調用destroy方法建議加{wait: true},目的是爲了只有在後端添加成功以後纔去更新UI,不然可能會出現後端沒有刪除成功,可是前端已經刪除了的問題
    var _async = this.model.destroy({wait: true});
    _async && _async.done(function () {
        TipView.create({info: '刪除成功', type: 'success'}).show();
    });
}

要特別注意那個wait: true選項,由於若是沒有這個,model會當即觸發destroy事件,有可能致使數據並無從數據庫刪除成功,可是界面上已經看不到了,你再次刷新的時候又能看到。

最後說改:

其實改的問題跟前面的增刪也差很少,主要是要注意異步請求的回調處理。還有一點要說明的是,wait: true這個選項,要準確使用,由於在有了這種數據驅動模式來修改DOM的時候,咱們修改DOM的方式除了用戶的輸入,js代碼直接操做DOM,還有js代碼改變跟DOM相關的數據,致使數據的change,最終引發DOM的從新渲染。wait: true這個選項,會影響到咱們可否保證model與view始終保持一致性,就是說model在某個狀態的時候,view應該就是某個狀態,而不是另一個不相符的狀態。什麼狀況下會致使這種不一致性,看下面的這個代碼:

toggle: function () {
        //1. 將異步對象返回,方便view層作交互
        //2. 此處調用save方法不建議加{wait: true},若是加了,就只能等到異步請求成功纔會觸發change事件,而此時可能UI已經發生變化,最終致使UI與model不一致的問題
        return this.save('complete', !this.get('complete'));
    }

這個方法是todos應用裏面,Todo類提供的一個實例方法,它是在點擊todo列表上的單個todo項的複選框的時候被調用的:

toggle: function (e) {
    var _async = this.model.toggle();
    _async && _async.done(function () {
        TipView.create({info: '修改爲功', type: 'success'}).show();
    });
},

這個代碼是TodoView類上的一個實例方法,跟上面的toggle方法位置不一樣。因爲點擊todo列表上的單個todo項的複選框這個操做,引起的數據流向是從dom到model,因此當你點擊一個沒有完成的todo的時候,它的複選框會先勾上,而後你準備調用model.toggle方法,去把model的屬性跟複選框的狀態同步起來,假如你在model.toggle裏面使用wait: true這個選項,那麼model的屬性同步就只能能到請求成功才行;可是在請求過程當中,用戶有可能有再次點擊複選框的狀況,請求也有失敗的狀況,最終可能會存在UI與model數據不一致的問題。

以上就是一些在作增刪改的時候要注意問題,同時還得考慮一些交互的邏輯,這個也是必不可少的,在我前面給出的demo地址中,這些東西都有考慮進去。

5. 如何作批量操做

這個問題也是我當時比較苦惱的一個問題,backbone官方文檔也說了,要不就本身寫單獨的ajax請求吧,好像現有的api方法也不支持批量操做。又得提到官方todos的問題了,官方todos在作批量處理的時候,是直接遍歷調用各個todo的相關方法,來完成各個異步操做的,這在實際的工做中,能這麼搞嗎,得有多少個請求阿,通常批量處理無非就是把要改的東西的主鍵以數組的形式統一傳遞給後臺,固然要修改的內容也得傳遞過去,就能夠了。因此我是這麼來完成todos裏面的兩個批量操做的:

toggleAll: function (e) {
    //1. 批量修改model,可是先不發異步請求
    var complete = this.$complete_all.find('input')[0].checked, data = [];
    this.todos.each(function (todo) {
        todo.set({complete: complete});
        data.push(todo.toJSON());

        //因爲這個批量功能只是對真實的功能場景的模擬,數據實際上仍是存在localStorage裏面的
        //前面並無調用todo的save方法,致使數據的修改並無同步到localStorage裏面,因此爲了保存數據,必須直接拿localStorage對象來更新todo。
        //在真實的環境中,也就是使用ajax的場景裏面,這一步不須要。
        todo.collection.localStorage.update(todo);
    });

    //2. 發送異步請求批量更新
    $.ajax({
        url: '',//這裏應該是真實的批量修改的接口地址
        data: {
            data: JSON.stringify(data)
        }
    }).done(function(){
        TipView.create({info: '批量更新成功!', type: 'success'}).show();
    });
},
clearCompleted: function () {
    //1. 先獲取全部要刪除的model id,放到一個數組裏面
    var data = [],completes = this.todos.getComplete();
    completes.forEach(function (todo) {
        data.push(todo.id);
    });

    //2. 發送異步請求批量刪除
    $.ajax({
        url: '',//這裏應該是真實的批量刪除的接口地址
        data: {
            ids: JSON.stringify(data)
        }
    }).done(function(){
        TipView.create({info: '批量刪除成功!', type: 'success'}).show();

        completes.forEach(function (todo) {
            //因爲這個批量功能只是對真實的功能場景的模擬,數據實際上仍是存在localStorage裏面的
            //後面的clear跟destory會致使todo不能自動從localStorage裏面刪除,因此也必須手動的去更新localStorage裏面的數據
            //在真實的環境中,也就是使用ajax的場景裏面,這一步不須要。
            todo.collection.localStorage.destroy(todo);

            //清空todo的內容,讓backbone認爲它是一個新建立的對象,以便在下一步調用destroy的時候不會發送請求!
            todo.clear({slient: true});
            todo.destroy();
        });
    });
}

要注意這個批量代碼中,有一部分代碼在真實的環境下,應該是不須要的,由於我這裏只是對真實場景的模擬,若是不加那些代碼,我批量修改的數據,就沒法持久化到localStorage裏面了。

6. 總結

backbone雖然如今有點早了,可是當時剛出的也仍是挺火熱的,如今還在用的也很多,並且相比react vue那些更高大上的框架,這個框架有本身的特色:第一,它很簡單,思想不錯,用多了對本身寫代碼確定有幫助;第二,其它框架多多少少都會有借鑑它的一些想法,我原來看react的文檔的時候,我看到單向數據流,想到一個相似的東西就是backbone,因此用多了再去學其它框架,應該也有好處。而後也沒有其它內容可介紹的了,畢竟這也只是一篇介紹backbone大概內容的文章,而後把我本身發現的一些問題以及不少我的的想法說明了一下,總而言之,就是但願能給一樣對backbone有興趣的朋友提供一些參考的東西,若是文中有什麼不對的,歡迎任何方式幫我糾正,先謝謝了。

相關文章
相關標籤/搜索