也许单页程序(Single Page Application)并不是什么时髦的玩意,像Gmail在很早之前就已经在使用这种模式。通常的说法是它通过避免页面刷新大大提高了网站的响应性,像操作桌面应用程序一样。特别是在当今的移动时代,单页程序如果放在移动设备上去浏览就能够拥有像native app一样的体验,也许我们web开发者们应该期待这种技术的大力普及,这样不管前端还是后端都是我们的天下啊,让那些Andrioid和IOS开发者们追赶我们吧!好吧,废话不说了,我们会从0开始搭建这样一个单页的web站点,并且会向大家展示我们标题所列的这些开源框架是如何帮助我们快速构建的。新技术比较多,我也是学习,有不足的地方请海涵 :)
注:由于这个Demo是要给国外的同事看的,所以页面内容显示是英文的,请见谅。戳这里看线上Demo。http://myspademo.cloudapp.net
源码地址: https://github.com/jesselew/SpaDemo
目录- 需求介绍
- 单页程序介绍
- 项目架构
- 开源框架介绍
- bootstrap
- requireJS
- director
- knockout
- 用requireJS实现远程模板的调用
- rest中关于局部更新的讨论
- WEB API的Model验证
- 小结
我们的需求很简单,通过这个单页程序完成对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()); } }
前端拿到这个消息之后,就可以通知给用户了。当然最后还是需要加上前端验证,可以大大的提高用户体验以及减轻服务器的压力。
小结没有小结,大伙都散了吧!我要骑车上班去了,你呢?