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

 

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

 

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

 

 

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

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

例如,下面的代碼:java

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

會生成下面的 HTML:json

<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 輔助方法時須要刪除這些選項。api

能夠經過 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,一旦靜態資源文件的內容發生變化,其文件名就會發生變化。這樣,咱們就不須要每次手動使某個靜態資源文件的緩存失效。經過使用惟一的新文件名,咱們就能確保用戶訪問的老是靜態資源文件的最新版本。

相關文章
相關標籤/搜索