使用 Ember.js、REST API 和 SignalIR 实现实时的 Web 应用 已翻译 100%

oschina 投递于 2012/12/20 06:43 (共 15 段, 翻译完成于 01-24)
阅读 5018
收藏 13
3
加载中
本文涉及的源码下载:

Sample Image - maximum width is 600 pixels

介绍

Ember是一个强大的JavaScript MVC框架,可以用来创建复杂的web应用程序,消除了样板,并提供了一个标准的应用程序架构。支持绑定UI,视图组成,Web表示层, 和其它插件兼容性也很好。为了建立一个实时交互的web应用程序,添加了一个SignalR中心和带有REST service的ASP.NET MVC。

进击的程序员
进击的程序员
翻译于 2012/12/28 14:40
2

MVC 基础

MVC模式的意义在于分离对视图(view),模型(model)以及控制器(Controller)的注意。模型是保持数据的部分,视图描述应用的表现(外观),而控制器起连接前二者的作用。

这是Ember.js客户端(client)部分的MVC具体实现

在Ember.js中还有另一个重要的概念:路由(Router),或者叫状态管理器(StateManager),它的作用很像ASP.NET的MVC路由,不过它是客户端的。在这篇文章中,一个路由的职责是连接主控制器与主视图。为了更简单地生成、维护视图,Ember.js使用完整的Handlebars模板

nighca
nighca
翻译于 2013/01/01 10:34
2

开始一个设置项目

    首先,我们需要新建一个空的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.min.js" type="text/javascript"></script>
    <script src="../../Scripts/libs/ember-1.0.0-pre.2.min.js" type="text/javascript"></script>

</head>
<body>
    @RenderBody()
</body>
</html>
还有 Index.cshtml:
@{
    ViewBag.Title = "Ember.n.SignalR";
}
<div id="app"></div>
<script src="../../Scripts/u.js" type="text/javascript"></script>
<script src="../../Scripts/app.js" type="text/javascript"></script>
  • u.js 包含两个JS的方法,主要生成随机字符串和随机数
  • app.js 包含整个项目所以的JS文件。

木川瓦兹
木川瓦兹
翻译于 2012/12/27 11:19
1

服务器端的模型

我们创建一个包含基础信息和用于REST方法的简单结果集的顾客 DTO (在这个例子里我们使用 DTO 作为模型) 。

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; }
}

木川瓦兹
木川瓦兹
翻译于 2012/12/27 11:43
1

REST 服务
我们已经定义了DTO,现在我们需要创建CustomerController.cs,这是customer的一个简易的由 /customer/ 来提供JSON格式数据的转换的REST 服务 。
为保证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)
    {
        //...
    }
}
至于customers的数据存储,我简单实现了CustomerDS.cs将数据以二进制格式保存在 ~/App_Data/Customers.em 文件中。至此服务器端需编写的代码已经完成。接下来我们将关注Ember的应用架构

 

 

 

qingfeng哥
qingfeng哥
翻译于 2013/01/07 20:28
1

Ember应用
先大致了解一下Ember,创建一个ember应用对象先。

var app = Ember.Application.create({
    name: "ManageCustomerApp",
    rootElement: '#app',
    // Extend to inherit outlet support
    ApplicationController: Ember.Controller.extend(),
    ready: function () {
        this.initialize();
    },
    getView: function (name) {
        var template = '';
        $.ajax(
            {
                url: '/Templates/' + name + '.htm',
                async: false,
                success: function (text) {
                    template = text;
                }
            });
        return Ember.Handlebars.compile(template);
    }
});

rootElement元素定义了应用出口 是Index.cshtml中该id标识的主div
这要通过扩展ApplicationController 来获得outlet支持
ready:事件,可选,需要增加额外代码时可以定义
getView:方法,这个方法通过Ajax异步的获取模板(template),然后由Handlebars将其编译为视图view

qingfeng哥
qingfeng哥
翻译于 2013/01/07 20:43
1
Ember的模型
我们需要在客户端创建一个数据存储对象来和/customer/ REST服务交互。

如下

// 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中的两个随机方法来产生随机的customer数据.
  • plain:  在发送到 REST 服务之前获取JSON 对象以提升性能 (Ember.Object的附加属性则没有必要).

qingfeng哥
qingfeng哥
翻译于 2013/01/07 20:53
1

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.customerController = app.CustomerController.create();
我们的customerController控制着创建,更新或者删除一个customer的逻辑,它会把每次操作的结果存入currentResult属性中,把正在编辑/正在创建的customer存入currentCustomer属性中。customers阵列模型就是我们的customers的存储,它通过app.Store与服务器一直保持同步,并用来约束我们的视图。当我们的控制器初始化的时候,我们在服务器端调用this.read()来检索所有的customers。你知道我们在这儿为什么要用set或者get方法吗?这是JavaScript中应对操纵属性变化的方式。事实上,在C#中,get/set属性会被编译成CLR中的get/set方法。
c
crAzyli0n
翻译于 2013/01/21 23:07
1

Ember的视图

我们看到视图连接着控制器和模型,现在我们就来研究一下视图。视图通过控制器锚来显示/约束出入于模型的的值。我在相互分离的htm文件中定义视图模板,通过AJAX来加载它,使用Ember.Handlebars来编译响应文本,相比于把视图模板放置于脚本标签中,这种方式更易于修改,我们可以使用任何html编辑器来编辑视图模板,例如MS 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="content.currentCustomer.firstName"}}
            <a href="#" {{action "random" 
              on="click" target="this" }}>Random new customer</a>
    </div>
    <div>
        <label for="lastName">
            Last name</label><br />
        {{view Ember.TextField valueBinding="content.currentCustomer.lastName"}}
    </div>
    <div>
        <label for="email">
            Email</label><br />
        {{view Ember.TextField valueBinding="content.currentCustomer.email"}}
    </div>
    <div>
        <label for="phone">
            Phone</label><br />
        {{view Ember.TextField valueBinding="content.currentCustomer.phone"}}
    </div>
    <p>
        <button id="submit" {{action "save" 
          on="click" target="this" }} >Submit</button>
    </p>
</div>
CreateEditCustomerView
app.CreateEditCustomerView = Ember.View.extend({
    template: app.getView('create_edit_customer'),
    contentBinding: 'controller.namespace.customerController',
    name: "create_edit_customer",
    save: function (event) {
        this.get('content').save();
    },
    random: function () {
        this.get('content').random();
    }
});
c
crAzyli0n
翻译于 2013/01/21 23:52
1

正如你在模板中所见,有一些Handlebars的语法存在其中,First name文本框定义为{{view Ember.TextField valueBinding="content.currentCustomer.firstName"}}。根目录约束对象,而内容则是由我们的customer控制器通过Ember的语法“contentBinding”限制的。这时候,你也许会问“它是怎么限制的?”。这就是Ember的奇妙的语法所在,我们用content+“Binding”的方式告诉Ember controller.namespace.customerController是受CreateEditCustomerView的内容属性限制的。这个视图中有两个操作,第一个就是随机生成一个新的customer,第二个就是保存一个customer,在模板中,你可以很容易的找到这两行,{{action "random" on="click" target="this" }}和{{action "save" on="click" target="this" }}。这两个操作都是单击事件。

为了显示一个customers的列表,我们需要一个模板。

<div id="customerListHeader">
    List of customers
</div>
<div id="customerListContent">
    <table>
        {{#unless view.customers.length}}
        <tr>
            <th> First name </th>
            <th> Last name </th>
            <th> Email </th>
            <th> Phone </th>
            <th> #Action </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 view.customers}}
        <tr {{bindAttr class="this.active:active:normal"}} {{bindAttr id="this.id"}}>
            <td>
                {{this.firstName}}
            </td>
            <td>
                {{this.lastName}}
            </td>
            <td>
                {{this.email}}
            </td>
            <td>
                {{this.phone}}
            </td>
            <td align="center">
                <a href="#" class="edit" {{action "edit" 
                   on="click" target="this" }} {{bindAttr value="this.id"}}>Edit</a>
                |
                <a href="#" class="delete" {{action "remove" 
                  on="click" target="this" }} {{bindAttr value="this.id"}}>Delete</a>
            </td>
        </tr>
        {{/each}} 
        {{/unless}}
    </table>
</div>
CustomerListView

app.CustomerListView = Ember.View.extend({
    contentBinding: 'controller.namespace.customerController',
    customersBinding: 'controller.namespace.customerController.customers',
    template: app.getView('customer_list'),
    name: "customer_list",
    edit: function (event) {
        var id = $(event.target).attr('value');
        var controller = this.get('content').edit(id);
    },
    remove: function (event) {
        var id = $(event.target).attr('value');
        var controller = this.get('content');
        this.animateItem(id, function () {
            controller.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);
            }
        });
    }
});
对于每个创建/编辑/删除customer的操作,我们都需要显示消息结果,以下就是MessageView中的消息模板。
{{#unless content.currentResult.errorCode}}
<div id='message'>
    {{content.currentResult.errorMessage}} &nbsp;
</div>
{{else}}
<div id='error'>
    {{content.currentResult.errorMessage}} &nbsp;
</div>
{{/unless}}
这个视图不进行任何操作。
app.MessageView = Ember.View.extend({
    template: app.getView('message'),
    name: "message"
});
CreateEditCustomerView,CustomerListView和MessageVieware共同组成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: app.getView('main'),
    name: "ApplicationView"
});
c
crAzyli0n
翻译于 2013/01/22 00:32
1
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(1)

guoyh
guoyh
不错,正常在找相关学习资料
返回顶部
顶部