65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Ember.js、REST API 和 SignalR 的实时 Web 应用程序示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (33投票s)

2012年12月17日

MIT

6分钟阅读

viewsIcon

231151

downloadIcon

3940

使用 Ember.js、REST API 和 SignalR 的实时 Web 应用程序示例。

Sample Image - maximum width is 600 pixels

引言

Ember.js 是一个强大的 JavaScript MVC 框架,用于创建复杂的 Web 应用程序,它消除了样板代码并提供了标准的应用程序架构,它支持 UI 绑定、复合视图、Web 演示层,并且与其他组件配合良好。为了创建实时交互的 Web 应用程序,我添加了一个 SignalR Hub 和带有 ASP.NET MVC 的 REST 服务。

MVC 基础

MVC 模式的目的是分离视图、模型和控制器。模型是数据存储的地方,视图描述了应用程序的呈现,控制器充当模型和视图之间的链接。

这是 Ember.js 的 MVC 实现,客户端部分。

Ember.js 中还有一个重要的概念,那就是 Router StateManager,它在客户端的工作方式与 ASP.NET MVC Route 大致相同。在本文中,路由器负责将主控制器连接到主应用程序视图。Ember.js 使用 Handlebars 集成模板,便于创建和维护视图。

设置项目

首先,我们需要创建一个空的 ASP.NET MVC 项目。我使用带有 Razor 视图引擎的 ASP.NET MVC4,这是 _Layout.cshtml

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="../../Scripts/libs/jquery-1.7.2.min.js" type="text/javascript"></script>
    <script src="../../Scripts/libs/json3.min.js" type="text/javascript"></script>
    <script src="../../Scripts/libs/handlebars-1.0.0-rc.4.js" type="text/javascript"></script>
    <script src="../../Scripts/libs/ember-1.0.0-rc.6.js" type="text/javascript"></script>
    <script src="../../Scripts/libs/ember-data.js" type="text/javascript"></script>

    <script src="../../Scripts/jquery.signalR-1.0.0-alpha2.min.js" type="text/javascript"></script>
</head>
<body>
    @RenderBody()
</body>
</html>

以及 Index.cshtml

@{
    ViewBag.Title = "Ember.n.SignalR";
}
<script type="text/x-handlebars">
  {{outlet}}
</script>

<script type="text/x-handlebars" data-template-name="application">
    {{view App.ApplicationView}}
</script>

<script src="../../Scripts/u.js" type="text/javascript"></script>
<script src="../../Scripts/app.js" type="text/javascript"></script>
  • u.js 包含两个在 JavaScript 中生成随机字符串和数字的方法
  • app.js 包含应用程序的所有 JavaScript 代码。

服务器端模型

我们创建一个简单的客户 DTO(在此示例中,我们使用 DTO 作为模型),包含基本信息和 REST 方法的简单结果。

Customer.cs
[Serializable]
public class Customer
{
    public Guid? Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public bool Active { get; set; }
}
Result.cs
public class Result
{
    public int ErrorCode { get; set; }
    public object ErrorMessage { get; set; }
    public object Data { get; set; }
}

REST 服务

我们已经定义了 DTO,现在我们需要创建一个简单的客户 REST 服务,以 JSON 格式传输数据,CustomerController.cs 托管在 /customer/。为了符合 C# 和 JavaScript JSON 对象之间的命名约定,我使用 Newtonsoft.Json.SerializationCamelCasePropertyNamesContractResolver

public class CustomerController : Controller
{
    [AcceptVerbs(HttpVerbs.Post)]
    public string Read(Guid? id)
    {
        //...
    }

    [AcceptVerbs(HttpVerbs.Delete)]
    public string Delete(Guid id)
    {
        //...
    }

    [AcceptVerbs(HttpVerbs.Put)]
    public string Update(Customer customer)
    {
        //...
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public string Create(Customer customer)
    {
        //...
    }
}

关于客户数据仓库,我创建了一个简单的实现 CrudDS.cs,以二进制格式将数据存储在 ~/App_Data/Customer.em 物理文件中。现在所有所需的服务器端代码都已完成。下一步我们将重点关注 Ember 应用程序架构。

Ember 应用程序

简单来说,我们创建一个应用程序对象

    function getView(name) {
        var template = '';
        $.ajax(
                {
                    url: '/Templates/' + name + '.htm',
                    async: false,
                    success: function (text) {
                        template = text;
                    }
                });
        return Ember.Handlebars.compile(template);
    };

    wnd.App = Ember.Application.create();
  • getView:方法,我定义此方法以通过 Ajax 同步获取模板,然后使用 Handlebars 将其编译为视图。

Ember 模型

我们需要在客户端创建一个数据存储对象,以与 REST 服务 /customer/ 交互。

// Data store
App.Store = Ember.Object.extend({
    update: function (customer) {
        var message = null;
        var xhr = $.ajax(
            {
                url: '/customer/update/',
                dataType: 'json',
                contentType: 'application/json; charset=utf-8',
                data: JSON.stringify(customer),
                type: 'PUT',
                async: false,
                success: function (data) {
                    message = data;
                }
            });

        if (xhr.status != 200) { // error
            message = { errorCode: xhr.status, errorMessage: xhr.statusText };
        }

        return message;
    },
    read: function (id) // !id read all
    {
        var message = null;
        var xhr = $.ajax(
            {
                url: '/customer/read/',
                dataType: 'json',
                contentType: 'application/json; charset=utf-8',
                data: JSON.stringify({ 'id': id }),
                type: 'POST',
                async: false,
                success: function (data) {
                    message = data;
                }
            });

        if (xhr.status != 200) { // error
            message = { errorCode: xhr.status, errorMessage: xhr.statusText };
        }

        return message;
    },
    remove: function (id) // !id delete all
    {
        var message = null;
        var xhr = $.ajax(
            {
                url: '/customer/delete/',
                dataType: 'json',
                contentType: 'application/json; charset=utf-8',
                data: JSON.stringify({ 'id': id }),
                type: 'DELETE',
                async: false,
                success: function (data) {
                    message = data;
                }
            });

        if (xhr.status != 200) { // error
            message = { errorCode: xhr.status, errorMessage: xhr.statusText };
        }

        return message;
    },
    create: function (customer) {
        var message = null;
        var xhr = $.ajax(
            {
                url: '/customer/create/',
                dataType: 'json',
                contentType: 'application/json; charset=utf-8',
                data: JSON.stringify(customer),
                type: 'POST',
                async: false,
                success: function (data) {
                    message = data;
                }
            });

        if (xhr.status != 200) { // error
            message = { errorCode: xhr.status, errorMessage: xhr.statusText };
        }

        return message;
    }
});

在上一部分中,我们已经在服务器端定义了模型,REST 服务只返回 JSON 格式的对象,为了将其绑定到视图,我们需要将其定义为 Ember 模型。

App.CustomerModel = Ember.Object.extend({
    id: null,
    firstName: null,
    lastName: null,
    email: null,
    phone: null,
    active: false,
    quiet: false,
    random: function () {
        this.setProperties({ firstName: String.random(), lastName: String.random(), 
                email: String.random().toLowerCase() + '@gmail.com', 
                phone: '(097) ' + Number.random(3) + '-' + Number.random(4) });
        return this;
    },
    plain: function () {
        return this.getProperties("id", "firstName", 
        "lastName", "email", "phone", "active");
    }
});
App.ResultModel = Ember.Object.extend({
    errorCode: 0,
    errorMessage: null
});
  • random:使用 u.js 中的两个随机方法创建随机客户。
  • plain:在发送到 REST 服务之前获取 JSON 对象,以提高性能(不需要 Ember.Object 的附加属性)。

Ember 控制器

App.CustomerController = Ember.Controller.extend({
    store: App.Store.create(),
    currentResult: null,
    currentCustomer: null,
    random: function () {
        var customer = App.CustomerModel.create().random();
        if (this.get('currentCustomer')) {
            this.get('currentCustomer')
                .set('active', false)
                .setProperties(this.get('currentResult').data);
        }
        this.set('currentCustomer', customer);
    },
    create: function (customer) {
        this.set('currentResult', this.get('store').create(customer.plain()));
        if (!this.currentResult.errorCode) {
            this.set('currentCustomer', App.CustomerModel.create());
            var newCustomer = App.CustomerModel.create(this.get('currentResult').data);
            this.get('customers').pushObject(newCustomer);
        }
    },
    remove: function (id) {
        var customer = this.get('customers').findProperty('id', id);
        if (!customer) return;
        this.set('currentResult', this.store.remove(customer.id));
        if (!this.currentResult.errorCode) {
            if (this.get('currentCustomer').id === id) {
                this.set('currentCustomer', App.CustomerModel.create());
            }
            this.get('customers').removeObject(customer);
        }
    },
    read: function (id) {
        this.set('currentResult', this.store.read(id));
        if (!this.currentResult.errorCode) {
            if (Ember.isArray(this.currentResult.data)) { // Read all
                var array = Ember.ArrayController.create({ content: [] });
                this.currentResult.data.forEach(function (item, index) {
                    array.pushObject(App.CustomerModel.create(item));
                });
                return array;
            }
            else { // An object
                var customer = this.get('customers').findProperty('id', this.currentResult.data.id)
                customer && customer.setProperties(this.currentResult.data);
                return customer;
            }
        }
        else { // Empty result
            return id ? null : Ember.ArrayController.create({ content: [] });
        }
    },
    update: function (customer) {
        this.set('currentResult', this.store.update(customer.plain()));
        if (!this.currentResult.errorCode) {
        }
    },
    save: function (customer) {
        var customer = this.get('currentCustomer');
        if (!customer.id) { // create
            this.create(customer);
        }
        else { // edit
            this.update(customer);
        }
    },
    edit: function (id) {
        if (this.get('currentCustomer').id != id) { // Rollback
            this.get('currentCustomer')
            .setProperties({ active: false })
            .setProperties(this.get('currentResult').data);
        }
        else {
            return;
        }
        var customer = this.read(id);
        this.set('currentCustomer', customer.set('active', true));
        this.set('currentResult',
            App.ResultModel.create({
                errorMessage: 'Click Submit to save current customer.',
                data: customer.getProperties("firstName", 
                "lastName", "email", "phone") // Keep copy
            }));
    },
    customers: Ember.ArrayController.create({ content: [] }),
    initialize: function () {
        var array = this.read();
        this.set('customers', array);
        this.random();
        this.set('currentResult', App.ResultModel.create({ errorMessage: 'Click Submit to create new customer.' }));
    }
});
App.applicationController = App.CustomerController.create();

我们的 ApplicationController 控制创建、更新或删除客户的逻辑,它将每个操作的结果存储在 currentResult 属性中,并将正在编辑/创建的客户存储在 currentCustomer 属性中。客户数组模型是我们的客户存储,它将始终通过 App.Store 与服务器同步,并用于绑定到我们的视图。当我们的控制器初始化时,我们调用 this.read() 从服务器检索所有客户。你知道为什么我们必须在这里使用 get 或 set 方法吗?这是 JavaScript 处理属性更改的方式。实际上,在 C# 中,get/set 属性编译为 CLR 中的 get/set 方法。

Ember 视图

我们看到了控制器和模型之间的连接,现在我们深入研究视图。视图通过控制器锚点从模型显示/绑定值。我将视图模板定义在单独的 htm 文件中,通过 AJAX 加载它们并使用 Ember.Handlebars 编译响应文本,这比将其放在 script 标签中更容易修改,我们可以使用任何 HTML 编辑器,例如 Microsoft Visual Studio、Notepad++ 等来编辑视图模板。让我们看看 create_edit_customer 模板,它定义在 create_edit_customer.htm 中。

<div id="mainForm">
    
    <div>
        <label for="firstName">
            First name</label><br />
            {{view Ember.TextField valueBinding="controller.currentCustomer.firstName"}}
            <a href="#" {{action "random" 
            on="click" target="view" }}>Random new customer</a>
    </div>
    <div>
        <label for="lastName">
            Last name</label><br />
        {{view Ember.TextField valueBinding="controller.currentCustomer.lastName"}}
    </div>
    <div>
        <label for="email">
            Email</label><br />
        {{view Ember.TextField valueBinding="controller.currentCustomer.email"}}
    </div>
    <div>
        <label for="phone">
            Phone</label><br />
        {{view Ember.TextField valueBinding="controller.currentCustomer.phone"}}
    </div>
    <p>
        <button id="submit" {{action "save" 
        on="click" target="view" }} >Submit</button>
    </p>
</div>

CreateEditCustomerView 如下

    App.CreateEditCustomerView = Ember.View.extend({
        template: getView('create_edit_customer'),
        init: function () {
            this._super();
            this.set('controller', App.applicationController);
        },
        save: function (event) {
            this.get('controller').send('save');
        },
        random: function () {
            this.get('controller').send('random');
        },
        name: "CreateEditCustomerView"
    });

如您在模板中看到的,有一些 Handlebars 语法,姓氏文本框定义为 {{view Ember.TextField valueBinding="controller.currentCustomer.firstName"}}。此视图中有两个操作,第一个是随机生成一个新客户,第二个是保存一个客户,在模板中,您可以轻松找到两行 {{action "random" on="click" target="view" }}{{action "save" on="click" target="view" }}。这两个操作都是点击事件。

要显示客户列表,我们需要一个模板

<div id="customerListHeader">
    List of customers
</div>
<div id="customerListContent">
    <table>
        {{#unless controller.customers.length}}
        <tr>
            <th> First name </th>
            <th> Last name </th>
            <th> Email </th>
            <th> Phone </th>
        </tr>
        <tr>
            <td colspan="4" align="center">
                There is no customer yet
            </td>
        </tr>
        {{else}}
        <tr>
            <th> First name </th>
            <th> Last name </th>
            <th> Email </th>
            <th> Phone </th>
            <th> #Action </th>
        </tr>
        {{#each customer in controller.customers}}
        <tr {{bindAttr class="customer.active:active:normal"}} 
        {{bindAttr id="customer.id"}}>
            <td>
                {{customer.firstName}}
            </td>
            <td>
                {{customer.lastName}}
            </td>
            <td>
                {{customer.email}}
            </td>
            <td>
                {{customer.phone}}
            </td>
            <td align="center">
                <a href="#" class="edit" 
                {{action edit customer target="view" }}>Edit</a>
                |
                <a href="#" class="delete" 
                {{action remove customer target="view" }}>Delete</a>
            </td>
        </tr>
        {{/each}} 
        {{/unless}}
    </table>
</div>

CustomerListView 如下

    App.CustomerListView = Ember.View.extend({
        template: getView('customer_list'),
        init: function () {
            this._super();
            this.set('controller', App.applicationController);
        },
        edit: function () {
            var id = customer.id;
            var controller = this.get('controller').send('edit', id);
        },
        remove: function () {
            var id = customer.id;
            var controller = this.get('controller');
            this.animateItem(id, function () {
                controller.send('remove', id);
            }, controller);
        },
        animateItem: function (id, callback, target) {
            $('#' + id).animate({ opacity: 0 }, 200, "linear", function () {
                $(this).animate({ opacity: 1 }, 200);
                if (typeof callback == 'function') {
                    target = target | null;
                    callback.call(target);
                }
            });
        },
        name: "CustomerListView"
    });

每次创建/编辑/删除客户操作,我们都需要显示消息结果,在 MessageView 中,这是消息模板。

{{#unless controller.currentResult.errorCode}}
<div id='message'>
    {{controller.currentResult.errorMessage}} &nbsp;
</div>
{{else}}
<div id='error'>
    {{controller.currentResult.errorMessage}} &nbsp;
</div>
{{/unless}}

CreateEditCustomerViewCustomerListViewMessageView 组合成 ApplicationView,模板定义在 main.htm 中。

<h3>{{view.Title}}</h3>
<div>
    <div id="message">
        {{view App.MessageView}}
    </div>
    <div id="createEditCustomer">
        {{view App.CreateEditCustomerView}}
    </div>
    <div id="customerList">
        {{view App.CustomerListView}}
    </div>
</div>

<div id="footer">

</div>

    App.ApplicationView = Ember.View.extend({
        Title: "Example of Ember.js application",
        template: getView('main'),
        init: function () {
            this._super();
            this.set('controller', App.applicationController);
        },
        name: "ApplicationView"
    });

Ember 路由

我们已经创建了应用程序、控制器、视图、模型,但是还有一件事可以让我们的应用程序工作,那就是路由。路由将连接应用程序控制器到应用程序视图,初始化应用程序控制器,然后按照顺序,应用程序视图被绑定/渲染到屏幕。在这个例子中,我使用默认的 IndexRoute ,路径为 /。现在用户可以看到、触摸视图并从控制器获得正确的响应。

    App.IndexRoute = Ember.Route.extend({
        model: function () {
            return App.applicationController.initialize();
        }
    });

嵌入 SignalR

使用 Visual Studio,我们可以从 Nuget 添加 SignalR。

Install-Package Microsoft.AspNet.SignalR -pre

RegisterHubs.cs 已自动添加到 App_Start 文件夹中。

using System.Web;
using System.Web.Routing;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hosting.AspNet;

[assembly: PreApplicationStartMethod(typeof(Ember.n.SignalR.RegisterHubs), "Start")]

namespace Ember.n.SignalR
{
    public static class RegisterHubs
    {
        public static void Start()
        {
            // Register the default hubs route: ~/signalr/hubs
            RouteTable.Routes.MapHubs();
        }
    }
}

我们使用 Hub 而不是 PersistentConnection,以便轻松地从服务器与所有客户端建立通信。

namespace Ember.n.SignalR.Hubs
{
    using Ember.n.SignalR.DTOs;
    using Microsoft.AspNet.SignalR.Hubs;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;
    using Microsoft.AspNet.SignalR;

    public class CustomerHub : Hub
    {
        public static IHubContext Instance
        {
            get{
                return GlobalHost.ConnectionManager.GetHubContext<customerhub>();
            }
        }

        JsonSerializerSettings _settings = new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            NullValueHandling = NullValueHandling.Ignore
        };

        public void Add(Customer customer)
        {
            Clients.All.add(JsonConvert.SerializeObject(customer, _settings));
        }

        public void Update(Customer customer)
        {
            Clients.All.update(JsonConvert.SerializeObject(customer, _settings));
        }

        public void Remove(Customer customer)
        {
            Clients.All.remove(JsonConvert.SerializeObject(customer, _settings));
        }
    }
}  

为了让客户端通过 SignalR 管道与服务器通信,我们需要包含 hub 脚本并在客户端实现 customerHub

<script src="/signalr/hubs " type="text/javascript"></script>

/// <reference path="_references.js" />

(function (App) {
    var hub = $.connection.customerHub;

    function findCustomer(id) {
        var c = App.applicationController.get('customers').findProperty('id', id);
        return c;
    }

    hub.client.add = function (message) {
        var customer = JSON.parse(message);
        var c = findCustomer(customer.id);
        !c && App.applicationController.get('customers').pushObject(App.CustomerModel.create(customer));
    }

    hub.client.update = function (message) {
        var customer = JSON.parse(message);
        var c = findCustomer(customer.id);
        c && c.set('quiet', true) && c.setProperties(customer) && c.set('quiet', false);
    }

    hub.client.remove = function (message) {
        var customer = JSON.parse(message);
        var c = findCustomer(customer.id);
        if (c) {
            if (c.id === App.applicationController.get('currentCustomer').id) {
                App.applicationController.set('currentCustomer', null);
                App.applicationController.random();
            }
            App.applicationController.get('customers').removeObject(c);
        }
    }

    $.connection.hub.start();

    App.hub = hub;

})(window.App); 

我们只需通过 applicationController 操作模型,Ember.js 绑定将处理其余部分并确保 UI 正确反映修改后的模型。

Ember 观察者

在上一节中,注入 SignalR 后,每个客户端都已经实时获取服务器上发生的变化。更有趣的是,我们使用 Ember 观察者来观察用户键入时属性的变化,并通过 customerHub 通知服务器,之后服务器将把变化广播给所有客户端。让我们将以上代码行添加到 CustomerModel

  propertyChanged: function () {
            try {
                if (!this.get('quiet') && this.get('id')) {
                    App.hub.server.update(this.plain());
                }
            }
            catch (e) {

            }
        } .observes('firstName', 'lastName', 'email', 'phone', 'active'), 

我们只在现有客户的 firstNamelastNameemailphone active 属性发生变化时通知服务器。quiet 检查可以防止客户端到服务器和服务器到客户端的循环通知,当一个对象从服务器发生变化时,它不会再次通知服务器它正在变化。

享受我们的工作

现在让我们构建并运行 Web 应用程序,随机创建一个新客户并点击提交。如您在屏幕上所见,UI 更改运行良好,没有任何 DOM 操作。点击编辑链接编辑并修改姓氏、名字等文本框中的任何内容,下面编辑的行会立即反映我们刚刚输入的内容。

要了解 Ember.js 如何与 SignalR 交互,请复制浏览器中的 URL 并粘贴到另一个浏览器窗口中,将两个窗口并排放置,然后在一个窗口中创建/编辑/删除客户,另一个窗口中的客户也会像镜子一样随之改变。这真的很酷,对吧。

结论

Ember.js 中的控制器、视图、模型和路由与 ASP.NET MVC 的工作方式非常相似,因此那些已经使用过 ASP.NET MVC 的人可以很容易地理解并从 Ember.js 中受益。本文只是对 Ember.js 的介绍,要创建一个复杂且实时的 Web 应用程序,我们必须深入研究更多的 Ember 对象。此示例中的 SignalR 是一个桥梁,它确保客户端的模型与服务器同步。

参考文献

使用 Ember.js、REST API 和 SignalR 的实时 Web 应用程序示例 - CodeProject - 代码之家
© . All rights reserved.