惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
Martin Fowler
Martin Fowler
The GitHub Blog
The GitHub Blog
B
Blog RSS Feed
U
Unit 42
阮一峰的网络日志
阮一峰的网络日志
量子位
GbyAI
GbyAI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
云风的 BLOG
云风的 BLOG
小众软件
小众软件
博客园 - 三生石上(FineUI控件)
L
LangChain Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园_首页
IT之家
IT之家
V
Visual Studio Blog
Y
Y Combinator Blog
Blog — PlanetScale
Blog — PlanetScale
宝玉的分享
宝玉的分享
Apple Machine Learning Research
Apple Machine Learning Research
I
InfoQ
D
Docker
V
V2EX

Nic Lin's Blog

謝明真 - 高效領導力的課後筆記 NFT 開發實戰!基礎智能合約入門 (3) NFT 開發實戰!基礎智能合約入門 (2) NFT 開發實戰!基礎智能合約入門 (1) 如何自我檢測 log4j CVE 漏洞 Rails 如何在資料寫入時記錄來源 IP 位置 如何經營工程師 Youtube 頻道 - Part 8 營收篇 如何經營工程師 Youtube 頻道 - Part 7 酸民文化篇 如何經營工程師 Youtube 頻道 - Part 5 設備器材篇 如何經營工程師 Youtube 頻道 - Part 4 後製剪輯篇 如何經營工程師 Youtube 頻道 - Part 3 文案企劃篇 如何經營工程師 Youtube 頻道 - Part 2 設備器材篇 如何經營工程師 Youtube 頻道 - Part 1 制訂頻道方向篇 如何經營工程師 Youtube 頻道 - Part 0 Rails 中避免 race condition 的最佳實踐(二) Rails 中避免 race condition 的最佳實踐(一) 10 分鐘整合 google sheet 做自動化開發功能週報 經營 Side Project 300 天所帶來的收穫及挑戰 我的 Youtube 影片製作流程 API 設計時必須注意的 HTTP header 底線問題 如何提升你的程式可讀性之實務技巧(三) 如何提升你的程式可讀性之實務技巧(二) 如何提升你的程式可讀性之實務技巧(一) Ruby 中使用 freeze 優化效能的時機 避免 React 中的 useEffect 無限 render 在 Rails 內輕量使用 Vue Component 的最佳實踐 如何在區域網路用 Docker 架設有 SSL 的 Gitlab 從被問到問人,那些我常問的面試問題 [Rails] 如何漂亮寫出可維護的 query (Maintainable Rails Query) 在已知長度情況下優化 slice 的性能
[Rails] 用串流的方式讓用戶下載 CSV
Nic Lin · 2018-07-11 · via Nic Lin's Blog

開頭先瞭解一下什麼是串流,來自維基百科

串流媒體(streaming media)是指將一連串的媒體資料壓縮後,經過網路分段傳送資料,在網路上即時傳輸影音以供觀賞的一種技術與過程,此技術使得資料封包得以像流水一樣傳送;如果不使用此技術,就必須在使用前下載整個媒體檔案。

同常有一個情況是這樣的,如果我要讓用戶下載自己的交易記錄,假設有五萬筆好了

  1. 用戶點擊下載後,開始準備資料、輸出資料,如果資料量稍大,網頁這時候會卡住,因為在等 server 回應
  2. 用戶點擊下載後,隨後跳出通知用戶幾分鐘後會收到 Email, 由異步執行的 worker 去處理(例如 Sidekiq)

不管是一或二我個人認為對使用者都不是很好的體驗。

第一種作法會讓覺得網站是不是當機了,如果超過一分鐘, server 又將我斷開連接,直接給我一個 504,我會很崩潰的。 第二種作法雖然將工作拆至背景作業,身為使用者的我也無法知道當前的進度,而且有可能會掉信?

當然第二種作法如果還遇到商業邏輯是用手機註冊的就行不大通了,難不成要輸出報表前讓用戶先去進行郵箱驗證?

在 Rails 中其實可以實現串流下載,這樣當資料準備到多少,用戶就會下載到多少,當用戶中斷下載,執行的部分也會直接斷掉。

那麼,如何實做呢?


class OrdersController
  def index
    respond_to do |format|
      format.csv render_csv
    end
  end
  
  private

  def render_csv
    set_file_headers
    set_streaming_headers

    response.status = 200

    self.response_body = orders_csv_enumerator(orders)
  end

  def set_file_headers
    file_name = "orders.csv"
    
    # 告訴 Browser 這是 CSV file
    headers["Content-Type"] = "text/csv"
    
    # 使用指定的文件名稱下載文件
    headers["Content-disposition"] = "attachment; filename=\"#{file_name}\""
  end

  def set_streaming_headers
    # 通過代理服務器時不要緩衝
    headers["X-Accel-Buffering"] = "no"
    
    # 從這個 endpoint 建立時不要 cache 任何東西
    headers["Cache-Control"] ||= "no-cache"
    
    # 讓 Rack 知道要做串流
    headers.delete("Content-Length")
  end

  def orders_csv_enumerator(orders)
    ExportService::Order.new(orders).build_csv_enumerator
  end
end

Order csv enumerator 我是另外用 service 做,這樣比較不會混亂。

class ExportService::Order
  def initialize(orders)
    @orders = orders
  end
  
  def build_csv_enumerator
    csv_enumerator =
      Enumerator.new do |yielder|
        yielder << CSV.generate_line(order_headers).to_s

        @orders.find_each do |order|
          yielder << CSV.generate_line(order_row(order)).to_s
        end
      end

    csv_enumerator
  end
end

整個數據過程如下:

我們呼叫使用 lazy enumerator(惰性枚舉器) 的 build_csv_enumerator 方法

由於 response body 由 rails 處理,他不斷要求 enumerator 輸出下一筆資料,為了生成下一筆資料,CSV.generate_line 會請求下一筆數據,直到第一批資料完成(這裡是 find_each, 預設批次處理是 1000 筆資料)之後下發送給瀏覽器,過程中無需一次性生成文件,而是堆疊起來,就是我們實現的串流式下載了。

參考資料