在本章中,我们将探讨在将 Angular.js 与 Ruby on Rails 框架相结合时如何解决常见问题。本章中使用的示例基于管理联系人列表的示例应用程序。
您希望使用在您的 Rails 应用程序中实现的 JSON REST 应用编程接口。
使用$resource
服务是一个很好的开始,对于 Rails 开发人员来说,可以通过根据 Rails 操作配置方法来调整它,使其感觉更自然:
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
动作:
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 请求与其他请求分开:
Contacts::Application.routes.draw do
scope "api" do
resources :contacts
end
end
这将为 HTTP GET 方法生成路径,例如api/contacts
和api/contacts/:id
。
你可以在 GitHub 上找到完整的例子。
如果你想跟上 Ruby on Rails 的速度,我建议你看看 Rails Guides ,这将帮助你理解所有的部分是如何组合在一起的。
上面的示例代码工作得很好,直到我们对资源使用 HTTP 方法POST
、PUT,
和DELETE
。作为一种安全机制,Rails 期望真实性令牌来防止跨站点请求伪造(CSRF) 攻击。我们需要用令牌提交一个额外的 HTTP 头X-CSRF-Token
。它由 Rails 方便地呈现在 HTML meta
标签csrf-token
中。使用 jQuery,我们可以获取该元标签定义并适当地配置$httpProvider
:
var app = angular.module("Contacts", ["ngResource"]);
app.config(function($httpProvider) {
$httpProvider.defaults.headers.common['X-CSRF-Token'] =
$('meta[name=csrf-token]').attr('content');
});
如果您使用的是 3.1 之前的 Rails 版本,您会注意到 JSON 响应将使用contact
命名空间作为模型属性,这破坏了您的 Angular.js 代码。要禁用此行为,您可以相应地配置您的 Rails 应用程序:
ActiveRecord::Base.include_root_in_json = false
Ruby 和 JavaScript 世界之间仍然存在不一致。例如,在 Ruby 中,我们使用带下划线的属性名(display_name),而在 JavaScript 中,我们使用 camelCase (displayName)。
有一个定制的$resource
实现, angularjs-rails-resource ,可用于简化对 rails 资源的消耗。它使用转换器和接收器来重命名属性字段,并为您处理根包装行为。
您希望将客户端路由与 Ruby on Rails 后端结合使用。
对后端的每个请求都应该首先呈现完整的页面,以便加载我们的 Angular 应用程序。只有到那时,客户端渲染才会接管。让我们先来看看这条“通用”路线的路线定义:
Contacts::Application.routes.draw do
root :to => "layouts#index"
match "*path" => "layouts#index"
end
它使用路由全局化来匹配所有 URL,并定义一个根 URL。两者都将由布局控制器处理,其唯一目的是呈现初始布局:
class LayoutsController < ApplicationController
def index
render "layouts/application"
end
end
实际的布局模板定义了我们的ng-view
指令,并且驻留在app/views/layouts/application.html
中——这里没有什么新东西。所以,让我们跳到app.js.erb
中的角度路线定义:
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 上找到完整的例子。
让我们再来看看路线的定义。首先,文件名以erb
结尾,因为它在 JavaScript 文件中使用了 ERB 标签,这是由 Rails 资产管道提供的。asset_path
方法用于检索网页组件的网址,因为它会根据环境而变化。在生产中,文件名包含 MD5 校验和,实际的 ERB 输出将从/assets/contacts/index.html
变为/assets/contacts/index-7ce113b9081a20d93a4a86e1aacce05f.html
。如果您的 Rails 应用程序被配置为使用资产主机,路径实际上是绝对的。
您希望使用 Rails 提供的服务器端 REST API 来验证表单。
Rails 已经提供了现成的模型验证支持。让我们从联系人活动记录模型开始:
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
的动作:
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 响应的例子:
{ "age": ["must be less than or equal to 50"] }
它是一个哈希,每个属性都有一个条目,带有验证错误。该值是一个字符串数组,因为可能同时存在多个错误。
让我们进入应用程序的客户端。Angular.js 触点$resource
调用创建函数并传递失败回调函数:
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
将表单字段标记为无效。
现在,我们已经准备好向用户展示一些反馈,使用表单一章中已经讨论过的相同方法:
<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 类。这将以红色呈现标签、输入字段和帮助块:
$scope.errorClass = function(name) {
var s = $scope.form[name];
return s.$invalid && s.$dirty ? "error" : "";
};
errorMessage
将打印更详细的错误信息,并在同一控制器中定义:
$scope.errorMessage = function(name) {
result = [];
_.each($scope.form[name].$error, function(key, value) {
result.push(value);
});
return result.join(", ");
};
它遍历每个错误消息,并从中创建一个逗号分隔的字符串。
你可以在 GitHub 上找到完整的例子。
最后,errorMessage
的处理当然相当原始。用户会期待本地化的故障消息,而不是这种技术演示。Rails 国际化指南描述了如何在 Rails 中翻译验证错误消息,并且可能对客户端代码的进一步使用有所帮助。