# 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