也許單頁程序(Single Page Application)并不是什么時髦的玩意,像Gmail在很早之前就已經在使用這種模式。通常的說法是它通過避免頁面刷新大大提高了網站的響應性,像操作桌面應用程序一樣。特別是在當今的移動時代,單頁程序如果放在移動設備上去瀏覽就能夠擁有像native app一樣的體驗,也許我們web開發者們應該期待這種技術的大力普及,這樣不管前端還是后端都是我們的天下啊,讓那些Andrioid和IOS開發者們追趕我們吧!好吧,廢話不說了,我們會從0開始搭建這樣一個單頁的web站點,并且會向大家展示我們標題所列的這些開源框架是如何幫助我們快速構建的。新技術比較多,我也是學習,有不足的地方請海涵 :)
注:由于這個Demo是要給國外的同事看的,所以頁面內容顯示是英文的,請見諒。戳這里看線上Demo。http://myspademo.cloudapp.net
源碼地址: https://github.com/jesselew/SpaDemo
目錄
需求介紹
我們的需求很簡單,通過這個單頁程序完成對Event的管理,下面簡單列幾條需求。
功能性需求
- 添加修改Event
- Event 有opening和closed的狀態,也就是需要有關閉Event的功能
- Event列表頁可以根據狀態過濾
- Closed的Event不能再進行修改
非功能性需求
- 盡可能的減少對服務器的請求
- 數據完整性(驗證)
- 認證和授權(系統會有至少2種角色,并且擁有不同的權限)
- 可維護性
認證和授權這一塊暫時沒有做,后面可以繼續完善,驗證這一塊只做了后端的,通常為了安全和用戶體驗是需要后端和前端都要實現驗證的。這個Demo我已經上傳到Windows Azure上去了,大家來體驗一把。http://myspademo.cloudapp.net
單頁程序介紹
首先我覺得可以把頁面的響應模式分成這樣大概3個階段:
1. 最傳統的階段:什么都得刷新
最傳統的web站點中,客戶端向服務器發送請求,服務器響應之后把生成好的HTML通過Response返回給客戶端,這樣一來一往。體驗當然是最不好的,同時對服務器來說也需要處理的更多。
2. 頁面局部刷新
至從Ajax火起來之后,大家就想起了這一點。頁面某一塊局部的數據可以在頁面在客戶端加載完之后,再從新發起一個請求去把某一塊的HTML代碼再拿下來顯示到頁面中。這里面有兩種做法,一種是后臺直接把HTML生成好了直接返回,另一種做法是服務器只返回數據,客戶端再拼出HTML。采取第二種做法的時候,有人可能已經用上了先進的模板技術,有人可能還在使用強大的字符串拼接技術。 不管怎么說,我們進步了,用戶可以先看到頁面,然后某一塊慢慢加載,用戶感覺爽了,再也不是一片空白在那里轉啊轉啊的了。
3. 整站單頁
整站單頁的時代到來最早是在2005年,當然那時候還只是一個術語。具體的例子,我最早接觸到的是Gmail,當然最簡單的單頁其實很簡單比如說某Q郵箱,整了個Frame在頁面里面,不管你怎么點,它懶是感覺沒有刷新呀。這里先簡單說說我們要實現的這個單頁和用Frame實現的單頁相比有什么優勢。
- 擁有良好定義的URL,對用戶和搜索引擎都更友好。
- 可以實現銜接動畫,這一點在移動設備上特別重要。
頁面生命周期對比
這里從MSDN上面扒來了一張圖,上面的傳統的頁面生命周期,下面是我們這種單頁程序頁面的生命周期。我們來看看這種模式的頁面會為我們的用戶和開發者帶來哪些優勢和難題。
優勢
- 對于用戶而言,更好的用戶體驗,特別體現在可移動端和可觸摸設備上
- 對于開發都者而言,在定義了良好的分層架構之后,UI與數據可以完全分離,只要后臺的數據接口不改變,后臺的邏輯可以隨意的改動頁不影響前端展示,而在加上前端MVVM框架之后,我們前端的數據也可以與UI完成分離。
難題
- 最大的難題是Javascript部分,由于全部在一個頁面,我們需要處理變量覆蓋,變量作用域,對于前端開發人員來說要求會更上一層樓
- 對于全球化,授權等模塊都需要重新考慮和設計以便更適合這種單頁程序的開發
項目架構
扒了一張圖之后,我的圖就得畫的跟它的協調,沒有我的手寫風格好看,有木有?
- 用Knockout作前端MVVM框架
- 用requireJS來加載遠程模板
- 用director來作前端route
- model數據是直接和web api交互的,包括驗證和授權
- 模板是一個Controller,每一個模板對應一個Action
View Container
這是一個客戶端的模板容器,在requireJS的基礎封裝了一下,第一次調用某個模板的時候會去服務器上拿,后來就直接用客戶端的了。
為什么模板不直接用html的?
這個問題我也想過,用純html的就不必走mvc那一套生命周期了,服務器壓力減少不小。但是考慮到我們view當中的授權模塊和全球化資源,其實是可以直接在服務器端處理好再返回的。而且我也偷了一個懶,沒有把這些放在客戶去實現,大家有好的點子可以分享的么?
開源框架介紹
上面用了這么多的開源框架,那么它們都是干什么的,又是如何使用的呢? 這里我們就小小的來聊一聊這些開源的框架吧。
Bootstrap
這玩意我想很多人都知道,我就不多說了。有了它之后,我們程序員不需要美工也可以做出很漂亮的界面了,雖然我這個Demo沒有很好看,但要是沒有它那還真不知道要丑上多少倍。它還有中文版的站點: http://www.bootcss.com/
director.js
這是一個前端的route框架,什么叫前端route呢?大家如果去看我的那個Demo就會發現,URL并不是像某Q郵箱那樣一直不變的,我們還是可以像以前那樣每一個單一的功能一個URL。比如說:
- #/events/create
- #/events/all
- #/events/closed
- #/events/1
除了對用戶比較友好之后,寫代碼的時候也會更加邏輯清晰,因為director會為每一個url綁定一個函數,就像mvc里面的action一樣。當用戶輸入對應的url的時候,相應的函數就會被觸發。
下面是來自官方首頁的一個小小的例子,讓你一眼就會用director。
requireJS
這玩意我也不用多介紹了吧,它具有延遲加載和避免重復加載的功能,來自官方的定義: requireJS是一個JavaScript文件和模塊加載器。
knockout.js
這玩意就算我想給你介紹也不是三言兩語就能說的清的,具體您還是參考源碼吧。或者園子里面的大叔曾經翻譯了官方的一個教程,有興趣同學可以看看。 總之它是一個JavaScript的MVVM框架,當然這種框架有很多,backboneJS, breezeJS, Durandal,EmberJS,Angular 等等,我并沒有全部了解過,所以我也不能告訴你他們的優勢和缺點分別在哪里。選擇knockout.js是因為之前了解過,好上手,然后以上這3種開源的框架全是基于MIT開源協議的,這樣我們就可以用它做商業開發了。
用requireJS實現遠程模板的調用
直接用require來加載html模板是不行的,人家已經說了是一個Javascript文件和模塊的加載器。所以這里面我們需要用到requireJS的文本插件,這樣我們就可以用它來加載文本了。https://github.com/requirejs/text
把那個text.js下載下來,直接放到我們程序的根目錄下,然后我們就可以用像加載js一樣的方法來加載html代碼了,除了要在我們文件位置前面加上一個text! 之外。
require(['text!/template/createevent'], function (template) { // 你在這里就可以拿到模板了。 })
rest中關于局部更新的討論
我們常用的http verb有四種:
我們用PUT方式去更新的話,是將整個Model全部更新。當然你也可以換成下面這種方式,只更新你想要更新的字段。
[HttpPut] public void Put(Event item) { var newItem = new Event(); newItem.Id = item.Id; // 在下面將你想要更新的值轉到newItem下 newItem.Title = item.Title; if (!repository.Update(newItem)) { throw new HttpResponseException(HttpStatusCode.NotFound); } }
注意:Put方式的URl只有一種(在我們不建其它route的情況下),也就是我們上面列出來的 /api/events/{id},然后將event對象作為body傳過去。比如說在我們的demo中,我們有更新操作,還有像“關閉”這樣的操作,我想這樣的操作幾乎在每一個系統里面都會遇到,這樣的操作只會更新一個字段(在這里是“狀態”列)。 那我怎么樣再建一個Put方法去更改這一個字段呢?而且最好的方法是我只用傳id過去就可以了。
通過google,我找到一個叫Patch的玩意, 它也是一種http verb,并且同樣也是提供更新操作。但是與Put不一樣的是Patch允許只將你需要更改的字段傳到服務器端。
var obj = { Revision : "2"}; $.ajax({ url: 'api/values/1', type: 'PATCH', data: JSON.stringify(obj), dataType: 'json', contentType: 'application/json', success: function (data) { } });
但是不管怎么說,這種方式我是沒有行通的,一旦我的實體對象加上一些驗證的Attribute比如說Required之后,那些字段全都會被賦上默認值。 最后我不得不放棄了這種做法。
添加Route來創建兩個PUT方法
另外一種做法,也就是我們Demo中實現的做法是增加了一個Route,在我們的web api中實現了兩個put的方法。
[Route("api/events/{id}/close")] public void Put(int id) { var item = repository.Get(id); if (item == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } item.Status = EventStatus.Closed; if (!repository.Update(item)) { throw new HttpResponseException(HttpStatusCode.NotFound); } }
這樣當我用PUT的方式提交到 api/events/3/close 的時候,我們的web api就會執行上面的方法然后把我們的event關閉了。
WEB API的驗證
基本上任何系統都避免不了與驗證打交道,除非那個系統壓根不從用戶那里獲取數據。WEB API的驗證方式大至相同,我們仍舊可以在我們的Model中采用Attribute的方式去聲明驗證條件。
public class Event { public int Id { get; set; } [Required] [MinLength(10)] public string Title { get; set; } public string Description { get; set; } public DateTime Start { get; set; } public DateTime End { get; set; } [Required] public string Owner { get; set; } public EventStatus Status { get; set; } }
在api方法中我們用ModelState.IsValid判斷就可以了。
public HttpResponseMessage Post(Event item) { if (ModelState.IsValid) { // 保存操作 return new HttpResponseMessage(HttpStatusCode.OK); } else { return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); } }
用AOP的方式去實現驗證
或者我們可以換成下面的這種方式,先創建一個Filter。
public class ValidateModelAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { if (!actionContext.ModelState.IsValid) { actionContext.Response = actionContext.Request.CreateErrorResponse( HttpStatusCode.BadRequest, actionContext.ModelState); } } }
再到Post和PUT的方法上面打上這個標簽。
[HttpPut] [ValidateModel] public void Put(Event item) { if (!repository.Update(item)) { throw new HttpResponseException(HttpStatusCode.NotFound); } }
我們還需要在我們的WebApiConfig中注冊這個Filter。
public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.Filters.Add(new ValidateModelAttribute()); } }
前端拿到這個消息之后,就可以通知給用戶了。當然最后還是需要加上前端驗證,可以大大的提高用戶體驗以及減輕服務器的壓力。
小結
沒有小結,大伙都散了吧!我要騎車上班去了,你呢?
文章列表