Ruby on Rails 通过在配置上使用约定,以及我们遵守的其它 DRY 来消除重复。我相信我们在域模型的制度化方面做了很多工作。在布局中很少有重复。从数据结构的内省到数据结构的迁移来观察视图错误及明显的控制器参数与列的映射。
但对控制器本身是怎么做的呢?我们如何保证在同一个控制器逻辑内处理客户端的不同输出需要的DRY呢?直到现,至少还没有一个制度化的形式。
Backpack 网站实际上为各个用户重用控制器动作。它有一个 Mobile 版本,一个 API 版本,及一个常规的 web 版本。大约有80%的动作通过各种 hack 达到共享。
现在这些 hacks 与 想法能够通过 HTTP 规范内奇妙的 Accept header 得到实现。下面我们看段代码:
class CommentController < ActionController::Base
def create
@comment = Comment.create(params[:comment])
respond_to do |type|
type.html { redirect_to :action => "index" }
type.js
type.xml do
headers["Location"] =url_for(:action => "show", :id => @comment.id)
render(:nothing, :status => "201 Created")
end
end
end
end
这是一个用于给 weblog 添加评论的简单控制器。它能够服务于早期浏览器,支持 Ajax 的浏览器,以及对 blog 的 API 访问。这三个客户,使用同样的控制器逻辑。
如果在早期的浏览器上递交这个表单,我们将通过一个简单的,早期的 POST 来完成,浏览器将使用类似 “Accept: */*” 来发送它。
这意味着“我不关心你给我什么样的应答,只要给我些东西就可以了”。因为浏览器不关心,所以我们将决定用于完成什么的类型声明次序。第一个类型是 .html ,因为这就是我们将要完成的东西,在这种情况下它指示浏览器去取回 index 索引。
在支持 Ajax 的浏览器递交该表单时,Prototype 将截获递交的东西并转送到一个 Ajax 调用内。这个调用将使用 "Accept: text/javascript, text/html, application/xml, text/xml */*" 来发送,该头指定了一个优先次序,首先是 javascript ,如果无效则使用 HTML ,如果再无效则使用 XML,最后无论首选的形式是否有效,它都将最后接受一个。
注意,Prototype 在 respond_to 调用内不匹配声明次序时是如何优先选择次序的。当使用 “*/*” 做为 Accept header 时,次序是相当重要的,但是当优先选择有效时,我们将通过声明在匹配的搜索内完成多次通行。除非即没有找到 javascript,HTML,也没有找到 XML,否则声明的次序并不是主要问题。
但在这个例子中,Javascript 的优先选择是有效,因此我们将其发送回去。因为声明没有指定一个描述在它被触发时应该发生什么行为的块,所以我们依赖于那个缺省类型,来渲染 :action => “creates.rjs”。
最后,如果我们想通过 API 来创建一个评论,我们将做出类似下面的请求:
POST /comments/create HTTP/1.1
Host: example.org
User-Agent: Thingio/1.0
Accept: application/xml
Content-Type: application/xml
Content-Length: nnn
<comment>
<body>First post!!</body>
<author>David</author>
</comment>
首先,要注意我们是在 XML 内递交评论的。因为我们使用了 application/xml 的 Content-Type,所以 Rails 将自动地把 XML body 内容翻译成"comment[body]=First%20post!!&comment[author]=David" ,它采取的格式与常规格式相同,而不论其来自于一个早期的浏览器还是一个 Ajax 调用。因此我们就不必修改我们 create 动作的第一行以在输出上容纳不同的客户。它们看起来都是一样的。
其次,要注意到我们对 Accept header 是明确的。在 Content-Type header 内我们说“XML 是我给出的东西”,而 Accept header 说“XML 是我想要东西”。这就将触发 type.xml 定义,它将产生类似如下的应答:
HTTP/1.1 201 Created
Content-Length: 0
Location: http://example.org/comments/show/5
一、但是谁使用 Accept 呢?
现在,当然这很好,但是实际上谁要使用 Accept header 呢?不是很多,但值得我们去研究。
浏览器指定 “*/*” ,它是精巧的。它意味着我们可以使用 respond_to 定义的次序来给它们我们想要的东西(通常是 HTML)。Prototype 指定一套祥细的优先选择集,但是你通常只使用类型 js 来发送 RJS 。与这两个不同,现在你还有一个非常好的方式,来对常规 POST 及 Ajax 调用使用同样动作。
并且如果你控制你的 API (就像我们在 37singals 上做的),你可规定,如果客户想要传回 XML,它必须指定 Accept header 。因些例子中就做了三个。
如果你的 feed 被路由到同一个动作,并且你有个 before_filter ,它转换请求 “.rss” 到 "Accept: application/rss+xml" ,以及 ".atom" 到 "Accept: application/atom+xml" 中,那么你应该有个 index 动作,它支持对同样数据的五种输出:
class WeblogController < ActionController::Base
def index
@entries = Entry.find :all
respond_to do |type|
type.html
type.js
type.xml(@entries.to_xml)
type.rss { render :action => "rss.rxml" }
type.atom { render :action => "atom.rxml" }
end
end
end
假设在请求中没有指定一个 Accept header ,那么它们将更愿意返回它们提供的东西,或者是在这些都无效时,你有的任何什么东西,并且你甚至不需要为大多数动作指定 Accept header。对我讲 XML 呢?你将得到被返回的 XML。
进一步说,我自然希望所有的 HTTP 客户端开始由它们自己使用 Accept header。NetNewsWire 可以发送 "Accept: application/atom+xml; application/rss+xml" 来指定它更喜欢 Atom ,但是如果你有的话,它也接受 RSS。
二、Mixing and matching
The cool thing about separating what you give and what you want into two concerns is that you can make your API even easier to use for normal people. With the CommentController above, I could make a call like this:
curl -H 'Accept: application/xml'
-d 'comment[body]=Yoyo&comment[author]=David'
http://example.org/comments/create
我要查询数据,但得到的回馈是 XML。或者你有个在 XML 内发送的 Ruby 脚本,但指定了 "Accept: application/x-yaml" ,那么它将触发你已定义的任何类型的 .yaml 定义。有许多可能性。
三、Nifty, nifty, but why?!
如果你的控制器逻辑像上面两个例子那样只有一行,那么代码量有多大并不是很明显的。但控制器逻辑通常是2,3,5,或者甚至是10行代码。那么你真的要重复2,3,或5个不同的动作来服务那么多客户端吗?我希望你不要这样。实际上我们可以做的像 Backpack API 那样,所以我们要把它们分开。这个技术是个纯净版本。
但是要记住你不会有在所有客户端之间的完全映射。这不是要为跨越所有客户端情况创建一种100%的解决方案。有些客户端将需要其它的动作。有时候逻辑是不同的即使动作是类似的。 只是这都不是什么大事。如果你能让你的应用程序可自由使用一个 API 的80%,你就更有可能完成余下的20%,It doesn't matter, though. If you can get 80% of an API for your application out of the box almost for free, you're much more likely to finish the last 20%, and actually launch with one instead of 2 years after (as is the case with the Basecamp API we're about to introduce).
http://www.loudthinking.com/arc/000572.html
Accept: text/css, application/vnd.wap.wmlscriptc, application/vnd.wap.wbxml, application/x-wap-prov.browser-settings, application/x-nokia.settings, application/vnd.wap.sic, application/vnd.wap.slc, text/x-vCard, text/x-vCalendar, application/vnd.wap.wtls-ca-certificate, application/vnd.wap.hashed-certificate, application/vnd.wap.signed-certificate, application/vnd.wap.cert-response, application/x-wap-prov.browser-bookmarks, text/html, application/vnd.wap.wmlc, application/xhtml xml, application/vnd.wap.xhtml xml, text/x-co-desc, application/vnd.oma.drm.message, image/gif, image/jpeg, image/jpg, image/bmp, image/png, image/vnd.wap.wbmp, image/vnd.nok-wallpaper, audio/midi, audio/mid, audio/x-midi, audio/x-mid, audio/sp-midi, application/vnd.nokia.ringing-tone, image/vnd-nok-camera-snap, image/vnd-nok-camera-snsp, text/vnd.sun.j2me.app-descriptor, application/java, image/vnd.nok-oplogo-color, application/java-archive, application/x-java-archive, application/vnd.wap.mms-message, application/vnd.wap.sic, application/vnd.wap.slc, application/vnd.wap.mms-message, application/vnd.wv.csp.cir, image/bmp, image/vnd.wap.wbmp, application/x-nokia.settings-xml, text/x-wap.wml, text/vnd.wap.wml, text/x-hdml, text/html, text/vnd.wap.wmlscript, text/vnd.wap.si, text/vnd.wap.sl, */*
Yes, that’s right. Accept every single MIME type in the world. And then accept */*.
http://weblog.sinteur.com/?p=10649
http://www.xml.com/pub/a/2005/06/08/restful.html |