最近都没怎么写博客,因为在公司的工作内容实在是太多。这一篇写技术方面的博文来记录一下最近使用的新东西吧。
基本上,我们要做一个Single Page Application,也就是看上去网页只有一页,页面之间的切换很流畅,不会像传统的网页那样可以明显地感觉出来在请求新的页面。做SPA,一个很好的设计模式就是用MVVM,实现MVVM有很多成熟的js库,比如angular和durandal等。我们使用的是durandal,而在数据访问层,也就是获取实体对象的时候使用了breeze库。
由于我也是第一次使用这种全新的玩意,再加上前端的开发经验较少,所以着实折腾了不少时间。现在这个sprint要做的原型基本上完工了,也就有时间来写日志了。
自然,使用客户端编程不能像以前asp.net、jsp和php一样完全由服务器控制。SPA的页面控制、数据请求(多为异步)全部都在客户端(浏览器端)完成,在服务器端要做的就是提供访问所需的WCF协议接口或者HTTP请求接口就行了。在这里我们采用的是HTTP请求方式,摘取两个方法示例如下:
[ServiceContract]
public interface ITableManagerWebService
{
/// <summary>
/// Get installed CMS service message processor list
/// </summary>
/// <returns></returns>
[OperationContract]
[WebGet(UriTemplate = "GetAllCMSServices",
RequestFormat = WebMessageFormat.Json,
ResponseFormat = WebMessageFormat.Json,
BodyStyle = WebMessageBodyStyle.Bare)]
IEnumerable<CMSServiceData> GetCMSServiceList();
/// <summary>
/// Get unprocessed messages which are received after the specified time
/// </summary>
/// <param name="time"></param>
[OperationContract]
[WebGet(UriTemplate = "GetLatestMessageList?cmsId={cmsId}&time={time}",
RequestFormat = WebMessageFormat.Json,
ResponseFormat = WebMessageFormat.Json,
BodyStyle = WebMessageBodyStyle.Bare)]
IEnumerable<LogMessageData> GetLatestMessageList(string time, int cmsId);
......
}
这是 ServiceContracts/ITableManagerWebService.cs文件,服务器端的东西就不多说了,下面主要讲浏览器端。浏览器端项目的结构如下:
durandal文件夹和scripts文件夹存放了breeze和durandal的库,这两个库都可以直接从github上下载到。
先看怎么请求数据吧。在services文件夹下有个dataservice.js文件,在这里可以调用breeze的库来生成实体。breeze支持两种方式产生实体:从服务器请求以及从本地缓存(浏览器)取。这有点像微软的Entity Framework,支持实例化实体对象,而上层模块不需要关心怎么去取。
首先我们需要一个JSON格式的Adapter,以便让breeze知道如何从序列化后的字符串产生实体。这个adapter有一个visitNode方法,需要返回这个实体的类型。实体的类型是在下面的代码段中定义的:
var metadatastore = new breeze.MetadataStore();
metadatastore.addEntityType({
shortName: "CmsService",
namespace: "TmCmsMonitor",
dataProperties: {
Id: { dataType: breeze.DataType.Int32, isPartOfKey: true },
LastUpdatedTime: { dataType: breeze.DataType.String },
Name: { dataType: breeze.DataType.String },
Status: { dataType: breeze.DataType.String },
},
});
在请求数据的时候,breeze支持类似linq的表达式,使用起来非常方便,示例如下:
GetLatestMessageList: function (servicesId, fromTime) {
var query = breeze.EntityQuery
.from("GetLatestMessageList")
.withParameters( {cmsId: servicesId, time: fromTime });
return manager.executeQuery(query).then(querySucceeded).fail(queryFailed);
}
这里的manager是告诉breeze使用哪一个dataservice,以及使用的JSON adapter。原来的breeze模版大多都包含了这些结构,只要自己具体实现就行了。
接下来就是MVVM的重点——durandal库了。首先我们在views中定义的是每个页面的视图,我最近实现的一个视图如下:
<section>
<h2 class="page-title" data-bind="text: title"></h2>
<table width='500px' class="table table-bordered table-condensed table-hover">
<thead>
<tr>
<th>Time Stamp</th>
<th>Body</th>
<th>Destination</th>
<th>Source</th>
</tr>
</thead>
<tbody data-bind="foreach: cmsMessages">
<tr>
<td width='10%' data-bind='text: TimeStamp'></td>
<td width='70%' data-bind='text: Body'></td>
<td width='10%' data-bind='text: Destination'></td>
<td width='10%' data-bind='text: Source'></td>
</tr>
</tbody>
</table>
<label id='splashLabel' data-bind = 'text: splashLabelText'></label><br /><br /><br />
<nav class="navbar navbar-fixed-bottom">
</nav>
</section>
里面的data-bind属性就是durandal库会自动实现的数据绑定。与views对应的就是viewmodels文件夹了。在ViewModel里面定义了一个vm对象,基本上是存储这个页面的数据。
var vm = {
activate: activate,
deactivate: deactivate,
title: 'Message View',
cmsMessages: ko.observableArray([]),
lastCmsId: null,
lastMessageTime: null,
splashLabelText: ko.observable(''),
pauseButtonText: ko.observable('Stop scroll'),
onPauseButtonClick: onPauseButtonClick,
autoScrolling: true
};
durandal会自动从对应的ViewModel里面取出数据实现数据绑定。这里有两种特殊的数据类型:ko.observable和ko.observableArray。durandal提供的类型实现了MVVM的机制:只要更改了数据,就会自动刷新在视图上的显示。
每一个ViewModel都可以提供activate、deactivate、exit等方法,这些方法会在进入视图、切换到别的视图时调用,具体可以参考官方的文档。
有一个要特别注意的地方,估计也是刚学js常犯的错误,就是this指针的问题。activate方法是由 durandal调用的,用this可以访问到vm对象。而如果是从页面触发的(比如你绑定了一个按钮事件),那么this对象就不一样了,vm不能直接通过this访问到。durandal应该提供了对应的库来取这个对象,但我为了偷懒,每次都会在activate里添加一个parentVM的全局变量记录viewmodel的指针(感觉不好啊),比如下面这段恶心的代码:
function onPauseButtonClick() {
parentVM.autoScrolling = !parentVM.autoScrolling;
if (parentVM.autoScrolling == true) {
parentVM.pauseButtonText('Stop scroll');
}
else {
parentVM.pauseButtonText('Auto scroll');
}
}
不提我碰到的钉子了,最后需要说明的是durandal提供的数据绑定是非常好用的,比如我们用下面一段代码更新一个observableArray,这个数组就是在View中显示数据的,代码如下:
return dataservice.GetLatestMessageList(cmsId, parentVM.lastMessageTime).then(function (allCmsMessages) {
for (var i = allCmsMessages.length - 1; i >= 0; i--) {
allCmsMessages[i].TimeStamp = parseMicrosoftJsonDateTime(allCmsMessages[i].TimeStamp);
allCmsMessages[i].Body = allCmsMessages[i].Body.replace(/n/g, ' ');
parentVM.cmsMessages.push(allCmsMessages[i]);
}
这里先要说明两点。
第一是从.NET传过来的DateTime类型不能直接被breeze解析,所以我们只好先用字符串收着,具体显示的时候需要自己写代码把它转换成js能识别的日期格式,代码如下:
function parseMicrosoftJsonDateTime(content) {
try {
content = content.replace(///g, '');
var contentDate = eval('new ' + content);
return contentDate.toLocaleString();
} catch (ex) {
return content;
}
}
第二点,也就是刚用durandal时99.99%的人会碰到的问题——请求的数据有时不会被显示出来。假如我们写下面这样的代码:
function activate() {
var self = this;
dataservice.getAllCmsServices().then(function (allCmsServices) {
for (var i = 0; i < allCmsServices.length; i++)
{
self.cmsServices.push(allCmsServices[i]);
}
});
return true;
}
就会碰到没数据的问题。原因是breeze库的请求是异步的,在执行return true返回给durandal之前数据还没有过来,数组里面啥都没有。所以我们要直接return它返回的数据给durandal(就像之前的代码),这样就没有问题了。
我们设置了一个定时器,每2秒从服务器请求一次数据(取的是距离上次请求以后,新增的数据)。我们直接添加到ko的observableArray中,在View中就会实时显示,而且速度很快,根本不会觉得页面在闪。
这就是我们为什么喜欢durandal的原因。
不过,使用这个也不是一帆风顺的,碰了无数个钉子。最难处理的bug还是多线程的问题,比如我们切换到另外一个页面后,原来的异步方法可能还在执行(或者定时器还是有效),那么就可能出现N多莫名其妙的问题…… 所以还得慢慢来多积累开发经验才是。