CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
资产与后台任务
这一课你将能做到
说清楚 Rails 7 资产方案(Propshaft + Importmap)如何免打包加载 JS/CSS
写一个 Active Job 后台任务并用 Solid Queue 跑起来
判断什么该进后台队列,并配置失败重试
为什么要学
两件事:第一,Rails 7 彻底告别了 Webpacker 时代——现在默认「不打包」,浏览器原生 ES Module + Importmap 直接加载 JS,这是现代前端观念在 Rails 的落地,和本站 Next.js 的重打包路线形成鲜明对照。第二,任何真实应用都有「不能让用户等」的操作:发邮件、生成报表、同步第三方——这些必须进后台队列,否则一个慢操作拖垮整个 Web 进程。
先建立图像
资产管道(前端)
app/assets/stylesheets ─┐
app/javascript ├─► Propshaft(指纹+服务)─► 浏览器原生加载
vendor/javascript ─┘ Importmap 声明依赖映射
后台任务(后端)
Web 进程: 请求进来 → SomeJob.perform_later ─► 队列表(数据库)
│
Queue 进程:solid_queue 轮询 ◄──────────────────────┘
│
▼
执行任务 → 成功 / 失败重试
Rails 8/7.2 的默认队列后端 Solid Queue 把任务存在数据库表里,不需要 Redis ——中小项目又省一个中间件。
跟着做
步骤 1:理解 Importmap 工作方式
config/importmap.rb:
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin_all_from "app/javascript/controllers", under: "controllers"
布局里的 <%= javascript_importmap_tags %> 生成:
<script type="importmap">
{"imports": {"application": "/assets/application-abc123.js", ...}}
</script>
<script type="module">import "application"</script>
浏览器拿到 importmap 后按 ES Module 原生机制加载,文件名带内容指纹(abc123)实现永久缓存 + 更新即失效。加新库不用打包:
bin/importmap pin lodash
# importmap.rb 多一行 pin "lodash",JS 里直接 import _ from "lodash"
步骤 2:Stimulus 写一个交互
bin/rails generate stimulus clipboard
// app/javascript/controllers/clipboard_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["source"]
copy() {
navigator.clipboard.writeText(this.sourceTarget.value)
}
}
HTML 里用 data 属性挂接:
<div data-controller="clipboard">
<input data-clipboard-target="source" value="https://example.com/article/1" readonly>
<button data-action="clipboard#copy">复制链接</button>
</div>
data-controller 挂载、data-xxx-target 标记元素、data-action 绑定事件——Stimulus 三件套,这是 Rails 全栈处理「小交互」的标准姿势,大交互才考虑前端框架。
步骤 3:CSS 与静态资产 app/assets/stylesheets/application.css 是入口,可用普通 CSS(现代浏览器支持的嵌套、变量都能直接写)。图片放 app/assets/images/,视图里:
<%= image_tag "logo.png", alt: "Logo" %>
<%# 输出带指纹路径:/assets/logo-5d41402abc.png %>
bin/rails assets:precompile
步骤 4:写一个后台任务 bin/rails generate job ArticleNotify
# app/jobs/article_notify_job.rb
class ArticleNotifyJob < ApplicationJob
queue_as :default
retry_on StandardError, wait: 5.seconds, attempts: 3
discard_on ActiveRecord::RecordNotFound # 记录已删,放弃执行
def perform(article_id)
article = Article.find(article_id)
# 调用推送/邮件服务,这里以日志示意
Rails.logger.info "推送新文章通知:#{article.title}"
end
end
def create
@article = current_user.articles.build(article_params)
if @article.save
ArticleNotifyJob.perform_later(@article.id) # 只传 id,别传对象
redirect_to @article, notice: "已发布,通知推送中"
else
render :new, status: :unprocessable_entity
end
end
步骤 5:配置并运行队列 config/application.rb(或对应 environment 配置):
config.active_job.queue_adapter = :solid_queue
# 安装队列表并迁移
bin/rails solid_queue:install
bin/rails db:migrate
# 开发环境跑 worker(另开一个终端)
bin/jobs
任务进入数据库的 solid_queue_jobs 表,worker 进程轮询取出执行。验证:创建一个文章,看 worker 终端输出推送日志。
定时任务(如每天清理草稿)用 config/recurring.yml:
production:
cleanup_drafts:
command: "DraftCleanupJob.perform_later"
schedule: every day at 3am
讲清楚要点
Rails 7 资产哲学:No-Build :不写 JSX/TypeScript 就不需要打包器。Propshaft 负责指纹和服务静态文件,Importmap 负责模块映射,浏览器干剩下的。需要 npm 重生态时再选 esbuild/jsbundling-rails。
perform_later 只传简单参数 :传 article.id 不传 article 对象——任务执行可能在几分钟后,期间对象可能已删或已改,队列里序列化的是参数快照。GlobalID 虽支持传模型,但最佳实践仍是传 id 重新查。
任务要幂等 :同一任务执行两次不应出错(重试机制可能重复执行)。用唯一约束、find_or_create_by、状态检查来保证。
失败策略显式声明 :retry_on 处理瞬时错误(网络抖动),discard_on 处理永久错误(记录已删)。不声明则默认失败进死信。
Solid Queue 让队列零依赖 :任务存数据库,省去 Redis/RabbitMQ。QPS 极高的场景再考虑 Sidekiq + Redis。
你可能会懵的地方
Importmap 下能用 React 吗?
技术上可以 pin React 并用 htm 写组件,但很痛苦。要用 React/Vue 认真做前端,就引入 jsbundling-rails + esbuild,或者干脆前后端分离(本站路线)。Importmap 的主场是 Stimulus 级交互。
perform_now 和 perform_later 区别?
perform_now 同步立即执行(测试和调试用);perform_later 入队异步执行。还有 perform_later(wait: 10.minutes) 延迟执行。
开发环境任务不执行?
检查队列适配器:开发默认是 :async(进程内线程,重启即丢)。想用真实流程就配 :solid_queue 并跑 bin/jobs;只想看日志用 :async 也行。
多服务器部署时任务会重复执行吗?
不会。Solid Queue 用数据库行锁保证一条任务只被一个 worker 取走。这也是「数据库即队列」方案天然的优势。
邮件也是任务吗?
是。ArticleMailer.notify(article).deliver_later 自动包成 Active Job 入队。邮件、Active Storage 图片处理、Action Cable 广播底层都走队列。
错法对照 错法 问题 正解 控制器里同步发邮件/调第三方 慢操作阻塞请求,拖垮 Puma deliver_later / Job 入队Job.perform_later(@article) 传对象执行时对象可能已删/数据过期 传 id,任务里重新 find 任务非幂等(如直接 increment 计数) 重试导致重复计数 幂等设计或数据库唯一约束 不声明 retry/discard 策略 瞬时故障任务直接死亡 retry_on + discard_on 组合生产忘了跑 worker 进程 任务堆积无人执行 部署脚本包含 bin/jobs 进程管理
卡点急救
改了 JS 浏览器没变化 :指纹没变说明文件没改对位置;确认在 app/javascript/ 下且 importmap 有对应 pin;强刷(Ctrl+F5)。
任务入队了但 worker 无输出 :worker 进程没启动(bin/jobs);或队列名不匹配——queue_as :default 而 worker 只听别的队列。
生产报 Missing host to link to(邮件里生成 URL) :配 config.action_mailer.default_url_options = { host: "example.com" },任务里生成完整 URL 同理需要 host。
面试常问
Rails 7 为什么放弃 Webpacker?
答:浏览器原生支持 ES Module 后,「不打包」成为可能:文件即模块、指纹即缓存策略,开发零编译、刷新即生效。Webpack 生态对 Rails 全栈场景过重——配置复杂、构建慢。Rails 的选择是把简单留给多数人(Importmap + Stimulus),把复杂留给真需要的人(jsbundling/cssbundling 可选接入)。
后台任务为什么要求幂等?怎么实现?
答:网络重试、worker 崩溃重启都可能导致同一任务执行多次。实现手段:数据库唯一约束(重复插入直接失败)、find_or_create_by、先查状态再操作(return if article.notified?)、外部 API 用幂等键。原则是「执行 N 次 = 执行 1 次的效果」。
Solid Queue 和 Sidekiq 怎么选?
答:Solid Queue 用数据库当队列,零额外中间件、事务一致性(任务和业务数据同库提交)、部署简单,中小规模首选;Sidekiq + Redis 吞吐更高(内存操作)、生态成熟,日均千万级任务或已有 Redis 基础设施时用。Rails 8 默认 Solid Queue,代表官方对「够用且简单」的背书。
自测三问
Importmap 模式下浏览器如何找到 application.js?指纹解决什么问题?
为什么 perform_later 推荐传 id 而不是模型对象?
Solid Queue 相比 Sidekiq 省掉了什么?代价是什么?
小结一句话 资产走 No-Build 哲学(Importmap + Propshaft),慢操作走后台队列(Active Job + Solid Queue),任务参数传 id、执行要幂等。
延伸