1 什麼是資產管道?
資產管道提供一個框架來處理 JavaScript 和 CSS 資產的交付。這是透過利用 HTTP/2 等技術和串聯與最小化等技術來完成的。最後,它允許您的應用程式自動與來自其他 gem 的資產結合。
資產管道由 importmap-rails、sprockets 和 sprockets-rails gem 實作,並且預設啟用。您可以在建立新應用程式時傳遞 --skip-asset-pipeline
選項來停用它。
$ rails new appname --skip-asset-pipeline
本指南著重於僅使用 sprockets
處理 CSS 和 importmap-rails
處理 JavaScript 的預設資產管道。這兩者的主要限制是它們不支援轉譯,因此您無法使用 Babel、TypeScript、Sass、React JSX 格式或 Tailwind CSS 等工具。如果您需要為您的 JavaScript/CSS 進行轉譯,我們建議您閱讀替代程式庫章節。
2 主要功能
資產管道的第一個功能是在每個檔案名稱中插入 SHA256 指紋識別,以便讓網頁瀏覽器和 CDN 快取該檔案。當您變更檔案內容時,此指紋會自動更新,這會使快取失效。
資產管道的第二個功能是在提供 JavaScript 檔案時使用 匯入地圖。這讓您可以使用專為 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 什麼是匯入地圖,我為什麼要在意?
匯入地圖讓您可以使用映射到已版本化/已摘要的檔案的邏輯名稱來匯入 JavaScript 模組,直接從瀏覽器匯入。因此,您可以使用專為 ES 模組 (ESM) 製作的 JavaScript 程式庫來建構現代 JavaScript 應用程式,而無需進行轉譯或捆綁。
透過這種方法,您將發送許多小的 JavaScript 檔案,而不是一個大的 JavaScript 檔案。由於 HTTP/2 在初始傳輸期間不再帶來實質的效能損失,並且實際上由於更好的快取動態性而在長期內提供實質的優勢。
3 如何使用匯入地圖作為 JavaScript 資產管道
匯入地圖是預設的 JavaScript 處理器,產生匯入地圖的邏輯由 importmap-rails
gem 處理。
匯入地圖僅用於 JavaScript 檔案,不能用於 CSS 交付。請查看 Sprockets 章節以了解有關 CSS 的資訊。
您可以在 Gem 首頁上找到詳細的使用說明,但了解 importmap-rails
的基礎知識非常重要。
3.1 運作方式
匯入地圖本質上是對所謂的「裸模組規範」的字串替換。它們允許您標準化 JavaScript 模組匯入的名稱。
以這樣的匯入定義為例,如果沒有匯入地圖,它將無法運作
import React from "react"
你必須像這樣定義它才能使其運作
import React from "https://ga.jspm.io/npm:react@17.0.2/index.js"
接著是導入映射(import map),我們將 react
名稱指定為指向 https://ga.jspm.io/npm:react@17.0.2/index.js
這個位址。有了這些資訊,我們的瀏覽器就能接受簡化的 import React from "react"
定義。可以把導入映射想成是函式庫來源位址的別名。
3.2 使用方式
透過 importmap-rails
,你可以建立導入映射設定檔,將函式庫路徑對應到一個名稱。
# config/importmap.rb
pin "application"
pin "react", to: "https://ga.jspm.io/npm:react@17.0.2/index.js"
所有設定好的導入映射都應該透過加入 <%= javascript_importmap_tags %>
到應用程式的 <head>
元素中。javascript_importmap_tags
會在 head
元素中渲染一系列的腳本。
- 包含所有設定好的導入映射的 JSON
<script type="importmap">
{
"imports": {
"application": "/assets/application-39f16dc3f3....js"
"react": "https://ga.jspm.io/npm:react@17.0.2/index.js"
}
}
</script>
- 從
app/javascript/application.js
載入 JavaScript 的進入點。
<script type="module">import "application"</script>
在 v2.0.0 之前,importmap-rails
會將 Es-module-shims
放入 javascript_importmap_tags
的輸出中作為 polyfill,以確保舊版瀏覽器支援導入映射。然而,隨著所有主流瀏覽器原生支援導入映射,v2.0.0 已經捨棄了捆綁的 shim。如果你想支援不支援導入映射的舊版瀏覽器,請在 javascript_importmap_tags
之前手動插入 Es-module-shims
。更多資訊,請參閱 importmap-rails 的 README。
3.3 透過 JavaScript CDN 使用 npm 套件
你可以使用 bin/importmap
命令,這個命令是 importmap-rails
安裝的一部分,用來在你的導入映射中釘選、取消釘選或更新 npm 套件。這個 binstub 使用 JSPM.org
。
它的運作方式如下:
$ bin/importmap pin react react-dom
Pinning "react" to https://ga.jspm.io/npm:react@17.0.2/index.js
Pinning "react-dom" to https://ga.jspm.io/npm:react-dom@17.0.2/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js
Pinning "scheduler" to https://ga.jspm.io/npm:scheduler@0.20.2/index.js
bin/importmap json
{
"imports": {
"application": "/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js",
"react": "https://ga.jspm.io/npm:react@17.0.2/index.js",
"react-dom": "https://ga.jspm.io/npm:react-dom@17.0.2/index.js",
"object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js",
"scheduler": "https://ga.jspm.io/npm:scheduler@0.20.2/index.js"
}
}
如你所見,透過 jspm 預設解析,react 和 react-dom 這兩個套件總共解析為四個相依性。
現在你可以在你的 application.js
進入點中使用它們,就像使用任何其他模組一樣。
import React from "react"
import ReactDOM from "react-dom"
你也可以指定一個特定的版本來釘選。
$ bin/importmap pin react@17.0.1
Pinning "react" to https://ga.jspm.io/npm:react@17.0.1/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js
或者甚至移除釘選。
$ bin/importmap unpin react
Unpinning "react"
Unpinning "object-assign"
你可以為具有獨立「production」(預設)和「development」建置版本的套件控制其環境。
$ bin/importmap pin react --env development
Pinning "react" to https://ga.jspm.io/npm:react@17.0.2/dev.index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js
你也可以在釘選時選擇其他支援的 CDN 提供者,例如 unpkg
或 jsdelivr
(jspm
是預設值)。
$ bin/importmap pin react --from jsdelivr
Pinning "react" to https://cdn.jsdelivr.net/npm/react@17.0.2/index.js
但請記住,如果你將一個釘選從一個提供者切換到另一個提供者,你可能必須清除第一個提供者新增但第二個提供者未使用的相依性。
執行 bin/importmap
以查看所有選項。
請注意,此命令只是一個方便的包裝器,用於將邏輯套件名稱解析為 CDN URL。你也可以自己查找 CDN URL,然後釘選它們。例如,如果你想使用 Skypack for 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/hotkey@1.4.4/dist/index.js", preload: true
pin "md5", to: "https://cdn.jsdelivr.net/npm/md5@2.3.0/md5.js"
<%# app/views/layouts/application.html.erb %>
<%= javascript_importmap_tags %>
<%# will include the following link before the importmap is setup: %>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@github/hotkey@1.4.4/dist/index.js">
...
請參閱 importmap-rails
儲存庫以取得最新的文件。
4 如何使用 Sprockets
將應用程式資源暴露於網路最直接的方式是將它們儲存在 public
資料夾的子目錄中,例如 images
和 stylesheets
。手動執行此操作會很困難,因為大多數現代網路應用程式都需要以特定的方式處理資源,例如壓縮和為資源新增指紋。
Sprockets 的設計目的是自動預先處理儲存在設定的目錄中的資源,並在處理後將它們暴露在 public/assets
資料夾中,並具有指紋、壓縮、產生 source map 和其他可設定的功能。
資源仍然可以放置在 public
階層中。當 config.public_file_server.enabled
設定為 true 時,應用程式或網路伺服器會將 public
下的任何資源作為靜態檔案提供。你必須為必須在提供服務之前經過某些預先處理的檔案定義 manifest.js
指令。
在生產環境中,Rails 預設會將這些檔案預先編譯到 public/assets
。然後,預先編譯的副本會由網路伺服器作為靜態資源提供。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.css
和 marketing.css
檔案,它會允許你從你的視圖使用 <%= stylesheet_link_tag "application" %>
或 <%= stylesheet_link_tag "marketing" %>
來載入這些樣式表。
你可能會注意到,我們的 JavaScript 檔案預設不是從 assets
目錄載入的,這是因為 ./app/javascript
是 importmap-rails
gem 的預設進入點,而 vendor
資料夾是儲存下載的 JS 套件的地方。
在 manifest.js
中,你也可以指定 link
指令來載入特定的檔案,而不是整個目錄。link
指令需要提供明確的檔案副檔名。
Sprockets 載入指定的檔案,在必要時處理它們,將它們串連成一個單個檔案,然後壓縮它們(根據 config.assets.css_compressor
或 config.assets.js_compressor
的值)。壓縮可以減少檔案大小,使瀏覽器更快地下載檔案。
4.2 控制器特定的資源
當你產生 scaffold 或控制器時,Rails 也會為該控制器產生一個層疊樣式表檔案。此外,當產生 scaffold 時,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 資源組織
管道資源可以放置在應用程式內的三個位置之一:app/assets
、lib/assets
或 vendor/assets
。
app/assets
用於應用程式擁有的資源,例如自訂圖片或樣式表。app/javascript
用於你的 JavaScript 程式碼vendor/[assets|javascript]
用於外部實體擁有的資源,例如 CSS 框架或 JavaScript 函式庫。請記住,引用其他由資源管道處理的檔案(圖片、樣式表等)的第三方程式碼,需要重寫以使用諸如asset_path
之類的助手。
其他位置可以在 manifest.js
檔案中設定,請參閱Manifest 檔案和指令。
4.3.1 搜尋路徑
當從 manifest 或 helper 引用檔案時,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
會作為此函式庫中所有檔案的 manifest。此檔案可以包含按順序排列的所有必要檔案的清單,或簡單的 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" %>
如果使用 turbo-rails
gem,該 gem 預設包含在 Rails 中,請包含 data-turbo-track
選項,這會導致 Turbo 檢查資源是否已更新,如果已更新則將其載入到頁面中。
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
在一般視圖中,你可以像這樣存取 app/assets/images
目錄中的圖片:
<%= image_tag "rails.png" %>
前提是管道已在你的應用程式中啟用(且未在目前的環境內容中停用),此檔案由 Sprockets 提供。如果 public/assets/rails.png
中存在檔案,則會由網路伺服器提供。
或者,對具有 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
中,則會引用該路徑。
如果你想使用 資料 URI - 一種將圖片資料直接嵌入 CSS 檔案的方法 - 你可以使用 asset_data_uri
輔助方法。
#logo { background: url(<%= asset_data_uri 'logo.png' %>) }
這會將格式正確的資料 URI 插入 CSS 來源中。
請注意,結束標籤不能使用 -%>
這種樣式。
4.5 當找不到資源時引發錯誤
如果你正在使用 sprockets-rails >= 3.2.0,你可以設定當執行資源查找但找不到任何資源時會發生什麼。如果你關閉「資源回退」,則當找不到資源時會引發錯誤。
config.assets.unknown_asset_fallback = false
如果啟用「資源回退」,則當找不到資源時,會輸出路徑,且不會引發錯誤。預設情況下,資源回退行為是停用的。
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 假設資源已預先編譯,並將由你的網頁伺服器作為靜態資源提供服務。
在預先編譯階段,會從已編譯檔案的內容生成 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 遠期過期標頭
預先編譯的資源存在於檔案系統中,並由你的網頁伺服器直接提供服務。它們預設沒有遠期標頭,因此若要獲得指紋的好處,你必須更新伺服器設定以加入這些標頭。
對於 Apache
# The Expires* directives requires the Apache module
# `mod_expires` to be enabled.
<Location /assets/>
# Use of ETag is discouraged when Last-Modified is present
Header unset ETag
FileETag None
# RFC says only cache for 1 year
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 請求會命中快取副本。當 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 以將你的 CDN 設定為資源主機,而不是使用相對路徑來執行此操作。若要在 Rails 中設定你的資源主機,你需要設定 config/environments/production.rb
中的 config.asset_host
config.asset_host = "mycdnsubdomain.fictional-cdn.com"
你只需要提供「主機」,這是子網域和根網域,你不需要指定通訊協定或「配置」,例如 http://
或 https://
。當請求網頁時,預設情況下,產生的資源連結中的通訊協定將與網頁的存取方式一致。
你也可以透過 環境變數 設定此值,讓執行網站的暫存副本更容易
config.asset_host = ENV["CDN_HOST"]
您需要在伺服器上將 CDN_HOST
設定為 mycdnsubdomain.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 資源管道的 CDN 時,預設會將 config.assets.digest
設定為 true,這樣每個資源在變更時都會有不同的檔案名稱。這樣您就永遠不必手動使快取中的任何項目失效。透過使用不同的唯一資源名稱,您的使用者可以獲得最新的資源。
6 自訂管道
6.1 CSS 壓縮
壓縮 CSS 的選項之一是 YUI。YUI CSS 壓縮器提供最小化功能。
以下這一行會啟用 YUI 壓縮,並需要 yui-compressor
gem。
config.assets.css_compressor = :yui
6.2 JavaScript 壓縮
JavaScript 壓縮的可能選項有 :terser
、:closure
和 :yui
。這些選項分別需要使用 terser
、closure-compiler
或 yui-compressor
gem。
以 terser
gem 為例。這個 gem 將 Terser (為 Node.js 撰寫) 包裝在 Ruby 中。它會透過移除空白和註解、縮短區域變數名稱,以及執行其他微最佳化來壓縮您的程式碼,例如在可能的情況下將 if
和 else
陳述式變更為三元運算子。
以下這一行會調用 terser
來進行 JavaScript 壓縮。
config.assets.js_compressor = :terser
您需要一個 ExecJS 支援的執行環境才能使用 terser
。如果您使用的是 macOS 或 Windows,您的作業系統中已安裝 JavaScript 執行環境。
當您透過 importmap-rails
或 jsbundling-rails
gem 載入資源時,JavaScript 壓縮也適用於您的 JavaScript 檔案。
6.3 GZIP 您的資源
預設情況下,除了非 GZIP 版本的資源外,也會產生編譯資源的 GZIP 版本。GZIP 資源有助於減少透過網路傳輸的資料量。您可以透過設定 gzip
旗標來設定此功能。
config.assets.gzip = false # disable gzipped assets generation
請參閱您的網頁伺服器文件,以了解如何提供 GZIP 資源的相關說明。
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" # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for 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 將資源新增至您的 Gem
資源也可以來自外部來源,以 Gem 的形式存在。
一個很好的例子是 jquery-rails
gem。這個 gem 包含一個從 Rails::Engine
繼承的引擎類別。這樣做,Rails 會得知這個 gem 的目錄可能包含資源,並且將此引擎的 app/assets
、lib/assets
和 vendor/assets
目錄新增至 Sprockets 的搜尋路徑。
9 將您的程式庫或 Gem 設為預處理器
Sprockets 使用處理器、轉換器、壓縮器和匯出器來擴充 Sprockets 的功能。請查看 擴充 Sprockets 以了解更多資訊。在這裡,我們註冊了一個預處理器,以便在 text/css (.css
) 檔案的結尾新增註解。
module AddComment
def self.call(input)
{ data: input[:data] + "/* Hello From my sprockets extension */" }
end
end
現在您已經有一個可以修改輸入資料的模組,是時候將其註冊為您的 MIME 類型的預處理器了。
Sprockets.register_preprocessor "text/css", AddComment
10 替代程式庫
多年來,處理資源的方式有很多種預設方法。網路不斷演進,我們開始看到越來越多以 JavaScript 為主的應用程式。在 Rails 原則中,我們認為 菜單是主廚推薦,因此我們專注於預設設定:Sprockets 與 Import Maps。
我們知道,對於各種可用的 JavaScript 和 CSS 框架/擴充功能,沒有一體適用的解決方案。Rails 生態系統中還有其他捆綁程式庫,在預設設定不足的情況下,它們應該可以讓您更有能力。
10.1 jsbundling-rails
jsbundling-rails
是一種依賴 JavaScript 執行環境的替代方案,可以取代使用 Bun、 esbuild、 rollup.js 或 Webpack 捆綁 JS 的 importmap-rails
方法。
這個 gem 在 package.json
中提供了一個建置工作,用於監看變更並在開發過程中自動產生輸出。對於生產環境,它會自動將 javascript:build
工作掛接到 assets:precompile
工作,以確保已安裝所有套件相依性,並為所有進入點建置了 JavaScript。
何時取代 importmap-rails
使用? 如果您的 JavaScript 程式碼依賴於轉譯,例如您正在使用 Babel、TypeScript 或 React JSX 格式,那麼 jsbundling-rails
是正確的選擇。
10.2 Webpacker/Shakapacker
Webpacker
是 Rails 5 和 6 的預設 JavaScript 預處理器和捆綁器。現在已停用。存在一個名為 shakapacker
的後繼者,但不是由 Rails 團隊或專案維護。
與此清單中的其他程式庫不同,webpacker
/shakapacker
完全獨立於 Sprockets,並且可以處理 JavaScript 和 CSS 檔案。
請閱讀 與 Webpacker 的比較 文件,以了解 jsbundling-rails
與 webpacker
/shakapacker
之間的差異。
10.3 cssbundling-rails
cssbundling-rails
允許使用 Tailwind CSS、Bootstrap、Bulma、PostCSS 或 Dart 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-rails
和 tailwindcss-rails
gem 使用獨立版本的 Tailwind CSS 和 Dart Sass,這表示不需要 Node 相依性。如果您使用 importmap-rails
來處理您的 JavaScript,並使用 dartsass-rails
或 tailwindcss-rails
來處理 CSS,您可以完全避免 Node 相依性,從而獲得更簡化的解決方案。
10.4 dartsass-rails
如果您想在您的應用程式中使用 Sass,dartsass-rails
可以替代舊有的 sassc-rails
gem。dartsass-rails
使用 Dart Sass 實作,取代 2020 年已棄用的 LibSass,而 sassc-rails
使用的是 LibSass。
與 sassc-rails
不同,這個新的 gem 並未直接與 Sprockets 整合。請參閱 gem 首頁 以獲取安裝/遷移說明。
熱門的 sassc-rails
gem 自 2019 年起已不再維護。
10.5 tailwindcss-rails
tailwindcss-rails
是 Tailwind CSS v3 框架的獨立可執行版本的封裝 gem。當使用 rails new
命令並提供 --css tailwind
時,會用於新的應用程式。它提供了一個 watch
處理程序,可在開發過程中自動產生 Tailwind 輸出。對於生產環境,它會掛鉤到 assets:precompile
任務。