Rails的静态资源管理(四)—— 生产环境的 Asset Pipeline
官方文档: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
,一旦静态资源文件的内容发生变化,其文件名就会发生变化。这样,我们就不需要每次手动使某个静态资源文件的缓存失效。通过使用唯一的新文件名,我们就能确保用户访问的总是静态资源文件的最新版本。