Rails的靜態資源管理(四)—— 生產環境的 Asset Pipeline


 

官方文檔:http://guides.ruby-china.org/asset_pipeline.html

 

http://guides.rubyonrails.org/asset_pipeline.html

 

 

在生產環境中,Sprockets 會使用前文介紹的指紋機制。默認情況下,Rails 假定靜態資源文件都經過了預編譯,並將由 Web 服務器處理。

在預編譯階段,Sprockets 會根據靜態資源文件的內容生成 SHA256 哈希值,並在保存文件時把這個哈希值添加到文件名中。Rails 輔助方法會用這些包含指紋的文件名代替清單文件中的文件名。

例如,下面的代碼:

<%= javascript_include_tag "application" %>
<%= stylesheet_link_tag "application" %>

會生成下面的 HTML:

<script src="/assets/application-908e25f4bf641868d8683022a5b62f54.js"></script>
<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen" rel="stylesheet" />

Rails 開始使用 Asset Pipeline 后,不再使用 :cache 和 :concat 選項,因此在調用 javascript_include_tag 和 stylesheet_link_tag 輔助方法時需要刪除這些選項。

可以通過 config.assets.digest 初始化選項(默認為 true)啟用或禁用指紋功能。

在正常情況下,請不要修改默認的 config.assets.digest 選項(默認為 true)。如果文件名中未包含指紋,並且 HTTP 頭信息的過期時間設置為很久以后,遠程客戶端將無法在文件內容發生變化時重新獲取文件。

 

1 預編譯靜態資源文件

Rails 提供了一個 Rake 任務,用於編譯 Asset Pipeline 清單文件中的靜態資源文件和其他相關文件。

經過編譯的靜態資源文件將儲存在 config.assets.prefix 選項指定的路徑中,默認為 /assets 文件夾。

部署 Rails 應用時可以在服務器上執行這個 Rake 任務,以便直接在服務器上完成靜態資源文件的編譯。關於本地編譯的介紹,請參閱下一節。

這個 Rake 任務是:

$ RAILS_ENV=production bin/rails assets:precompile

Capistrano(v2.15.1 及更高版本)提供了對這個 Rake 任務的支持。只需把下面這行代碼添加到 Capfile 中:

load 'deploy/assets'

就會把 config.assets.prefix 選項指定的文件夾鏈接到 shared/assets 文件夾。當然,如果 shared/assets 文件夾已經用於其他用途,我們就得自己編寫部署任務了。

需要注意的是,shared/assets 文件夾會在多次部署之間共享,這樣引用了這些靜態資源文件的遠程客戶端的緩存頁面在其生命周期中就能正常工作。

編譯文件時的默認匹配器(matcher)包括 application.jsapplication.css,以及 app/assets 文件夾和 gem 中的所有非 JS/CSS 文件(會自動包含所有圖像):

[ Proc.new { |filename, path| path =~ /app\/assets/ && !%w(.js .css).include?(File.extname(filename)) },
/application.(css|js)$/ ]

這個匹配器(及預編譯數組的其他成員;見后文)會匹配編譯后的文件名,這意味着無論是 JS/CSS 文件,還是能夠編譯為 JS/CSS 的文件,都將被排除在外。例如,.coffee 和 .scss 文件能夠編譯為 JS/CSS,因此被排除在默認的編譯范圍之外。

要想包含其他清單文件,或單獨的 JavaScript 和 CSS 文件,可以把它們添加到 config/initializers/assets.rb 配置文件的 precompile 數組中:

Rails.application.config.assets.precompile += %w( admin.js admin.css )

添加到 precompile 數組的文件名應該以 .js 或 .css 結尾,即便實際添加的是 CoffeeScript 或 Sass 文件也是如此。

assets:precompile 這個 Rake 任務還會成生 .sprockets-manifest-md5hash.json 文件(其中 md5hash 是一個 MD5 哈希值),其內容是所有靜態資源文件及其指紋的列表。有了這個文件,Rails 輔助方法不需要 Sprockets 就能獲得靜態資源文件對應的指紋。下面是一個典型的 .sprockets-manifest-md5hash.json 文件的例子:

{"files":{"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js":{"logical_path":"application.js","mtime":"2016-12-23T20:12:03-05:00","size":412383,
"digest":"aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b","integrity":"sha256-ruS+cfEogDeueLmX3ziDMu39JGRxtTPc7aqPn+FWRCs="},
"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css":{"logical_path":"application.css","mtime":"2016-12-23T19:12:20-05:00","size":2994,
"digest":"86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18","integrity":"sha256-hqKStQcHk8N+LA5fOfc7s4dkTq6tp/lub8BAoCixbBg="},
"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico":{"logical_path":"favicon.ico","mtime":"2016-12-23T20:11:00-05:00","size":8629,
"digest":"8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda","integrity":"sha256-jSOHuNTTLOzZP6OQDfDp/4nQGqzYT1DngMF8n2s9Dto="},
"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png":{"logical_path":"my_image.png","mtime":"2016-12-23T20:10:54-05:00","size":23414,
"digest":"f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493","integrity":"sha256-9AKBVv1+ygNYTV8vwEcN8eDbxzaequY4sv8DP5iOxJM="}},
"assets":{"application.js":"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js",
"application.css":"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css",
"favicon.ico":"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico",
"my_image.png":"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png"}}

.sprockets-manifest-md5hash.json 文件默認位於 config.assets.prefix 選項所指定的位置的根目錄(默認為 /assets 文件夾)。

在生產環境中,如果有些預編譯后的文件丟失了,Rails 就會拋出 Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError 異常,提示所丟失文件的文件名。

 

1.1 在 HTTP 首部中設置為很久以后才過期

預編譯后的靜態資源文件儲存在文件系統中,並由 Web 服務器直接處理。默認情況下,這些文件的 HTTP 首部並不會在很久以后才過期,為了充分發揮指紋的作用,我們需要修改服務器配置中的請求頭過期時間。

對於 Apache:

# 在啟用 Apache 模塊 `mod_expires` 的情況下,才能使用
# Expires* 系列指令。
<Location /assets/>
  # 在使用 Last-Modified 的情況下,不推薦使用 ETag
  Header unset ETag
  FileETag None
  # RFC 規定緩存時間為 1 年
  ExpiresActive On
  ExpiresDefault "access plus 1 year"
</Location>

對於 Nginx:

location ~ ^/assets/ {
  expires 1y;
  add_header Cache-Control public;

  add_header ETag "";
}

 

2 本地預編譯

在本地預編譯靜態資源文件的理由如下:

  • 可能沒有生產環境服務器文件系統的寫入權限;
  • 可能需要部署到多台服務器,不想重復編譯;
  • 部署可能很頻繁,但靜態資源文件很少變化。

本地編譯允許我們把編譯后的靜態資源文件納入源代碼版本控制,並按常規方式部署。

有三個注意事項:

  • 不要運行用於預編譯靜態資源文件的 Capistrano 部署任務;
  • 開發環境中必須安裝壓縮或簡化靜態資源文件所需的工具;
  • 必須修改下面這個設置:

在 config/environments/development.rb 配置文件中添加下面這行代碼:

config.assets.prefix = "/dev-assets"

在開發環境中,通過修改 prefix,可以讓 Sprockets 使用不同的 URL 處理靜態資源文件,並把所有請求都交給 Sprockets 處理。在生產環境中,prefix 仍然應該設置為 /assets。在開發環境中,如果不修改 prefix,應用就會優先讀取 /assets 文件夾中預編譯后的靜態資源文件,這樣對靜態資源文件進行修改后,除非重新編譯,否則看不到任何效果。

實際上,通過修改 prefix,我們可以在本地預編譯靜態資源文件,並把這些文件儲存在工作目錄中,同時可以根據需要隨時將其納入源代碼版本控制。開發模式將按我們的預期正常工作。

 

3 實時編譯

在某些情況下,我們需要使用實時編譯。在實時編譯模式下,Asset Pipeline 中的所有靜態資源文件都由 Sprockets 直接處理。

通過如下設置可以啟用實時編譯:

config.assets.compile = true

如前文所述,靜態資源文件會在首次請求時被編譯和緩存,輔助方法會把清單文件中的文件名轉換為帶 SHA256 哈希值的版本。

Sprockets 還會把 Cache-Control HTTP 首部設置為 max-age=31536000,意思是服務器和客戶端瀏覽器的所有緩存的過期時間是 1 年。這樣在本地瀏覽器緩存或中間緩存中找到所需靜態資源文件的可能性會大大增加,從而減少從服務器上獲取靜態資源文件的請求次數。

但是實時編譯模式會使用更多內存,性能也比默認設置更差,因此並不推薦使用。

如果部署應用的生產服務器沒有預裝 JavaScript 運行時,可以在 Gemfile 中添加一個:

group :production do
  gem 'therubyracer'
end

 

4 CDN

CDN 的意思是內容分發網絡,主要用於緩存全世界的靜態資源文件。當 Web 瀏覽器請求靜態資源文件時,CDN 會從地理位置最近的 CDN 服務器上發送緩存的文件副本。如果我們在生產環境中讓 Rails 直接處理靜態資源文件,那么在應用前端使用 CDN 將是最好的選擇。

使用 CDN 的常見模式是把生產環境中的應用設置為“源”服務器,也就是說,當瀏覽器從 CDN 請求靜態資源文件但緩存未命中時,CDN 將立即從“源”服務器中抓取該文件,並對其進行緩存。例如,假設我們在 example.com 上運行 Rails 應用,並在mycdnsubdomain.fictional-cdn.com 上配置了 CDN,在處理對 mycdnsubdomain.fictional-cdn.com/assets/smile.png 的首次請求時,CDN 會抓取 example.com/assets/smile.png 並進行緩存。之后再請求 mycdnsubdomain.fictional-cdn.com/assets/smile.png 時,CDN 會直接提供緩存中的文件副本。對於任何請求,只要 CDN 能夠直接處理,就不會訪問 Rails 服務器。由於 CDN 提供的靜態資源文件由地理位置最近的 CDN 服務器提供,因此對請求的響應更快,同時 Rails 服務器不再需要花費大量時間處理靜態資源文件,因此可以專注於更快地處理應用代碼。

 

4.1 設置用於處理靜態資源文件的 CDN

要設置 CDN,首先必須在公開的互聯網 URL 地址上(例如 example.com)以生產環境運行 Rails 應用。下一步,注冊雲服務提供商的 CDN 服務。然后配置 CDN 的“源”服務器,把它指向我們的網站 example.com,具體配置方法請參考雲服務提供商的文檔。

CDN 提供商會為我們的應用提供一個自定義子域名,例如 mycdnsubdomain.fictional-cdn.com(注意 fictional-cdn.com 只是撰寫本文時杜撰的一個 CDN 提供商)。完成 CDN 服務器配置后,還需要告訴瀏覽器從 CDN 抓取靜態資源文件,而不是直接從 Rails 服務器抓取。為此,需要在 Rails 配置中,用靜態資源文件的主機代替相對路徑。通過 config/environments/production.rb 配置文件的 config.action_controller.asset_host 選項,我們可以設置靜態資源文件的主機:

config.action_controller.asset_host = 'mycdnsubdomain.fictional-cdn.com'

這里只需提供“主機”,即前文提到的子域名,而不需要指定 HTTP 協議,例如 http:// 或 https://。默認情況下,Rails 會使用網頁請求的 HTTP 協議作為指向靜態資源文件鏈接的協議。

還可以通過環境變量設置靜態資源文件的主機,這樣可以方便地在不同的運行環境中使用不同的靜態資源文件:

config.action_controller.asset_host = ENV['CDN_HOST']

這里還需要把服務器上的 CDN_HOST 環境變量設置為 mycdnsubdomain.fictional-cdn.com

服務器和 CDN 配置好后,就可以像下面這樣引用靜態資源文件:

<%= asset_path('smile.png') %>

這時返回的不再是相對路徑 /assets/smile.png(出於可讀性考慮省略了文件名中的指紋),而是指向 CDN 的完整路徑:

http://mycdnsubdomain.fictional-cdn.com/assets/smile.png

如果 CDN 上有 smile.png 文件的副本,就會直接返回給瀏覽器,而 Rails 服務器甚至不知道有瀏覽器請求了 smile.png 文件。如果 CDN 上沒有 smile.png 文件的副本,就會先從“源”服務器上抓取 example.com/assets/smile.png 文件,再返回給瀏覽器,同時保存文件的副本以備將來使用。

如果只想讓 CDN 處理部分靜態資源文件,可以在調用靜態資源文件輔助方法時使用 :host 選項,以覆蓋 config.action_controller.asset_host 選項中設置的值:

<%= asset_path 'image.png', host: 'mycdnsubdomain.fictional-cdn.com' %>

 

4.2 自定義 CDN 緩存行為

CDN 的作用是為內容提供緩存。如果 CDN 上有過期或不良內容,那么不僅不能對應用有所助益,反而會造成負面影響。本小節將介紹大多數 CDN 的一般緩存行為,而我們使用的 CDN 在特性上可能會略有不同。

4.2.1 CDN 請求緩存

我們常說 CDN 對於緩存靜態資源文件非常有用,但實際上 CDN 緩存的是整個請求。其中既包括了靜態資源文件的請求體,也包括了其首部。其中,Cache-Control 首部是最重要的,用於告知 CDN(和 Web 瀏覽器)如何緩存文件內容。假設用戶請求了 /assets/i-dont-exist.png 這個並不存在的靜態資源文件,並且 Rails 應用返回的是 404,那么只要設置了合法的 Cache-Control 首部,CDN 就會緩存 404 頁面。

4.2.2 調試 CDN 首部

檢查 CDN 是否正確緩存了首部的方法之一是使用 curl。我們可以分別從 Rails 服務器和 CDN 獲取首部,然后確認二者是否相同:

$ curl -I http://www.example/assets/application-
d0e099e021c95eb0de3615fd1d8c4d83.css
HTTP/1.1 200 OK
Server: Cowboy
Date: Sun, 24 Aug 2014 20:27:50 GMT
Connection: keep-alive
Last-Modified: Thu, 08 May 2014 01:24:14 GMT
Content-Type: text/css
Cache-Control: public, max-age=2592000
Content-Length: 126560
Via: 1.1 vegur

CDN 中副本的首部:

$ curl -I http://mycdnsubdomain.fictional-cdn.com/application-
d0e099e021c95eb0de3615fd1d8c4d83.css
HTTP/1.1 200 OK Server: Cowboy Last-
Modified: Thu, 08 May 2014 01:24:14 GMT Content-Type: text/css
Cache-Control:
public, max-age=2592000
Via: 1.1 vegur
Content-Length: 126560
Accept-Ranges:
bytes
Date: Sun, 24 Aug 2014 20:28:45 GMT
Via: 1.1 varnish
Age: 885814
Connection: keep-alive
X-Served-By: cache-dfw1828-DFW
X-Cache: HIT
X-Cache-Hits:
68
X-Timer: S1408912125.211638212,VS0,VE0

在 CDN 文檔中可以查詢 CDN 提供的額外首部,例如 X-Cache

4.2.3 CDN 和 Cache-Control 首部

Cache-Control 首部是一個 W3C 規范,用於描述如何緩存請求。當未使用 CDN 時,瀏覽器會根據 Cache-Control 首部來緩存文件內容。在靜態資源文件未修改的情況下,瀏覽器就不必重新下載 CSS 或 JavaScript 等文件了。通常,Rails 服務器需要告訴 CDN(和瀏覽器)這些靜態資源文件是“公共的”,這樣任何緩存都可以保存這些文件的副本。此外,通常還會通過 max-age 字段來設置緩存失效前儲存對象的時間。max-age 字段的單位是秒,最大設置為 31536000,即一年。在 Rails 應用中設置 Cache-Control 首部的方法如下:

config.public_file_server.headers = {
  'Cache-Control' => 'public, max-age=31536000'
}

現在,在生產環境中,Rails 應用的靜態資源文件在 CDN 上會被緩存長達 1 年之久。由於大多數 CDN 會緩存首部,靜態資源文件的 Cache-Control 首部會被傳遞給請求該靜態資源文件的所有瀏覽器,這樣瀏覽器就會長期緩存該靜態資源文件,直到緩存過期后才會重新請求該文件。

 4.2.4 CDN 和基於 URL 地址的緩存失效

大多數 CDN 會根據完整的 URL 地址來緩存靜態資源文件的內容。因此,緩存

http://mycdnsubdomain.fictional-cdn.com/assets/smile-123.png

和緩存

http://mycdnsubdomain.fictional-cdn.com/assets/smile.png

被認為是兩個完全不同的靜態資源文件的緩存。

如果我們把 Cache-Control HTTP 首部的 max-age 值設得很大,那么當靜態資源文件的內容發生變化時,應同時使原有緩存失效。例如,當我們把黃色笑臉圖像更換為藍色笑臉圖像時,我們希望網站的所有訪客看到的都是新的藍色笑臉圖像。如果我們使用了 CDN,並使用了 Rails Asset Pipeline config.assets.digest 選項的默認值 true,一旦靜態資源文件的內容發生變化,其文件名就會發生變化。這樣,我們就不需要每次手動使某個靜態資源文件的緩存失效。通過使用唯一的新文件名,我們就能確保用戶訪問的總是靜態資源文件的最新版本。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM