Asset Pipeline
A language that doesn’t affect the way you think about programming is not worth knowing. - Alan Perlis
–
Rails 前端打包工具,近年來變化較大,詳見 Blog文章(2022版) 說明。
本章 Asset Pipeline 內容有個摘要版本在這裡。
Assets指的是JavaScript、Stylesheets和圖檔等靜態檔案,這些檔案並不會隨Requests不同而有所不同。而在Rails目錄中,只有public這個目錄是公開讀取的,所以通常我們會將靜態檔案都放在public這個目錄下,好讓瀏覽器可以直接讀取。但是隨著JavaScript和Stylesheet檔案越來越多時,如何管理這些檔案變為一項議題,為了加快瀏覽器的下載速度,我們會合併JavaScript和Stylesheet檔案,來減少瀏覽器Request下載次數。更進一步的還會壓縮這些檔案來加速下載時間。像是Yahoo!和Google都有各自開源出自己的壓縮工具YUI Compressor和Closure Compiler。
而Rails的Assets pipeline可以讓我們突破public目錄限制,可以將靜態檔案依需求放在不同目錄下,Rails會幫你組合並壓縮起來。特別是有一些Rails的外掛套件需要使用JavaScript等靜態檔案,在沒有這個功能之前,我們必須將JavaScript等檔案複製放在public目錄下,這樣瀏覽器才能讀取的到。
Assets的位置在app/assets/下,首先最重要的就是app/assets/javascripts/application.js和app/assets/stylesheets/application.css,這兩個檔案看起來充滿註解,其實它是個manifest檔案,列出了所有要載入的靜態檔案,這些檔案的位置依照慣例放在app/assets或vendor/assets目錄下。
讓我們先看看application.js
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require_tree .
其中的require jquery
和require jquery_ujs
會載入JQuery和Rails的JQuery adapater,這是因為我們在Gemfile中有裝jquery-rails這個套件,所以這裡可以讀取的到。require turbolinks
在上一章有提到。而require_tree .
會載入這個目錄下的所有JavaScript檔案。總之,這個manifest的最後輸出結果就是通通壓縮成一個application.js檔案。
同理application.css也是一樣載入所有stylesheets目錄下的CSS檔案,最後壓縮成application.css:
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any styles
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
* file per style scope.
*
*= require_tree .
*= require_self
*/
實務上不喜歡用
require_tree
的方式來一次載入全部的 css 和 javascript,而是喜歡逐條require
載入,一來比較清楚,二來也可以確保 css 和 javascript 載入的順序是正確的。另外也不喜歡用require_self
將 css 寫在application.css
裡面,我們應該放到另一個檔案再require
它即可。
讓我們看看View,在Layout檔案中:
<%= stylesheet_link_tag "application" %>
<%= javascript_include_tag "application" %>
因為最後輸出都壓縮成一個檔案了,所以這裡只需要載入application.css和application.js。
除了require_tree
之外,還有其他的用法,你都可以使用絕對或是相對路徑來指定檔案位置,副檔名可有可無:
require
[路徑]:載入某支特定檔案,如果這支檔案被載入多次,Sprockets 也會很聰明的只幫你載入一次。require_tree
[路徑] : 會將路徑下包含子目錄的檔案全部載入。require_self
[路徑] : 告訴 Sprockets 再載入其他的檔案前,先將自己的內容插入。
你可以看 Sprockets 的官方說明 來獲得更多的資訊。
另外,在Rails中的assets目錄其實有三個:
app/assets
(放置我們自己為了自己的程式所寫的 js、css 或是 images)lib/assets
(可以放我們自己寫的 js 和 css library)vendor/assets
(放一些我們從別的地方借用的 assets,例如說一些 jQuery 的套件)
這三個目錄,在預設情況下這三個資料夾的東西是共通的(因為都會被打包成一個檔案),你可以把你的 rails app 跑起來後在 http://localhost:3000/assets/application.js
中看到你所有的 js 都在這支檔案中,css 同理亦然,你可以在 terminal 中輸入 Rails.application.config.assets.paths
來查看所有的 assets 路徑。你可以發現,除了原本我們剛剛說的三個 assets 目錄之外,還出現了包含在我們 GemFile 中的 jquery,這代表你的 assets 現在也可以包成 gem 來用,如果你有很多個 projects 常重複使用一些共通的 assets,不妨考慮包成 gem 來使用,方便又愉快。
支援 Preprocessor 預處理
放在app/assets目錄下的Assets,Rails支援使用不同的語法,例如使用Sass語法產生CSS、CoffeeScript產生JavaScript,或是用ERb樣板也可以。使用的方法是將附檔名命名成.sass、.scss、.coffee、.css.erb或.js.erb,Rails就會編譯出結果給瀏覽器。
Sass是一種CSS3語法的擴充,可以使用巢狀、變數、混入、選擇子繼承等等功能,可以更有效率有彈性的撰寫Stylesheet。Sass最後會編譯出合法的CSS讓瀏覽器使用。使用上它區分成兩種形式的語法scss和sass,前者可以與現有的css程式碼直接混合在一起,後者則透過縮排來省略了大括號{}
(就像Python用縮排一樣)。CoffeeScript也是類似原理,它是一種迷你的程式語言,編譯之後會輸出可讀性高、符合JavaScript Lint規範的JavaScript程式碼。
學習這兩種新語法需要額外的時間投入,但是對需要常常撰寫CSS和JavaScript的設計師來說,應該是很不錯的工具,讀者可以自行斟酌是否採用。
安裝 Bootstrap 樣式
Bootstrap 是一個目前非常流行的前端框架,非常適合作為團隊開發時,前端、後端開發者當作預設的前端框架。
這裡我們使用 boostrap-sass 這個官方的 gem,安裝步驟如下:
- 編輯 Gemfile 加上
gem 'bootstrap-sass'
,然後bundle
-
將
app/assets/stylesheets/application.css
改成app/assets/stylesheets/application.scss
,這是因為 Bootstrap 使用 Sass 語法,內容如下@import "bootstrap-sprockets"; @import "bootstrap"; @import "bootstrap/theme"; // this is optional
這裡 @import
的作用等同於本來的 require
,會載入該 css 檔案編譯成同一份 CSS 檔案。注意結尾是沒有 .css
副檔名的,加了反而之後在 production 上會有問題(註)不會載入。另外,結尾一定要有分號 ;
,不然也會出錯。
這是因為 CSS 本身也有
@import
語法,所以加了.css
後,@import
反而就不編譯了,而是直接顯示出例如@import "bootstrap.css"
;,瀏覽器看到這個 css 語法會去下載bootstrap.css
這個檔案,當然是下載不到因為整個 assets pipepline 最後只會編譯出一個.css
檔案,就是application-加上一長串digest碼.css
。
-
修改
app/assets/javascripts/application.js
,在下方加上//= require bootstrap-sprockets
這樣我們就安裝好了。
其他參考資料 https://launchschool.com/blog/integrating-rails-and-bootstrap-part-1
如何處理 image 圖檔
放在app/assets/images下的圖片該怎麼使用呢?在實際佈署後,Rails會將檔案名稱加以編碼,例如rails.png會變成rails-bd9ad5a560b5a3a7be0808c5cd76a798.png。這麼做的原因是當圖片有變更的時候,編碼就會不同而有不同的檔名,這樣就可以避免瀏覽器快取到舊的檔案。也因為檔案名稱會變動,所以放在app/assets/images下的圖片,要用的時候就沒有辦法寫死檔名。在一般的View中,可以使用image_tag
這個Helper:
<%= image_tag("rails.png") %>
如果在CSS裡的話,有兩種辦法:一是將檔案命名為erb結尾,例如app/assets/stylesheets/main.css.erb,然後使用asset_path
這個Helper:
h1 {
background-image: url('<%= asset_path("rails.png") %>');
}
另一種方法是使用Sass或SCSS語法。其中SCSS相容於CSS。例如命名為app/assets/stylesheets/main.scss,然後使用image-url
這個Sass提供的方法:
h1 {
background-image: image-url("rails.png")
}
如果是js檔案中想要拿圖片的位置,就只能用js.erb的格式,然後內嵌asset_path Helper方法了。
其他 Helper
Assets 提供了很多路徑 helper 來讓你指向你的 assets,可以在 erb 樣板中使用:
audio_path("horse.wav") # => /audios/horse.wav
audio_tag("sound") # => <audio src="/audios/sound" />
font_path("font.ttf") # => /fonts/font.ttf
image_path("edit.png") # => "/images/edit.png"
image_tag("icon.png") # => <img src="/images/icon.png" alt="Icon" />
video_path("hd.avi") # => /videos/hd.avi
video_tag("trailer.ogg") # => <video src="/videos/trailer.ogg" />
Sass 還提供了像是 -url
和 -path
這樣的 helper 來協助你,因此你可以在 scss
樣板中這樣使用:
image-url("rails.png") # => url(/assets/rails.png)
image-path("rails.png") # => "/assets/rails.png"
asset-url("rails.png", image) # => url(/assets/rails.png)
asset-path("rails.png", image) # => "/assets/rails.png"
Precompile 編譯靜態檔案
開發的時候,Rails會自動將Asset的壓縮結果快取在tmp下,所以開發者不需要特別處理。但是實際正式上線時,最後壓縮的檔案還是必須放在public目錄下由網頁伺服器直接提供(或是由CDN)效能較好,以下的rake指令可以產生出來:
rake assets:precompile
產生出來的檔案在public/assets/下。
rake assets:clobber
這樣會刪除。
注意,如果在開發模式下執行了rake assets:precompile
,那麼因為放在public/assets/下的靜態檔案會優先丟給瀏覽器,所以這時候再修改app/assets下的原始碼會沒有作用。所以,開發時請記得要刪除這個目錄。
一些實務技巧
前端套件安裝
前端套件五花八門,當我們想要安裝一個第三方前端套件時,會先優先找找看有沒有包成 gem 的合用版本。 如果沒有 gem 或不合用,再手動下載放到 /vendor/assets/下。
不過如果 CSS 裡面有圖片的話,則很可能會有路徑問題,需要手動修理。如果只是 JavaScript 就比較沒有問題。
例如我們想要安裝 Autosize 這個 jQuery plugin,但是找到的 autosize-rails gem 年久失修。因此只好自己手動複製它的程式碼 autosize.min.js,放到 vendor/assets/javascripts/autosize.min.js
,然後在 app/assets/javascript/application.js
裡面去載入它:
//= require autosize.min
$(document).on("turbolinks:load", function(){
autosize($('textarea'));
});
如果該前端套件不需要跟我們的 application.js 壓縮在一起,而且它已經是壓縮過後的版本,也可以考慮就放到 public
目錄下,就完全不經過 Asset Pipeline,你可以在 HTML layout 中直接載入它。
自己的 JavaScript 程式碼要寫在哪裡?
如果只有那一個 HTML template 有用到的 javascript script,我們可以直接塞到該 template 下方:
<script>
//your js code
</script>
當然這樣的缺點就是它其實沒有經過 Asset Pipeline,當然也就沒有壓縮或預處理的功能。另外,在新版的 Turbolinks 中,因為 Turbolinks 的快取功能,會讓上述放在 body 中的 script 重複執行兩遍,我們在 Ajax 一章的 Turbolinks 一節說明過。
如果是每一頁都會執行,或是共用的 javascript function,我們放進 assets/javascript/ 下。例如編輯 assets/javascript/application.js
我們多一個 common.js
檔案:
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require bootstrap-sprockets
//= require common
新增 assets/javascript/common.js
檔案,內容例如:
function your_function() {
// your js code
}
$(document).on("turbolinks:load", function(){
/// you js code
});
另外還有一個小技巧是可以將 js 和 css code 放進 Asset Pipeline 裡面,但是只有特定頁面才會作用(這一招不管 Turbolinks 有沒有移除都適用)。方法是每一頁的 body
標籤加上 id
,修改 app/views/layout/application.html.erb
:
<body id="<%= controller.controller_name %>-<%= controller.action_name %>">
這樣如果 welcome
controller 的 index
頁面的話,就會產生出 <body id="welcome-index">
的標籤。如此這樣我們的 css 或 javascript 就可以定位了。例如可以在上述的 assets/javascript/common.js
寫:
$(document).on("turbolinks:load", function() {
if ( $("#welcome-index").length > 0 ) {
console.log("this is welcome-index page");
}
})
Rails 產生 controller 檔案時,也會產生一個同名的
.coffee
和.scss
檔案,這沒有什麼實際的用途,它並不是說該 controller 的頁面才會執行該 js 和 css。建議可以砍掉這個檔案,自己安排 js 和 css 的檔案即可。
自己的 CSS 程式碼要寫在哪裡?
和上一節類似,我們可以新增一個 app/assets/stylesheets/style.scss
來放我們自己的 CSS,然後編輯 app/assets/stylesheets/application.scss
去載入它:
@import "bootstrap-sprockets";
@import "bootstrap";
@import "bootstrap/theme";
@import "style";
當然,你可以依照自己的需求去分拆 CSS 檔案,只要在 app/assets/stylesheets/application.scss
中依序載入即可。
如何新增 manifest 檔案 (拆成數個壓縮檔)?
Rails 預設只會壓 application.js和application.css 這兩個 JavaScript 和 CSS 檔案,如果你的網站不大的話就夠用了。不過如果你需要根據不同情境 layout 區分不同的 JavaScript 和 CSS 時,就需要新增 manifest 檔案來拆開,例如區分前台、後台等等。
在上述的application.js和application.css中,預設會壓縮所有app/assets目錄下的檔案,要拆開的話,首先需要修改其中的內容把require_tree
那行移除,那麼就只會壓縮你所指定的目錄或檔案。
例如,要新增新的Manifest檔案的話,假設叫做app/assets/javascripts/widget.js,內容如:
//= require ./foobar
這樣就會將assets/javascripts/foobar這個目錄下的檔案通通壓縮成widget.js,而在View中,加上:
<%= javascript_include_tag "widget" %>
就會載入這個 widget.js。注意到如果啟用了assets功能,javascript_include_tag
也只能接受一個參數,即Manifest檔案的名稱。
為了讓rake assets:precompile
也能產生新的壓縮檔案,你還需要編輯config/initializers/assets.rb加入:
Rails.application.config.assets.precompile += %w( widget.js )