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,一旦静态资源文件的内容发生变化,其文件名就会发生变化。这样,咱们就不须要每次手动使某个静态资源文件的缓存失效。经过使用惟一的新文件名,咱们就能确保用户访问的老是静态资源文件的最新版本。

相关文章
相关标签/搜索