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 的默认资产管道。这两个 gem 的主要限制是它们不支持转译,因此您无法使用 Babel、TypeScript、Sass、React JSX 格式或 Tailwind CSS 等功能。如果您需要为 JavaScript/CSS 进行转译,我们建议您阅读 替代库部分。
2 主要功能
资产管道的第一个功能是将 SHA256 指纹插入每个文件名,以便该文件被 Web 浏览器和 CDN 缓存。当您更改文件内容时,此指纹会自动更新,这会使缓存失效。
资产管道的第二个功能是使用 导入地图 提供 JavaScript 文件。这使您能够使用为 ES 模块 (ESM) 制定的 JavaScript 库构建现代应用程序,而无需转译和捆绑。反过来,这消除了对 Webpack、yarn、node 或 JavaScript 工具链中任何其他部分的需求。
资产管道的第三个功能是将所有 CSS 文件连接到一个主要的 .css
文件中,然后对其进行缩小或压缩。正如您将在本指南的后面学习到的,您可以自定义此策略以根据需要对文件进行分组。在生产环境中,Rails 会将 SHA256 指纹插入每个文件名,以便该文件被 Web 浏览器缓存。您可以通过更改此指纹来使缓存失效,当您更改文件内容时会自动发生。
资产管道的第四个功能是它允许通过更高层次的 CSS 语言来编码资产。
2.1 什么是指纹,为什么我应该关心它?
指纹是一种使文件名依赖于文件内容的技术。当文件内容更改时,文件名也会更改。对于静态或不经常更改的内容,这提供了一种简单的方法来判断两个版本的特定文件是否相同,即使是在不同的服务器或部署日期之间。
当文件名是唯一的并且基于其内容时,可以设置 HTTP 标头以鼓励各处(无论是在 CDN、ISP、网络设备还是 Web 浏览器中)的缓存保留其自身的副本。当内容更新时,指纹将会更改。这将导致远程客户端请求内容的新副本。这通常称为缓存清除。
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:[email protected]/index.js"
现在,导入地图来了,我们将 react
名称定义为固定到 https://ga.jspm.io/npm:[email protected]/index.js
地址。有了这些信息,我们的浏览器将接受简化的 import React from "react"
定义。将导入地图视为库源地址的别名。
3.2 用法
使用importmap-rails
,您可以创建导入映射配置文件,将库路径固定到一个名称
# config/importmap.rb
pin "application"
pin "react", to: "https://ga.jspm.io/npm:[email protected]/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:[email protected]/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 的自述文件。
3.3 通过 JavaScript CDN 使用 npm 包
您可以使用作为 importmap-rails
安装的一部分添加的 bin/importmap
命令来固定、取消固定或更新导入映射中的 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 提供商,例如 unpkg
或 jsdelivr
(jspm
是默认值)
$ bin/importmap pin react --from jsdelivr
Pinning "react" to https://cdn.jsdelivr.net.cn/npm/[email protected]/index.js
但是,请记住,如果您将固定项从一个提供商切换到另一个提供商,您可能需要清理第一个提供商添加的但第二个提供商未使用 的依赖项。
运行 bin/importmap
以查看所有选项。
请注意,此命令仅仅是一个方便的包装器,用于将逻辑包名称解析为 CDN URL。您也可以自己查找 CDN URL,然后固定这些 URL。例如,如果您想使用 Skypack 来获取 React,您可以将以下内容添加到 config/importmap.rb
中
pin "react", to: "https://cdn.skypack.dev/react"
3.4 预加载固定的模块
为了避免浏览器必须在加载最深层的嵌套导入之前一个接一个地加载文件的瀑布效应,importmap-rails 支持 模块预加载链接。通过将 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.cn/npm/[email protected]/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/[email protected]/dist/index.js">
...
有关最新文档,请参阅 importmap-rails
存储库。
4 如何使用 Sprockets
将您的应用程序资源公开到网络的简单方法是将它们存储在 public
文件夹的子目录中,例如 images
和 stylesheets
。手动这样做会很困难,因为大多数现代 Web 应用程序都需要以特定方式处理资源,例如压缩和向资源添加指纹。
Sprockets 旨在自动预处理存储在配置目录中的资源,并在处理后将它们公开在 public/assets
文件夹中,并提供指纹、压缩、源地图生成和其他可配置功能。
资源仍然可以放在 public
层次结构中。当 config.public_file_server.enabled
设置为 true 时,public
下的任何资源都将作为应用程序或 Web 服务器的静态文件提供服务。您必须为必须经过一些预处理才能提供服务的 文件定义 manifest.js
指令。
在生产环境中,Rails 默认情况下会将这些文件预编译到 public/assets
。然后,预编译的副本将作为 Web 服务器的静态资源提供服务。app/assets
中的文件永远不会在生产环境中直接提供服务。
4.1 清单文件和指令
使用 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 控制器特定资源
当您生成脚手架或控制器时,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 资源组织
管道资源可以放置在应用程序中的三个位置之一:app/assets
、lib/assets
或 vendor/assets
。
app/assets
用于应用程序拥有的资源,例如自定义图像或样式表。app/javascript
用于您的 JavaScript 代码vendor/[assets|javascript]
用于外部实体拥有的资源,例如 CSS 框架或 JavaScript 库。请记住,包含对资产管道(图像、样式表等)也处理的其他文件引用的第三方代码需要重写为使用asset_path
等助手。
其他位置可以在 manifest.js
文件中配置,请参阅 清单文件和指令。
4.3.1 搜索路径
从清单或助手引用文件时,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
指令。
它也与 public/library_name/index.html
中的文件可以通过对 /library_name
的请求访问的方式类似。这意味着您不能直接使用索引文件。
可以在 .css
文件中访问整个库,如下所示
/* ...
*= require library_name
*/
这简化了维护工作并保持代码整洁,因为它允许将相关代码在包含到其他地方之前进行分组。
4.4 对资源进行编码链接
Sprockets 不会添加任何新方法来访问您的资源 - 您仍然使用熟悉的 stylesheet_link_tag
<%= stylesheet_link_tag "application", media: "all" %>
如果使用 turbo-rails
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
处存在文件,则该文件由 Web 服务器提供服务。
或者,对具有 SHA256 哈希的文件(例如 public/assets/rails-f90d8a84c707a8dc923fca1ca1895ae8ed0a09237f6992015fef1e11be77c023.png
)的请求将以相同方式处理。这些哈希值是如何生成的将在本指南后面的 生产环境 部分中介绍。
如果需要,图像也可以组织到子目录中,然后可以通过在标签中指定目录名称来访问它们
<%= image_tag "icons/rails.png" %>
如果您正在预编译资源(请参阅下面的 生产环境),则链接到不存在的资源会在调用页面中引发异常。这包括链接到空字符串。因此,在使用 image_tag
和其他助手处理用户提供的数据时要小心。
4.4.1 CSS 和 ERB
资产管道会自动评估 ERB。这意味着如果您向 CSS 资源添加了 erb
扩展名(例如 application.css.erb
),那么 asset_path
等助手将在您的 CSS 规则中可用
.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' %>) }
这会将格式正确的 data 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
当此选项为真时,将为资产 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
# 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` 存储在临时文件中。这在将资产预编译到生产环境中时非常有用,因为这属于构建步骤,而该构建步骤无需访问生产秘密。
$ 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(和 Web 浏览器)如何缓存内容。这意味着如果有人请求一个不存在的资产,比如 `/assets/i-dont-exist.png`,并且您的 Rails 应用程序返回 404,那么您的 CDN 很可能会缓存 404 页面,前提是存在有效的 `Cache-Control` 标头。
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
如果要设置远期的 `max-age` 在您的 `Cache-Control` 中(实际上您需要这样做),那么确保在更改资产时使缓存失效。例如,当将图像中的笑脸从黄色更改为蓝色时,您希望所有访问您网站的访问者都能看到新的蓝色笑脸。当使用带 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` 标志来配置此选项。
config.assets.gzip = false # disable gzipped assets generation
请参阅您的 Web 服务器文档,了解有关如何提供压缩资产的说明。
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 标头是指示 Web 服务器忽略来自应用程序的响应,而是从磁盘提供指定文件的指令。此选项默认情况下处于关闭状态,但如果您的服务器支持,则可以启用它。启用后,这会将提供文件责任传递给 Web 服务器,速度更快。请查看 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`)。
有关更多详细信息,请查看您生产 Web 服务器的文档
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 替代库
多年来,处理资产有多种默认方法。Web 发展了,我们开始看到越来越多的以 JavaScript 为主的应用程序。在 Rails 教义中,我们相信 菜单是 Omakase,因此我们专注于默认设置:**Sprockets with Import Maps**。
我们意识到,对于各种可用的 JavaScript 和 CSS 框架/扩展来说,没有一个通用的解决方案。Rails 生态系统中还有其他捆绑库,这些库应该在默认设置不够用时为您提供帮助。
10.1 jsbundling-rails
jsbundling-rails
是使用 Bun、 esbuild、 rollup.js 或 Webpack 捆绑 JS 的 `importmap-rails` 方式的 JavaScript 运行时依赖的替代方案。
此 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` 的方式工作,因此使用 `yarn build:css --watch` 过程将 Node.js 依赖项添加到您的应用程序中,以在开发环境中重新生成样式表,并在生产环境中挂钩到 `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。用于使用 --css tailwind
命令创建新应用程序时。提供 watch
进程,在开发过程中自动生成 Tailwind 输出。对于生产环境,它会挂钩到 assets:precompile
任务。