RESTful 应用程式

    请注意本章内容衔接前一章,请先完成前一章内容。

    RESTful路由设计是Rails的一项独到的发明,它使用了REST的概念来建立一整组的命名路由(named routes)

    什么是REST呢?表象化状态转变Representational State Transfer,简称REST,是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。相较于SOAPXML-RPC更为简洁容易使用,也是众多网络服务中最为普遍的API格式,像是AmazonYahoo!Google等提供的API服务均有REST接口。

    REST有主要有两个核心精神:1. 使用Resource来当做识别的资源,也就是使用一个URL网址来代表一个Resource 2. 同一个Resource则可以有不同的Representations格式变化。这一章的路由实作了Resource概念,而Representation则是用了方法来实作,稍候我们也会介绍如何使用。

    关于REST的理论可以参考笔者整理的。不过,了解理论并不是在Rails中使用RESTful路由的前提条件,所以大可以跳过不甚理解没关系。我们只要知道它可以带来什么技术上的具体好处,以及如何使用就足够了。

    RESTful带给Rails最大的好处是:它帮助我们用一种比较标准化的方式来命名跟组织ControllersActions。在没有RESTful之前,我们上一章介绍了外卡路由设计方式,也就是一个个指定ControllerAction,虽然十分地简便,但是却没有什么准则。同一个Action让不同的开发者设计,就很可能放在不同的Controller之下,更常见的是让一个Controller放太多不相关的Action,造成单一Controller过于庞大。

    RESTful带入Rails路由系统的点子,出自它对应了HTTP动词POSTGETPATCH/PUTDELETE到资料的新增、读取、更新、删除等四项操作。一旦将HTTP动词考虑进来,如此我们就将上一章手工打造CRUD的路由

    • /events/create
    • /events/show/1
    • /events/update/1
    • /events/destroy/1

    变成

    • POST /events对应到Controller中的create action
    • GET /events/1对应到Controller中的show action
    • PATCH /events/1对应到Controller中的update action
    • DELETE /events/1对应到Controller中的destroy action

    HTTP GET和其他动词最大的差别在于它被认为是一个纯读取、不会修改任何资料的操作,不像POSTPATCHDELETE会修改服务器上的资料。我们一般用浏览器GET网页,可以回上一页或重新整理,但是POST网页要重新整理时,浏览器会提示你是否要在执行一次,就是这个道理。

    Rails用这套惯例来大大简化了路由设定。那程式该怎么写呢?我们在config/routes.rb加入以下一行程式:

    如此就会自动建立四个命名路由(named routes),搭配四个HTTP动词,对应到七个Actions。它的实际作用,就如同以下的routes.rb设定:

    1. get '/events' => "events#index", :as => "events"
    2. post '/events' => "events#create", :as => "events"
    3. get '/events/:id' => "events#show", :as => "event"
    4. patch '/events/:id' => "events#update", :as => "event"
    5. put '/events/:id' => "events#update", :as => "event"
    6. delete '/events/:id' => "events#destroy", :as => "event"
    7. get '/events/new' => "events#new", :as => "new_event"
    8. get '/events/:id/edit' => "events#edit", :as => "edit_event"

    用这张表格会更清楚:

    输入bin/rake routes也会列出目前设定的路由规则有哪些:

    1. $ bin/rake routes
    2. Prefix Verb URI Pattern Controller#Action
    3. events GET /events(.:format) events#index
    4. POST /events(.:format) events#create
    5. new_event GET /events/new(.:format) events#new
    6. edit_event GET /events/:id/edit(.:format) events#edit
    7. event GET /events/:id(.:format) events#show
    8. PATCH /events/:id(.:format) events#update
    9. PUT /events/:id(.:format) events#update
    10. welcome GET /welcome(.:format) welcome#index
    11. welcome_say_hello GET /welcome/say_hello(.:format) welcome#say
    12. root GET / welcome#index

    其中的Prefix指的是在ViewHelper命名,搭配path(相对网址,不带有http://your_domain)或_url(绝对网址,带有)结尾就可以组合出_Helper方法,例如welcomesay_hello_path方法会产生出/welcome/sayhello这样的网址。一般来说在网站的情境内都会使用相对网址,会用到绝对网址的情境则是在寄出去Email里面的超连结。

    另外,注意到这七个Action方法的名字,Rails是定好的,无法修改。这一套惯例建议你背起来,你可以这样记忆:

    • showneweditupdatedestroy是单数,对单一元素操作
    • indexcreate是复数,对群集操作
    • eventpath(@event)需要参数,根据_HTTP动词决定showupdatedestroy
    • eventspath毋需参数,根据_HTTP动词决定indexcreate

    因此,最后我们不写:

    1. link_to event.name, :controller => 'events', :action => :show , :id => event.id

    而改写成:

    1. link_to event.name, event_path(event)

    只需记得resources名称,就可以推导出一整组的URL Helper方法。Rails就是利用这样的高阶概念,来简化路由的设计。

    浏览器支援PATCH/PUTDELETE吗?Rails其实偷藏了_method参数。HTML规格只定义了GET/POST,所以HTML表单是没有PUT/DELETE的。但是XmlHttpRequest规格(也就是Ajax用的)有定义GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS

    修改成一个RESTful版本的CRUD

    编辑config/routes.rb,加入一个Resources

    1. resources :events

    请加在上方,routes.rb里面越上面的规则优先权较高。

    步骤二

    编辑app/views/events/index.html.erb,修改各个link_to的路径:

    1. <% @events.each do |event| %>
    2. <li>
    3. <%= event.name %>
    4. <%= link_to "Show", event_path(event) %>
    5. <%= link_to 'Edit', edit_event_path(event) %>
    6. <%= button_to 'Delete', event_path(event), :method => :delete, :data => { :confirm => "Are you sure?" } %>
    7. </li>
    8. </ul>
    9. <%= link_to 'New Event', new_event_path %>

    注意到删除的地方,我们多一个参数:method => :delete。非GET的操作,顾及网页亲和力我们顺道把linkto改成用button_tolink_to如果浏览器的_JavaScript没开,就会无法送出GET之外的操作。buttonto就无此困扰,因为_Rails是产生form标籤夹带method参数。建议你可以用浏览器打开_HTML原始码观察看看Rails实际产生出来的HTML标籤,可以有更好的认识。额外加上的:confirm参数则会让RailsJavaScript跳出确认视窗。

    步骤三

    编辑app/views/events/show.html.erb,修改link_to的路径:

    1. <%= @event.name %>
    2. <%= simple_format(@event.description) %>
    3. <p><%= link_to 'Back to index', events_path %></p>

    步骤四

    修改app/views/events/new.html.erb的表单送出位置如下:

    步骤五

    修改app/views/events/edit.html.erb的表单送出位置如下:

    1. <%= form_for @event, :url => event_path(@event), :method => :patch do |f| %>

    :url:method也可以省略,Rails自动会根据@event是新建的还是修改来决定要不要使用PATCH

    修改app/controllers/events_controller.rb,将create Actiondestroy Action里的redirect_to改成

    1. redirect_to events_url

    update Action中的redirect_to改成

    1. redirect_to event_url(@event)

    步骤七

    一旦完成RESTful之后,我们在上一章一开始设定的外卡路由就用不到了,编辑config/routes.rb将以下程式注砍掉:

    1. match ':controller(/:action(/:id(.:format)))', :via => :all

    外卡路由虽然设定上很方便,但已经不被推荐使用,它让所有Actions都可以透过GET访问到,而有安全上的顾虑。我们希望限制接收表单的create Action只允许POST请求。

    至于就完成了RESTful的改造,将来我们就不会再用到外卡路由了。会直接使用resources的方式来建立CRUD应用。

    Unknown action

    明明有在config/routes.rb里面定义了resources路由,但是出现以下的Unknown action错误:

    排除打错字之外,其原因多半是跟routes.rb里面的定义顺序有关。注意到在routes.rb里面,越上面的路由规则越优先,例如如果你定义成:

    1. Rails.application.routes.draw do
    2. match ':controller(/:action(/:id(.:format)))', :via => :all
    3. resources :events
    4. end

    那么网址/events/4就会优先比对到:controller/:action而去找4这个Action,这就错了。

    Routing Error

    这错误通常发生在link_to里,它抱怨找不到适合的路由规则来产生网址:

    Routing Error

    1. link_to "foobar", :controller => "No such controller", :action => "blah"

    因为{ :controller => "No such controller", :action => "blah" }比对不出有这个路由规则。但是如果是用RESTful路由呢?那多半是因为参数传错了,例如:

    1. link_to "Show", event_path(@foobar)

    这个@foobar没有定义所以是nileventpath(@foobar)对_Rails内部来说等同于{ :controller => "events", :action => "show", :id => nil },这就造成了找不到路由的错误,它必须知道:id才能知道是那一个活动的show Action网址。

    使用respond_to

    respondto可以让我们在同一个_Action中,支援不同的资料格式,例如XMLJSONAtom等。让我们来实作看看。

    Atom是一种基于XML的供稿格式,被设计为RSS的替代品,广泛应用于Blog feed

    修改app/controllers/events_controller.rbindex Action加上XMLJSONAtom的支援,其中toxmlto_json是_ActiveRecord内建的方法,可以将ActiveRecord物件快速地转成XMLJSON资料格式。也因为是纯资料格式,所以不像HTML需要erb template档案,我们可以直接在Action中直接呼叫render将内容回传给浏览器:

    至于Atom格式比较复杂一点,可以使用template。这里使用了builder这个引擎可以用Ruby语法来产生XML。新增app/views/events/index.atom.builder档案,内容如下:

    1. atom_feed do |feed|
    2. feed.title( @feed_title )
    3. feed.updated( @events.last.created_at )
    4. feed.entry(event) do |entry|
    5. entry.title( event.name )
    6. entry.content( event.description, :type => 'html' )
    7. end
    8. end
    9. end

    打开浏览器分别浏览看看http://localhost:3000/events.xmlhttp://localhost:3000/events.atom这几个附档名不同的网址。

    步骤二

    修改app/controllers/events_controller.rbshow Action加上XMLJSON的支援,这回我们试试看比较手工的方式,用Builder格式来建构XML,以及手动组Hash再转成JSON字串:

    1. def show
    2. respond_to do |format|
    3. format.html { @page_title = @event.name } # show.html.erb
    4. format.xml # show.xml.builder
    5. format.json { render :json => { id: @event.id, name: @event.name }.to_json }
    6. end
    7. end

    编辑app/views/events/show.xml.builder

    1. xml.event do |e|
    2. e.name @event.name
    3. e.description @event.description
    4. end

    打开浏览器分别浏览看看http://localhost:3000/events/1.json等网址。

    步骤三

    如果想要加上这些格式的超连结,可以在URL Helper中传入:format参数。让我们修改app/views/events/index.html.erb加上不同格式的超连结:

    1. <% @events.each do |event| %>
    2. <li>
    3. <%= link_to event.name, event_path(event) %>
    4. <%= link_to " (XML)", event_path(event, :format => :xml) %>
    5. <%= link_to " (JSON)", event_path(event, :format => :json) %>
    6. <%= link_to 'edit', edit_event_path(event) %>
    7. <%= button_to 'delete', event_path(event), :method => :delete, :data => { :confirm => "Are you sure?" } %>
    8. </li>
    9. <% end %>
    10. </ul>
    11. <%= link_to 'new event', new_event_path %>
    12. <%= link_to "Atom feed", events_path(:format => :atom) %>

    到目前为止,总共写了多少程式了呢?Rails提供了一个简单的指令可以知道:

    1. $ bin/rake stats

    就会输出这样的表格:

    1. +----------------------+-------+-------+---------+---------+-----+-------+
    2. | Name | Lines | LOC | Classes | Methods | M/C | LOC/M |
    3. +----------------------+-------+-------+---------+---------+-----+-------+
    4. | Controllers | 86 | 61 | 2 | 7 | 3 | 6 |
    5. | Helpers | 4 | 4 | 0 | 0 | 0 | 0 |
    6. | Models | 2 | 2 | 1 | 0 | 0 | 0 |
    7. | Libraries | 0 | 0 | 0 | 0 | 0 | 0 |
    8. | Integration tests | 0 | 0 | 0 | 0 | 0 | 0 |
    9. | Functional tests | 49 | 39 | 1 | 0 | 0 | 0 |
    10. | Unit tests | 11 | 6 | 2 | 0 | 0 | 0 |
    11. +----------------------+-------+-------+---------+---------+-----+-------+
    12. | Total | 152 | 112 | 6 | 7 | 1 | 14 |
    13. +----------------------+-------+-------+---------+---------+-----+-------+
    14. Code LOC: 67 Test LOC: 45 Code to Test Ratio: 1:0.7

    其中LOC是指不包含空行的行数。

    如何除错?

    如果是Model中的程式,你可以在命令列下输入rails console,然后在Console中呼叫看看Model的方法看看正确与否。而除错ControllerViews一个简单的方法是你可以使用debug这个Helper方法,例如在app/views/events/show.html.erb中插入:

    1. <%= debug(@event) %>

    这样就会输出@event这个值的详细内容。不过,更为常见的是使用Logger来记录资讯到log/development.log里。

    关于Logger

    Rails环境中,你可以直接使用logger或是Rails.logger来拿到这个Logger物件,它有几个方法可以呼叫:

    • logger.debug 除错用的讯息,Production环境会忽略
    • logger.info 值得记录的一般讯息
    • logger.warn 值得记录的警告讯息
    • logger.error 错误讯息,但还不到网站无法执行的地步
    • logger.fatal 严重错误到网站无法执行的讯息

    例如,你想要观察程式中变量@event的值,你可以插入以下程式到要观察的程式段落之中:

    接着开浏览器跑实际跑过这段程式,那么就会在rails server的标准输出中,看到这个除错讯息。或是你也可以另开一个指令视窗执行来观察log档案。

    我们会在测试一章进一步介绍如何撰写测试程式,撰写单元测试可以大大降低除错时间。