Rails 如何从 Webpacker 切换到 CSS/JS bundling

Rei · 2021年12月27日 · 最后由 Rei 回复于 2022年05月10日 · 1801 次阅读

原文链接:https://geeknote.net/Rei/posts/375


最近 Rails 7 正式发布,其中一个引人注目的特性是 CSS/JS bundling,用于取代 Rails 6 的 Webpacker。我在之前的文章中介绍过新的方案带来什么变化。

现在 Rails 创建项目,将会增加两个参数:

-j, [--javascript=JAVASCRIPT]  # Choose JavaScript approach [options: importmap (default), webpack, esbuild, rollup]
-c, [--css=CSS]                # Choose CSS processor [options: tailwind, bootstrap, bulma, postcss, sass...]

例如要使用 esbuild 和 sass 可以用以下命令:

$ rails new myapp -j esbuild -c sass

但大多数人关心的可能是如何从已有项目上切换到新方案,下面进一步说明。

脚本切换

Rails 增加了两个 gem cssbundling-railsjsbundling-rails,用来支持 bundling 方案。他们依赖 Rails >= 6,所以不需要升级到 Rails 7 也可以使用。在 Gemfile 中加入以下内容:

gem 'cssbundling-rails'
gem 'jsbundling-rails'

执行 bundle install 之后,用以下命令安装需要的前端工具:

$ bin/rails css:install:sass
$ bin/rails javascript:install:esbuild

通过 git diff 可以看到有什么变动,要注意的地方有:

  1. 现在 CSS 和 JS 的构建源文件分别是 app/assets/stylesheets/application.sass.scssapp/javascript/application.js,需要手工迁移内容。
  2. app/assets/builds 是存放编译后静态文件的地方。
  3. 增加了一个 bin/dev 脚本,通过 foreman 一起启动 rails server 和 CSS/JS 构建进程,配置文件是 Procfile.dev。
  4. 检查 layout 里的标签,删除 webpacker 的引用。

如果项目的文件都按照 Rails 6 默认的目录结构摆放,那么用 bin/dev 启动开发进程就能看到 CSS 和 JS 构建进程在工作。

人工切换

如果你的项目比较复杂,脚本安装不能满足需要,那么可以用人工方式处理。实际上 cssbundling-railsjsbundling-rails 的内容只是一些安装脚本和 Rake task,没有运行时代码。理解如何手工切换到 CSS/JS bundling 可以让配置更符合项目的需求。下面介绍如何实现。

添加 builds 目录

假设项目的静态文件存放结构如下:

app/assets/
├── config
│   └── manifest.js
├── images
└── stylesheets
    └── application.css
app/javascript/
└── packs
    └── application.js

首先创建 builds 目录,用来存放编译后的文件:

$ mkdir app/assets/builds
$ touch app/assets/builds/.keep

.gitignore 中添加这个目录,避免 check in 里面的文件:

/app/assets/builds/*
!/app/assets/builds/.keep

修改 app/assets/config/manifest.js,删除以下配置:

//= link_directory ../stylesheets .css

添加以下配置:

//= link_tree ../builds

现在 Assets pipeline 就知道要从 builds 目录获取静态文件。

配置 sass

安装 sass

$ yarn add sass

package.json 中添加以下内容:

"scripts": {
  "build:css": "sass app/assets/stylesheets/application.scss app/assets/builds/application.css --no-source-map --load-path=node_modules"
}

创建文件 /app/assets/stylesheets/application.scss,将原先 app/assets/stylesheets/application.css 的内容迁移进去。

要注意 Assets Pipeline 提供的 require_tree require_self 等注释不再起作用,要用 sass@import 替代。

如果正常,使用 yarn build:css 可以看到 CSS 编译成功。

配置 esbuild

安装 esbuild

$ yarn add esbuild

package.json 中添加以下内容:

"scripts": {
  "build:js": "esbuild app/javascript/application.js --bundle --outdir=app/assets/builds"
}

创建文件 app/javascript/application.js,将原先 app/javascript/packs/application.js 的内容迁移进去。

要注意如果之前用的 webpacker,转到 esbuild 有可能需要修改 require 语法,例如 import "channels" 要改成 import "./channels"。另外依赖 webpack 的语句也需要修改,例如 Stimulus 的 Webpack Helpers 将不可用。

如果正常,使用 yarn build:js 可以看到 JS 编译成功。

添加 bin/dev 脚本

现在开发环境需要启动三个进程,一个 rails server,一个 sass,还有一个 esbuild。每次开发启动这三个进程会比较繁琐,所以可以借助一些工具管理,这里以 foreman 为例。

添加一个 bin/dev 文件,内容为:

#!/usr/bin/env bash

if ! command -v foreman &> /dev/null
then
  echo "Installing foreman..."
  gem install foreman
fi

foreman start -f Procfile.dev

bin/dev 添加可执行权限:

$ chmod +x bin/dev

添加文件 Procfile.dev,内容为:

web: bin/rails server -p 3000
css: yarn build:css --watch
js: yarn build:js --watch

这样开发的时候就可以用 bin/dev 一起启动三个进程。

添加 Rake task

最后是让 Assets Pipeline 编译静态文件的时候正确的调用外部编译。

添加文件 lib/tasks/build.rake,内容为:

namespace :build do
  desc "Run yarn install"
  task :install do
    system "yarn install"
  end

  desc "Build Javascript and CSS"
  task :all => [:javascript, :css]

  desc "Build JavaScript"
  task :javascript => :install do
    system "yarn build:js"
  end

  desc "Build CSS"
  task :css => :install do
    system "yarn build:css"
  end
end

Rake::Task["assets:precompile"].enhance(["build:all"])

如果正常,现在执行 bin/rails assets:precompile 时会自动执行 build:cssbuild:js

以上就是手工操作切换 CSS/JS bundling 的过程。根据你项目的复杂程度可能需要进行相应的更改。在切换完成后,就可以删掉 webpacker 相关的包和配置。

总结

看完如何手工切换 bundling,你应该会发现 bundling 的操作是在 Assets Pipeline 之前完成的。编译完成后把文件放到 builds 目录,然后告诉 Assets Pipeline 从这个目录读取文件就行了。

这是一次把 Rails 主体和前端工具链解藕的过程,我们可以用任意偏好的工具和方式处理前端文件,只要最后把它纳入 Assets Pipeline 管理就行了。这大大增加了灵活性,也不需要和框架的默认配置做斗争——这都取决于自己的选择。

希望这篇文章有助于理解如何按 Rails 7 的方式管理静态文件。

很棒!捉个虫:“desc "Rum yarn install"” -> desc "Run yarn install"

FinnG 回复

已更正

很及时 我让团队研究搞一下

👍 👍 👍 👍 👍 除了点赞,就只能点赞了

厉害啊,终于可以从 Webpacker 的噩梦中解脱出来了

引入 javascript 库还是用 yarn add 吗 需要额外设置吗

hooopo 回复

这个方案是引用 npm 包,安装 npm 包用 yarn 或者 npm 都可以。

例如 yarn add @hotwired/turbo-rails,然后就可以在 app/javascript/application.js 里面:

import "@hotwired/turbo-rails"

esbuild 已经默认把 node_modules 加到 resolving paths 里。

Rails 官方添加了一个教程,如何从 webpacker 切换到 webpack:

https://github.com/rails/jsbundling-rails/blob/main/docs/switch_from_webpacker.md

Rei 回复

我再试一下 刚从一个教程的 demo 代码里这么操作 import 报错 感觉是没有把 node modules 加进去

重新建了一个项目可以了

Rei 回复

请问,是不是只有 webpack 能够设定 process.env.NODE_ENV

我发现,esbuild 没有办法获取 rails_env 的 值。最后,我是用 window.location.hostname 来判断环境。

还是,我什么地方没留意到?

charlie_hsieh 回复

我看文档可以使用 process.env.NODE_ENV,其实就是一个环境变量所有 Linux 进程都支持。

我的设置开发环境和生产环境只差了一个 --watch 参数,不太理解什么时候要用到 ENV,你可以贴代码和问题的详情看看要怎么解决。

Rei 回复

感谢回覆! 我主要是想 PROD 环境 加入 google analytics 如果开发中 DEV 就不要执行 google analytics 目前其实也是简单的 if else

// --- OLD (rails 6.1 + webpacker) ---
 const run_at_env = "production"
 if (process.env.NODE_ENV === run_at_env) {
    ga.js stuff here
 }
// --- NOW (rails 7.0.2 + esbuild) ---
 const run_at_env = "prod.url.com"
if (window.location.hostname === run_at_env) {
    ga.js stuff here
}
charlie_hsieh 回复

测了一下可以使用 process.env。

源码:

// test.js
process.env.NODE_ENV

编译:

$ esbuild test.js
"development";

通过参数设置可以:

$ esbuild test.js --define:process.env.NODE_ENV=production
production;

通过 Linux 方式设置环境变量不行:

$ NODE_ENV=production esbuild test.js
"development";

所以可以在生产环境编译的时候加个 --define 参数。

https://esbuild.github.io/api/#define

Rei 回复

感谢!😀

好的,我研究看看,应该要把 --define:process.env.NODE_ENV=production

可以设置在哪边~

charlie_hsieh 回复

如果接着顶楼的实现,可以放在 rake task 里面,yarn run 可以在后面加参数:

desc "Build JavaScript"
task :javascript => :install do
  system "yarn build:js --define:process.env.NODE_ENV=production"
end
Rei 回复

感谢! !

我再试试看!

@Rei 你好,感谢分享。有个新手问题麻烦指导一下。谢谢!

按照教程设置,用 foreman 已经跑起来了,builds 目录能看到编译后的文件。

app/assets/
├── builds
   └── .keep
   └── application.css
   └── application.js

manifest.js

//= link_tree ../images
//= link_tree ../builds

但是 html 引用是这样的

<link rel="stylesheet" href="/assets/application-a72913f4c604bc8f97dd576fc8777bf029401f6af0e14b6a6c9d3874acfe73e4.css" media="all" />
<script src="/assets/application-e60554b2942a0b8c33bd15b131e49fb89b11b8fed7d713525c7a1027702358be.js"></script>

所有 js 和 css 还是没有加载成功。

layout

<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
gxlonline 回复

第一个问题,没有加载成功是指找不到这个带 hash 的文件,还是加载了文件但是内容报错?尝试以下操作:

  • 检查 assets 下面的目录有没有其他 application.js 文件导致了被覆盖,删掉它。
  • 重启 rails 进程,添加新目录的时候 assets pipeline 需要重启才能识别。
  • 清除 public 目录下已经生成的 manifest.json 和 assets 目录
  • 升级 sprockets。
  • 用 propshaft 替换 sprockets https://github.com/rails/propshaft

或者直接在 builds 目录新建一个 foo.js,然后在页面引用看看是否工作,可以排除很多其他干扰。

第二个问题涉及 ES Module 的工作方式,根据每个 js 库的情况不同要分别处理,最好是引用的库自己有关于 esbuild 的说明。

例如找不到 jQuery 的问题,是因为这段代码:

import 'jquery'

这是引用了文件,但没有导入模块到这个上下文。要使用 jquery 里的 $ 或者 jQuery 方法,需要这样:

import $ from 'jquery'

但是这个 $ 只能在 application.js 这个文件的上下文用,jquery.qrcode 和其他依赖 window.jQuery 对象的库依然找不到,这时候可以把 $ 绑定到全局:

import $ from 'jquery'

window.$ = window.jQuery = $

我猜想你的 webpacker 配置设置了:

new webpack.ProvidePlugin({
  $: 'jquery',
  jQuery: 'jquery',
});

就是做了类似的事。

es module 也是这几年才成为事实标准的,有的库不一定做了相应的适配,得逐个看情况处理。所以这个切换并不是很容易的。

@Rei

感谢回复,刚才把上个回复编辑乱了,部分内容没有了。

第一个问题,我重新配置一次,可以了。我自己都不知道原因在哪(太菜。。。)。修改 js 和 css 文件后,开发环境下可以自动更新、加载了

第二个问题,Jquery 不能加载,就算这样导入确实也不行,不是全局绑定。

import $ from 'jquery'
window.$ = window.jQuery = $

最后单独加了个 javascript_include_tag 解决

<%= javascript_include_tag "jquery3.min", "data-turbo-track": "reload", defer: true %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>

这个教程帮了大忙。感谢。 最近一个新项目尝试了几种方式:

  1. importmap 在手机上,微信内置浏览器、百度浏览器不支持,面对的用户很多使用这类入口。
  2. Webpacker(现在是 Shakapacker) 配置太复杂了,还有个项目是 rails6+webpacker 当时都还顺利。现在 Rails7+Shakapacker 各种问题。
  3. 最后发现 CSS/JS bundling 对于我来说是心知负担最小的。
gxlonline 回复

importmap 最大作用是让 rails 默认创建的时候不依赖任何打包工具,有打包需求的还是应该用打包器。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号