Hacking Limbo

Reading / Coding / Hacking

Rails Asset Pipeline 经验分享

Rails 开发者从 3.0 升级到 3.1 时往往都会被 asset pipeline 的配置问题卡住,我一直等到 3.1 正式发布才敢升级(10月中旬),像 Compass 兼容这些问题那个时候已经被解决了,也有很多人写了详细的升级流程,比如我参考的就是 Upgrade your projects to Rails 3.1 这一系列文章,少走了很多弯路,整个过程中只遇到以下几个(那个时候 Google 不到答案的)“新”问题。

1. 最省事的 config.assets.compile 写法

官方推荐的 asset pipeline 用法是在 application.cssapplication.js 里包含整个项目的所有 CSS / JavaScript 文件的内容,在执行 rake assets:precompile 的时候只有这两个文件和其他非 CSS / JavaScript 会被处理。按照 Ruby on Rails Guides 所说的,如果有其他文件需要被处理,就要像这样设置:

config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']

由于我这个项目的 CSS / JavaScript 文件比较分散,我期望的效果是默认包含 assets 目录中所有非 Compass partial 的文件(文件名是下划线开头的,扩展名是 sass),逐个文件指定不太现实,就自己摸索着写了一个很复杂的正则去匹配——然后失败了,总是会漏掉一些文件,觉得不对劲,去翻 Sprockets (准确来说是 ActionPack 对 Sprockets 的扩展)的源代码,在 static_compiler.rb 里找到了 compile_path? 这个关键方法,也明白了为什么自己的正则匹配会不成功:这里传入的参数不是文件名,而是一组完整的路径,几乎不可能写出“完美”的正则去匹配……

解决方案很简单,从 compile_path? 的源码里可以看出,config.assets.precompile 可接受的参数值除了 String 和 Regexp 之外,还可以是 Proc,所以最省事的写法应该是:

# 写在 config/environments/production.rb 里:

ASSET_PRECOMPILE_PROC = Proc.new do |path|
  if File.basename(path) =~ /^[^_].*\.\w+$/
    puts "Compiling: #{path}"
    true
  else
    puts "Ignoring: #{path}"
    false
  end
end

MyApplication::Application.configure do
  #...
  config.assets.precompile = [ASSET_PRECOMPILE_PROC]
  #...
end

2. 开发环境中的 AJAX Bug

之前遇到的一个比较诡异的 bug 是执行 AJAX 操作的时候总是会重复提交,在 Chrome 的 Inspector 里可以看到一次 AJAX 请求是 application.js 发出的,另一次则是 jquery_ujs.js 发出的,原因是在开发环境里 config.assets.debug 被设为了 true,因此 application.jsrequire 的文件会被额外单独载入一次,导致 jquery_ujs.js 里的 AJAX 提交事件被绑定了两次……

解法是把 config.assets.debug 设为 false(不是很明白这个设置有什么用 = =),如果真要用可以在 URL 里加入 ?debug_assets=true 参数,效果是一样的。

3. 生产环境的一些小问题

  • 如果用的是 Nginx,记得在 config/environments/production.rb 里加入 config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 这行。Nginx 服务器配置中静态文件部分也要更新:

    # Rails 3.0 的写法
    location ~ ^/(images|javascripts|stylesheets)/  {
        root /path/to/project/public;
        gzip_static on;
        expires 30d;
    }
    
    # Rails 3.1 的写法
    location ~ ^/assets/  {
        root /path/to/project/public;
        gzip_static on;
        expires max;
        add_header Cache-Control public;
    
        # Some browsers still send conditional-GET requests if there's a
        # Last-Modified header or an ETag header even if they haven't
        # reached the expiry date sent in the Expires header.
        add_header Last-Modified "";
        add_header ETag "";
        break;
    }
  • 执行 rake assets:precompile 过程中如果报错说找不到 JS Runtime 什么的,就在 Gemfileassets 组加入 gem 'execjs'gem 'therubyracer' 这两行,然后 bundle install 一下。貌似安装一个 NodeJS 也可以,我没有试验过。