Assets Pipeline
Asset Pipeline
A language that doesn’t affect the way you think about programming is not worth knowing. - Alan Perlis
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 )
