edge
更多資訊請參考 rubyonrails.org: 更多 Ruby on Rails

資產管道

本指南介紹了資產管道。

閱讀本指南後,您將了解以下內容:

1 什麼是資產管道?

資產管道提供了一個處理 JavaScript 和 CSS 資產交付的框架。這是通過利用 HTTP/2 等技術和串聯和最小化等技巧來實現的。最後,它允許您的應用程式自動與其他 gem 的資產結合。

資產管道由 importmap-railssprocketssprockets-rails gem 實現,並且默認情況下啟用。您可以在創建新應用程式時禁用它,只需傳遞 --skip-asset-pipeline 選項。

$ rails new appname --skip-asset-pipeline

注意:本指南專注於使用只有 sprockets 用於 CSS 和 importmap-rails 用於 JavaScript 處理的默認資產管道。這兩者的主要限制是它們不支援轉譯,因此您無法使用像 BabelTypescriptSassReact JSX 格式TailwindCSS 等功能。如果您需要對 JavaScript/CSS 進行轉譯,我們建議您閱讀 替代庫部分

2 主要功能

資產管道的第一個功能是將 SHA256 指紋插入到每個檔案名稱中,以便網頁瀏覽器和 CDN 緩存該檔案。當您更改檔案內容時,此指紋會自動更新,從而使緩存無效。

資產管道的第二個功能是在提供 JavaScript 檔案時使用 import maps。這使您可以使用針對 ES 模組(ESM)製作的 JavaScript 函式庫來構建現代應用程式,而無需進行轉譯和打包。因此,這消除了 Webpack、yarn、node 或 JavaScript 工具鏈的任何其他部分的需求

資產管道的第三個功能是將所有 CSS 檔案串聯到一個主要的 .css 檔案中,然後對其進行最小化或壓縮。正如您稍後在本指南中了解的那樣,您可以自定義此策略以任何您喜歡的方式分組檔案。在生產環境中,Rails 會將 SHA256 指紋插入到每個檔案名稱中,以便網頁瀏覽器緩存該檔案。您可以通過更改此指紋來使緩存失效,每當您更改檔案內容時,這將自動發生。

資產管道的第四個功能是它允許使用一種更高級的語言來編寫 CSS 資產。

2.1 什麼是指紋和為什麼我應該關心?

指紋是一種技術,使檔案名稱依賴於檔案的內容。當檔案內容更改時,檔案名稱也會更改。對於靜態或不經常更改的內容,這提供了一種簡單的方法來判斷兩個版本的檔案是否相同,即使在不同的伺服器或部署日期上也是如此。

當檔案名稱是唯一的並基於其內容時,可以設置 HTTP 標頭以鼓勵各處的快取(無論是在 CDN、ISP、網絡設備還是網頁瀏覽器中)保留其自己的內容副本。當內容更新時,指紋將更改。這將導致遠程客戶端請求新的內容副本。這通常被稱為 緩存破壞

Sprockets 用於指紋的技術是將內容的哈希插入到名稱中,通常是在末尾。例如,CSS 檔案 global.css

global-908e25f4bf641868d8683022a5b62f54.css

這是 Rails 資產管道採用的策略。

指紋在開發和生產環境中都默認啟用。您可以通過配置中的 config.assets.digest 選項來啟用或禁用它。

2.2 什麼是 Import Maps 以及為什麼我應該關心?

Import Maps 允許您使用邏輯名稱將 JavaScript 模組導入到網頁瀏覽器中的版本化/摘要檔案中。因此,您可以使用針對 ES 模組(ESM)製作的 JavaScript 函式庫來構建現代 JavaScript 應用程式,而無需進行轉譯或打包。

使用此方法,您將發送許多小的 JavaScript 檔案,而不是一個大的 JavaScript 檔案。由於 HTTP/2 在初始傳輸期間不再帶來實質的性能懲罰,並且實際上由於更好的快取動態而在長期運行中提供了實質的好處。

3 如何使用Import Maps作為Javascript資源管道

Import Maps是預設的Javascript處理器,生成import maps的邏輯由importmap-rails gem處理。

警告:Import Maps僅用於Javascript文件,無法用於CSS傳遞。請查看Sprockets部分以了解CSS。

您可以在Gem主頁上找到詳細的使用說明,但了解importmap-rails的基本原理很重要。

3.1 工作原理

Import Maps本質上是對所謂的“裸模塊標識符”進行字符串替換。它們允許您標準化JavaScript模塊導入的名稱。

例如,如果沒有import map,以下import定義將無法工作:

import React from "react"

您必須像這樣定義它才能使其工作:

import React from "https://ga.jspm.io/npm:[email protected]/index.js"

這就是import map的作用,我們將react名稱定義為https://ga.jspm.io/npm:[email protected]/index.js地址。有了這樣的信息,我們的瀏覽器接受簡化的import React from "react"定義。將import map視為庫源地址的別名。

3.2 使用方法

使用importmap-rails創建importmap配置文件,將庫路徑固定到一個名稱上:

# config/importmap.rb
pin "application"
pin "react", to: "https://ga.jspm.io/npm:[email protected]/index.js"

所有配置的import map都應該通過添加<%= javascript_importmap_tags %>將其附加到應用程序的<head>元素中。javascript_importmap_tagshead元素中呈現一堆腳本:

  • 包含所有配置的import map的JSON:
<script type="importmap">
{
  "imports": {
    "application": "/assets/application-39f16dc3f3....js"
    "react": "https://ga.jspm.io/npm:[email protected]/index.js"
  }
}
</script>
  • Es-module-shims作為polyfill,確保舊版瀏覽器支持import maps
<script src="/assets/es-module-shims.min" async="async" data-turbo-track="reload"></script>
  • app/javascript/application.js加載JavaScript的入口點:
<script type="module">import "application"</script>

3.3 通過JavaScript CDNs使用npm包

您可以使用作為importmap-rails安裝的一部分添加的./bin/importmap命令來固定、取消固定或更新import map中的npm包。該binstub使用JSPM.org

它的工作方式如下:

./bin/importmap pin react react-dom
Pinning "react" to https://ga.jspm.io/npm:[email protected]/index.js
Pinning "react-dom" to https://ga.jspm.io/npm:[email protected]/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:[email protected]/index.js
Pinning "scheduler" to https://ga.jspm.io/npm:[email protected]/index.js

./bin/importmap json

{
  "imports": {
    "application": "/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js",
    "react": "https://ga.jspm.io/npm:[email protected]/index.js",
    "react-dom": "https://ga.jspm.io/npm:[email protected]/index.js",
    "object-assign": "https://ga.jspm.io/npm:[email protected]/index.js",
    "scheduler": "https://ga.jspm.io/npm:[email protected]/index.js"
  }
}

如您所見,兩個包react和react-dom通過jspm默認解析為四個依賴項。

現在,您可以像使用其他模塊一樣在application.js入口點中使用它們:

import React from "react"
import ReactDOM from "react-dom"

您還可以指定要固定的特定版本:

./bin/importmap pin [email protected]
Pinning "react" to https://ga.jspm.io/npm:[email protected]/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:[email protected]/index.js

或者甚至刪除固定:

./bin/importmap unpin react
Unpinning "react"
Unpinning "object-assign"

您可以控制包的環境,對於具有單獨的“生產”(默認)和“開發”構建的包:

./bin/importmap pin react --env development
Pinning "react" to https://ga.jspm.io/npm:[email protected]/dev.index.js
Pinning "object-assign" to https://ga.jspm.io/npm:[email protected]/index.js

在固定時,您還可以選擇另一個支持的CDN提供程序,例如unpkgjsdelivr(默認為jspm):

./bin/importmap pin react --from jsdelivr
Pinning "react" to https://cdn.jsdelivr.net/npm/[email protected]/index.js

請記住,如果您將固定從一個提供程序切換到另一個提供程序,您可能需要清理第一個提供程序添加的第二個提供程序不使用的依賴項。

運行./bin/importmap以查看所有選項。

請注意,此命令僅是將邏輯包名解析為CDN URL的便利包裝器。您也可以自己查找CDN URL,然後固定它們。例如,如果您想使用Skypack來使用React,您可以將以下內容添加到config/importmap.rb中:

pin "react", to: "https://cdn.skypack.dev/react"

3.4 預加載固定模塊

為了避免瀏覽器在可以到達最深層嵌套導入之前必須加載一個文件的瀑布效應,importmap-rails支持modulepreload鏈接。可以通過將preload: true附加到固定中來預加載固定模塊。

對於在整個應用程序中使用的庫或框架,預加載它們是一個好主意,因為這將告訴瀏覽器更早地下載它們。

示例:

# config/importmap.rb
pin "@github/hotkey", to: "https://ga.jspm.io/npm:@github/[email protected]/dist/index.js", preload: true
pin "md5", to: "https://cdn.jsdelivr.net/npm/[email protected]/md5.js"

# app/views/layouts/application.html.erb
<%= javascript_importmap_tags %>

# 在設置importmap之前,將包含以下鏈接:
<link rel="modulepreload" href="https://ga.jspm.io/npm:@github/[email protected]/dist/index.js">
...

注意:有關最新的文件,請參考 importmap-rails 存儲庫。

4 如何使用 Sprockets

將應用程序資源公開到網絡的一種簡單方法是將它們存儲在 public 文件夾的子目錄中,例如 imagesstylesheets。手動這樣做會很困難,因為大多數現代網絡應用程序需要以特定方式處理資源,例如壓縮和添加指紋。

Sprockets 設計用於自動預處理存儲在配置目錄中的資源,並在處理後將它們公開在 public/assets 文件夾中,包括指紋、壓縮、源映射生成和其他可配置功能。

資源仍然可以放置在 public 層級結構中。當 config.public_file_server.enabled 設置為 true 時,public 下的任何資源都將由應用程序或 Web 服務器作為靜態文件提供。您必須為需要在提供之前進行某些預處理的文件定義 manifest.js 指令。

在生產環境中,Rails 默認將這些文件預編譯到 public/assets。然後,Web 服務器將這些預編譯的副本作為靜態資源提供。在生產環境中,app/assets 中的文件永遠不會直接提供。

4.1 Manifest 文件和指令

使用 Sprockets 編譯資源時,Sprockets 需要決定要編譯的頂級目標,通常是 application.css 和圖像。頂級目標在 Sprockets 的 manifest.js 文件中定義,默認情況下如下所示:

//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js

它包含 指令 - 指示 Sprockets 要求哪些文件以構建單個 CSS 或 JavaScript 文件。

這意味著它將包含在 ./app/assets/images 目錄或任何子目錄中找到的所有文件的內容,以及在 ./app/javascript./vendor/javascript 中直接識別為 JS 的任何文件。

它將加載 ./app/assets/stylesheets 目錄中的任何 CSS(不包括子目錄)。假設您在 ./app/assets/stylesheets 文件夾中有 application.cssmarketing.css 文件,它將允許您使用 <%= stylesheet_link_tag "application" %><%= stylesheet_link_tag "marketing" %> 從視圖中加載這些樣式表。

您可能會注意到,我們的 JavaScript 文件默認情況下不是從 assets 目錄加載的,這是因為 ./app/javascriptimportmap-rails gem 的默認入口點,而 vendor 文件夾是下載的 JS 包的存放位置。

manifest.js 中,您還可以使用 link 指令來加載特定文件而不是整個目錄。link 指令需要提供明確的文件擴展名。

Sprockets 加載指定的文件,如果需要,對其進行處理,將它們連接成一個單一文件,然後壓縮它們(基於 config.assets.css_compressorconfig.assets.js_compressor 的值)。壓縮可以減小文件大小,使瀏覽器能夠更快地下載文件。

4.2 控制器特定的資源

當您生成一個脚手架或控制器時,Rails 還會為該控制器生成一個層疊樣式表文件。此外,生成脚手架時,Rails 還會生成 scaffolds.css 文件。

例如,如果您生成一個 ProjectsController,Rails 還會在 app/assets/stylesheets/projects.css 中添加一個新文件。默認情況下,這些文件將立即可以在應用程序中使用,使用 manifest.js 文件中的 link_directory 指令。

您還可以選擇僅在相應的控制器中包含特定於控制器的樣式表文件,使用以下代碼:

<%= stylesheet_link_tag params[:controller] %>

這樣做時,請確保您的 application.css 中沒有使用 require_tree 指令,因為這可能導致您的控制器特定資源被多次包含。

4.3 資源組織

Pipeline 資源可以放置在應用程序的三個位置之一:app/assetslib/assetsvendor/assets

  • app/assets 用於應用程序擁有的資源,例如自定義圖像或樣式表。

  • app/javascript 用於您的 JavaScript 代碼。

  • vendor/[assets|javascript] 用於由外部實體擁有的資源,例如 CSS 框架或 JavaScript 庫。請記住,具有對其他由資源 Pipeline 處理的文件的引用的第三方代碼(圖像、樣式表等)需要重寫以使用像 asset_path 這樣的輔助方法。

其他位置可以在 manifest.js 文件中進行配置,請參考Manifest 文件和指令

4.3.1 搜索路徑

當從 manifest 或輔助方法引用文件時,Sprockets 會在 manifest.js 中指定的所有位置中搜索該文件。您可以通過在 Rails 控制台中檢查 Rails.application.config.assets.paths 來查看搜索路徑。

4.3.2 使用索引文件作為文件夾的代理

Sprockets使用名為index(帶有相關擴展名)的文件來進行特殊用途。

例如,如果您有一個包含許多模塊的CSS庫,存儲在lib/assets/stylesheets/library_name中,則文件lib/assets/stylesheets/library_name/index.css用作此庫中所有文件的清單。此文件可以包含按順序列出的所有所需文件,或者一個簡單的require_tree指令。

這也有點類似於通過請求/library_name可以訪問public/library_name/index.html中的文件。這意味著您不能直接使用索引文件。

可以在.css文件中像這樣訪問整個庫:

/* ...
*= require library_name
*/

這樣可以簡化維護工作,並通過在其他地方包含相關代碼來保持代碼整潔。

4.4 鏈接到資源的編碼

Sprockets不會添加任何新的方法來訪問您的資源 - 您仍然使用熟悉的stylesheet_link_tag

<%= stylesheet_link_tag "application", media: "all" %>

如果使用預設包含在Rails中的turbo-rails gem,則需要包含data-turbo-track選項,這導致Turbo檢查資源是否已更新,如果是,則將其加載到頁面中:

<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>

在常規視圖中,您可以像這樣訪問app/assets/images目錄中的圖像:

<%= image_tag "rails.png" %>

只要在應用程序中啟用了pipeline(並且在當前環境上下文中未禁用),Sprockets就會提供此文件。如果在public/assets/rails.png存在文件,則由Web服務器提供。

或者,對於具有SHA256哈希的文件的請求(例如public/assets/rails-f90d8a84c707a8dc923fca1ca1895ae8ed0a09237f6992015fef1e11be77c023.png),處理方式相同。如何生成這些哈希在本指南的在生產中部分中有介紹。

如果需要,圖像也可以組織到子目錄中,然後可以通過在標籤中指定目錄名稱來訪問:

<%= image_tag "icons/rails.png" %>

警告:如果您正在預編譯資源(參見在生產中),並且鏈接到不存在的資源,則會在調用頁面中引發異常。這包括鏈接到空字符串。因此,在使用image_tag和其他輔助函數時要小心使用用戶提供的數據。

4.4.1 CSS和ERB

資源管道會自動評估ERB。這意味著如果您將erb擴展名添加到CSS資源中(例如,application.css.erb),則在CSS規則中可以使用asset_path等輔助函數:

.class { background-image: url(<%= asset_path 'image.png' %>) }

這將寫入對特定資源的路徑。在此示例中,可以在資源加載路徑之一(例如app/assets/images/image.png)中有一個圖像,並在此處引用它。如果此圖像已經作為指紋文件存在於public/assets中,則引用該路徑。

如果要使用data URI - 將圖像數據直接嵌入CSS文件的方法 - 可以使用asset_data_uri輔助函數。

#logo { background: url(<%= asset_data_uri 'logo.png' %>) }

這將在CSS源碼中插入格式正確的data URI。

請注意,結尾標籤不能是-%>風格。

4.5 當找不到資源時引發錯誤

如果您使用的是sprockets-rails >= 3.2.0,則可以配置在執行資源查找時找不到資源時發生的情況。如果關閉了"asset fallback",則在找不到資源時將引發錯誤。

config.assets.unknown_asset_fallback = false

如果啟用了"asset fallback",則在找不到資源時將輸出路徑,並且不會引發錯誤。默認情況下禁用"asset fallback"行為。

4.6 關閉指紋

您可以通過更新config/environments/development.rb來關閉指紋:

config.assets.digest = false

當此選項為true時,將為資源URL生成指紋。

4.7 打開源映射

您可以通過更新config/environments/development.rb來打開源映射:

config.assets.debug = true

當調試模式打開時,Sprockets將為每個資源生成一個源映射。這使您可以在瀏覽器的開發者工具中單獨調試每個文件。

資源在服務器啟動後的第一個請求上編譯並緩存。Sprockets設置了must-revalidate的Cache-Control HTTP標頭,以減少後續請求的開銷 - 在這些請求中,瀏覽器會收到304(未修改)的響應。 如果清單中的任何文件在請求之間發生變化,服務器將回應一個新的編譯文件。

5 在生產環境中

在生產環境中,Sprockets使用上面概述的指紋方案。默認情況下,Rails假設資源已經被預編譯並將由Web服務器作為靜態資源提供。

在預編譯階段,從編譯文件的內容生成SHA256,並將其插入到寫入磁盤的文件名中。這些帶有指紋的名稱由Rails助手在清單名稱的位置使用。

例如:

<%= stylesheet_link_tag "application" %>

生成類似於:

<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" rel="stylesheet" />

指紋行為由config.assets.digest初始化選項控制(默認為true)。

注意:在正常情況下,不應更改默認的config.assets.digest選項。如果文件名中沒有摘要,並且設置了遠程客戶端的遠期標題,則在其內容更改時,遠程客戶端將永遠不知道重新獲取文件。

5.1 預編譯資源

Rails附帶了一個命令來編譯資源清單和管道中的其他文件。

編譯後的資源寫入到config.assets.prefix指定的位置。默認情況下,這是/assets目錄。

您可以在部署期間在服務器上調用此命令,直接在服務器上創建編譯後的資源版本。有關在本地編譯的信息,請參閱下一節。

命令如下:

$ RAILS_ENV=production rails assets:precompile

這將將config.assets.prefix中指定的文件夾鏈接到shared/assets。如果您已經使用此共享文件夾,您需要編寫自己的部署命令。

重要的是,此文件夾在部署之間是共享的,以便對舊的編譯資源進行遠程緩存的頁面在緩存頁面的生命週期內仍然可用。

注意:始終指定以.js.css結尾的預期編譯文件名。

該命令還生成一個.sprockets-manifest-randomhex.json(其中randomhex是一個16字節的隨機十六進制字符串),其中包含所有資源及其相應指紋的列表。這是Rails助手方法使用的,以避免將映射請求返回給Sprockets。典型的清單文件如下所示:

{"files":{"application-<fingerprint>.js":{"logical_path":"application.js","mtime":"2016-12-23T20:12:03-05:00","size":412383,
"digest":"<fingerprint>","integrity":"sha256-<random-string>"}},
"assets":{"application.js":"application-<fingerprint>.js"}}

在您的應用程序中,清單中將列出更多的文件和資源,<fingerprint><random-string>也將被生成。

清單的默認位置是config.assets.prefix指定的位置的根目錄(默認為'/assets')。

注意:如果在生產中缺少預編譯文件,您將收到一個Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError異常,指示缺少文件的名稱。

5.1.1 遠期到期標題

預編譯的資源存在於文件系統上,並由Web服務器直接提供。它們默認情況下不具有遠期標題,因此要獲得指紋的好處,您需要更新服務器配置以添加這些標題。

對於Apache:

# Expires*指令需要啟用Apache模塊`mod_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 "";
}

5.2 本地編譯

有時,您可能不希望或無法在生產服務器上編譯資源。例如,您可能對生產文件系統的寫入訪問權限有限,或者您可能計劃經常部署而不對資源進行任何更改。

在這種情況下,您可以在推送到生產之前將一組最終編譯的生產就緒資源添加到源代碼庫中,以便預編譯它們不需要在每次部署時在生產服務器上單獨進行。

如上所述,您可以使用以下命令執行此步驟:

$ RAILS_ENV=production rails assets:precompile

請注意以下注意事項:

  • 如果可用,將提供預編譯的資源 - 即使它們不再與原始(未編譯)資源匹配,即使在開發服務器上也是如此

    為了確保開發服務器始終即時編譯資源(並且始終反映代碼的最新狀態),開發環境 必須配置為將預編譯的資源保存在與生產不同的位置。 否則,任何為生產使用預編譯的資源將覆蓋開發中對它們的請求(即,您對資源所做的後續更改將不會在瀏覽器中反映出來)。 您可以通過將以下行添加到config/environments/development.rb文件中來實現:

config.assets.prefix = "/dev-assets"
  • 在部署工具(例如Capistrano)中禁用資源預編譯任務。
  • 開發系統上必須可用所需的壓縮器或最小化工具。

您還可以設置ENV["SECRET_KEY_BASE_DUMMY"]以觸發使用存儲在臨時文件中的隨機生成的secret_key_base。這在將資源預編譯為生產的一部分時非常有用,因為它不需要訪問生產密鑰的其他構建步驟。

$ SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile

5.3 即時編譯

在某些情況下,您可能希望使用即時編譯。在此模式下,管道中的所有資源請求都由Sprockets直接處理。

要啟用此選項,請設置:

config.assets.compile = true

在第一個請求中,資源將根據資源緩存存儲的規定進行編譯和緩存,並且輔助函數中使用的清單名稱將被修改以包含SHA256哈希。

Sprockets還將Cache-Control HTTP標頭設置為max-age=31536000。這向您的服務器和客戶端瀏覽器之間的所有緩存發出信號,表明此內容(提供的文件)可以緩存1年。這樣做的效果是減少來自您的服務器的對該資源的請求數量;該資源有很大機會在本地瀏覽器緩存或某個中間緩存中。

此模式使用的內存更多,性能比默認模式差,不建議使用。

5.4 CDN

CDN代表內容交付網絡,它們主要用於在全球範圍內緩存資源,以便當瀏覽器請求資源時,將有一個緩存的副本地理位置靠近該瀏覽器。如果您在生產中直接從Rails服務器提供資源,最佳做法是在應用程序前面使用CDN。

使用CDN的常見模式是將生產應用程序設置為“源”服務器。這意味著當瀏覽器從CDN請求資源並且缺少緩存時,它將即時從您的服務器獲取文件並緩存它。例如,如果您在example.com上運行Rails應用程序並在mycdnsubdomain.fictional-cdn.com上配置了CDN,那麼當請求mycdnsubdomain.fictional-cdn.com/assets/smile.png時,CDN將在example.com/assets/smile.png上向您的服務器發出一次請求並緩存該請求。對於相同URL的下一個請求,將命中緩存的副本。當CDN可以直接提供資源時,請求不會觸及您的Rails服務器。由於CDN的資源地理位置靠近瀏覽器,因此請求速度更快,並且由於您的服務器不需要花時間提供資源,它可以專注於盡快提供應用程序代碼。

5.4.1 設置CDN以提供靜態資源

要設置CDN,您必須在公共可用的URL上在生產環境中運行應用程序,例如example.com。接下來,您需要從雲主機提供商註冊CDN服務。在這樣做時,您需要將CDN的“源”配置為指向您的網站example.com。請查看您的提供商的文檔以了解如何配置源服務器。

您配置的CDN應該為您的應用程序提供一個自定義子域名,例如mycdnsubdomain.fictional-cdn.com(注意,fictional-cdn.com在撰寫本文時不是有效的CDN提供商)。現在,您已經配置了CDN服務器,您需要告訴瀏覽器使用CDN來獲取資源,而不是直接從您的Rails服務器獲取。您可以通過在Rails中配置您的資源主機(而不是使用相對路徑)來實現這一點。要在Rails中設置資源主機,您需要在config/environments/production.rb中設置config.asset_host

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

注意:您只需要提供“主機”,這是子域名和根域名,您不需要指定協議或“方案”,例如http://https://。當請求網頁時,生成的資源鏈接中的協議將與默認情況下訪問網頁的協議匹配。

您還可以通過環境變量設置此值,以便更輕鬆地運行站點的測試副本: ruby config.asset_host = ENV['CDN_HOST']

注意:您需要在伺服器上設置 CDN_HOSTmycdnsubdomain.fictional-cdn.com 才能使此功能正常運作。

一旦您配置了伺服器和 CDN,諸如以下幫助程式所產生的資產路徑:

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

將被渲染為完整的 CDN URL,例如 http://mycdnsubdomain.fictional-cdn.com/assets/smile.png (為了可讀性,摘要被省略)。

如果 CDN 有 smile.png 的副本,它將將其提供給瀏覽器,而您的伺服器甚至不知道它被請求了。如果 CDN 沒有副本,它將嘗試在「源」example.com/assets/smile.png 中找到它,然後將其存儲以供將來使用。

如果您只想從 CDN 提供某些資產,您可以在資產幫助程式中使用自定義的 :host 選項,該選項將覆蓋 config.action_controller.asset_host 中設定的值。

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

5.4.2 自定義 CDN 快取行為

CDN 通過快取內容來工作。如果 CDN 有過期或錯誤的內容,則對您的應用程序的幫助不大,反而有害。本節的目的是描述大多數 CDN 的一般快取行為。您的特定提供商可能會有稍微不同的行為。

5.4.2.1 CDN 請求快取

雖然 CDN 被描述為對快取資產有好處,但它實際上是對整個請求進行快取。這包括資產的主體以及任何標頭。其中最重要的是 Cache-Control,它告訴 CDN(和瀏覽器)如何快取內容。這意味著如果有人請求一個不存在的資產,例如 /assets/i-dont-exist.png,並且您的 Rails 應用程序返回 404,那麼如果存在有效的 Cache-Control 標頭,您的 CDN 可能會快取 404 頁面。

5.4.2.2 CDN 標頭調試

檢查 CDN 中的標頭是否正確快取的一種方法是使用 curl。您可以請求來自您的伺服器和 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 文件以獲取他們可能提供的任何其他信息,例如 X-Cache 或他們可能添加的任何其他標頭。

5.4.2.3 CDN 和 Cache-Control 標頭

Cache-Control 標頭描述了請求的快取方式。當不使用 CDN 時,瀏覽器將使用此信息來快取內容。這對於未修改的資產非常有用,這樣瀏覽器就不需要在每次請求時重新下載網站的 CSS 或 JavaScript。通常,我們希望我們的 Rails 伺服器告訴我們的 CDN(和瀏覽器)該資產是「公共的」。這意味著任何快取都可以存儲該請求。同樣,我們通常希望設置 max-age,這是快取在無效之前存儲該對象的時間。max-age 的值以秒為單位,最大值為 31536000,即一年。您可以在 Rails 應用程序中通過設置以下方式來實現:

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

現在,當您的應用程序在生產環境中提供資產時,CDN 將將該資產存儲長達一年。由於大多數 CDN 也會快取請求的標頭,這個 Cache-Control 將傳遞給所有未來尋求此資產的瀏覽器。然後,瀏覽器知道在需要重新請求之前可以將此資產存儲很長時間。

5.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 中設置遠期 max-age(您確實希望這樣做),請確保在更改資產時使快取失效。例如,當將圖像中的笑臉從黃色更改為藍色時,您希望網站的所有訪問者都能獲得新的藍色笑臉。使用 Rails 資產管道時,config.assets.digest 默認為 true,這樣每個資產在更改時都會有不同的文件名。這樣,您就不必手動使快取中的任何項目失效。通過使用不同的唯一資產名稱,您的用戶可以獲得最新的資產。

6 自訂化流程

6.1 CSS 壓縮

壓縮 CSS 的選項之一是 YUI。YUI CSS compressor 提供了壓縮功能。

以下程式碼啟用 YUI 壓縮,需要 yui-compressor gem。

config.assets.css_compressor = :yui

6.2 JavaScript 壓縮

JavaScript 壓縮的可能選項有 :terser:closure:yui。這些選項分別需要使用 terserclosure-compileryui-compressor gem。

terser gem 為例。 這個 gem 將 Terser(用於 Node.js)封裝成 Ruby。它通過刪除空格和註釋、縮短局部變量名稱以及執行其他微優化操作(例如在可能的情況下將 ifelse 語句更改為三元運算符)來壓縮代碼。

以下程式碼調用 terser 進行 JavaScript 壓縮。

config.assets.js_compressor = :terser

注意:使用 terser 需要一個支援 ExecJS 的運行時。如果你使用的是 macOS 或 Windows,你的操作系統已經安裝了一個 JavaScript 運行時。

注意:當你通過 importmap-railsjsbundling-rails gem 加載資源時,JavaScript 壓縮也會適用於你的 JavaScript 檔案。

6.3 壓縮資源

預設情況下,編譯後的資源將生成壓縮和未壓縮的版本。壓縮後的資源有助於減少數據在網絡上的傳輸。你可以通過設置 gzip 標誌來配置此功能。

config.assets.gzip = false # 禁用壓縮資源的生成

請參考你的網頁伺服器的文檔,了解如何提供壓縮資源的指示。

6.4 使用自定義的壓縮器

CSS 和 JavaScript 的壓縮器配置設置也可以接受任何對象。該對象必須具有一個接受字符串作為唯一參數的 compress 方法,並且必須返回一個字符串。

class Transformer
  def compress(string)
    do_something_returning_a_string(string)
  end
end

要啟用此功能,請將一個新的對象傳遞給 application.rb 中的配置選項:

config.assets.css_compressor = Transformer.new

6.5 更改 assets 路徑

Sprockets 默認使用的公共路徑是 /assets

你可以將其更改為其他路徑:

config.assets.prefix = "/some_other_path"

如果你正在更新一個之前沒有使用資源管道並且已經使用此路徑的舊項目,或者你希望為新資源使用此路徑,這是一個方便的選項。

6.6 X-Sendfile 標頭

X-Sendfile 標頭是一個指示網頁伺服器忽略應用程序的響應,而是從磁盤上提供指定文件的指令。默認情況下,此選項是關閉的,但如果你的伺服器支援,可以啟用它。啟用後,這將把提供文件的責任轉交給網頁伺服器,這樣可以更快地提供文件。請參考 send_file 以了解如何使用此功能。

Apache 和 NGINX 支援此選項,可以在 config/environments/production.rb 中啟用:

# config.action_dispatch.x_sendfile_header = "X-Sendfile" # 用於 Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # 用於 NGINX

警告:如果你正在升級一個現有的應用程序並打算使用此選項,請注意只將此配置選項粘貼到 production.rb 和任何其他定義了生產行為的環境中(而不是 application.rb)。

提示:有關詳細信息,請查看你的生產網頁伺服器的文檔:

7 資源快取存儲

默認情況下,Sprockets 在開發和生產環境中將資源快取到 tmp/cache/assets。你可以按照以下方式進行更改:

config.assets.configure do |env|
  env.cache = ActiveSupport::Cache.lookup_store(:memory_store,
                                                { size: 32.megabytes })
end

要禁用資源快取存儲:

config.assets.configure do |env|
  env.cache = ActiveSupport::Cache.lookup_store(:null_store)
end

8 將資源添加到你的 Gems

資源也可以來自於以 gem 形式提供的外部來源。

一個很好的例子是 jquery-rails gem。 這個 gem 包含一個繼承自 Rails::Engine 的引擎類。通過這樣做,Rails 會知道這個 gem 的目錄可能包含資源,並將這個引擎的 app/assetslib/assetsvendor/assets 目錄添加到 Sprockets 的搜索路徑中。

9 使你的庫或 gem 成為預處理器

Sprockets 使用處理器(Processors)、轉換器(Transformers)、壓縮器(Compressors)和導出器(Exporters)來擴展 Sprockets 的功能。請參考 擴展 Sprockets 以了解更多。這裡我們註冊了一個預處理器,將一個註釋添加到 text/css(.css)檔案的末尾。

module AddComment
  def self.call(input)
    { data: input[:data] + "/* Hello From my sprockets extension */" }
  end
end

現在你有了一個修改輸入資料的模塊,是時候將其註冊為你的 MIME 類型的預處理器了。 ruby Sprockets.register_preprocessor 'text/css', AddComment

10 替代庫

多年來,處理資源的方式有多種預設方法。隨著網路的演進,我們開始看到越來越多以JavaScript為主的應用程式。在《Rails信條》中,我們相信「菜單是大廚選擇」,所以我們專注於預設設定:使用Sprockets和Import Maps

我們知道,對於各種JavaScript和CSS框架/擴展,沒有一個適用於所有情況的解決方案。在Rails生態系統中,還有其他捆綁庫可以在預設設定不足的情況下提供支援。

10.1 jsbundling-rails

jsbundling-rails是一個依賴於Node.js的選擇,用於使用esbuildrollup.jsWebpack將JavaScript進行捆綁。

這個gem提供了yarn build --watch的過程,以在開發中自動生成輸出。對於生產環境,它會自動將javascript:build任務鉤入assets:precompile任務,以確保所有的套件依賴都已安裝並為所有的入口點建立了JavaScript。

何時使用而不是importmap-rails 如果您的JavaScript代碼依賴於轉譯,例如使用BabelTypeScript或React的JSX格式,那麼jsbundling-rails是正確的選擇。

10.2 Webpacker/Shakapacker

Webpacker是Rails 5和6的預設JavaScript預處理器和捆綁工具。它現在已經停用。有一個名為shakapacker的後繼者存在,但它不由Rails團隊或項目維護。

與此列表中的其他庫不同,webpacker/shakapacker完全獨立於Sprockets,可以處理JavaScript和CSS文件。請閱讀Webpacker指南以了解更多信息。

注意:請閱讀與Webpacker的比較文件,以了解jsbundling-railswebpacker/shakapacker之間的差異。

10.3 cssbundling-rails

cssbundling-rails允許使用Tailwind CSSBootstrapBulmaPostCSSDart Sass進行CSS的捆綁和處理,然後通過資源管道傳遞CSS。

它的工作方式與jsbundling-rails類似,因此在應用程式中添加了Node.js依賴,並提供了yarn build:css --watch的過程,以在開發中重新生成樣式表,並在生產環境中鉤入assets:precompile任務。

與Sprockets的區別是什麼? 單獨的Sprockets無法將Sass轉譯為CSS,需要使用Node.js從.sass文件生成.css文件。一旦生成了.css文件,Sprockets就能夠將它們傳遞給客戶端。

注意:cssbundling-rails依賴於Node來處理CSS。dartsass-railstailwindcss-rails gem使用獨立版本的Tailwind CSS和Dart Sass,因此不需要Node依賴。如果您使用importmap-rails來處理JavaScript,並使用dartsass-railstailwindcss-rails來處理CSS,則可以完全避免Node依賴,從而獲得更簡單的解決方案。

10.4 dartsass-rails

如果您想在應用程式中使用Sassdartsass-rails是對傳統的sassc-rails gem的替代品。dartsass-rails使用了2020年被棄用的LibSassDart Sass實現。

sassc-rails不同,這個新的gem沒有直接集成到Sprockets中。有關安裝/遷移說明,請參閱gem主頁

警告:自2019年以來,受歡迎的sassc-rails gem已停止維護。

10.5 tailwindcss-rails

tailwindcss-rails是Tailwind CSS v3框架的獨立執行版本的包裝gem。當在rails new命令中提供--css tailwind時,用於新應用程式。在開發中,它提供了一個watch過程,以自動生成Tailwind輸出。對於生產環境,它鉤入assets:precompile任務。

回饋

歡迎協助提升本指南的品質。

如果您發現任何錯別字或事實錯誤,請貢獻您的力量。 開始之前,您可以閱讀我們的 文件貢獻 部分。

您也可能會發現不完整的內容或過時的資訊。 請為主要的文件補充任何遺漏的內容。請先檢查 Edge 指南,以確認問題是否已經修復或尚未在主分支上修復。 請參考 Ruby on Rails 指南指引 以了解風格和慣例。

如果您發現需要修復但無法自行修補的問題,請 開啟一個問題

最後但同樣重要的是,關於 Ruby on Rails 文件的任何討論都非常歡迎在 官方 Ruby on Rails 論壇 上進行。