美文网首页
Action Dispatch and Action Contr

Action Dispatch and Action Contr

作者: Cloneable | 来源:发表于2021-02-06 15:32 被阅读0次

    Action Pack 处于 Rails 应用的核心地位。它由三个 Ruby 模块组成,分别是 ActionDispatch、ActionController 和 ActionView。Action Dispatch 负责将路由导航至 controller。Action Controller 负责将请求转换为响应。Action View 被 Action Controller 用来格式化响应内容。

    有一个实例,在 Depot 应用中,我们可以将应用的根路径(/)路由至 StoreController 的 index() 方法中。当 action 方法结束后,app/views/store/index.html 模板将被渲染。这些工作都是由 Action Pack 中的组件合作完成。

    结合这三个子组件即可支持输入请求和输出响应的流程。在本章中,我们会学习 Action Dispatch 和 Action Controller。在下一章中再了解 Action View。

    当我们学习过 Action Record 后已经知道它是一个独立的库,所以也可以将它应用于非 web 类型的 Ruby 应用中。但 Action Pack 不同,尽管它可以直接作为框架使用,但通常我们不会这样选择。相反,你更应该选择选择由 Rails 提供的集成使用方式。Action Controller、Action View 和 Action Record 都要处理请求,而 Rails 将它们都组织到一致的环境中(而且易用)。由于这些原因,我们需要在 Rails 环境中学习 Action Controller。首先我们要学习 Rails 应用如何处理请求。我们将深入到路由细节中以及 URL 的处理中。接着了解如何在 controller 中编写代码。最后,对 session、flash 和回调都会进行讲解。

    向 Controller 分发请求

    通常最简单的形式是,web 应用接收来自浏览器的请求并处理它,然后将响应结果返回。

    首先要展开讲解的问题是应用如何知道怎样处理请求?购物车应用知道接收到请求后便展示商品清单,可以将商品添加至购物车,创建订单等等。但它是如何请求导航至相应的代码呢?

    Rails 提供了两种方式定义如何导航请求:其中一种较繁复,只有在需要的时候再使用,另一种较方便,我们通常都使用它。

    较繁复的方式需要我们直接定义 URL 与 action 的匹配,其中要包括匹配模式、描述及条件。较方便的方法只需要定义基于资源的路由即可,就像我们定义的 model 一样。由于较方便的方式是基于繁复的方式构建,所以你可以自由地组合两种方法。

    在所有的例子中,Rails 都会将 URL 请求中的信息编码,然后再调用 Action Dispatch 决定该请求应该做什么。真实的请求处理更加灵活,但在最后 Rails 还是会确定处理该请求的 controller 名字,当然还包括请求参数列表。在处理请求时,请求携带的参数或 HTTP 请求方法都将用来定位目标 controller 中的 action。

    Rails 路由支持 URL 与 action 之间的匹配,而且 action 是基于请求的 URL 和 HTTP 方法。我们要学习基于 URL-by-URL 且使用匿名或命名的路由是如何做到的。Rails 也支持一种更高级别的方式创建一组路由。要理解这些行为的动机,我们需要先了解一些 Representational State Transfer 的相关知识。

    REST:Representational State Transfer(表述性状态传递)

    关于 REST 的设计和想法是在 Roy Fielding 2000 年的博士论文中得到正式讲解。通过 REST 服务端和客户端便可以通过无状态的连接交流。所有关于交互状态的信息都被编码至服务器与客户端之间的请求和响应中。更长周期的状态被作为一组可识别资源保存在服务器端。客户端通过一组定义明确的(有严格限制的)识别信息(也就是 URL)访问这些资源。REST 通过展现的内容区分资源的内容。REST 就是设计来支持高度可伸缩计算,并且约束应用体系进行解耦。

    在上述描述中有许多抽象的概念。在实际生产中 REST 又意味着什么?

    首先,RESTful 的方法行为就要求网络开发人员了解他们在什么时间和地点可以缓存请求的响应。这就允许通过网络推送的加载,增加的性能和降低延迟时的弹性。

    其次,由 REST 产生的约束导致应用更易于编写(及维护)。RESTful 应用不需要考虑访问远程服务的实现,只需要向一组资源提供合规的(也是简洁的)接口即可。应用实现列举、创建、编辑和删除资源的方法,剩下的交由客户端处理。

    让我们讲得更具体一点。在 REST 中,我们通过一组简单的动词表示对一组丰富的名词的操作。如果是使用 HTTP,动词与 HTPP 方法(GET、PUT、PATCH、POST 和 DELETE)对应。名词就是应用中的资源,我们使用 URL 命名资源。

    在 Depot 中,我们创造了一些资源,包括 product,其中隐含了两个资源。第一个是独立的商品,由每一个组成一个资源后就产生了第二个资源,也就是一组商品。

    要获取所有商品的列表一般通过 HTTP GET 请求并使用 /products 地址。要得到单独的资源我们也可以指定它。Rails 的方式是提供一个主键(在这里是一般是 ID)。同样也是使用 GET 请求,不过这次的地址变为 /products/1。

    如果要创建一个新的商品,可以通过 /products 路径直接使用 POST 请求完成,不过需要携带添加的商品信息。不错,你会发现这里与获取商品列表的地址是一样的。如果你使用 GET 请求将得到一组商品,如果使用 POST 请求就新增一个商品。

    更加深入一些,我们已经知道如何检索商品信息了,只需要通过 /products/1 发送 GET 请求即可。如果要更新商品信息,也是使用这个 URL,不过要使用 HTTP PUT 请求。

    按照讲述的知识,或许我们的系统已经可以为用户所用,但是还有一组资源需要处理。REST 也是在告知我们要使用一组动词(GET、POST、PATCH、PUT 和 DELETE)及相匹配的 URL(/users、/users/1 等等)。

    同时我们也了解到一些关于 REST 的约束。我们已经熟悉 Rails 约束的方式,按照这种方式我们便可以将应用按指定的方式结构化。目前,REST 哲学告诉我们,我们还需要将应用的接口结构化。严格按此执行的话我们的应用将会更加简洁。

    Rails 直接支持这种类型的接口,它增加了一种宏路由工具,叫做 resources。我们需要回到 config/routes.rb 文件中学习相关知识。

    Depot::Application.routes.draw do
      resources :products
    end
    

    resources 将向应用添加 7 个新路由。按照这种配置,它会假设应用已经存在 ProductsController,并且包含指定名称的 action。

    你可以看看为我们生成的路由,通过 rake routes 命令可以查看路由列表。

    Prefix Verb URI Pattern
    Controller#Action
    products GET /products(.:format) {:action=>"index", :controller=>"products"}
    POST /products(.:format) {:action=>"create", :controller=>"products"}
    new_product GET /products/new(.:format) {:action=>"new", :controller=>"products"}
    edit_product GET /products/:id/edit(.:format) {:action=>"edit", :controller=>"products"}
    product GET /products/:id(.:format) {:action=>"show", :controller=>"products"}
    PATCH /products/:id(.:format) {:action=>"update", :controller=>"products"}
    DELETE /products/:id(.:format) {:action=>"destroy", :controller=>"products"

    所有的路由定义都按照分栏格式显示。在你的屏幕上将逐行显示,为了适应书籍纸张所以分断为两行显示。每列分别表示路由名称,HTTP 方法,路由地址和路由描述。

    小括号中的字段是地址中的选填部分。分号前面的字段名是用于路径匹配部分放置于其中的变量,以便稍后的 controller 进行处理。

    现在我们要看看 7 个 controller action 关联的路由。尽管我们已经创建了路由用于管理应用,但还是要展开谈谈资源,毕竟这 7 个方法是资源路由都需要的。

    index:返回一组资源

    create: 根据 POST 请求提供的数据创建一个新资源,并将其添加至资源组中

    new: 构建一个新资源并将其传送给客户端。此资源并不需要保存于服务器,new action 相当于创建了一个空表单,由客户端填充其中的数据。

    show: 返回由 params[:id] 指定的资源内容

    update: 使用请求携带的数据更新由 params[:id] 指定的资源内容

    edit: 返回由 params[:id] 指定的资源,以便填充编辑使用的表单。

    destroy: 删除由 params[:id] 指定的资源

    从中你会发现 7 个 action 包含了四个基本的 CRUD 操作(创建、读取、更新和删除)。同时还包含一个用于列举资源的 action 和两个辅助 action 返回新的和已经存在的资源,方便客户端编辑相应的表单。

    如果由于某些原因你不需要所有的 7 个 action,通过 :only:except 限制由 resources 产生的 action。

    resources :comments, except: [:update, :destroy]

    这些路由都是命名路由,你可以通过辅助方法获取它们,比如 products_urledit_product_url(id: 1)

    每个路由都是被一个可选填格式的区分器定义。在 318 页可以了解更多关于格式的细节。

    接下来看看 controller 中的代码:

    class ProductsController < ApplicationController
      before_action :set_product, only: [:show, :edit, :update, :destroy]
      # GET /products
      # GET /products.json
      def index
        @products = Product.all
      end
    
      # GET /products/1
      # GET /products/1.json
      def show
      end
    
      # GET /products/new
      def new
        @product = Product.new
      end
    
      # GET /products/1/edit
      def edit
      end
    
      # POST /products
      # POST /products.json
      def create
        @product = Product.new(product_params)
    
        respond_to do |format|
          if @product.save
            format.html { redirect_to @product,
              notice: 'Product was successfully created.' }
            format.json { render action: 'show', status: :created,
              location: @product }
          else
            format.html { render action: 'new' }
            format.json { render json: @product.errors,
              status: :unprocessable_entity }
          end
        end
      end
    
      # PATCH/PUT /products/1
      # PATCH/PUT /products/1.json
      def update
        respond_to do |format|
          if @product.update(product_params)
            format.html { redirect_to @product,
              notice: 'Product was successfully updated.' }
            format.json { head :no_content }
          else
            format.html { render action: 'edit' }
            format.json { render json: @product.errors,
              status: :unprocessable_entity }
          end
        end
      end
    
      # DELETE /products/1
      # DELETE /products/1.json
      def destroy
        @product.destroy
        respond_to do |format|
          format.html { redirect_to products_url }
          format.json { head :no_content }
        end
      end
    
      private
        # Use callbacks to share common setup or constraints between actions.
        def set_product
          @product = Product.find(params[:id])
        end
    
        # Never trust parameters from the scary internet, only allow the white
        # list through.
        def product_params
          params.require(:product).permit(:title, :description, :image_url, :price)
        end
    end
    

    每个 RESTful 的操作都有对应的 action,action 方法前的注释也展示了调用 URL 的格式。

    其中许多 action 都含有 respond_to() block。就像我们在 135 页看到的一样,Rails 通过它决定传递响应的方式。脚手架生成器自动创建与请求对应的 HTML 或 JSON 信息的响应。我们稍后再探讨相关内容。

    由生成器生成的 view 十分粗陋。需要特别关注的是必须通过正确的 HTTP 方法向服务器发送请求。比如,index action 的 view 如下:

    <h1>Listing products</h1>
    
    
    <% @products.each do |product| %>
    <% end %>
    <table><tbody><tr class="<%= cycle(&#39;list_line_odd&#39;, &#39;list_line_even&#39;) %>">
        <td>
          <%= image_tag(product.image_url, class: 'list_image') %>
        </td>
        <td class="list_description">
          <dl>
            <dt><%= product.title %></dt>
            <dd><%= truncate(strip_tags(product.description), length: 80) %></dd>
          </dl>
        </td>
        <td class="list_actions">
          <%= link_to 'Show', product %><br>
          <%= link_to 'Edit', edit_product_path(product) %><br>
          <%= link_to 'Destroy', product, method: :delete,
                      data: { confirm: 'Are you sure?' } %>
        </td>
      </tr></tbody></table>
    <br>
    <%= link_to 'New product', new_product_path %>
    

    编辑商品信息和添加商品信息关于 action 的链接通常都使用 GET 请求方法,所以 link_to 就刚好适合。但是删除商品信息的请求要使用 HTTP DELETE 请求,所以使用 link_to 时要添加 method: :delete 参数。

    增加其他的 Action

    Rails 资源会提供一组初始的 action,但你肯定不能止步于此。在 172 页,我们添加了一个接口,用于获取购物用户的名单。通过 Rails 完成此功能时就需要调用 resources 的扩展。

    Depot::Application.routes.draw do
      resources :products do
        get :who_bought, on: :member
      end
    end
    

    配置的语法直白易懂,它表示我们想添加一个新的 action 叫做 who_bought,并通过 HTTP GET 请求,它对于商品集合中的每个用户都可用。

    如果不是指定 :member 而是 :collection,路由将应用于整个集合。这种方式通常用于限定范围,比如商品已经清空或者不再继续生产。

    内嵌资源

    通常资源本身已经包含了其他的资源集合。比如,允许人们复查商品时。每次复查都可以作为一个资源,而且复查的资源集合可能与商品资源为关联关系。

    Rails 提供了另一种更加方便和直观的方式声明该情况下的路由类型。

    resources :products do
      resources :reviews
    end
    

    这定义了一组顶级的商品路由,并且可以额外创建一组关于复查的子路由。因为复查资源在商品 block 内部声明,所以复查资源必须由商品资源认证。也就是说复查的访问路径必须以标准的商品路径为前缀。如果要获取 ID 为 99 的商品下的 ID 为 4 的复查信息就需要通过 /products/99/reviews/4 地址。

    /products/:product_id/reviews/:id 的名称方法是 product_review,而不是 review。这种命名方式可以简洁地反应资源的内嵌关系。

    通常情况下使用 rake routes 命令便可以查看配置生成的所有路由。

    路由 concern

    目前为止我们已经了解了一小部分 resources。在一个大型系统中,对于对象类型来说 review 也是合适的,不过使用 who_bought 也可以。相比重复编写每个资源的命令,可以考虑通过 concerns 重构提取通用的行为。

    conern :reviewable do
      resources :reviews
    end
    
    resources :products, concern: :reviewable
    resources :users, concern: :reviewable
    

    此处 products 资源的定义与前一节中的定义等价。

    浅路由嵌套

    无论何时资源嵌套都会产生繁复的 URL,不过浅路由嵌套可以解决此问题。

    resources :products, shallow: true do
      resources :reviews
    end
    

    上述配置将产生下列路由:

    /products/1         => prudct_path(1)
    /products/1/reviews => product_reviews_index_path(1)
    /reviews/2          => reviews_path(2)
    

    试试通过 rake routes 命令查看完整信息。

    选择数据表达方式

    REST 体系的其中一个目的便是将数据与表现方式解耦。如果用户通过 /products 获取商品,但需要使用 HTML 进行更佳的展示。如果另一个应用也访问这个路径,它肯定更加需要对代码友好的格式(YAML,JSON,XML 等)。

    我们已经看到过 Rails 可以在 respond_to block 中处理 HTTP Accept 头。但是也没有想象的那么简单(有时确实比较普通)。为了处理相关设置,Rails 允许你将响应的格式作为 URL 的一部分进行传递。之前你已经看过,Rails 在路由定义中以 :format 字段处理此配置。当你向路由中的 :format 属性设置将要返回的文件的相应 MIME 类型即可实现。

    GET /products(.:format) {:action=>"index", :controller=>"products"}
    

    由于路由定义中的一段是以分隔符判定,所以 :format 会被视为其他字段。而且我们提供的默认值是 nil,所以它也是一个可选填的参数。

    当完成准备工作后,我们便可以在 respond_to() block 中选择与请求格式相应的响应格式。

    def show
      respond_to do |format|
        format.html
        format.xml { render xml: @product.to_xml }
        format.yaml { render text: @product.to_yaml }
      end
    end
    

    根据提供的代码,/store/show/1 或 /store/show/1.html 都将返回 HTML 内容,而 /store/show/1.xml 将返回 XML 格式结果,/store/show/1.yaml 将返回 YAML 类型结果。当然,你也可以将格式作为 HTTP 请求参数传递。

    GET HTTP://pragprog.com/store/show/123?format=xml

    resources 定义的路由默认都开启了此功能。

    尽管在同一个 controller 的 action 中返回不同响应类型的结果十分吸引人,但在真实情况中却非常棘手。特别是要处理错误时将变得困难。虽然当出现错误时重定向至表单是可接受的,也就是向用户展示 flash 信息,不过在处理 XML 格式时就需要调整为另外的策略。所以将所有流程都打包在一个 controller 中处理时要三思。

    当基于 Rails 的资源型路由开发应用会十分简便。许多人坚称这更加简化了应用中的代码。但它也有不适宜的场景,如果你暂时无法找到使它正常运行的方式就不要强行强行使用它。不过你可以考虑将不同的方法混合使用,将一些 controller 基于资源,另一些 controller 基于 action。有一些 controller 甚至可以是基于额外 action 的资源。

    请求过程

    在前一节中,我们了解了 Action Dispatch 如何将请求导航至应用指定的代码中。现在让我们看看代码内部发生了什么?

    Action 方法

    当一个 controller 对象处理请求时,它将查找与请求的 action 相同的公共实例方法。如果它找到一个相应的方法,该方法将被调用。如果没有找到相应方法,controller 实现的 method_missing() 方法将被调用,并将 action 名字作为第一个参数,空的参数列表作为第二个参数。如果没有方法可以被调用,controller 会查找当前 controller 和 action 结束时的模板。如果找到模板,它将被直接渲染。如果所有的东西都没有找到,AbstractController::ActionNotFound 错误将被报告。

    Controller 环境

    controller 是为 action 准备的环境(其实展开来讲也是为调用的 view 准备的环境)。许多提供给方法直接访问的信息都包含在 URL 请求中。

    action_name: 当前处理的 action 名称

    cookie: cookie 是与请求相关联。当响应被返回时会将数值设置到浏览器中存储 cookie 的对象中。Rails 需要基于 cookie 才能支持 session。我们会在 331 页讨论 session。

    headers: 在响应中使用的 HTTP 头哈希数据。默认情况下,Cache-Control 值为 no-cache。也许对于某些特殊的应用你会希望将 Content-Type 设置为其他值。要注意的是,不要直接在头中设置 cookie 值,而要用 cookie API 处理。

    params: 包含请求参数的类哈希对象(也会有在路由过程中产生的伪参数)。由于它是类哈希对象,所以你可以通过标识或字符串定位它,比如 params[:id]parmas['id'] 将返回相同的结果。Rails 应用习惯使用标识方式。

    request: 输入的请求对象。它包含下列属性:

    • request_method 返回请求方法,结果为 :delete、:get、:head、:post 或 :put 的其中一种。

    • method 返回与 request_method 结果相同的值,除了 :head 之外,作为替换它将返回 :get,因为从应用的 view 角度出发这两种表述是一致的。

    • delete?,get?,head?,post? 和 put? 将基于请求方法返回 truefalse

    • xml_http_request? 和 xhr? 在请求是由 Ajax 辅助方法发起时为 true。注意此参数与 method 参数是独立的。

    • url(),将返回请求使用的 URL。

    • protocol(),port(),path() 和 query_string(),将返回请求 URL 的各个部分,一般基于 protocol://host:port/path?query_string 这种模式。

    • domain(),将返回请求域名的最后两个部分。

    • host_with_port(),将返回请求的 host:port 字符串。

    • post_string(),如果端口不是默认端口(HTTP 就是 80,HTTPS 就是 443)将返回 :port 字符串。

    • ssl?(),如果是 SSL 请求将返回 true,换言之请求是遵照 HTTPS 协议。

    • remote_ip(),将返回远程 IP 地址字符串。如果客户端使用了代理将返回不止一个地址。

    • env(),表示请求的环境。你可以通过这个方法获得浏览器的配置数据,比如:request.env['HTTP_ACCEPT_LANGUAGE']

    • accepts(),将返回一个 Mime::Type 对象数组,它表示 Accept 头的 MIME 类型。

    • format(),将返回基于 Accept 头数值计算的结果,Mime::HTML 是它的备选值。

    • content_type(),将返回请求的 MIME 类型。对于 putpost 请求比较有用。

    • headers(),将返回完整的一组 HTTP 头。

    • body(),将请求体作为 I/O 流返回。

    • content_length(),返回请求体中的字节数。

    Rails 通过 Rakc gem 提供这些功能。更多细节可以查看 Rack::Request 的文档。

    response: response 对象,在处理请求时填充。通常此对象都是由 Rails 管理。当我们在 337 页学习回调时将会看到,有时我们需要在特别的流程中处理其内容数据。

    session: 一个表示当前 session 数据的类哈希对象。我们会在 331 页讲解。

    除此之外,logger 也可以在 Action Pack 中使用。

    向用户发送响应

    controller 工作内容的一部分便是向用户发送响应。关于响应有四种常用方法:

    • 最常用的方法是渲染模板。在 MVC 的范例中模板就是 view,它将接收来自 controller 的信息,并向浏览器生成一个响应。

    • controller 也可以不调用 view 直接向浏览器返回字符串。虽然比较生硬,但是可以用来向浏览器发送错误提示。

    • controller 可以不向浏览器返回任何东西。有时需要通过这种方式向 Ajax 请求返回响应。但是在所有的示例中,controller 将返回一组 HTTP 头,因为一些种类的响应需要这些内容。

    • controller 也会返回其他类型的数据给客户端(是除了 HTML 之外的类型)。典型例子就是下载文件(可能是 PDF 文件也可能是其他文件)。

    controller 处理的每个请求都会向用户进行明确的响应。也就是说,在每个请求处理中只调用一次 render()redirect_to()send_xxx() 方法。(如果进行多次渲染将会抛出 DoubleRenderError)。

    因为 controller 必须进行一次明确的响应,所有在完成对请求的处理前它将检查响应是否已经生成。如果没有生成,controller 将在 controller 和 action 之后查找相应名字的模板并自动渲染此模板。这是产生渲染的最常用方式。你可能已经注意到在购物车的多数 action 中都没有明确需要渲染任何东西。取而代之的是 action 方法会设置 view 的上下文并返回。当 controller 无渲染情况出现时需要注意,它将会调用合适的模板进行渲染。

    你可以拥有多个同名的模板,但它们的扩展名必须区别开(比如 .html.erb.xml.builder.js.coffee 扩展名)。如果在渲染请求中你没有指定扩展名,Rails 默认指定为 html.erb

    渲染模板

    模板就是定义了应用响应内容的文件。Rails 支持三种模板类型,分别是 erb 用于嵌套 Ruby 代码(通常与 HTML 结合使用),builder 使用更加程序化的方式构建 XML 内容,还有 RJS 用于生成 JavaScript。我们会在 341 页开始讨论这些文件的内容。

    方便起见,controller 中 action 的模板都归置在 app/views/controller/action.type.xxx ( type 表示文件类型,通常是 html,atom 或 js,xxx 是 erb,builder,coffee 或 scss 中的一种)路径中,路径中的 controller 和 action 与处理请求的 controller action 对应。路径中的 app/views 部分是默认的。可以通过配置重写此部分的路径:

    ActionController.prepend_view_path dir_path

    render() 方法是 Rails 渲染的核心步骤。它接收哈希参数,这些参数会告知渲染什么以及如何渲染。

    如果要在 controller 中编写代码就如下所示:

    # DO NOT DO THIS
    def update
      @user = User.find(params[:id])
      if @user.update(user_params)
        render action: show
      end
      render template: "fix_user_errors"
    end
    

    代码中非常自然地调用了 render(也可以 是 redirect_to)然后结束了 action 的流程。这不是正常的例子,因为上面的代码在 update 执行成功后将会报告错误(因为 render 被调用了两次)。

    接着看看在 controller 中用到的渲染参数(关于 view 中渲染的知识会另外在 363 页学习):

    render()

    对于未填写参数的情况,render() 方法将渲染当前 controller 当前 action 的默认模板。下面的代码将渲染模板 app/views/blog/index.html.erb:

    class BlogController < ApplicationController
      def index
        render
      end
    end
    

    下面的代码也是一样的结果(如果 action 没有调用渲染时 controller 将调用默认行为):

    class BlogController < ApplicationController
      def index
      end
    end
    

    还有下面这种情况(如果 action 方法未定义 controller 将直接调用模板):

    class BlogController < ApplicationController
    end
    

    render(text: string)

    表示向客户端传递指定的字符串。不会有模板被解析或额外的 HTML 被执行。

    class HappyController < ApplicationController
      def index
        render(text: "Hello there!")
      end
    end
    

    render(inline: string, [ type: "erb"|"builder"|"coffee"|"scss" ], [ locals: hash ])

    按指定类型方式解析传递的字符串,并将渲染结果返回客户端。你可以使用 :locals 哈希数据设置模板中的局部变量。

    如果应用在开发模式中运行下面的代码会向 controller 添加 method_missing() 方法。如果 controller 中的无效 action 被调用,将通过内联模板的方式展示 action 名字以及格式化请求参数版本。

    class SomeController < ApplicationController
      if RAILS_ENV == "development"
        def method_missing(name, *args)
          render(inline: %{
            <h2>Unknown action: #{name}</h2>
            Here are the request parameters:<br/>
            <%= debug(params) %> })
        end
      end
    end
    

    render(action: action_name)

    渲染 controller 中指定 action 的模板。有时人们在需要重定向时会在 render() 中使用 :action 格式。我们会在 327 页讨论为什么这样是个坏主意。

    def display_cart
      if @cart.empty?
        render(action: :index)
      else
        # ...
      end
    end
    

    注意调用 render(:action) 时并没有调用 action 方法:它将直接展示模板。如果模板中需要实例参数,这些实例参数由调用 render() 的方法提供。

    让我们再回顾一下这个知识点,因为初学者非常容易在此处犯错,render(:action...) 并不是调用 action 方法,它只是直接渲染了指定 action 的默认模板。

    render(template: name, [locals: hash])

    渲染模板并且将结果传递回客户端。:template 必须包含 controller 和 action 部分,并用斜杠隔开。下面的代码将渲染 app/views/blog/short_list 模板:

    class BlogController < ApplicationController
      def index
        render(template: "blog/short_list")
      end
    end
    

    render(file: path)

    渲染应用外部的 view(可能是一个与其他 Rails 应用共享的模板)。默认情况下,被渲染的文件不使用当前的布局。不过可以通过 layout: true 的设置重置。

    render(partial: name, ...)

    渲染 partial 模板。我们会在 363 页深入探讨 partial 模板。

    render(nothing: true)

    什么都不返回,只向浏览器返回一个空的响应体。

    render(xml: stuff)

    将 stuff 作为文本呈现,强制将文本类型设置为 application/xml

    render(json: stuff, [callback: hash])

    将 stuff 作为 JSON 呈现,并使文本类型为 application/json。结果将由指定的 :callback 回调函数包装。

    render(:update) do |page| ... end

    将 block 作为 RJS 模板渲染,将传递至页面对象。

    render(:update) do |page|
      page[:cart].replace_html partial: 'cart', object: @cart
      page[:cart].visual_effect :blind_down if @cart.total_items = 1
    end
    

    所有形式的 render() 方法都接受 :status:layout:content_type 参数。:status 参数提供用于响应 HTTP 头的状态值。默认情况下是 [200 OK]。不使用 render() 而使用重定向时是 3xx 的状态值,重定向时需使用 redirect()

    :layout 参数决定渲染结果是否使用布局。(我们已经在 96 页了解过布局,在 358 页将更深入地学习模板)。如果参数是 false,将不会应用布局。如果设置数值为 niltrue,则只有与当前 action 关联的布局才会被应用。如果 :layout 参数值是字符串,它将作为用于渲染的布局名字。当 :nothing 参数被设置时布局将不会被应用。

    :content_type 设置的值将作为 HTTP 头的 Content-Type 的数值传递给浏览器。

    有时获取传递给浏览器的字符串是有用的。render_to_string() 方法可接受 render() 类似的参数,但返回的渲染结果是字符串,渲染结果并不是存储于响应对象,如果你没有采取其他步骤将不会发送给用户。

    调用 render_to_string 并不会被作为一个真正的渲染器承认。你可以稍后调用 render 方法而不用担心发生 DoubleRender 错误。

    传递文件及其他数据

    我们已经学习到了渲染模板并在 controller 中传递字符串。响应的第三种类型就是向客户端发送数据(典型例子就是文件内容)。

    发送数据

    向客户端发送携带二进制数据的字符串。

    send_data(data, options...)

    向客户端传递数据流。通常浏览器会使用内容类型与展示的结合,相关内容都会作为 options 参数进行设置,目的是为了决定利用数据做什么。

    def sales_graph
      png_data = Sales.plot_for(Date.today.month)
      send_data(png_data, type: "image/png", disposition: "inline")
    end
    

    Options:

    参数 类型 描述
    :disposition string 建议浏览器将文件按内联方式展示(参数值为 inline)或者进行下载保存(参数值为 attachment,此参数值也是默认值)
    :filename string 当浏览器保存数据时使用此参数值作为默认文件名
    :status string HTTP 状态值(默认为 [200 OK])
    :type string 数据内容类型,默认是 application/octet-stream
    :url_based_filename boolean 如果此参数值不为 ture,并且 :filename 未设置,此设置将阻止 Rails 在响应头的 Content-Disposition 中提供返回路径中的文件名。一些浏览器处理文件名的国际化问题时必须指定返回路径中的文件名

    发送文件

    向客户端发送文件内容。

    send_file(path, options...)

    向客户端发送指定的文件。此方法可以设置请求头中 Content-Length、Content-Type、Content-Disposition 和 Content-Transfer-Encoding 的值。

    Options:

    参数 类型 描述
    :buffer_size number 在流允许的情况下确定每次向浏览器发送的内容数量(需要 :streamtrue
    :disposition string 建议浏览器使用内联方式展示(参数值为 inline 时)或者下载保存(参数值为 attachment,此参数值为默认值)
    :filename string 当浏览器保存文件时默认使用的文件名。如果没有设置,默认将路径的一部分作为文件名
    :status string 网络状态码(默认值为 [200 OK]
    :stream true of false 如果参数值为 false,整个文件将被读取至服务器内存中并发送给客户端。否则,文件将按 :buffer_size 的设置值分块读取并传送给客户端
    :type string 文件内容类型,默认值为 application/octet-stream

    通过在 controller 中使用 headers 属性可以为 send_ 方法设置其他的头数据。

    def send_secret_file
      send_file("/files/secret_list")
      headers["Content-Description"] = "Top secret"
    end
    

    在 348 页我们再学习如何提交文件。

    重定向

    HTTP 重定向是根据请求从服务器向客户端发送响应。实际上它就表示,我在此次请求时进行处理,但你需要到下一个地方查看结果。包含客户端跳转 URL 的重定向响应会携带状态信息,无论此次重定向是永久的(状态码 301)或临时重定向(状态码 307)。重定向有时用于重新组织 web 页面,客户端通过旧地址访问网页时将跳转至新地址。更常用的场景是 Rails 应用通过重定向跳过请求操作直达其他的 action。

    重定向会由浏览器的后台处理。一般情况下,你如果想感知是重定向只能通过轻微的延迟现象和你发起请求的地址发生变化。最后也是最重要的一点,也是浏览器关注的,来自服务器的重定向与终端用户手工输入新 URL 地址是等价的。

    当你打算编写良好的 web 应用时重定向就显示出它的重要性。让我们参考一下一个可以提交评论的博客应用。当用户提交评论后,应用应该重新显示文章,并将新评论排列在底部。

    上述评论的逻辑的代码如下:

    class BlogController
      def display
        @article = Article.find(params[:id])
      end
    
      def add_comment
        @article = Article.find(params[:id])
        comment = Comment.new(params[:comment])
        @article.comments << comment
        if @article.save
          flash[:note] = "Tank you for your valuable comment"
        else
          flash[:note] = "We threw your worthless comment away"
        end
    
        render(action: 'display')
      end
    end
    

    此处的逻辑目标是在评论提交后显示文章。为了实现此功能,开发者在 add_comment() 方法末尾调用了 render(action:'display')。它将渲染 display view,并向终端用户显示更新后的文章。但是从浏览器 view 的角度思考,浏览器发送 URL blog/add_comment 接着返回了文章。而浏览器关心的事情是,当前 URL 依然是 blog/add_comment。如果用户此时点击刷新或重新加载(有可能想看看是否有了新评论),add_comment URL 将再次发送至服务器。用户真正的目的是想重新显示文章和评论,但应用接收到的请求却是添加新评论。在博客应用中,无意识下多次进入会带来不便,在线上商城中,这种现象需要付出高昂的代价。

    在这些情况下,正确的做法应该是新增评论并重定向至 display action。要达到此目的需要使用 Rails 的 redirect_to() 方法。如果用户随后点击刷新,也只是再次调用 display action,而不会添加评论。

    def add_comment
      @article = Article.find(params[:id])
      comment = Comment.new(params[:comment])
      @article.comments << comment
      if @article.save
        flash[:note] = "Tank you for your valuable comment"
      else
        flash[:note] = "We threw your worthless comment away"
      end
      redirect_to(action: 'display')
    end
    

    Rails 提供了一个功能强大但操作简便的重定向体系。它可以在指定 controller 中重定向至 action(需要传递参数),或重定向至 URL(可以是服务器内的 URL,也可以是服务器外的),或者重定向至访问历史的前一个页面。下面让我们看看这三种形式。

    redirect_to(action: ..., options...)

    基于 options 的哈希数值向浏览器发送临时重定向。目标 URL 通过 url_for() 生成,所以这种形式的重定向方法聚集了所有 Rails 路由代码的智慧。

    redirect_to(path)

    重定向至提供的路径。如果路径未以协议开头(比如 http://),当前请求的协议和端口需要预定义。此方法并不会在 URL 上执行任何重写操作,所以它不能用来创建链接至应用中 action 的路径(除非你使用 url_for 生成路径,或者使用命名的路由 URL 生成器)。

    def save
      order = Order.new(params[:order])
      if order.save
        redirect_to action: "display"
      else
        session[:error_count] ||= 0
        session[:error_count] += 1
        if session[:error_count] < 4
          self.notice = "Please try again"
        else
          # Give up -- user is clearly struggling
          redirect_to("/help/order_entry.html")
        end
      end
    end
    

    redicrect_to(:back)

    重定向当前请求头的 HTTP_REFERER 提供的 URL。

    def save_details
      unless params[:are_you_sure] == 'Y'
        redirect_to(:back)
       else
          ...
      end
    end
    

    默认情况下所有重定向都是临时的(这表示重定向只影响当前请求)。也许某些情况下你希望重定向至 URL 的重定向是永久的。在下面示例中,展示了通过设置响应头中的数值达到目的。

    headers["Status"] = "301 Moved Permanently"
    reidrect_to("http://my.new.home")
    

    由于重定向方法向浏览器发送响应,所以渲染方法也适用此规则,你可以为每个请求都如此设置。

    直到现在我们都是学习单独的请求和响应。不过 Rails 也提供了一些跨请求的方法。

    跨请求的对象及操作

    当一堆跨请求的状态归属于数据库并通过 Active Record 访问时,还有一些状态拥有不同的生命周期并且需要不同的管理方式。在 Depot 中,购物车数据是被存储于数据库,但定位当前购物车的信息却由 session 管理。flash 中的提示信息用于在重定向后与下一请求间交流,比如 [不能删除最后一个用户] 这样的信息。而回调是用于从 URL 中提取本地化数据。

    在本节中,我们将逐个学习。

    Rails Sessions

    Rails 的 session 是一个类哈希的结构,在跨请求情况下它依然存在。与原生 cookie 不同,session 可以处理任意对象(甚至这些对象可以被编组),session 主要用于处理需要保存的 web 应用状态信息。比如,在商场应用中,我们可以利用 session 处理多个请求间的购物车对象。Cart 对象便可以如同其他对象一样在应用中使用。但 Rails 在每个请求结束时才将购物车存储,更重要的是,输入请求的正确购物车是在 Rails 开始处理请求时重新存储的。所以需要使用 session,这样便可以在多个请求间保持应用状态。

    同时这也会导致一个有趣的问题,如何确认多请求间的数据存储在哪里?一种选择是服务器将数据传递给客户端的 cookie。这种是 Rails 的默认方式。不过它限制了容量同时增加了带宽,但也意味着服务器只用少量地管理和清理数据。要注意 cookie 中的数据是加密的(默认情况下都是加密),所以用户无法对 cookie 内容作手脚。

    其他部分的数据将存储在服务器。它与 cookie 中的数据共同组成了这种方式。首先,Rails 必须能够定位 session。它通过创建一个 (默认情况下)32 位的字符作为密钥(这表示它有 1632 种可能性)。密钥也叫做 session ID,并且它是随机生成的。并且 Rails 将 session ID 作为 cookie(通过 _session_id 键获取)存储在客户浏览器中。因为随后的请求由浏览器向服务器发起请求,所以 Rails 可以从中还原 session ID。

    接着,Rails 会将 session 数据持久化在服务器端,并通过 session ID 获取。当接收到请求时,Rails 通过 session ID 查找存储的数据。获取的将是一个序列化 Ruby 对象。需要将其反序列化,并将结果存放于 controller 的 session 属性中,此时其中的数据便可在应用代码中操作。应用可以添加和修改数据的核心内容。当完成每个请求的流程后,Rails 会将 session 数据写回数据存储中。这种情况将持续至下次请求到来。

    应该向 session 中存储什么数据?你可以存储任意你需要的数据,不过有些限制和警告。

    • 关于向 session 中存储哪种数据其实有些限制。相关细节依赖于你使用的存储体系(稍后我们将进行讲解)。在通常的示例中,存储于 session 中的对象必须是可序列化的(要使用 Ruby 的 Marshal 函数)。举例来说,这意味着你不能在 session 中存储 I/O 对象。

    David 提问:有关基于 cookie 的 session 的疑惑

    当你初次听闻 Rails 的 session 存储方案时肯定十分惊愕。实际上你是打算将数据存储在客户端吗?但按此方法将核发射代码存储在 session 中时客户端也会知晓数据。

    是的,默认存储方法并不适合放置于客户端的机密数据。不过实际上它是一个有用的约束,它将引导你避免在 session 中存储复杂且易过期的对象数据的危险。毕竟核弹发射代码并不是十分现实的情况,我们还是得联系实际。

    同时 cookie 还存在容易限制。cookie 只能够存储 4KB 的数据,所以你无法随心所欲地存储数据。最佳实践是存储数据的指引信息,就像存储 cart_id 而是整个购物车数据。

    当然还有你所关心的安全问题,比如客户端是否能够修改 session。你希望能够保证存储数据的完整性。如果客户端能够将 cart_id 从 5 修改为 8 并获取其他购物车就太糟了。所幸,Rails 能够保证你重复多次轻松地获取 session,而且如果无法匹配 session 将报告相关错误。

    这种方式还将带来其他好处,它不用在每次请求时通过数据库加载和存储数据,而且还不用负责清理它们。如果你将 session 存储在文件中或数据库中,就需要自己清理废旧的 session,这真的很让人烦恼。并没有人愿意负责清理废弃的数据。基于 cookie 的 session 知道如何在不被需要时进行清理。如此说来还有什么理由不爱它呢?

    • 如果你将 Rails 的 model 对象储存于 session 中,你必须为其添加 model 声明。添加声明后从 session 取出数据反序列化时 Rails 会将其预加载并使其定义可用。如果 session 的使用被限制在一个 controller 中,需要在 controller 顶部声明。
    class BlogController < ApplicationController
      model :user_preferences
      # ...
    

    不过,如果 session 可能被其他 controller 读取时(任何多 controller 的应用都存在此可能性),你可能需要在 app/controllers/application_controller.rb 中添加声明。

    • 也许你并不想在 session 中存储大块的对象数据,可以考虑将它们存储于数据库中,而 session 中只存储索引数据。这对于基于 cookie 的 session 方式格外适合,特别是容量被限制为 4KB 时。

    • 你可能不希望将不稳定的对象存储于 session 数据中。例如,你可能想将博客网站的文章数存储在 session 中,当然可能还考虑性能的原因。但如果这样处理,当其他用户添加文章时统计数据可能不会更新。

      它会引起存储在 session 的数据只是代表当前登录的用户。如果你的应用需要验证用户这样做就不太明智。即使数据库中并不存在此用户,它们的 session 数据也要反应有效的状态。

      将不稳定的数据存储于数据库中,然后将其索引数据存储在 session 中。

    • 也许你并不想在 session 中保存临界信息。比如,如果你的应用通过一个请求生成了订单验证码,为了下次请求时将它存储至数据库所以暂时先将它保存在 session 中,如果用户从浏览器删除了 cookie 将会导致风险提升。

    除此之外还有一个需要小心的地方,而且这个问题十分重要。如果将对象存储于 session 中,当你下次回到浏览器时应用检索对象。但如果你同时更新了应用,session 中的对象数据与应用中对象的类定义可能不再一致,所以此次请求将失败。此处有三种选择。一种是通过便携的 model 将对象存储于数据库中然后将 ID 值存储于 session 中。 相比 Ruby 的序列化 model 对象对 schema 的变化容错更高。第二种方案是当修改了存储数据的类时就将所有 session 删除。

    第三个方案稍微有些复杂。如果你向 session 键添加一个版本号,并在更新存储的数据时修改版本号,你将只能获取与应用版本号匹配的 session 数据。最后一种方案需要做许多工作,所以你要决定自己是否能够接收相应的代价。

    因为 session 是类哈希的结构,所以可以将多个对象存储其中,每个对象都有自己的键。对于特定的 action 也不需要使 session 失效。因为 session 是懒加载的方式获取,在不需要 session 的 action 中不引用即可。

    Session Storage

    当 session 存储数据时可以填写一些参数。每个参数都有自己的优点和缺点。我们将逐一列举这些参数并在最后对它们进行比较。

    ActionController::Basesession_store 属性是处理 session 存储的方法,它将属性设置给实现了存储策略的类。相应的类必须定义在 ActiveSupport::Cache::Store 模块中。你可以使用标识命名 session 的存储策略,这些标识将被转化为类的驼峰名称。

    session_store = :cookie_store

    这是 Rails 默认使用的 session 存储方式,一直从 2.0 版本开始使用。这种类型代表存储对象的序列化格式,它只允许存储总共 4KB 的序列化数据。我们在 Depot 应用中使用的就是此方案。

    session_store = :active_record_store

    使用这种方案时需要 activerecord-session_store gem,然后会通过 ActiveRecordStore 存储 session 数据至数据库中。

    session_store = :drb_store

    DRb 是一种允许 Ruby 进程在网络间分享对象的进程。使用 DRbStore 数据库管理器时,Rails 会将 session 数据存储于 DRb 服务器(在 web 应用外也可以进行管理)。多实例应用,甚至是运行在分布式服务器上时,它们可以访问同一个 DRb 存储。DRb 也是通过 Marshal 序列化对象。

    session_store = :mem_cache_store

    memcached 是一种由 Dormando 支持的免费使用的分布式缓存系统。memcached 比其他方案更加复杂,只有你已经在其他网站中使用过它后才会感兴趣。

    session_store = :memory_store

    这种方案在应用的本地内存中存储数据。因为不需要序列化,所以任何对象都可以保存在内存 session 中。但稍后我们会了解到,在 Rails 应用中这并不是一个好主意。

    session_store = :file_store

    这种方案中 session 数据被保存在平面文件中。对于 Rails 应用作用不大,因为存储内容必须为字符串。这种方案还支持其他配置参数,比如 :prefix:suffix:tmpdir 等。

    对比 Session Storage 选项

    根据列举的 session 方案我们应该选择哪一种呢?答案是 [根据实际情况]。

    当涉及性能时就没有绝对情况,每个人处理的环境和场景都不尽相同。你的硬件、网络、数据库甚至是天气都可能影响 session 存储组件间的交互。我们的最佳实践是按最简单的可工作方案开始,然后再逐渐改善。如果性能下降,不要着急,先查明其中的原因。

    如果你的网站用户量较高,尽量存储小容量的 session 数据,并且使用 cookie_store 方案。

    如果我们因为过于简化所以排除内存存储方案,因为过多限制所以排除文件存储方案,而 memcahed 过犹不及,服务器端还是在 CookieStore、Active Record 存储 和 DRb 存储间作抉择。如果你需要在 session 中存储的数据比在 cookie 中存储的多,我们推荐使用 Active Record 存储方案。如果应用逐步扩张,性能将出现瓶颈,此时应该迁移到 DRb 方案。

    Session 到期及清除

    所有服务端的 session 存储都有一个问题,在运行中总有新的 session 被添加至存储中。所以你需要进行清理或者说释放服务器资源。

    当然还有其他原因导致我们要压缩 session。许多应用都不需要永久的 session。当用户从浏览器登录时,应该需要实施一些只有用户登录后才采用的规则,当他们登出或者长时间没有使用应用时,这些 session 需要被终结。

    cookie 保存的 session ID 失效时也会造成影响。不过这个开放的功能会被终端用户滥用。这种糟糕情况会导致对应的失效 cookie 的 session 无法被序列化。

    因此我们建议你通过简单移除服务端的 session 数据使 session 失效。浏览器随后的请求将包含已经被删除的 session ID,应用也就无法获取 session 数据,于是 session 也就清除了。

    要实现这种失效方案需要依赖正在使用的存储体系。

    对于 Active Record 的 session 存储需要在 sessions 表中使用 updated_at 列。最后一小时没有发生变化的 session 也需要删除(要忽略白天存储时间的变化),这种方案可以通过 SQL 实现清除任务,如下:

    delete from sessions where now() - updated_at > 3600;
    

    对于 DRb 存储方案,可以通过 DRb 服务进程完成失效处理。你可以在 session 数据中记录生成时的时间截。然后运行另一个线程(也可以是额外的进程)定期删除数据。

    在所有的示例中,当 session 不再被需要时通过调用 reset_session() 应用将帮助你删除它们(例如,当用户登出时)。

    Flash:Action 间的交流方式

    当我们通过 redirect_to() 将控制权交由其他 action 时,浏览器会生成另一个请求调用 action。新请求会在新生成的 controller 对象中被处理,原先 action 中赋值的实例变量对新 action 不再产生作用。但有时我们需要在两个实例间进行交互,通过 flash 这个工具便可以实现。

    flash 是数据的临时便笺。它可以像哈希和 session 数据一样操作,因此通过键存储数据稍后恢复它。不过它有一个专有属性。默认情况下在一个请求的进程中存储的数据也可以在下一个请求的进程中使用。一旦第二个请求进程结束,flash 中的数据将被移除。

    flash 最常用的场景就是从一个 action 向下一个 action 传递错误和信息。现在的目标是在第一个 action 中处理一些条件,并创建一条信息描述这些条件,然后重定向至另一个 action 中。通过在 flash 中存储信息,第二个 action 便可以获取信息,同时也可以在 view 中使用这些信息。在 125 页就有一个实际用例。

    有时在向当前 action 的模板传递信息时 flash 会带来便利。比如,display() 方法想在正常时显示一个樱桃色的横幅,然后打印提示信息。这种情况并不需要将信息传递给下一个 action,它只在当前请求中使用。要实现这个功能需要使用 flash.now,只需要更新 flash 而不用增加 session 数据。

    flash.now 会创建一个临时数据,而 flash.keep 的功能相反,它可以使当前 flash 中的信息在其他请求的生命周期中有效。如果你没有向 flash.keep 提供参数,所有的 flash 数据都将被保留。

    flash 不止能够存储字符信息,你也可以通过它在 action 之间传递所有种类的信息。很明显,对于需要更长存储周期的信息你可以使用 session(也可以结合数据库)存储,但如果你想从一个请求向下一个请求传递参数时 flash 是更佳选择。

    因为 flash 数据是存储于 session 中,所以常见规则都有效。需要特别声明的情况是每个对象都需要序列化。我们强烈建议只通过 flash 传递简单对象。

    回调

    回调允许你在 controller 中写代码,并包装 action 的执行流程,你可以编写一堆代码然后在一些 action 前后调用(或者 controller 子类的 action 前后)。它确实是一种实用的工具。通过回调,我们可以实现授权 schema,记录日志,压缩响应,甚至自定义响应。

    Rails 支持三种类型的回调:前置、后置和环绕。回调的调用优先级仅次于 action。回调的调用依赖于你如何定义它们,甚至它们还可以执行 controller 中的方法,或者在它们运行时传递 controller 对象。不论如何,回调都可以获取请求和响应的详情,以及其他的 controller 属性。

    前置和后置回调

    正如它们名字所表达的一样,前置和后置回调分别会在 action 之前和之后调用。Rails 为每个 controller 维护了两个回调链。当 controller 的 action 正在执行时,它将先执行此调用链前的所有回调。后置回调将在 action 执行完后执行。

    回调是被动的,它由执行的 controller 管理行为。不过回调也可以在请求的处理过程中进行更多活动。如果前置回调返回 false,回调链的流程将结束,并且 action 不再运行。回调也可以渲染输出内容或者重定向请求,不过这将导致原本被调用的 action 被跳过。

    在 202 页的例子中我们使用回调对权限进行了认证。如果当前的 session 没有登录用户信息时认证方法将重定向至登录界面。接着我们将 administration contoller 中的认证方法作为所有 action 的前置回调。

    声明回调时也可以使用 block 和类名。如果指定使用 block,它将在当前 controller 中作为参数被调用。如果使用的是类,它的 filter() 类方法将被 controller 作为参数调用。

    默认情况下,回调可以应用于所有 controller 的 action(以及任意 controller 子类)。你也可以通过 :only 配置限制回调可以应用的 action,还有 :except 选项可以排除不需要应用回调的 action。

    before_actionafter_action 声明都处于 controller 的回调链。使用变体方法 prepend_before_action()prepend_after_action() 可以将回调放置于调用链之前。

    后置回调可以用于修改已经被 action 处理完成的响应结果,必要情况下可以修改响应头和响应体内容。有些应用通过这种技术对 controller 模板生成的内容进行全局替换(比如,可以在响应体中将 <customer/> 替换为用户名)。还有其他用途,如果浏览器支持的话甚至可以用来压缩响应。

    环绕回调会包围被执行的 action。你可以使用两种方式编写环绕回调。第一种,回调是单独的一块代码,在 action 执行前被调用。如果回调代码是由 yield 执行,就执行 action。当 action 执行结束回调代码将继续执行。

    因此,在 yield 之前的代码就如同前置回调,yield 之后的代码就如同后置回调。如果回调代码从没有调用 yield,action 将不会运行,如果在 action 被执行前返回 false 效果与前置回调的一样。

    环绕回调的好处是可以保持调用 action 的上下文。

    要通过 around_action 声明进行环绕回调的方法,你可以向它传递 block 或 filter 类。

    如果你使用 block 作为回调,它需要传递两个参数,分别是 controller 对象和 action 代理。通过第二个参数调用 call() 可以操作原来的 action。

    第二种形式允许你传递一个对象作为回调。这个对象必须实现 filter() 方法。此方法可以接收 controller 对象,通过 controller 对象可以调用 action。

    如同前置和后置回调,环绕回调也可以使用 :only:except 参数。

    环绕回调默认情况下以不同的方式加入回调链,第一个添加的环绕回调方式将被优先执行。随后添加的环绕回调将内嵌于已经存在的环绕回调中。

    回调的继承

    如果你继承了一个包含回调的 controller,此回调会以同样的效果在子类中运转。但是定义在子类中的回调便不能在父类中运行。

    如果你不需要在子类中运行某个回调,可以通过 skip_before_actionskip_after_action 声明重写默认调用流程。这两个声明同样接收 :only:except 参数。

    通过 skip_action 可以跳过所有 action 回调(无论是前置、后置还是环绕)。不过,如果你指定方法名(使用标识)它将为此方法运行回调。

    我们在 202 页使用了 skip_before_action

    总结

    我们学习了 Action Dispatch 和 Action Controller 如何合作,并赋予了我们的服务处理请求和响应的能力。关于它们的重要性不言而喻。在最近接触的应用中,它们在应用最基本的地方赋予其创造力的表达。尽管 Active Record 和 Action View 不是被动操作,但仍然需要路由和 controller 中的 action 为其搭建桥梁。

    我们以 REST 开启本章,它是用于 Rails 路由请求的思想源头。然后了解了如何提供七个基本的 action 以及添加更多的 action。接着学习了如何挑选数据表达方式(例如 JSON 或 XML),以及如何测试路由。

    后面我们了解了 action 的环境也就是 Action Controller,它提供了渲染和重定向方法。最后,还学习了 session、flash 和回调,这些技术都可以在 controller 中使用。

    在介绍知识点的同时我们还穿插了 Depot 中相应的技术应用。现在,相关技术的使用已经展现眼前,同时背后的理论你也了若指掌,如今能够限制你应用理论的只有你的创造力。

    在下一章中,我们将继续学习 Action Pack 中的组件,也就是 Action View,它主要用于处理结果渲染。


    本文翻译自《Agile Web Development with Rails 4》,目的为学习所用,如有转载请注明出处。

    相关文章

      网友评论

          本文标题:Action Dispatch and Action Contr

          本文链接:https://www.haomeiwen.com/subject/aevmtltx.html