网页前端的学习——初识breeze和durandal库

最近都没怎么写博客,因为在公司的工作内容实在是太多。这一篇写技术方面的博文来记录一下最近使用的新东西吧。

基本上,我们要做一个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多莫名其妙的问题…… 所以还得慢慢来多积累开发经验才是。

✏️ 有任何想法?欢迎发邮件告诉老夫:daozhihun@outlook.com