如何从 Flex3.2 调用 Rails2.3.2 的 Restful API?
所谓 Restful API, 简单的说就是利用已有的四个 HTTP 动作实现对资源的四种操作:
- GET 获取
- POST 创建
- PUT 更新
- DELETE 删除
在这里对 REST 不作更详尽的解释, 相关资料网上有很多, 争议更多. 我们暂且不管这些争论(包括作者自己也在争论), 单从纯技术角度来看, 这种方式对于我们常见的 CRUD 操作实在是再方便不过了.
但是且慢, 如果你拥有一个独立的客户端, 发送这四种动作是不成问题的, 可是如果你是做 RIA 开发(Flex 或 Silverlight), 目前的浏览器宿主只能支持 GET 和 POST. 后两种动作怎么办?
注: 虽然 Flex 里的 HTTPService 组件有8种动作, 但并不起作用.
也许你已经见过很多动态网站系统采用的解决办法了, 那就是加 URL 参数.
http://www.example.com/admin.asp?action=newpost&title=abc
这样做的好处是通过直观的参数与服务器进行对话.
当然坏处也不少, 按照 HTTP 协议, GET 动作是不能对服务器有副作用的, 因为 GET 只负责读取, 另外这样的 URL 如果被搜索引擎收录了, 或者被用户加入收藏夹, 虽然不是什么致命的问题, 但终究还是对 HTTP 协议的一种误读.
OK~, 既然浏览器不支持, 提倡 Restful 的框架又普遍不满意这种 URL 参数的工作方式, 那 rails 是如何解决这个问题的呢?
Rails 也没有能力解决浏览器的问题, 她同样需要使用额外的方法来模拟 PUT 和 DELETE 动作, 查看产生的源代码, 你会发现她采用的方法是读取 POST 表单里的 hidden 域 _method
html
<input name="_method" type="hidden" value="put" />
采用这种办法对于网页的表单来说或许是合适的, 但对于像我这样使用 Flex 开发 RIA 客户端程序的人来说, 会存在下面的问题:
我读取的文档可能是这样的
xml
<user> <name>路飞</name> <age>20</age> </user>
那么我为什么要转换成类似下面的表单来提交?
user[name]=路飞&user[age]=20
如果可以读取也是 xml, 提交亦是 xml, 生活就简单的多了, 不是么?
提交 xml 就意味着 _method 必须放在别的地方(xml 是不能有两个根元素的), rails 2.2.2 之前的版本(具体到哪一版并不清楚), 服务器是接受 GET 参数的, 也就是说可以写成这样:
/users/1?_method=PUT
_method 虽然有了安身之处, 但是且慢! 这样安全吗? 正规吗? 我想 rails 的开发者也对这个东西看不顺眼, 所以在 rails 2.3.2 里, 这种方法被禁止了.
详见: lib/action_controller/vendor/rack-1.0/rack/methodoverride.rb
新版本只在 POST 表单里读取 _method, My God…
幸运的是, methodoverride.rb 这个文件的内容为我指明了另一个方向:
ruby
def call(env) if env["REQUEST_METHOD"] == "POST" req = Request.new(env) method = req.POST[METHOD_OVERRIDE_PARAM_KEY] || env[HTTP_METHOD_OVERRIDE_HEADER] method = method.to_s.upcase if HTTP_METHODS.include?(method) env["rack.methodoverride.original_method"] = env["REQUEST_METHOD"] env["REQUEST_METHOD"] = method end end end
这个文件告诉我们, 服务器除了接受 _method 隐藏域之外, 还接受一个叫 “HTTP_X_HTTP_METHOD_OVERRIDE” 的消息头, 太棒了, 让我们迎着光明继续前进 为了能够看到提交的消息头, 我们需要在 application_controller.rb 里添加点代码. (Rails 2.2.2 之前该文件名为 application.rb)
ruby
class ApplicationController < ActionController::Base before_filter :print_headers def print_headers request.headers.each { |k,v| logger.info("#{k} #{v}") } end end
现在回到 Flex 我们创建一个客户端试验一下效果.
actionscript
var svc:HTTPService = new HTTPService(); svc.method = "POST"; svc.url = "/users/1"; svc.headers = { HTTP_X_HTTP_METHOD_OVERRIDE:"PUT", Accept:"application/xml" }; svc.request = { user:{ age:30 } }; svc.send();
结果有点长, 删减之后你会发现类似下面这样的内容:
Processing UsersController#create to xml (for 127.0.0.1 at 2009-03-22 11:02:04) [POST] SERVER_NAME ... ....... HTTP_HTTP_X_HTTP_METHOD_OVERRIDE PUT .......
呃… 为什么会多出个 HTTP_ 前缀来呢? 这个问题留给你来解答吧, 我也实在是说不清楚.
不过没关系, 问题已经明朗了, 重新设计一下我们的消息头就可以了.
actionscript
// 首先创建服务代理, 这里只是个例子, 实例上你可能更喜欢用 Mxml. var svc:HTTPService = new HTTPService(); // 只能是 POST, 不要被其他那些参数给骗了. svc.method = "POST"; // 朝用户 ID 为 1 的资源发出 PUT 动作 svc.url = "/users/1"; // 既然不知道哪里来了个 HTTP_ 前缀, 那我们就少打5个字母好了. // Accept 消息头可以使我们的 url 不必以 .xml 结尾来说明 format. svc.headers = { X_HTTP_METHOD_OVERRIDE:"PUT", Accept:"application/xml" }; // 请求的内容是你想要更新的字段, 这里视情况而定, 也可以直接写 xml. svc.request = { user:{ age:30 } }; // 发射!!! svc.send();
`Processing UsersController#update to xml (for 127.0.0.1 at 2009-03-22 11:05:13 ) [PUT]
Parameters: {"id"=>"1"}
……. HTTP_X_HTTP_METHOD_OVERRIDE PUT …….
User Load (0.0ms) SELECT * FROM "users" WHERE ("users"."id" = 1) User Update (2.0ms) UPDATE "users" SET "age" = 30, "updated_at" = '2009-03-22 03:05:13' WHERE "id" = 1
Completed in 164ms (View: 4, DB: 2) | 200 OK [http://192.168.0.1/users/1]`
OK~ 看来 Rails 已经收到我们的信号, 并且正确识别了我们想要的动作 [PUT]. 其他类似的动作可以由我们自由发挥了, 再也不用受 _method 的牵拌.
嗯, 如果你也是一个像我一样的在技术领域无聊的完美主义者, 但愿这个结果能够令你满意.