# 九、Ruby on Rails 后端集成

在本章中，我们将探讨在将 Angular.js 与 [Ruby on Rails](http://rubyonrails.org/) 框架相结合时如何解决常见问题。本章中使用的示例基于管理联系人列表的示例应用程序。

## 消耗

### 问题

您希望使用在您的 Rails 应用程序中实现的 JSON REST 应用编程接口。

### 溶液

使用`$resource`服务是一个很好的开始，对于 Rails 开发人员来说，可以通过根据 Rails 操作配置方法来调整它，使其感觉更自然:

```js
    app.factory("Contact", function($resource) {
    return $resource("/api/contacts/:id", { id: "@id" },
    {
    'create': { method: 'POST' },
    'index': { method: 'GET', isArray: true },
    'show': { method: 'GET', isArray: false },
    'update': { method: 'PUT' },
    'destroy': { method: 'DELETE' }
    }
    );
    });

```

我们现在可以使用`Contact.index()`获取联系人列表，使用`Contact.show(id)`获取单个联系人。这些动作可以直接映射到您的 Rails 后端中的`ContactsController`动作:

```js
    class ContactsController < ApplicationController
    respond_to :json

    def index
    @contacts = Contact.all
    respond_with @contacts
    end

    def show
    @contact = Contact.find(params[:id])
    respond_with @contact
    end

    ...
    end

```

Rails 动作控制器使用一个`Contact`活动记录模型，该模型具有常用的联系属性，如名字、姓氏、年龄等。通过指定`respond_to :json,`控制器只响应 JSON 资源格式；我们可以使用`respond_with`自动将`Contact`模型转换为 JSON 响应。

路由定义使用 Rails 默认资源路由和`api`作用域将 API 请求与其他请求分开:

```js
    Contacts::Application.routes.draw do
    scope "api" do
    resources :contacts
    end
    end

```

这将为 HTTP GET 方法生成路径，例如`api/contacts`和`api/contacts/:id`。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter9/recipe1) 上找到完整的例子。

### 讨论

如果你想跟上 Ruby on Rails 的速度，我建议你看看 [Rails Guides](http://guides.rubyonrails.org/index.html) ，这将帮助你理解所有的部分是如何组合在一起的。

#### 使用真实性令牌的 Rails 安全性

上面的示例代码工作得很好，直到我们对资源使用 HTTP 方法`POST`、`PUT,`和`DELETE`。作为一种安全机制，Rails 期望真实性令牌来防止[跨站点请求伪造(CSRF)](http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf) 攻击。我们需要用令牌提交一个额外的 HTTP 头`X-CSRF-Token`。它由 Rails 方便地呈现在 HTML `meta`标签`csrf-token`中。使用 jQuery，我们可以获取该元标签定义并适当地配置`$httpProvider`:

```js
    var app = angular.module("Contacts", ["ngResource"]);
    app.config(function($httpProvider) {
    $httpProvider.defaults.headers.common['X-CSRF-Token'] =
    $('meta[name=csrf-token]').attr('content');
    });

```

#### Rails JSON 响应格式

如果您使用的是 3.1 之前的 Rails 版本，您会注意到 JSON 响应将使用`contact`命名空间作为模型属性，这破坏了您的 Angular.js 代码。要禁用此行为，您可以相应地配置您的 Rails 应用程序:

```js
    ActiveRecord::Base.include_root_in_json = false

```

Ruby 和 JavaScript 世界之间仍然存在不一致。例如，在 Ruby 中，我们使用带下划线的属性名(display_name)，而在 JavaScript 中，我们使用 camelCase (displayName)。

有一个定制的`$resource`实现， [angularjs-rails-resource](https://github.com/tpodom/angularjs-rails-resource) ，可用于简化对 rails 资源的消耗。它使用转换器和接收器来重命名属性字段，并为您处理根包装行为。

## 实现客户端路由

### 问题

您希望将客户端路由与 Ruby on Rails 后端结合使用。

### 溶液

对后端的每个请求都应该首先呈现完整的页面，以便加载我们的 Angular 应用程序。只有到那时，客户端渲染才会接管。让我们先来看看这条“通用”路线的路线定义:

```js
    Contacts::Application.routes.draw do
    root :to => "layouts#index"
    match "*path" => "layouts#index"
    end

```

它使用[路由全局化](http://guides.rubyonrails.org/routing.html#route-globbing)来匹配所有 URL，并定义一个根 URL。两者都将由布局控制器处理，其唯一目的是呈现初始布局:

```js
    class LayoutsController < ApplicationController
    def index
    render "layouts/application"
    end
    end

```

实际的布局模板定义了我们的`ng-view`指令，并且驻留在`app/views/layouts/application.html`中——这里没有什么新东西。所以，让我们跳到`app.js.erb`中的角度路线定义:

```js
    var app = angular.module("Contacts", ["ngResource"]);

    app.config(function($routeProvider, $locationProvider) {
    $locationProvider.html5Mode(true);
    $routeProvider
    .when("/contacts",
    { templateUrl: "<%= asset_path('contacts/index.html') %> ",
    controller: "ContactsIndexCtrl" })
    .when("/contacts/new",
    { templateUrl: "<%= asset_path('contacts/edit.html') %> ",
    controller: "ContactsEditCtrl" })
    .when("/contacts/:id",
    { templateUrl: "<%= asset_path('contacts/show.html') %> ",
    controller: "ContactsShowCtrl" })
    .when("/contacts/:id/edit",
    { templateUrl: "<%= asset_path('contacts/edit.html') %> ",
    controller: "ContactsEditCtrl" })
    .otherwise({ redirectTo: "/contacts" });
    });

```

我们将`$locationProvider`设置为使用 HTML 5 模式，并定义我们的客户端路线，用于列出、显示、编辑和创建新联系人。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter9/recipe1) 上找到完整的例子。

### 讨论

让我们再来看看路线的定义。首先，文件名以`erb`结尾，因为它在 JavaScript 文件中使用了 ERB 标签，这是由 [Rails 资产管道](http://guides.rubyonrails.org/asset_pipeline.html)提供的。`asset_path`方法用于检索网页组件的网址，因为它会根据环境而变化。在生产中，文件名包含 MD5 校验和，实际的 ERB 输出将从`/assets/contacts/index.html`变为`/assets/contacts/index-7ce113b9081a20d93a4a86e1aacce05f.html`。如果您的 Rails 应用程序被配置为使用资产主机，路径实际上是绝对的。

## 验证服务器端表单

### 问题

您希望使用 Rails 提供的服务器端 REST API 来验证表单。

### 溶液

Rails 已经提供了现成的模型验证支持。让我们从联系人[活动记录模型](http://guides.rubyonrails.org/active_record_validations_callbacks.html)开始:

```js
    class Contact < ActiveRecord::Base
    attr_accessible :age, :firstname, :lastname

    validates :age, :numericality => {
    :only_integer => true, :less_than_or_equal_to => 50 }
    end

```

它定义了对`age`属性的验证。它必须是整数，并且小于或等于 50 年。

在`ContactsController,`中，我们可以用它来确保 REST API 返回正确的错误消息。举个例子，让我们来看看`create`的动作:

```js
    class ContactsController < ApplicationController
    respond_to :json

    def create
    @contact = Contact.new(params[:contact])
    if @contact.save
    render json: @contact, status: :created, location: @contact
    else
    render json: @contact.errors, status: :unprocessable_entity
    end
    end

    end

```

成功时，它将使用 JSON 表示呈现联系模型，失败时，它将返回转换为 JSON 的所有验证错误。让我们看一个 JSON 响应的例子:

```js
    { "age": ["must be less than or equal to 50"] }

```

它是一个哈希，每个属性都有一个条目，带有验证错误。该值是一个字符串数组，因为可能同时存在多个错误。

让我们进入应用程序的客户端。Angular.js 触点`$resource`调用创建函数并传递失败回调函数:

```js
    Contact.create($scope.contact, success, failure);

    function failure(response) {
    _.each(response.data, function(errors, key) {
    _.each(errors, function(e) {
    $scope.form[key].$dirty = true;
    $scope.form[key].$setValidity(e, false);
    });
    });
    }

```

请注意，活动记录属性可以定义多个验证。这就是为什么`failure`函数遍历每个验证条目和每个错误，并使用`$setValidity`和`$dirty`将表单字段标记为无效。

现在，我们已经准备好向用户展示一些反馈，使用表单一章中已经讨论过的相同方法:

```js
    <div class="control-group" ng-class="errorClass('age')">
    <label class="control-label" for="age">Age</label>
    <div class="controls">
    <input ng-model="contact.age" type="text" name="age"
    placeholder="Age" required>
    <span class="help-block"
    ng-show="form.age.$invalid && form.age.$dirty">
    {{errorMessage('age')}}
    </span>
    </div>
    </div>

```

如果表单字段无效且脏，则`errorClass`函数添加`error` CSS 类。这将以红色呈现标签、输入字段和帮助块:

```js
    $scope.errorClass = function(name) {
    var s = $scope.form[name];
    return s.$invalid && s.$dirty ? "error" : "";
    };

```

`errorMessage`将打印更详细的错误信息，并在同一控制器中定义:

```js
    $scope.errorMessage = function(name) {
    result = [];
    _.each($scope.form[name].$error, function(key, value) {
    result.push(value);
    });
    return result.join(", ");
    };

```

它遍历每个错误消息，并从中创建一个逗号分隔的字符串。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter9/recipe1) 上找到完整的例子。

### 讨论

最后，`errorMessage`的处理当然相当原始。用户会期待本地化的故障消息，而不是这种技术演示。Rails [国际化指南](http://guides.rubyonrails.org/i18n.html#translations-for-active-record-models)描述了如何在 Rails 中翻译验证错误消息，并且可能对客户端代码的进一步使用有所帮助。