跳到內容 跳到搜尋

Action Controller Live

把這個模組混入你的控制器,該控制器中的所有動作都可以在撰寫時將資料串流到客戶端。

class MyController < ActionController::Base
  include ActionController::Live

  def stream
    response.headers['Content-Type'] = 'text/event-stream'
    100.times {
      response.stream.write "hello world\n"
      sleep 1
    }
  ensure
    response.stream.close
  end
end

這個模組有一些警告事項。在回應被提交之後,您無法寫入標頭(Response#committed? 會傳回真值)。呼叫 response stream 的 writeclose 將導致回應物件被提交。請務必在 stream 中呼叫 write 或 close 之前設定好所有標頭。

完成後,您必須在 stream 中呼叫 close,否則可能會讓插座永遠保持開啟狀態。

最後的警告事項是,您的動作會在一個與主線程不同的線程中執行。請確定您的動作是執行緒安全的,這樣就不會有問題(不要在執行緒之間共用狀態等)。

請注意,預設情況下 Rails 會包含 Rack::ETag,它會緩衝您的回應。因此串流回應可能無法正確與 Rack 2.2.x 搭配使用,您可能需要在應用程式中實作解決方法。您可以設定 ETagLast-Modified 回應標頭,或從中間層堆疊中移除 Rack::ETag 來解決此問題。

以下是一個設定 Last-Modified 標頭的範例,假設您的 Rack 版本為 2.2.x

def stream
  response.headers["Content-Type"] = "text/event-stream"
  response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x
  ...
end
命名空間
方法
L
P
R
S

類別公開方法

live_thread_pool_executor()

# File actionpack/lib/action_controller/metal/live.rb, line 385
def self.live_thread_pool_executor
  @live_thread_pool_executor ||= Concurrent::CachedThreadPool.new(name: "action_controller.live")
end

執行個體公開方法

process(name)

# File actionpack/lib/action_controller/metal/live.rb, line 276
def process(name)
  t1 = Thread.current
  locals = t1.keys.map { |key| [key, t1[key]] }

  error = nil
  # This processes the action in a child thread. It lets us return the response
  # code and headers back up the Rack stack, and still process the body in
  # parallel with sending data to the client.
  new_controller_thread {
    ActiveSupport::Dependencies.interlock.running do
      t2 = Thread.current

      # Since we're processing the view in a different thread, copy the thread locals
      # from the main thread to the child thread. :'(
      locals.each { |k, v| t2[k] = v }
      ActiveSupport::IsolatedExecutionState.share_with(t1)

      begin
        super(name)
      rescue => e
        if @_response.committed?
          begin
            @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
            @_response.stream.call_on_error
          rescue => exception
            log_error(exception)
          ensure
            log_error(e)
            @_response.stream.close
          end
        else
          error = e
        end
      ensure
        # Ensure we clean up any thread locals we copied so that the thread can reused.
        ActiveSupport::IsolatedExecutionState.clear
        locals.each { |k, _| t2[k] = nil }

        @_response.commit!
      end
    end
  }

  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
    @_response.await_commit
  end

  raise error if error
end

response_body=(body)

# File actionpack/lib/action_controller/metal/live.rb, line 326
def response_body=(body)
  super
  response.close if response
end

send_stream(filename:, disposition: "attachment", type: nil)

將串流傳送至瀏覽器,這在您產生外匯或其他執行中資料(您不希望檔案整個緩衝在記憶體中)時很有用。類似於 send_data,但資料是實時產生的。

選項: * :filename - 建議瀏覽器要使用的檔名。 * :type - 指定 HTTP 內容類型。您可以指定字串或註冊類型(使用 Mime::Type.register)的符號,例如 :json。如果省略,類型將從在 :filename 中指定的文件副檔名推斷。如果沒有為副檔名註冊內容類型,將使用預設類型「application/octet-stream」。 * :disposition - 指定檔案將內嵌顯示或下載。有效值為「inline」和「attachment」(預設)。

產生 csv 輸出的範例

send_stream(filename: "subscribers.csv") do |stream|
  stream.write "email_address,updated_at\n"

  @subscribers.find_each do |subscriber|
    stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
  end
end
# File actionpack/lib/action_controller/metal/live.rb, line 355
def send_stream(filename:, disposition: "attachment", type: nil)
  payload = { filename: filename, disposition: disposition, type: type }
  ActiveSupport::Notifications.instrument("send_stream.action_controller", payload) do
    response.headers["Content-Type"] =
      (type.is_a?(Symbol) ? Mime[type].to_s : type) ||
      Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete("."))&.to_s ||
      "application/octet-stream"

    response.headers["Content-Disposition"] =
      ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)

    yield response.stream
  end
ensure
  response.stream.close
end