backbone.js已經不是當前最流行的前端框架了,但是對於我而言,依然具有比較好的學習價值。雖然目前來說,react,vue等mvvm框架非常火熱,但是感覺自身還不到去使用這種框架的層次。這些技術雖好,但是對個人的挑戰也是比較大:首先是在編程基礎這個部分,包括數據結構,算法,面向對象編程,設計模式,設計原則等等,我覺得在這些方面積累地還不夠;其次是工作方法層面,對比angualr,react以及vue,跟傳統的用原生js或者jquery寫的代碼,包括html,css和js三個方面,你會發現這兩種技術直接導致的我們在工作結果上的巨大差別,這對於已經習慣了傳統開發的我來說,要挑戰的不僅僅是新技術的學習跟研究,更多的是工作思路,工作方法甚至是跟其它同事配合協作方式的改變,這個難度也很大。再加上我本人是一個還比較喜歡去鑽研細節的人,所以從去年開始做前端開發到現在,一直都沒有大膽地學習很多的新東西,相反,我把更多的精力花在了編程思維的鍛煉以及基礎知識的鞏固上,這件事情看起來很小,但是對我的提高很有幫助。
我還記得去年我剛到上家單位的時候,是頂着比較大的壓力去做前端的,因為工作第一年,我在武漢做的是VB.NET的開發工作,技術鍛煉的很少,邏輯思維方面跟sql方面鍛煉地多,因為公司做ERP系統,有封裝很好的技術平台,套着用就行了,所以工作都花在寫業務邏輯和數據邏輯上;工作第二年我來了北京,在用友做軟件實施,折騰了大半年,最后還是覺得在北京做技術最掙錢,就回到了這個本行;到上家單位的時候,情況是:公司當時沒有前端,也沒有封裝前端任何的東西,我也沒有專門做過前端,以前做的開發還是VB.NET的后台開發,所以當時我也比較擔心怕完成不了當時的工作任務。幸運的是,當時正好趕上一個緊急的項目,公司給機會讓我搭一套前端的架子,其實他們要求也不高,能把各種插件套上就行了。我當時想的是,既然要做前端開發的話,有這個機會,還不如自己動手好好模仿以前公司的開發平台寫一套東西出來。當時項目非常着急,我們一伙人連續20天的下班時間都在凌晨1-3點之間,我作為唯一的前端,能夠做的就是在后台的同事需要某個東西之前,就提前把它開發出來。我就是在那段時間憋出了自己的第一套可以當做開發平台用的東西,雖然這套東西,我到現在都覺得拿不出手,但是它對我起到的作用是,讓我開始對代碼的重構和設計產生興趣,我才開始去注意封裝思路,以及設計模式和設計原則在編程中的實際使用。曾經寫的那一套很粗糙的東西,后來我重構了3次,第一次重構是為了優化API的使用方式,讓它更好用;第二次重構是改寫各個組件內部實現的方式,並提供各個組件的詳細使用文檔,以便其它不是很擅長js的同事也能快速使用;第三次重構是使用requirejs做模塊化,並且完全跟后端分離,原來有些組件還是借助jsp來搞的。。。雖然我走了,曾經的同事還在使用我寫的東西繼續開發,而且會用的人都覺得用起來還挺簡單的;更有意思的是,同在上家單位的另外一個很好的朋友,5年的java開發經驗,幾乎不怎么做前端,連ajax都不怎么會,在他前段時間去另外一個單位的時候,用我跟他搭的這一套前后台的東西,竟然自己一個人搞定了一個內部管理系統的所有前端功能。
上家單位的工作經歷,讓我開始重視自己的代碼質量,關注設計模式與設計原則,平常都有意去看相關的博客和書籍,目前來看,成效也很明顯:首先是很習慣性地在代碼中融入職責分離與開閉原則的思考,我以前寫的幾篇博客都有相關的提及;其次是部分設計模式在代碼中的實際運用,包括單例模式,單例模式,適配器模式,狀態模式,觀察者模式等,用多了,對它們的理解也就更深刻了。在過去的一年,我還關注的東西有前端工程化構建,js模塊化,瀏覽器緩存管理,移動端頁面開發包括適配及優化等,這些都不是很高大上的一些技術名詞,僅僅是前端基礎知識的范疇,我去了解它們的目的,是覺得這是把這個崗位的工作做好的准備。正是這些對編程思路和基礎知識的學習,我現在看到一些新的功能,都能很快地形成工作思路,如果是要寫代碼的,很快就能想好要寫幾個類, 看清哪個地方又得拆分成多個類才能讓它們之間不會有強耦合,以及跨項目的復用等問題;在工作中,我在現在崗位上能夠一個人獨立地完成整個前端的工程化管理以及公司產品從后端到PC端以及移動端的所有開發工作,能夠整體把控項目的所有代碼,而在一年之前,我還是一個剛從軟件實施轉回軟件行業的前端小白。所以我今年也還是不着急去追逐其它火熱的技術,繼續搞自己的編程思維跟鞏固基礎知識,前端技術變化那么大,學那么多用不上也不見得是好事,相反把一些工作中也許會用到的好好琢磨透,也許會來的更有意義。比如說,我個人對動效和svg不是很擅長,那么過一段時間,我就會去專門研究這方面的內容,直到自己也能做出一些別人能比較認可的效果出來才行,這樣的好處是,在你工作需要的技術范圍內,你方方面面都能做地很好。
回到我這篇文章要介紹的backbonejs上來,我為什么在這段時間會去琢磨一個現在不是很火熱的前端框架,而且還認為它有比較好的學習價值。因為基於backbone的Model與View的開發方式,或者說它提供的面向對象的代碼組織方式,跟我目前慣用的思路還是比較像的,盡管我已有的代碼都是jquery搞的。但是它又要優於我現在的編程方式,因為它里面有一個數據驅動UI的思想在里面,而且還有一個很好的內置的事件管理機制,使得它在一些封裝層面的東西,比我寫的更要嚴謹,清晰一點。只要是能夠提高代碼質量的東西,我都認為是編程基礎的一部分,這正是我目前仍然想花時間去鑽研的東西,所以我想學習。即使不用它做任何的項目,只要把它的思想,能夠滲透到我現在的思維中即可,這個我感覺也不太容易,所以我得花一小段時間,才能掌握好它里面的一些機制。我學習它的主要方法是閱讀官方文檔和寫東西實踐,閱讀文檔過程中對於自己有疑問的api,必須寫一些簡單的代碼才能知道它的詳細作用。好在它官方文檔組織地還不錯,所以在學習過程中,需要去測試的api並不是很多。然后為了了解如何在實際工作中運用backbone編寫代碼以及它與我現在的編程方式上的區別,我分別用jquery跟backbone寫了一個todo app。簡單起見,jquery實現的版本與官網的功能完全一致,當然代碼是不同的;backbone實現的版本在官網的基礎上,考慮了異步回調處理以及操作的交互還有批量請求的處理,使得這個簡單的app看起來更符合實際的產品需求。最后我發現,雖然這兩個版本實現方式不同,但是思路層面卻有相似性,這個去看一下兩份代碼中定義的類名就清楚了。下面是兩個版本的demo地址:
源碼在:https://github.com/liuyunzhuge/blog/tree/master/todos
本文將從整體思想,數據驅動,事件機制,存在的問題以及實例分析等多個方面來介紹我對backbone的一些認識,里面有很多個人觀點,受技術水平和經驗的限制,不一定絕對合理,歡迎批評與指正。
1. backbone的整體思想
js的作用從大范圍上講,主要包含兩個內容,第一是將瀏覽器的數據與服務器的數據在適當的時候進行相互同步;第二是在用戶與瀏覽器,瀏覽器與服務器之間的交互過程中,控制頁面的變化來反饋不同的功能效果。從技術上來說,前者主要體現為異步請求的處理,后者主要體現為對DOM的操作。大家都知道,這兩個方面的東西雖然不難,但是很繁瑣,backbone也好,后來的angular react也好,它們誕生都有一個目的就是為了簡化這兩個方面的工作。不過本文的重點肯定只有backbone了,要說明backbone的作用,得先看下我們不采用backbone的時候是如何處理一個極其簡單的編輯頁面的,然后再來看backbone的解決方法,通過對比就能看出backbone的一些思想以及給我們工作帶來的好處。
這是要演練的編輯頁面的原型:
需求如下:
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官方文檔里面,描述這三個模塊之間的關系,用到了一張非常清晰明了的示意圖:
希望這個圖加上前面的舉例和描述,能夠讓你明白這三個模塊之間的關系以及作用。在下一部分我還會進一步的去說明這些模塊之間是如何互相影響的問題,尤其是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這四個模塊之間關系的示意圖,希望對理解這個模塊的作用能有所幫助:
以上就是我認為的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方法因為有模板引擎的幫助,渲染的步驟僅用一句話就能完成:
這種方式就可以看作是數據驅動式的DOM更新方式,相比原來手工的方法,它顯然要省事不少。如此以來,我們在編碼過程中對DOM操作的工作,就能簡化不少。而在數據驅動的背后,依賴backbone提供的Events這個基礎模塊的功能,Model,View以及Collection三個模塊都繼承了Events,使得它們的實例都擁有直接進行事件管理的能力,都可以直接使用on once trigger off等方法來注冊和移除事件監聽,另外backbone為了方便起見,還提供了一種主動式的事件綁定方式,相關的api是listenTo stopListenTo listenToOnce,名字都起得很明了,看到的話,非常好理解。這個Events模塊,除了讓Model等模塊的實例擁有強大的自定義事件管理,同時它還提供了一套內置的事件體系,這套事件體系其實就是前面數據驅動的關鍵:
注意紅框的這些事件,有些是只有Model實例才會觸發的,有的是只有Collection實例才會派發的,有的是都會觸發的,只要當View層的實例,與之相關聯的Model實例和Collection實例觸發了這些事件,都可以直接通知View層實例執行相應的事件回調,我們所要做的只需要在View層實例初始化的時候,注冊好跟它關聯的Model實例或Collection實例的事件監聽即可。如:
前面這些能夠解釋為什么當Model或Collection發生變化的時候,為什么能夠引起View層的變化。但是還有一個方面沒有說清楚,就是由於用戶與瀏覽器交互導致的View層的變化,如何同步到數據。這個方法,backbone給出的實現,其實跟平常使用jquery綁定各類鍵鼠事件,然后在事件監聽里面直接去更新關聯的Model或Collection實例沒有區別,事實上,它本身也是這么做的。比如todos這個小東西里面的TodoView,用events注冊了以下事件:
相應的回調都設置成了View層的實例方法:
結合這兩段代碼,就能發現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有興趣的朋友提供一些參考的東西,如果文中有什么不對的,歡迎任何方式幫我糾正,先謝謝了。








