Ruby 2015 年 1 月 20 日

iOS on Rails: RESTful 服务的提供和消费

很多的朋友可能对 REST 和 RESTful 的概念比较模糊,其实很简单:
  • REST 一种网络服务的架构风格或者设计模式.
  • RESTful 是符合 REST 原则的网络服务.
可以简单的认为 REST 是名词,表示一种范式,而 RESTful 描述是否符合该范式。REST 主要有六点约束原则:
  • Client-Server: 客户端发起通信
  • Stateless: 无状态服务,每一次请求相互独立,不依赖于任何之前状态.
  • Cacheable: 响应可以被缓存.
  • Uniform Interface: 同意的接口.
  • Layered System: 分层系统,仅相邻层互通。
  • Code-On-Demand: 按需编码.
那么具体如何去实现 RESTful 的服务呢。只要我们记住如下几个要点就可以实现符合 REST 范式的 RESTful 服务。
  • 资源和 URI 一一对应
  • 客户端和服务器之间,传递这种资源的某种表现层;
  • 客户端通过使用 HTTP 的标准方法对服务端的资源进行操作

具体实现

Rails 本身就是一个完美 REST 的架构,当我们创建一个资源时, Rails 已经自动的为我们生成相应的默认路由。
$ rake routes
可以看到相应的路由,现在我们来对我们的 Rails 进行进一步的设计,让我们的 API Server 更加的 RESTful,而 RESTful 的 API 服务将大大降低了我们 iOS 客户端和 Rails 的交互成本. 更多优秀的 API 设计方案,我们可以参考 GithubHeroku 的 API 方案。

Rails

  • API 版本化
良好的版本控制可以很方便我们的前后向兼容支持。Rails 可以很容易就实现 API 的版本化.
$ mkdir -p app/controllers/api/v1
$ mv app/controllers/* app/controllers/api/v1/
然后在对 controllers/api/v1/application_controller.rb 进行如下修改.
namespace :api do
  namespace :v1 do
    resource :news, only: [:index, :show]
  end
end
因为我们的新闻资源只需要支持GET就可以,客户端不能去修改和删除服务器端的新闻资源. 我们可以立即查看到我们的路由情况.
 $ rake routes
Prefix            Verb    URI Pattern                            Controller#Action
api_v1_news_index GET     /api/v1/news(.:format)                 api/v1/news#index
api_v1_news       GET     /api/v1/news/:id(.:format)             api/v1/news#show
  • 通过 Etag 支持缓存
Etag 是个好东西,客户端在请求头添加 If-Not-Match, Rails 可以通过检查 Etag 确定资源 是否发生了变化,决定给客户端发送资源还是返回 304 Not Motified.
def index
  all_news = News.all
  if stales(etag: all_news)
    render :json => all_news
  end
end

def show
  news = News.find_by(id: params[:id])
  if stale?(etag: news)
    render :json => news
  end
end
  • 将授权码放到 Header
上篇文章我们的 Token 和 UUID 都放在 URL, 这样其实有安全隐患, WEB服务器对访问做记录已经成为了行业的一个标准, 访问记录不仅可以用来做访问量统计还能用来做访问特征分析。互联网广告平台就是利用访问记录来做精准营销的。如果 Token 包含在URL中就有很大的安全风险。 包含在 URL 中的 Token 串可能被进行重定向传递。通过这两种方式入侵者可以不通过授权 而使用泄漏的授权码访问那些受保护的数据,会造成数据泄漏的风险。
def token_authentication
  device_uuid = request.headers[:UUID]
  token = request.headers[:Token]
  if User.find_by(device_uuid: device_uuid, token: token)
    return true
  end
  return false
end
  • 返回正确的 HTTP 状态码
Rails 可以在 render 方法中很方便的设置 HTTP 状态码,
  render :status => 304

iOS

  • 设置 HTTP request 的请求头
我们使用了 AFNetworking 来协作我们进行方便的网络操作,我们可以很简单往 HTTP Headers 中加入 API Server 的授权信息.
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager.requestSerializer setValue:[[UserManager sharedUserManager] getUUID] forHTTPHeaderField: @"UUID"];
[manager.requestSerializer setValue:[[UserManager sharedUserManager] getToken] forHTTPHeaderField: @"Token"];
  • Etag 和 iOS 的缓存
这个主题我们将在之后详细的探讨。
  • 创建和 Rails 的 Model 一致的 Model
本来我打算使用 RestKit 来负责 iOS 和 API Server 的交互, 不过后来我觉得用太多的库会 使得我们初学者缺少学习的机会,所以我们还是自己来写吧,当然善用开源的各种库可以大大减轻我们开发的难度。
@interface News : NSObject

@property NSString *title;
@property NSString *link;
@property NSString *image;
@property NSString *video;
@property NSString *category;
@property NSString *pubDate;
@property NSString *site;

- (id)initWithDictionary:(NSDictionary *)aDict;

@end
其实 iOS 作为 RESTful 的消费者,不慎不需要做太多的牵连改动. 为了让我们的新闻良好的显示。 我们本期开始设计一下我们的 iOS 客户端吧。
  • 设计
我们需要一个 UITableView 来列表式的显示我们的新闻,然后我们可以简单的设计每一列的单元格, 让以一种简洁而又有效的方式显示我们的新闻。所以我们需要添加两个类:NewsListViewController 和 NewsItemCell , 类的具体实现可以查看我们的完整代码示例。
与此同时,我们还需要让我们的新闻列表支持下拉刷新功能,我们借助 SVPullToRefresh 的威力.
 __weak NewsListViewController *weakSelf = self;
[self.tableView addPullToRefreshWithActionHandler:^{
    [weakSelf requestNews];
}];

// setup infinite scrolling
[self.tableView addInfiniteScrollingWithActionHandler:^{
    [weakSelf requestNews];
}];
关于设计本文就只是一个开始,后续我们将详细的介绍 iOS 的设计范式.

最后

你依然可以在 GitHub 上看我们的完整代码示例. https://github.com/metrue/iOSonRails