# Asset Pipeline
本文介绍 Asset Pipeline。
读完本文,你将学到:
* Asset Pipeline 是什么以及其作用;
* 如何合理组织程序的静态资源;
* Asset Pipeline 的优势;
* 如何向 Asset Pipeline 中添加预处理器;
* 如何在 gem 中打包静态资源;
### Chapters
1. [Asset Pipeline 是什么?](#asset-pipeline-%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F)
* [主要功能](#%E4%B8%BB%E8%A6%81%E5%8A%9F%E8%83%BD)
* [指纹是什么,我为什么要关心它?](#%E6%8C%87%E7%BA%B9%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%8C%E6%88%91%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%85%B3%E5%BF%83%E5%AE%83%EF%BC%9F)
2. [如何使用 Asset Pipeline](#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-asset-pipeline)
* [控制器相关的静态资源](#%E6%8E%A7%E5%88%B6%E5%99%A8%E7%9B%B8%E5%85%B3%E7%9A%84%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90)
* [静态资源的组织方式](#%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E7%9A%84%E7%BB%84%E7%BB%87%E6%96%B9%E5%BC%8F)
* [链接静态资源](#%E9%93%BE%E6%8E%A5%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90)
* [清单文件和指令](#%E6%B8%85%E5%8D%95%E6%96%87%E4%BB%B6%E5%92%8C%E6%8C%87%E4%BB%A4)
* [预处理](#%E9%A2%84%E5%A4%84%E7%90%86)
3. [开发环境](#%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83)
* [检查运行时错误](#%E6%A3%80%E6%9F%A5%E8%BF%90%E8%A1%8C%E6%97%B6%E9%94%99%E8%AF%AF)
* [关闭调试功能](#%E5%85%B3%E9%97%AD%E8%B0%83%E8%AF%95%E5%8A%9F%E8%83%BD)
4. [生产环境](#%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83)
* [事先编译好静态资源](#%E4%BA%8B%E5%85%88%E7%BC%96%E8%AF%91%E5%A5%BD%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90)
* [在本地预编译](#%E5%9C%A8%E6%9C%AC%E5%9C%B0%E9%A2%84%E7%BC%96%E8%AF%91)
* [实时编译](#%E5%AE%9E%E6%97%B6%E7%BC%96%E8%AF%91)
* [CDN](#cdn)
5. [定制 Asset Pipeline](#%E5%AE%9A%E5%88%B6-asset-pipeline)
* [压缩 CSS](#%E5%8E%8B%E7%BC%A9-css)
* [压缩 JavaScript](#%E5%8E%8B%E7%BC%A9-javascript)
* [使用自己的压缩程序](#%E4%BD%BF%E7%94%A8%E8%87%AA%E5%B7%B1%E7%9A%84%E5%8E%8B%E7%BC%A9%E7%A8%8B%E5%BA%8F)
* [修改 `assets` 的路径](#%E4%BF%AE%E6%94%B9-assets-%E7%9A%84%E8%B7%AF%E5%BE%84)
* [X-Sendfile 报头](#x-sendfile-%E6%8A%A5%E5%A4%B4)
6. [静态资源缓存的存储方式](#%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E7%BC%93%E5%AD%98%E7%9A%84%E5%AD%98%E5%82%A8%E6%96%B9%E5%BC%8F)
7. [在 gem 中使用静态资源](#%E5%9C%A8-gem-%E4%B8%AD%E4%BD%BF%E7%94%A8%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90)
8. [把代码库或者 gem 变成预处理器](#%E6%8A%8A%E4%BB%A3%E7%A0%81%E5%BA%93%E6%88%96%E8%80%85-gem-%E5%8F%98%E6%88%90%E9%A2%84%E5%A4%84%E7%90%86%E5%99%A8)
9. [升级旧版本 Rails](#%E5%8D%87%E7%BA%A7%E6%97%A7%E7%89%88%E6%9C%AC-rails)
### 1 Asset Pipeline 是什么?
Asset Pipeline 提供了一个框架,用于连接、压缩 JavaScript 和 CSS 文件。还允许使用其他语言和预处理器编写 JavaScript 和 CSS,例如 CoffeeScript、Sass 和 ERB。
严格来说,Asset Pipeline 不是 Rails 4 的核心功能,已经从框架中提取出来,制成了 [sprockets-rails](https://github.com/rails/sprockets-rails) gem。
Asset Pipeline 功能默认是启用的。
新建程序时如果想禁用 Asset Pipeline,可以在命令行中指定 `--skip-sprockets` 选项。
```
rails new appname --skip-sprockets
```
Rails 4 会自动把 `sass-rails`、`coffee-rails` 和 `uglifier` 三个 gem 加入 `Gemfile`。Sprockets 使用这三个 gem 压缩静态资源:
```
gem 'sass-rails'
gem 'uglifier'
gem 'coffee-rails'
```
指定 `--skip-sprockets` 命令行选项后,Rails 4 不会把 `sass-rails` 和 `uglifier` 加入 `Gemfile`。如果后续需要使用 Asset Pipeline,需要手动添加这些 gem。而且,指定 `--skip-sprockets` 命令行选项后,生成的 `config/application.rb` 文件也会有点不同,把加载 `sprockets/railtie` 的代码注释掉了。如果后续启用 Asset Pipeline,要把这行前面的注释去掉:
```
# require "sprockets/railtie"
```
`production.rb` 文件中有相应的选项设置静态资源的压缩方式:`config.assets.css_compressor` 针对 CSS,`config.assets.js_compressor` 针对 Javascript。
```
config.assets.css_compressor = :yui
config.assets.js_compressor = :uglify
```
如果 `Gemfile` 中有 `sass-rails`,就会自动用来压缩 CSS,无需设置 `config.assets.css_compressor` 选项。
#### 1.1 主要功能
Asset Pipeline 的第一个功能是连接静态资源,减少渲染页面时浏览器发起的请求数。浏览器对并行的请求数量有限制,所以较少的请求数可以提升程序的加载速度。
Sprockets 会把所有 JavaScript 文件合并到一个主 `.js` 文件中,把所有 CSS 文件合并到一个主 `.css` 文件中。后文会介绍,合并的方式可按需求随意定制。在生产环境中,Rails 会在文件名后加上 MD5 指纹,以便浏览器缓存,指纹变了缓存就会过期。修改文件的内容后,指纹会自动变化。
Asset Pipeline 的第二个功能是压缩静态资源。对 CSS 文件来说,会删除空白和注释。对 JavaScript 来说,可以做更复杂的处理。处理方式可以从内建的选项中选择,也可使用定制的处理程序。
Asset Pipeline 的第三个功能是允许使用高级语言编写静态资源,再使用预处理器转换成真正的静态资源。默认支持的高级语言有:用来编写 CSS 的 Sass,用来编写 JavaScript 的 CoffeeScript,以及 ERB。
#### 1.2 指纹是什么,我为什么要关心它?
指纹可以根据文件内容生成文件名。文件内容变化后,文件名也会改变。对于静态内容,或者很少改动的内容,在不同的服务器之间,不同的部署日期之间,使用指纹可以区别文件的两个版本内容是否一样。
如果文件名基于内容而定,而且文件名是唯一的,HTTP 报头会建议在所有可能的地方(CDN,ISP,网络设备,网页浏览器)存储一份该文件的副本。修改文件内容后,指纹会发生变化,因此远程客户端会重新请求文件。这种技术叫做“缓存爆裂”(cache busting)。
Sprockets 使用指纹的方式是在文件名中加入内容的哈希值,一般加在文件名的末尾。例如,`global.css` 加入指纹后的文件名如下:
```
global-908e25f4bf641868d8683022a5b62f54.css
```
Asset Pipeline 使用的就是这种指纹实现方式。
以前,Rails 使用内建的帮助方法,在文件名后加上一个基于日期生成的请求字符串,如下所示:
```
/stylesheets/global.css?1309495796
```
使用请求字符串有很多缺点:
1. **文件名只是请求字符串不同时,缓存并不可靠**
[Steve Souders 建议](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/):不在要缓存的资源上使用请求字符串。他发现,使用请求字符串的文件不被缓存的可能性有 5-20%。有些 CDN 验证缓存时根本无法识别请求字符串。
2. **在多服务器环境中,不同节点上的文件名可能不同**
在 Rails 2.x 中,默认的请求字符串由文件的修改时间生成。静态资源文件部署到集群后,无法保证时间戳都是一样的,得到的值取决于使用哪台服务器处理请求。
3. **缓存验证失败过多**
部署新版代码时,所有静态资源文件的最后修改时间都变了。即便内容没变,客户端也要重新请求这些文件。
使用指纹就无需再用请求字符串了,而且文件名基于文件内容,始终保持一致。
默认情况下,指纹只在生产环境中启用,其他环境都被禁用。可以设置 `config.assets.digest` 选项启用或禁用。
扩展阅读:
* [Optimize caching](http://code.google.com/speed/page-speed/docs/caching.html)
* [Revving Filenames: don't use querystring](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/)
### 2 如何使用 Asset Pipeline
在以前的 Rails 版本中,所有静态资源都放在 `public` 文件夹的子文件夹中,例如 `images`、`javascripts` 和 `stylesheets`。使用 Asset Pipeline 后,建议把静态资源放在 `app/assets` 文件夹中。这个文件夹中的文件会经由 Sprockets 中间件处理。
静态资源仍然可以放在 `public` 文件夹中,其中所有文件都会被程序或网页服务器视为静态文件。如果文件要经过预处理器处理,就得放在 `app/assets` 文件夹中。
默认情况下,在生产环境中,Rails 会把预先编译好的文件保存到 `public/assets` 文件夹中,网页服务器会把这些文件视为静态资源。在生产环境中,不会直接伺服 `app/assets` 文件夹中的文件。
#### 2.1 控制器相关的静态资源
生成脚手架或控制器时,Rails 会生成一个 JavaScript 文件(如果 `Gemfile` 中有 `coffee-rails`,会生成 CoffeeScript 文件)和 CSS 文件(如果 `Gemfile` 中有 `sass-rails`,会生成 SCSS 文件)。生成脚手架时,Rails 还会生成 `scaffolds.css` 文件(如果 `Gemfile` 中有 `sass-rails`,会生成 `scaffolds.css.scss` 文件)。
例如,生成 `ProjectsController` 时,Rails 会新建 `app/assets/javascripts/projects.js.coffee` 和 `app/assets/stylesheets/projects.css.scss` 两个文件。默认情况下,这两个文件立即就可以使用 `require_tree` 引入程序。关于 `require_tree` 的介绍,请阅读“[清单文件和指令](#manifest-files-and-directives)”一节。
针对控制器的样式表和 JavaScript 文件也可只在相应的控制器中引入:
`<%= javascript_include_tag params[:controller] %>` 或 `<%= stylesheet_link_tag params[:controller] %>`
如果需要这么做,切记不要使用 `require_tree`。如果使用了这个指令,会多次引入相同的静态资源。
预处理静态资源时要确保同时处理控制器相关的静态资源。默认情况下,不会自动编译 `.coffee` 和 `.scss` 文件。在开发环境中没什么问题,因为会自动编译。但在生产环境中会得到 500 错误,因为此时自动编译默认是关闭的。关于预编译的工作机理,请阅读“[事先编译好静态资源](#precompiling-assets)”一节。
要想使用 CoffeeScript,必须安装支持 ExecJS 的运行时。如果使用 Mac OS X 和 Windows,系统中已经安装了 JavaScript 运行时。所有支持的 JavaScript 运行时参见 [ExecJS](https://github.com/sstephenson/execjs#readme) 的文档。
在 `config/application.rb` 文件中加入以下代码可以禁止生成控制器相关的静态资源:
```
config.generators do |g|
g.assets false
end
```
#### 2.2 静态资源的组织方式
Asset Pipeline 的静态文件可以放在三个位置:`app/assets`,`lib/assets` 或 `vendor/assets`。
* `app/assets`:存放程序的静态资源,例如图片、JavaScript 和样式表;
* `lib/assets`:存放自己的代码库,或者共用代码库的静态资源;
* `vendor/assets`:存放他人的静态资源,例如 JavaScript 插件,或者 CSS 框架;
如果从 Rails 3 升级过来,请注意,`lib/assets` 和 `vendor/assets` 中的静态资源可以引入程序,但不在预编译的范围内。详情参见“[事先编译好静态资源](#precompiling-assets)”一节。
##### 2.2.1 搜索路径
在清单文件或帮助方法中引用静态资源时,Sprockets 会在默认的三个位置中查找对应的文件。
默认的位置是 `apps/assets` 文件夹中的 `images`、`javascripts` 和 `stylesheets` 三个子文件夹。这三个文件夹没什么特别之处,其实 Sprockets 会搜索 `apps/assets` 文件夹中的所有子文件夹。
例如,如下的文件:
```
app/assets/javascripts/home.js
lib/assets/javascripts/moovinator.js
vendor/assets/javascripts/slider.js
vendor/assets/somepackage/phonebox.js
```
在清单文件中可以这么引用:
```
//= require home
//= require moovinator
//= require slider
//= require phonebox
```
子文件夹中的静态资源也可引用:
```
app/assets/javascripts/sub/something.js
```
引用方式如下:
```
//= require sub/something
```
在 Rails 控制台中执行 `Rails.application.config.assets.paths`,可以查看所有的搜索路径。
除了标准的 `assets/*` 路径之外,还可以在 `config/application.rb` 文件中向 Asset Pipeline 添加其他路径。例如:
```
config.assets.paths << Rails.root.join("lib", "videoplayer", "flash")
```
Sprockets 会按照搜索路径中各路径出现的顺序进行搜索。默认情况下,这意味着 `app/assets` 文件夹中的静态资源优先级较高,会遮盖 `lib` 和 `vendor` 文件夹中的相应文件。
有一点要注意,如果静态资源不会在清单文件中引入,就要添加到预编译的文件列表中,否则在生产环境中就无法访问文件。
##### 2.2.2 使用索引文件
在 Sprockets 中,名为 `index` 的文件(扩展名各异)有特殊作用。
例如,程序中使用了 jQuery 代码库和许多模块,都保存在 `lib/assets/javascripts/library_name` 文件夹中,那么 `lib/assets/javascripts/library_name/index.js` 文件的作用就是这个代码库的清单。在这个清单中可以按顺序列出所需的文件,或者干脆使用 `require_tree` 指令。
在程序的清单文件中,可以把这个库作为一个整体引入:
```
//= require library_name
```
这么做可以减少维护成本,保持代码整洁。
#### 2.3 链接静态资源
Sprockets 并没有为获取静态资源添加新的方法,还是使用熟悉的 `javascript_include_tag` 和 `stylesheet_link_tag`:
```
<%= stylesheet_link_tag "application", media: "all" %>
<%= javascript_include_tag "application" %>
```
如果使用 Turbolinks(Rails 4 默认启用),加上 `data-turbolinks-track` 选项后,Turbolinks 会检查静态资源是否有更新,如果更新了就会将其载入页面:
```
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
```
在普通的视图中可以像下面这样获取 `public/assets/images` 文件夹中的图片:
```
<%= image_tag "rails.png" %>
```
如果程序启用了 Asset Pipeline,且在当前环境中没有禁用,那么这个文件会经由 Sprockets 伺服。如果文件的存放位置是 `public/assets/rails.png`,则直接由网页服务器伺服。
如果请求的文件中包含 MD5 哈希,处理的方式还是一样。关于这个哈希是怎么生成的,请阅读“[在生产环境中](#in-production)”一节。
Sprockets 还会检查 `config.assets.paths` 中指定的路径。`config.assets.paths` 包含标准路径和其他 Rails 引擎添加的路径。
图片还可以放入子文件夹中,获取时指定文件夹的名字即可:
```
<%= image_tag "icons/rails.png" %>
```
如果预编译了静态资源(参见“[在生产环境中](#in-production)”一节),链接不存在的资源(也包括链接到空字符串的情况)会在调用页面抛出异常。因此,在处理用户提交的数据时,使用 `image_tag` 等帮助方法要小心一点。
##### 2.3.1 CSS 和 ERB
Asset Pipeline 会自动执行 ERB 代码,所以如果在 CSS 文件名后加上扩展名 `erb`(例如 `application.css.erb`),那么在 CSS 规则中就可使用 `asset_path` 等帮助方法。
```
.class { background-image: url(<%= asset_path 'image.png' %>) }
```
Asset Pipeline 会计算出静态资源的真实路径。在上面的代码中,指定的图片要出现在加载路径中。如果在 `public/assets` 中有该文件带指纹版本,则会使用这个文件的路径。
如果想使用 [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme)(直接把图片数据内嵌在 CSS 文件中),可以使用 `asset_data_uri` 帮助方法。
```
#logo { background: url(<%= asset_data_uri 'logo.png' %>) }
```
`asset_data_uri` 会把正确格式化后的 data URI 写入 CSS 文件。
注意,关闭标签不能使用 `-%>` 形式。
##### 2.3.2 CSS 和 Sass
使用 Asset Pipeline,静态资源的路径要使用 `sass-rails` 提供的 `-url` 和 `-path` 帮助方法(在 Sass 中使用连字符,在 Ruby 中使用下划线)重写。这两种帮助方法可用于引用图片,字体,视频,音频,JavaScript 和样式表。
* `image-url("rails.png")` 编译成 `url(/assets/rails.png)`
* `image-path("rails.png")` 编译成 `"/assets/rails.png"`.
还可使用通用方法:
* `asset-url("rails.png")` 编译成 `url(/assets/rails.png)`
* `asset-path("rails.png")` 编译成 `"/assets/rails.png"`
##### 2.3.3 JavaScript/CoffeeScript 和 ERB
如果在 JavaScript 文件后加上扩展名 `erb`,例如 `application.js.erb`,就可以在 JavaScript 代码中使用帮助方法 `asset_path`:
```
$('#logo').attr({ src: "<%= asset_path('logo.png') %>" });
```
Asset Pipeline 会计算出静态资源的真实路径。
类似地,如果在 CoffeeScript 文件后加上扩展名 `erb`,例如 `application.js.coffee.erb`,也可在代码中使用帮助方法 `asset_path`:
```
$('#logo').attr src: "<%= asset_path('logo.png') %>"
```
#### 2.4 清单文件和指令
Sprockets 通过清单文件决定要引入和伺服哪些静态资源。清单文件中包含一些指令,告知 Sprockets 使用哪些文件生成主 CSS 或 JavaScript 文件。Sprockets 会解析这些指令,加载指定的文件,如有需要还会处理文件,然后再把各个文件合并成一个文件,最后再压缩文件(如果 `Rails.application.config.assets.compress` 选项为 `true`)。只伺服一个文件可以大大减少页面加载时间,因为浏览器发起的请求数更少。压缩能减小文件大小,加快浏览器下载速度。
例如,新建的 Rails 4 程序中有个 `app/assets/javascripts/application.js` 文件,包含以下内容:
```
// ...
//= require jquery
//= require jquery_ujs
//= require_tree .
```
在 JavaScript 文件中,Sprockets 的指令以 `//=` 开头。在上面的文件中,用到了 `require` 和 the `require_tree` 指令。`require` 指令告知 Sprockets 要加载的文件。在上面的文件中,加载了 Sprockets 搜索路径中的 `jquery.js` 和 `jquery_ujs.js` 两个文件。文件名后无需加上扩展名,在 `.js` 文件中 Sprockets 默认会加载 `.js` 文件。
`require_tree` 指令告知 Sprockets 递归引入指定文件夹中的所有 JavaScript 文件。文件夹的路径必须相对于清单文件。也可使用 `require_directory` 指令加载指定文件夹中的所有 JavaScript 文件,但不会递归。
Sprockets 会按照从上至下的顺序处理指令,但 `require_tree` 引入的文件顺序是不可预期的,不要设想能得到一个期望的顺序。如果要确保某些 JavaScript 文件出现在其他文件之前,就要先在清单文件中引入。注意,`require` 等指令不会多次加载同一个文件。
Rails 还会生成 `app/assets/stylesheets/application.css` 文件,内容如下:
```
/* ...
*= require_self
*= require_tree .
*/
```
不管创建新程序时有没有指定 `--skip-sprockets` 选项,Rails 4 都会生成 `app/assets/javascripts/application.js` 和 `app/assets/stylesheets/application.css`。这样如果后续需要使用 Asset Pipelining,操作就方便了。
样式表中使用的指令和 JavaScript 文件一样,不过加载的是样式表而不是 JavaScript 文件。`require_tree` 指令在 CSS 清单文件中的作用和在 JavaScript 清单文件中一样,从指定的文件夹中递归加载所有样式表。
上面的代码中还用到了 `require_self`。这么做可以把当前文件中的 CSS 加入调用 `require_self` 的位置。如果多次调用 `require_self`,只有最后一次调用有效。
如果想使用多个 Sass 文件,应该使用 [Sass 中的 `@import` 规则](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import),不要使用 Sprockets 指令。如果使用 Sprockets 指令,Sass 文件只出现在各自的作用域中,Sass 变量和混入只在定义所在文件中有效。为了达到 `require_tree` 指令的效果,可以使用通配符,例如 `@import "*"` 和 `@import "**/*"`。详情参见 [sass-rails 的文档](https://github.com/rails/sass-rails#features)。
清单文件可以有多个。例如,`admin.css` 和 `admin.js` 这两个清单文件包含程序管理后台所需的 JS 和 CSS 文件。
CSS 清单中的指令也适用前面介绍的加载顺序。分别引入各文件,Sprockets 会按照顺序编译。例如,可以按照下面的方式合并三个 CSS 文件:
```
/* ...
*= require reset
*= require layout
*= require chrome
*/
```
#### 2.5 预处理
静态资源的文件扩展名决定了使用哪个预处理器处理。如果使用默认的 gem,生成控制器或脚手架时,会生成 CoffeeScript 和 SCSS 文件,而不是普通的 JavaScript 和 CSS 文件。前文举过例子,生成 `projects` 控制器时会创建 `app/assets/javascripts/projects.js.coffee` 和 `app/assets/stylesheets/projects.css.scss` 两个文件。
在开发环境中,或者禁用 Asset Pipeline 时,这些文件会使用 `coffee-script` 和 `sass` 提供的预处理器处理,然后再发给浏览器。启用 Asset Pipeline 时,这些文件会先使用预处理器处理,然后保存到 `public/assets` 文件夹中,再由 Rails 程序或网页服务器伺服。
添加额外的扩展名可以增加预处理次数,预处理程序会按照扩展名从右至左的顺序处理文件内容。所以,扩展名的顺序要和处理的顺序一致。例如,名为 `app/assets/stylesheets/projects.css.scss.erb` 的样式表首先会使用 ERB 处理,然后是 SCSS,最后才以 CSS 格式发送给浏览器。JavaScript 文件类似,`app/assets/javascripts/projects.js.coffee.erb` 文件先由 ERB 处理,然后是 CoffeeScript,最后以 JavaScript 格式发送给浏览器。
记住,预处理器的执行顺序很重要。例如,名为 `app/assets/javascripts/projects.js.erb.coffee` 的文件首先由 CoffeeScript 处理,但是 CoffeeScript 预处理器并不懂 ERB 代码,因此会导致错误。
### 3 开发环境
在开发环境中,Asset Pipeline 按照清单文件中指定的顺序伺服各静态资源。
清单 `app/assets/javascripts/application.js` 的内容如下:
```
//= require core
//= require projects
//= require tickets
```
生成的 HTML 如下:
```
<script src="/assets/core.js?body=1"></script>
<script src="/assets/projects.js?body=1"></script>
<script src="/assets/tickets.js?body=1"></script>
```
Sprockets 要求必须使用 `body` 参数。
#### 3.1 检查运行时错误
默认情况下,在生产环境中 Asset Pipeline 会检查潜在的错误。要想禁用这一功能,可以做如下设置:
```
config.assets.raise_runtime_errors = false
```
`raise_runtime_errors` 设为 `false` 时,Sprockets 不会检查静态资源的依赖关系是否正确。遇到下面这种情况时,必须告知 Asset Pipeline 其中的依赖关系。
如果在 `application.css.erb` 中引用了 `logo.png`,如下所示:
```
#logo { background: url(<%= asset_data_uri 'logo.png' %>) }
```
就必须声明 `logo.png` 是 `application.css.erb` 的一个依赖件,这样重新编译图片时才会同时重新编译 CSS 文件。依赖关系可以使用 `//= depend_on_asset` 声明:
```
//= depend_on_asset "logo.png"
#logo { background: url(<%= asset_data_uri 'logo.png' %>) }
```
如果没有这个声明,在生产环境中可能遇到难以查找的奇怪问题。`raise_runtime_errors` 设为 `true` 时,运行时会自动检查依赖关系。
#### 3.2 关闭调试功能
在 `config/environments/development.rb` 中添加如下设置可以关闭调试功能:
```
config.assets.debug = false
```
关闭调试功能后,Sprockets 会预处理所有文件,然后合并。关闭调试功能后,前文的清单文件生成的 HTML 如下:
```
<script src="/assets/application.js"></script>
```
服务器启动后,首次请求发出后会编译并缓存静态资源。Sprockets 会把 `Cache-Control` 报头设为 `must-revalidate`。再次请求时,浏览器会得到 304 (Not Modified) 响应。
如果清单中的文件内容发生了变化,服务器会返回重新编译后的文件。
调试功能可以在 Rails 帮助方法中启用:
```
<%= stylesheet_link_tag "application", debug: true %>
<%= javascript_include_tag "application", debug: true %>
```
如果已经启用了调试模式,再使用 `:debug` 选项就有点多余了。
在开发环境中也可启用压缩功能,检查是否能正常运行。需要调试时再禁用压缩即可。
### 4 生产环境
在生产环境中,Sprockets 使用前文介绍的指纹机制。默认情况下,Rails 认为静态资源已经事先编译好了,直接由网页服务器伺服。
在预先编译的过程中,会根据文件的内容生成 MD5,写入硬盘时把 MD5 加到文件名中。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" />
```
注意,推出 Asset Pipeline 功能后不再使用 `:cache` 和 `:concat` 选项了,请从 `javascript_include_tag` 和 `stylesheet_link_tag` 标签上将其删除。
指纹由 `config.assets.digest` 初始化选项控制(生产环境默认为 `true`,其他环境为 `false`)。
一般情况下,请勿修改 `config.assets.digest` 的默认值。如果文件名中没有指纹,而且缓存报头的时间设置为很久以后,那么即使文件的内容变了,客户端也不会重新获取文件。
#### 4.1 事先编译好静态资源
Rails 提供了一个 rake 任务用来编译清单文件中的静态资源和其他相关文件。
编译后的静态资源保存在 `config.assets.prefix` 选项指定的位置。默认是 `/assets` 文件夹。
部署时可以在服务器上执行这个任务,直接在服务器上编译静态资源。下一节会介绍如何在本地编译。
这个 rake 任务是:
```
$ RAILS_ENV=production bundle exec rake assets:precompile
```
Capistrano(v2.15.1 及以上版本)提供了一个配方,可在部署时编译静态资源。把下面这行加入 `Capfile` 文件即可:
```
load 'deploy/assets'
```
这个配方会把 `config.assets.prefix` 选项指定的文件夹链接到 `shared/assets`。如果 `shared/assets` 已经占用,就要修改部署任务。
在多次部署之间共用这个文件夹是十分重要的,这样只要缓存的页面可用,其中引用的编译后的静态资源就能正常使用。
默认编译的文件包括 `application.js`、`application.css` 以及 gem 中 `app/assets` 文件夹中的所有非 JS/CSS 文件(会自动加载所有图片):
```
[ Proc.new { |path, fn| fn =~ /app\/assets/ && !%w(.js .css).include?(File.extname(path)) },
/application.(css|js)$/ ]
```
这个正则表达式表示最终要编译的文件。也就是说,JS/CSS 文件不包含在内。例如,因为 `.coffee` 和 `.scss` 文件能编译成 JS 和 CSS 文件,所以**不在**自动编译的范围内。
如果想编译其他清单,或者单独的样式表和 JavaScript,可以添加到 `config/application.rb` 文件中的 `precompile` 选项:
```
config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']
```
或者可以按照下面的方式,设置编译所有静态资源:
```
# config/application.rb
config.assets.precompile << Proc.new do |path|
if path =~ /\.(css|js)\z/
full_path = Rails.application.assets.resolve(path).to_path
app_assets_path = Rails.root.join('app', 'assets').to_path
if full_path.starts_with? app_assets_path
puts "including asset: " + full_path
true
else
puts "excluding asset: " + full_path
false
end
else
false
end
end
```
即便想添加 Sass 或 CoffeeScript 文件,也要把希望编译的文件名设为 .js 或 .css。
这个 rake 任务还会生成一个名为 `manifest-md5hash.json` 的文件,列出所有静态资源和对应的指纹。这样 Rails 帮助方法就不用再通过 Sprockets 获取指纹了。下面是一个 `manifest-md5hash.json` 文件内容示例:
```
{"files":{"application-723d1be6cc741a3aabb1cec24276d681.js":{"logical_path":"application.js","mtime":"2013-07-26T22:55:03-07:00","size":302506,
"digest":"723d1be6cc741a3aabb1cec24276d681"},"application-12b3c7dd74d2e9df37e7cbb1efa76a6d.css":{"logical_path":"application.css","mtime":"2013-07-26T22:54:54-07:00","size":1560,
"digest":"12b3c7dd74d2e9df37e7cbb1efa76a6d"},"application-1c5752789588ac18d7e1a50b1f0fd4c2.css":{"logical_path":"application.css","mtime":"2013-07-26T22:56:17-07:00","size":1591,
"digest":"1c5752789588ac18d7e1a50b1f0fd4c2"},"favicon-a9c641bf2b81f0476e876f7c5e375969.ico":{"logical_path":"favicon.ico","mtime":"2013-07-26T23:00:10-07:00","size":1406,
"digest":"a9c641bf2b81f0476e876f7c5e375969"},"my_image-231a680f23887d9dd70710ea5efd3c62.png":{"logical_path":"my_image.png","mtime":"2013-07-26T23:00:27-07:00","size":6646,
"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets"{"application.js":
"application-723d1be6cc741a3aabb1cec24276d681.js","application.css":
"application-1c5752789588ac18d7e1a50b1f0fd4c2.css",
"favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png":
"my_image-231a680f23887d9dd70710ea5efd3c62.png"}}
```
`manifest-md5hash.json` 文件的存放位置是 `config.assets.prefix` 选项指定位置(默认为 `/assets`)的根目录。
在生产环境中,如果找不到编译好的文件,会抛出 `Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError` 异常,并提示找不到哪个文件。
##### 4.1.1 把 Expires 报头设置为很久以后
编译好的静态资源存放在服务器的文件系统中,直接由网页服务器伺服。默认情况下,没有为这些文件设置一个很长的过期时间。为了能充分发挥指纹的作用,需要修改服务器的设置,添加相关的报头。
针对 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 "";
break;
}
```
##### 4.1.2 GZip 压缩
Sprockets 预编译文件时还会创建静态资源的 [gzip](http://en.wikipedia.org/wiki/Gzip) 版本(.gz)。网页服务器一般使用中等压缩比例,不过因为预编译只发生一次,所以 Sprockets 会使用最大的压缩比例,尽量减少传输的数据大小。网页服务器可以设置成直接从硬盘伺服压缩版文件,无需直接压缩文件本身。
在 Nginx 中启动 `gzip_static` 模块后就能自动实现这一功能:
```
location ~ ^/(assets)/ {
root /path/to/public;
gzip_static on; # to serve pre-gzipped version
expires max;
add_header Cache-Control public;
}
```
如果编译 Nginx 时加入了 `gzip_static` 模块,就能使用这个指令。Nginx 针对 Ubuntu/Debian 的安装包,以及 `nginx-light` 都会编译这个模块。否则就要手动编译:
```
./configure --with-http_gzip_static_module
```
如果编译支持 Phusion Passenger 的 Nginx,就必须加入这个命令行选项。
针对 Apache 的设置很复杂,请自行 Google。
#### 4.2 在本地预编译
为什么要在本地预编译静态文件呢?原因如下:
* 可能无权限访问生产环境服务器的文件系统;
* 可能要部署到多个服务器,避免重复编译;
* 可能会经常部署,但静态资源很少改动;
在本地预编译后,可以把编译好的文件纳入版本控制系统,再按照常规的方式部署。
不过有两点要注意:
* 一定不能运行 Capistrano 部署任务来预编译静态资源;
* 必须修改下面这个设置;
在 `config/environments/development.rb` 中加入下面这行代码:
```
config.assets.prefix = "/dev-assets"
```
修改 `prefix` 后,在开发环境中 Sprockets 会使用其他的 URL 伺服静态资源,把请求都交给 Sprockets 处理。但在生产环境中 `prefix` 仍是 `/assets`。如果没作上述修改,在生产环境中会从 `/assets` 伺服静态资源,除非再次编译,否则看不到文件的变化。
同时还要确保所需的压缩程序在生产环境中可用。
在本地预编译静态资源,这些文件就会出现在工作目录中,而且可以根据需要纳入版本控制系统。开发环境仍能按照预期正常运行。
#### 4.3 实时编译
某些情况下可能需要实时编译,此时静态资源直接由 Sprockets 处理。
要想使用实时编译,要做如下设置:
```
config.assets.compile = true
```
初次请求时,Asset Pipeline 会编译静态资源,并缓存,这一过程前文已经提过了。引用文件时,会使用加上 MD5 哈希的文件名代替清单文件中的名字。
Sprockets 还会把 `Cache-Control` 报头设为 `max-age=31536000`。这个报头的意思是,服务器和客户端浏览器之间的缓存可以存储一年,以减少从服务器上获取静态资源的请求数量。静态资源的内容可能存在本地浏览器的缓存或者其他中间缓存中。
实时编译消耗的内存更多,比默认的编译方式性能更低,因此不推荐使用。
如果要把程序部署到没有安装 JavaScript 运行时的服务器,可以在 `Gemfile` 中加入:
```
group :production do
gem 'therubyracer'
end
```
#### 4.4 CDN
如果用 CDN 分发静态资源,要确保文件不会被缓存,因为缓存会导致问题。如果设置了 `config.action_controller.perform_caching = true`,`Rack::Cache` 会使用 `Rails.cache` 存储静态文件,很快缓存空间就会用完。
每种缓存的工作方式都不一样,所以要了解你所用 CDN 是如何处理缓存的,确保能和 Asset Pipeline 和谐相处。有时你会发现某些设置能导致诡异的表现,而有时又不会。例如,作为 HTTP 缓存使用时,Nginx 的默认设置就不会出现什么问题。
### 5 定制 Asset Pipeline
#### 5.1 压缩 CSS
压缩 CSS 的方式之一是使用 YUI。[YUI CSS compressor](http://yui.github.io/yuicompressor/css.html) 提供了压缩功能。
下面这行设置会启用 YUI 压缩,在此之前要先安装 `yui-compressor` gem:
```
config.assets.css_compressor = :yui
```
如果安装了 `sass-rails` gem,还可以使用其他的方式压缩 CSS:
```
config.assets.css_compressor = :sass
```
#### 5.2 压缩 JavaScript
压缩 JavaScript 的方式有:`:closure`,`:uglifier` 和 `:yui`。这三种方式分别需要安装 `closure-compiler`、`uglifier` 和 `yui-compressor`。
默认的 `Gemfile` 中使用的是 [uglifier](https://github.com/lautis/uglifier)。这个 gem 使用 Ruby 包装了 [UglifyJS](https://github.com/mishoo/UglifyJS)(为 NodeJS 开发)。uglifier 可以删除空白和注释,缩短本地变量名,还会做些微小的优化,例如把 `if...else` 语句改写成三元操作符形式。
下面这行设置使用 `uglifier` 压缩 JavaScript:
```
config.assets.js_compressor = :uglifier
```
系统中要安装支持 [ExecJS](https://github.com/sstephenson/execjs#readme) 的运行时才能使用 `uglifier`。Mac OS X 和 Windows 系统中已经安装了 JavaScript 运行时。 I> NOTE: `config.assets.compress` 初始化选项在 Rails 4 中不可用,即便设置了也没有效果。请分别使用 `config.assets.css_compressor` 和 `config.assets.js_compressor` 这两个选项设置 CSS 和 JavaScript 的压缩方式。
#### 5.3 使用自己的压缩程序
设置压缩 CSS 和 JavaScript 所用压缩程序的选项还可接受对象,这个对象必须能响应 `compress` 方法。`compress` 方法只接受一个字符串参数,返回值也必须是字符串。
```
class Transformer
def compress(string)
do_something_returning_a_string(string)
end
end
```
要想使用这个压缩程序,请在 `application.rb` 中做如下设置:
```
config.assets.css_compressor = Transformer.new
```
#### 5.4 修改 `assets` 的路径
Sprockets 默认使用的公开路径是 `/assets`。
这个路径可以修改成其他值:
```
config.assets.prefix = "/some_other_path"
```
升级没使用 Asset Pipeline 的旧项目时,或者默认路径已有其他用途,或者希望指定一个新资源路径时,可以设置这个选项。
#### 5.5 X-Sendfile 报头
X-Sendfile 报头的作用是让服务器忽略程序的响应,直接从硬盘上伺服指定的文件。默认情况下服务器不会发送这个报头,但在支持该报头的服务器上可以启用。启用后,会跳过响应直接由服务器伺服文件,速度更快。X-Sendfile 报头的用法参见 [API 文档](http://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-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`。
详情参见生产环境所用服务器的文档: T> TIP: - [Apache](https://tn123.org/mod_xsendfile/) TIP: - [Nginx](http://wiki.nginx.org/XSendfile)
### 6 静态资源缓存的存储方式
在开发环境和生产环境中,Sprockets 使用 Rails 默认的存储方式缓存静态资源。可以使用 `config.assets.cache_store` 设置使用其他存储方式:
```
config.assets.cache_store = :memory_store
```
静态资源缓存可用的存储方式和程序的缓存存储一样。
```
config.assets.cache_store = :memory_store, { size: 32.megabytes }
```
### 7 在 gem 中使用静态资源
静态资源也可由 gem 提供。
为 Rails 提供标准 JavaScript 代码库的 `jquery-rails` gem 是个很好的例子。这个 gem 中有个引擎类,继承自 `Rails::Engine`。添加这层继承关系后,Rails 就知道这个 gem 中可能包含静态资源文件,会把这个引擎中的 `app/assets`、`lib/assets` 和 `vendor/assets` 三个文件夹加入 Sprockets 的搜索路径中。
### 8 把代码库或者 gem 变成预处理器
Sprockets 使用 [Tilt](https://github.com/rtomayko/tilt) 作为不同模板引擎的通用接口。在你自己的 gem 中也可实现 Tilt 的模板协议。一般情况下,需要继承 `Tilt::Template` 类,然后重新定义 `prepare` 方法(初始化模板),以及 `evaluate` 方法(返回处理后的内容)。原始数据存储在 `data` 中。详情参见 [`Tilt::Template`](https://github.com/rtomayko/tilt/blob/master/lib/tilt/template.rb) 类的源码。
```
module BangBang
class Template < ::Tilt::Template
def prepare
# Do any initialization here
end
# Adds a "!" to original template.
def evaluate(scope, locals, &block)
"#{data}!"
end
end
end
```
上述代码定义了 `Template` 类,然后还需要关联模板文件的扩展名:
```
Sprockets.register_engine '.bang', BangBang::Template
```
### 9 升级旧版本 Rails
从 Rails 3.0 或 Rails 2.x 升级,有一些问题要解决。首先,要把 `public/` 文件夹中的文件移到新位置。不同类型文件的存放位置参见“[静态资源的组织方式](#asset-organization)”一节。
其次,避免 JavaScript 文件重复出现。因为从 Rails 3.1 开始,jQuery 是默认的 JavaScript 库,因此不用把 `jquery.js` 复制到 `app/assets` 文件夹中。Rails 会自动加载 jQuery。
然后,更新各环境的设置文件,添加默认设置。
在 `application.rb` 中加入:
```
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
# Change the path that assets are served from config.assets.prefix = "/assets"
```
在 `development.rb` 中加入:
```
# Expands the lines which load the assets
config.assets.debug = true
```
在 `production.rb` 中加入:
```
# Choose the compressors to use (if any) config.assets.js_compressor =
# :uglifier config.assets.css_compressor = :yui
# Don't fallback to assets pipeline if a precompiled asset is missed
config.assets.compile = false
# Generate digests for assets URLs. This is planned for deprecation.
config.assets.digest = true
# Precompile additional assets (application.js, application.css, and all
# non-JS/CSS are already added) config.assets.precompile += %w( search.js )
```
Rails 4 不会在 `test.rb` 中添加 Sprockets 的默认设置,所以要手动添加。测试环境中以前的默认设置是:`config.assets.compile = true`,`config.assets.compress = false`,`config.assets.debug = false` 和 `config.assets.digest = false`。
最后,还要在 `Gemfile` 中加入以下 gem:
```
gem 'sass-rails', "~> 3.2.3"
gem 'coffee-rails', "~> 3.2.1"
gem 'uglifier'
```
### 反馈
欢迎帮忙改善指南质量。
如发现任何错误,欢迎修正。开始贡献前,可先行阅读[贡献指南:文档](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)。
翻译如有错误,深感抱歉,欢迎 [Fork](https://github.com/ruby-china/guides/fork) 修正,或至此处[回报](https://github.com/ruby-china/guides/issues/new)。
文章可能有未完成或过时的内容。请先检查 [Edge Guides](http://edgeguides.rubyonrails.org) 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 [Ruby on Rails 指南准则](ruby_on_rails_guides_guidelines.html)来了解行文风格。
最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 [rubyonrails-docs 邮件群组](http://groups.google.com/group/rubyonrails-docs)。
- Ruby on Rails 指南 (651bba1)
- 入门
- Rails 入门
- 模型
- Active Record 基础
- Active Record 数据库迁移
- Active Record 数据验证
- Active Record 回调
- Active Record 关联
- Active Record 查询
- 视图
- Action View 基础
- Rails 布局和视图渲染
- 表单帮助方法
- 控制器
- Action Controller 简介
- Rails 路由全解
- 深入
- Active Support 核心扩展
- Rails 国际化 API
- Action Mailer 基础
- Active Job 基础
- Rails 程序测试指南
- Rails 安全指南
- 调试 Rails 程序
- 设置 Rails 程序
- Rails 命令行
- Rails 缓存简介
- Asset Pipeline
- 在 Rails 中使用 JavaScript
- 引擎入门
- Rails 应用的初始化过程
- Autoloading and Reloading Constants
- 扩展 Rails
- Rails 插件入门
- Rails on Rack
- 个性化Rails生成器与模板
- Rails应用模版
- 贡献 Ruby on Rails
- Contributing to Ruby on Rails
- API Documentation Guidelines
- Ruby on Rails Guides Guidelines
- Ruby on Rails 维护方针
- 发布记
- A Guide for Upgrading Ruby on Rails
- Ruby on Rails 4.2 发布记
- Ruby on Rails 4.1 发布记
- Ruby on Rails 4.0 Release Notes
- Ruby on Rails 3.2 Release Notes
- Ruby on Rails 3.1 Release Notes
- Ruby on Rails 3.0 Release Notes
- Ruby on Rails 2.3 Release Notes
- Ruby on Rails 2.2 Release Notes