Asset Pipeline

A language that doesn’t affect the way you think about programming is not worth knowing. - Alan Perlis

Assets指的是JavaScriptStylesheets和图档等静态档案,这些档案并不会随Requests不同而有所不同。而在Rails目录中,只有public这个目录是公开读取的,所以通常我们会将静态档案都放在public这个目录下,好让浏览器可以直接读取。但是随着JavaScriptStylesheet档案越来越多时,如何管理这些档案变为一项议题,为了加快浏览器的下载速度,我们会合并JavaScriptStylesheet档案,来减少浏览器Request下载次数。更进一步的还会压缩这些档案来加速下载时间。像是Yahoo!Google都有各自开源出自己的压缩工具YUI CompressorClosure Compiler

RailsAssets pipeline可以让我们突破public目录限制,可以将静态档案依需求放在不同目录下,Rails会帮你组合并压缩起来。特别是有一些Rails的外挂套件需要使用JavaScript等静态档案,在没有这个功能之前,我们必须将JavaScript等档案复制放在public目录下,这样浏览器才能读取的到。

Assets的位置在app/assets/下,首先最重要的就是app/assets/javascripts/application.jsapp/assets/stylesheets/application.css,这两个档案看起来充满注解,其实它是个manifest档案,列出了所有要加载的静态档案,这些档案的位置依照惯例放在app/assetsvendor/assets目录下。

让我们先看看application.js

  1. // This is a manifest file that'll be compiled into application.js, which will include all the files
  2. // listed below.
  3. //
  4. // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
  5. // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
  6. //
  7. // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
  8. // compiled file.
  9. //
  10. // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
  11. // about supported directives.
  12. //
  13. //= require jquery
  14. //= require jquery_ujs
  15. //= require turbolinks
  16. //= require_tree .

其中的require jqueryrequire jqueryujs会加载_JQueryRailsJQuery adapater,这是因为我们在Gemfile中有装jquery-rails这个套件,所以这里可以读取的到。require turbolinks在上一章有提到。而requiretree .会加载这个目录下的所有_JavaScript档案。总之,这个manifest的最后输出结果就是通通压缩成一个application.js档案。

同理application.css也是一样加载所有stylesheets目录下的CSS档案,最后压缩成application.css

  1. /*
  2. * This is a manifest file that'll be compiled into application.css, which will include all the files
  3. * listed below.
  4. *
  5. * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
  6. * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
  7. *
  8. * You're free to add application-wide styles to this file and they'll appear at the bottom of the
  9. * compiled file so the styles you add here take precedence over styles defined in any styles
  10. * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
  11. * file per style scope.
  12. *
  13. *= require_tree .
  14. *= require_self
  15. */

实务上不喜欢用 require_tree 的方式来一次加载全部的 css 和 javascript,而是喜欢逐条 require加载,一来比较清楚,二来也可以确保 css 和 javascript 加载的顺序是正确的。另外也不喜欢用 require_self 将 css 写在 application.css 里面,我们应该放到另一个档案再 require 它即可。

让我们看看View,在Layout档案中:

  1. <%= stylesheet_link_tag "application" %>
  2. <%= javascript_include_tag "application" %>

因为最后输出都压缩成一个档案了,所以这里只需要加载application.cssapplication.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目录下的AssetsRails支援使用不同的语法,例如使用Sass语法产生CSSCoffeeScript产生JavaScript,或是用ERb样板也可以。使用的方法是将附档名命名成.sass.scss.coffee.css.erb.js.erbRails就会编译出结果给浏览器。

Sass是一种CSS3语法的扩充,可以使用巢状、变量、混入、选择子继承等等功能,可以更有效率有弹性的撰写StylesheetSass最后会编译出合法的CSS让浏览器使用。使用上它区分成两种形式的语法scsssass,前者可以与现有的css程式码直接混合在一起,后者则透过缩排来省略了大括号{}(就像Python用缩排一样)。CoffeeScript也是类似原理,它是一种迷你的程式语言,编译之后会输出可读性高、符合JavaScript Lint规范的JavaScript程式码。

学习这两种新语法需要额外的时间投入,但是对需要常常撰写CSSJavaScript的设计师来说,应该是很不错的工具,读者可以自行斟酌是否采用。

安装 Bootstrap 样式

Bootstrap 是一个目前非常流行的前端框架,非常适合作为团队开发时,前端、后端开发者当作默认的前端框架。

这里我们使用 boostrap-sass 这个官方的 gem,安装步骤如下:

  • 编辑 Gemfile 加上 gem 'bootstrap-sass',然后 bundle
  • app/assets/stylesheets/application.css 改成 app/assets/stylesheets/application.scss,这是因为 Bootstrap 使用 Sass 语法,内容如下
  1. @import "bootstrap-sprockets";
  2. @import "bootstrap";
  3. @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,在下方加上
  1. //= 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中,可以使用imagetag这个_Helper

  1. <%= image_tag("rails.png") %>

如果在CSS里的话,有两种办法:一是将档案命名为erb结尾,例如app/assets/stylesheets/main.css.erb,然后使用assetpath这个_Helper

  1. h1 {
  2. background-image: url('<%= asset_path("rails.png") %>');
  3. }

另一种方法是使用SassSCSS语法。其中SCSS相容于CSS。例如命名为app/assets/stylesheets/main.scss,然后使用image-url这个Sass提供的方法:

  1. h1 {
  2. background-image: image-url("rails.png")
  3. }

如果是js档案中想要拿图片的位置,就只能用js.erb的格式,然后内嵌asset_path Helper方法了。

其他 Helper

Assets 提供了很多路径 helper 来让你指向你的 assets,可以在 erb 样板中使用:

  1. audio_path("horse.wav") # => /audios/horse.wav
  2. audio_tag("sound") # => <audio src="/audios/sound" />
  3. font_path("font.ttf") # => /fonts/font.ttf
  4. image_path("edit.png") # => "/images/edit.png"
  5. image_tag("icon.png") # => <img src="/images/icon.png" alt="Icon" />
  6. video_path("hd.avi") # => /videos/hd.avi
  7. video_tag("trailer.ogg") # => <video src="/videos/trailer.ogg" />

Sass 还提供了像是 -url-path 这样的 helper 来协助你,因此你可以在 scss 样板中这样使用:

  1. image-url("rails.png") # => url(/assets/rails.png)
  2. image-path("rails.png") # => "/assets/rails.png"
  3. asset-url("rails.png", image) # => url(/assets/rails.png)
  4. asset-path("rails.png", image) # => "/assets/rails.png"

Precompile 编译静态档案

开发的时候,Rails会自动将Asset的压缩结果快取在tmp下,所以开发者不需要特别处理。但是实际正式上线时,最后压缩的档案还是必须放在public目录下由网页服务器直接提供(或是由CDN)效能较好,以下的rake指令可以产生出来:

  1. rake assets:precompile

产生出来的档案在public/assets/下。

  1. 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 里面去加载它:

  1. //= require autosize.min
  2. $(document).on("turbolinks:load", function(){
  3. autosize($('textarea'));
  4. });

如果该前端套件不需要跟我们的 application.js 压缩在一起,而且它已经是压缩过后的版本,也可以考虑就放到 public 目录下,就完全不经过 Asset Pipeline,你可以在 HTML layout 中直接加载它。

自己的 JavaScript 程式码要写在哪里?

如果只有那一个 HTML template 有用到的 javascript script,我们可以直接塞到该 template 下方:

  1. <script>
  2. //your js code
  3. </script>

当然这样的缺点就是它其实没有经过 Asset Pipeline,当然也就没有压缩或预处理的功能。另外,在新版的 Turbolinks 中,因为 Turbolinks 的快取功能,会让上述放在 body 中的 script 重复执行两遍,我们在 Ajax 一章的 Turbolinks 一节说明过。

如果是每一页都会执行,或是共享的 javascript function,我们放进 assets/javascript/ 下。例如编辑 assets/javascript/application.js 我们多一个 common.js 档案:

  1. //= require jquery
  2. //= require jquery_ujs
  3. //= require turbolinks
  4. //= require bootstrap-sprockets
  5. //= require common

新增 assets/javascript/common.js 档案,内容例如:

  1. function your_function() {
  2. // your js code
  3. }
  4. $(document).on("turbolinks:load", function(){
  5. /// you js code
  6. });

另外还有一个小技巧是可以将 js 和 css code 放进 Asset Pipeline 里面,但是只有特定页面才会作用(这一招不管 Turbolinks 有没有移除都适用)。方法是每一页的 body 标籤加上 id,修改 app/views/layout/application.html.erb

  1. <body id="<%= controller.controller_name %>-<%= controller.action_name %>">

这样如果 welcome controller 的 index 页面的话,就会产生出 <body id="welcome-index"> 的标籤。如此这样我们的 css 或 javascript 就可以定位了。例如可以在上述的 assets/javascript/common.js 写:

  1. $(document).on("turbolinks:load", function() {
  2. if ( $("#welcome-index").length > 0 ) {
  3. console.log("this is welcome-index page");
  4. }
  5. })

Rails 产生 controller 档案时,也会产生一个同名的 .coffee.scss 档案,这没有什么实际的用途,它并不是说该 controller 的页面才会执行该 js 和 css。建议可以砍掉这个档案,自己安排 js 和 css 的档案即可。

自己的 CSS 程式码要写在哪里?

和上一节类似,我们可以新增一个 app/assets/stylesheets/style.scss来放我们自己的 CSS,然后编辑 app/assets/stylesheets/application.scss 去加载它:

  1. @import "bootstrap-sprockets";
  2. @import "bootstrap";
  3. @import "bootstrap/theme";
  4. @import "style";

当然,你可以依照自己的需求去分拆 CSS 档案,只要在 app/assets/stylesheets/application.scss 中依序加载即可。

如何新增 manifest 档案 (拆成数个压缩档)?

Rails 默认只会压 application.jsapplication.css 这两个 JavaScript 和 CSS 档案,如果你的网站不大的话就够用了。不过如果你需要根据不同情境 layout 区分不同的 JavaScript 和 CSS 时,就需要新增 manifest 档案来拆开,例如区分前台、后台等等。

在上述的application.jsapplication.css中,默认会压缩所有app/assets目录下的档案,要拆开的话,首先需要修改其中的内容把require_tree那行移除,那么就只会压缩你所指定的目录或档案。

例如,要新增新的Manifest档案的话,假设叫做app/assets/javascripts/widget.js,内容如:

  1. //= require ./foobar

这样就会将assets/javascripts/foobar这个目录下的档案通通压缩成widget.js,而在View中,加上:

  1. <%= javascript_include_tag "widget" %>

就会加载这个 widget.js。注意到如果启用了assets功能,javascriptinclude_tag也只能接受一个参数,即_Manifest档案的名称。

为了让rake assets:precompile也能产生新的压缩档案,你还需要编辑config/initializers/assets.rb加入:

  1. Rails.application.config.assets.precompile += %w( widget.js )

更多线上资源