官方文檔: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.js
、application.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
,一旦靜態資源文件的內容發生變化,其文件名就會發生變化。這樣,我們就不需要每次手動使某個靜態資源文件的緩存失效。通過使用唯一的新文件名,我們就能確保用戶訪問的總是靜態資源文件的最新版本。