Ruby on Rails 8.2 新手实战手册:从 `rails new` 构建 store 电商应用到 Kamal 生产部署
本指南是 Ruby on Rails(当前仓库版本 8.2.0.alpha,见 RAILS_VERSION)官方入门教程的中文深度解析。我们将以构建一个名为 store 的简易电商应用为主线,循序渐进地掌握 Rails 的核心技术栈:MVC 架构、Active Record 数据建模、路由与控制器、身份认证、缓存、富文本与文件上传、国际化、异步邮件、前端资产管线、自动化测试与 CI,最终用 Kamal 部署到生产服务器。读完本文你将能够独立完成一个 Rails 应用的"从零到生产上线"全流程。
Rails 的设计哲学:DRY 与约定优于配置
Rails 是基于 Ruby 语言的 Web 应用开发框架,它通过对"每个开发者起步都需要什么"做出假设,让你用更少的代码完成更多的工作。Rails 是"有主见"(opinionated)的软件——它假定存在一种"最佳做法",并默认引导你走这条"Rails Way":学习并顺应它,你会获得巨大的生产力提升;反之,把其他语言的习惯硬搬进来,体验可能并不愉快。
Rails 哲学包含两大指导原则:
- Don't Repeat Yourself(DRY,不要重复自己):系统中每条知识都必须有单一、无歧义、权威的表达。避免重复编写相同信息,代码会更易维护、更易扩展、更少 bug。
- Convention Over Configuration(约定优于配置):Rails 对 Web 应用中的许多事项持有默认看法,并默认采用这套约定,而不是要求开发者用无穷无尽的配置文件去逐一定义。
学习前的准备:环境与工具
本教程采用文档中的示例:构建一个名为 store 的电商项目,用于演示 Rails 内置的多项功能。你需要准备:
- Ruby 3.4 或更新版本
- Rails 8.2.0 或更新版本(本文仓库对应的正是
8.2.0.alpha) - 一个趁手的代码编辑器
如需安装 Ruby/Rails,请参考 安装 Ruby on Rails 指南。安装后先在终端验证版本:
$ rails --version
Rails 8.2.0
版本号应不低于 Rails 8.2.0。Rails 自带大量便捷命令,rails --help 可查看全部命令;rails new --help 则能查看自定义生成应用的各种 flag。
MVC:Rails 代码的组织架构
Rails 代码基于 Model-View-Controller(MVC) 架构组织,绝大多数业务代码分布在三个概念层中:
- Model(模型):管理应用数据,通常是数据库表及其关联逻辑。
- View(视图):负责以 HTML、JSON、XML 等不同格式渲染响应。
- Controller(控制器):处理用户交互与每个请求背后的逻辑。
Rails 各功能组件在仓库中也遵循同样的分层(例如模型引擎在 activerecord,视图引擎在 actionview/,控制器引擎在 actionpack/),理解 MVC 后你在 Rails 源码中查找任意功能都能快速定位。
创建第一个 Rails 应用
rails new 会为你生成一个全新的 Rails 应用基础框架,执行:
$ rails new store
生成完毕后进入应用目录,后续所有命令都应在此目录内运行:
$ cd store
新应用目录结构一览
对初学者来说,最关键的是 app/ 目录——控制器、模型、视图、辅助方法、邮件器、任务与资产都放在这里。下表汇总了新应用的各目录职责:
| 文件/目录 | 用途 |
|---|---|
| app/ | 存放应用的控制器、模型、视图、helpers、mailers、jobs 和 assets,本教程后续几乎都在此目录工作 |
| bin/ | 包含启动应用的 rails 脚本,以及用于 setup、update、deploy、run 的其他脚本 |
| config/ | 应用路由、数据库等配置,详见 Configuring Rails Applications |
| config.ru | 基于 Rack 的服务器启动配置 |
| db/ | 当前数据库 schema 以及数据库迁移文件 |
| Dockerfile | Docker 配置文件 |
| Gemfile / Gemfile.lock | 通过 Bundler 声明应用所需 gem 依赖 |
| lib/ | 应用的扩展模块 |
| log/ | 应用日志文件 |
| public/ | 静态文件与编译后的资源;应用运行时此目录按原样对外暴露 |
| Rakefile | 定位并加载可从命令行运行的任务;自定义任务建议加入 lib/tasks 而非直接修改它 |
| script/ | 一次性脚本与基准测试脚本 |
| storage/ | SQLite 数据库文件与 Active Storage 的 Disk Service 文件,详见 Active Storage Overview |
| test/ | 单元测试、fixtures 及其他测试装置,详见 Testing Rails Applications |
| tmp/ | 临时文件(缓存与 pid 文件等) |
| vendor/ | 第三方代码(典型应用包含 vendored gems) |
| .kamal/ | Kamal secrets 与部署钩子 |
| .ruby-version | 默认 Ruby 版本 |
| .gitignore / .dockerignore / .gitattributes | Git 忽略规则、Docker 忽略规则与 Git 路径元数据 |
| .github/ | GitHub 专属文件 |
| .rubocop.yml | RuboCop 静态检查配置 |
| README.md | 应用说明书,应编写"应用做什么、如何 setup"等 |
Hello, Rails!——创建数据库并首次启动
在 store 目录中依次执行以下命令:
$ bin/rails db:create
该命令首先创建应用的数据库。接着启动 Web 服务器:
$ bin/rails server
NOTE:在应用目录内运行命令时应使用
bin/rails,以确保使用的是本应用锁定的 Rails 版本。
服务器启动后会运行 Puma,输出大致如下:
=> Booting Puma
=> Rails 8.2.0 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 6.4.3 (ruby 3.3.5-p100) ("The Eagle of Durango")
* Min threads: 3
* Max threads: 3
* Environment: development
* PID: 12345
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
在浏览器打开 http://localhost:3000,会看到默认的 Rails 欢迎页:
欢迎页是新应用的"冒烟测试"(smoke test),证明后台各环节能正常协作来渲染一个页面。随时可用 Ctrl-C 停止服务器。
开发环境的自动加载
开发者幸福感是 Rails 的核心理念之一,具体体现之一就是开发环境下的自动代码重载:一旦启动 Rails 服务器,新增文件或对既有文件的修改都会被侦测并自动加载/重载,无需每次改动后重启。你可能还注意到 Rails 应用几乎不使用其他语言常见的 require 语句——Rails 靠命名约定自动按需加载文件,你可以专心写业务代码。详见 Autoloading and Reloading Constants。
Active Record:创建数据模型与数据库迁移
Active Record 是 Rails 将关系型数据库映射为 Ruby 代码的核心功能,它自动生成与数据库交互的 SQL(建表、增删改查记录等)。我们的应用默认使用 SQLite(Rails 的默认数据库)。
为商品建模,运行:
$ bin/rails generate model Product name:string
该命令生成名为 Product 的模型,其在数据库中对应一个 name 字符串列。终端输出:
invoke active_record
create db/migrate/20240426151900_create_products.rb
create app/models/product.rb
invoke test_unit
create test/models/product_test.rb
create test/fixtures/products.yml
一次生成三类文件:1)db/migrate 下的迁移文件;2)app/models/product.rb 中的 Active Record 模型;3)该模型的测试与测试 fixtures。
NOTE:模型名是单数的,因为实例化的模型代表数据库中的单条记录(你在创建一件product)。而数据库表名是复数的,因为表容纳的是该模型的所有实例(你创建的是一个装着大量products 的数据库)。
迁移文件:描述数据库变更
迁移(migration) 是一组希望对数据库施加的变更。通过迁移定义如何添加、修改、删除表/列/其他属性,使开发期的变更可以被安全地同步到生产环境。打开 Rails 生成的迁移:
# db/migrate/<timestamp>_create_products.rb
class CreateProducts < ActiveRecord::Migration[8.2]
def change
create_table :products do |t|
t.string :name
t.timestamps
end
end
end
create_table 块定义表结构与列类型;t.string :name 在 products 表创建名为 name 的字符串列;t.timestamps 是定义 created_at 与 updated_at 两列的快捷方式——它们是大多数 Active Record 模型的标配,由 Active Record 在创建/更新记录时自动维护。
运行迁移
执行:
$ bin/rails db:migrate
该命令检查新迁移并将其应用到数据库,输出大致如下:
== 20240426151900 CreateProducts: migrating ===================================
-- create_table(:products)
-> 0.0030s
== 20240426151900 CreateProducts: migrated (0.0031s) ==========================
TIP:操作失误时可运行
bin/rails db:rollback回滚上一次迁移。
Rails Console 与 Active Record 基础操作
Console(控制台)是测试应用代码的交互式工具:
$ bin/rails console
会看到如下提示符(store(dev)> 表示 development 环境):
Loading development environment (Rails 8.2.0)
store(dev)>
输入代码回车即执行,例如验证版本:
store(dev)> Rails.version
=> "8.2.0"
空模型为何能工作:Attribute 动态生成
app/models/product.rb 的类体几乎为空:
# app/models/product.rb
class Product < ApplicationRecord
end
Rails 之所以知道这个模型的定义,是因为当 Product 被使用时,它会查询数据库表取得列名与类型,在后台自动为这些属性生成代码——这就是"约定优于配置"的又一实例。在 console 验证 Rails 探测到的列:
store(dev)> Product.column_names
=> ["id", "name", "created_at", "updated_at"]
创建记录
store(dev)> product = Product.new(name: "T-Shirt")
=> #<Product:0x000000012e616c30 id: nil, name: "T-Shirt", created_at: nil, updated_at: nil>
此刻 product 尚未入库,因此没有 id 与时间戳。调用 save 落库:
store(dev)> product.save
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
Product Create (0.9ms) INSERT INTO "products" ("name", "created_at", "updated_at") VALUES ('T-Shirt', '2024-11-09 16:35:01.117836', '2024-11-09 16:35:01.117836') RETURNING "id" /*application='Store'*/
TRANSACTION (0.9ms) COMMIT TRANSACTION /*application='Store'*/
=> true
save 会生成 INSERT SQL,并将数据库返回的 id、created_at、updated_at 回填到内存对象中。与 save 类似,create 可在一次调用内完成"实例化 + 保存":
store(dev)> Product.create(name: "Pants")
=> #<Product:0x0000000120485c80 id: 2, name: "Pants", ...>
查询、过滤、排序与单条查找
查询全部记录用 all(类方法):
store(dev)> Product.all
Product Load (0.1ms) SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=> [#<Product ... id: 1, name: "T-Shirt" ...>, #<Product ... id: 2, name: "Pants" ...>]
TIP:
all返回ActiveRecord::Relation对象——一种类数组的数据库记录集合,支持过滤、排序等链式操作。
用 where 按列过滤:
store(dev)> Product.where(name: "Pants")
Product Load (1.5ms) SELECT "products".* FROM "products" WHERE "products"."name" = 'Pants' /* loading for pp */ LIMIT 11
=> [#<Product:0x... id: 2, name: "Pants", ...>]
用 order(name: :asc) 按名称升序排序。若需按 ID 精确取单条记录,用 find:
store(dev)> Product.find(1)
Product Load (0.2ms) SELECT "products".* FROM "products" WHERE "products"."id" = 1 LIMIT 1 /*application='Store'*/
=> #<Product:0x... id: 1, name: "T-Shirt", ...>
find 生成的 SELECT 带 WHERE id = 1 与 LIMIT 1,返回 Product 实例(而非 Relation,因为只取一条)。
更新与删除记录
更新有两种方式:update 一步完成"赋值 + 校验 + 保存":
store(dev)> product.update(name: "Shoes")
=> true
或先逐个赋值再调用 save:
store(dev)> product.name = "T-Shirt"
=> "T-Shirt"
store(dev)> product.save
=> true
删除用 destroy:
store(dev)> product.destroy
TRANSACTION (0.1ms) BEGIN immediate TRANSACTION
Product Destroy (0.4ms) DELETE FROM "products" WHERE "products"."id" = 1
TRANSACTION (0.1ms) COMMIT TRANSACTION
=> #<Product:0x... id: 1, name: "T-Shirt", ...>
校验:保证数据合法
Active Record validations 可确保写入数据库的数据满足规则。给 Product 加 name 必填校验:
# app/models/product.rb
class Product < ApplicationRecord
validates :name, presence: true
end
console 中如果代码有改动需要手动刷新(开发服务器会自动重载,但已运行的 console 不会):reload!。随后尝试创建无 name 的商品:
store(dev)> product = Product.new
store(dev)> product.save
=> false
save 返回 false。Rails 会在 create、update、save 时自动执行校验。通过 errors 查看错误详情:
store(dev)> product.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=name, type=blank, options={}>]>
store(dev)> product.errors.full_messages
=> ["Name can't be blank"]
errors.full_messages 生成可直接用于用户界面的友好提示。验证完毕后输入 exit 退出 console。
一次请求的旅程:路由、控制器与视图
要让 Rails "开口说话",至少需要三样东西:route(路由)、带 action(动作) 的controller(控制器)、以及view(视图)。路由把请求映射到控制器动作;动作完成处理请求所需的工作并准备视图数据;视图以目标格式渲染数据。实现层面:路由是 Ruby DSL(领域特定语言)书写的规则,控制器是 Ruby 类、其公有方法即动作,视图是模板(HTML 与 Ruby 的混合体)。
URL 的构成与 HTTP 方法
以 https://example.org/products?sale=true&sort=asc 为例:
https是协议(protocol)example.org是主机(host)/products是路径(path)?sale=true&sort=asc是查询参数(query parameters)
HTTP 请求通过方法告知服务器对 URL 执行什么操作:
GET:取回数据(加载页面或读取记录)POST:提交数据用于处理(通常创建记录)PUT/PATCH:提交数据更新既有记录DELETE:删除记录
定义路由
路由是一行把"HTTP 方法 + URL 路径"配对、并指明由哪个 controller#action 响应的代码。编辑 config/routes.rb:
# config/routes.rb
Rails.application.routes.draw do
# ...
get "/products", to: "products#index"
end
这条路由指示 Rails 接收指向 /products 路径的 GET 请求,并转交给 ProductsController 的 index 动作。路由中无需声明协议、域名(它们负责把请求送达你的服务器)与查询参数(它们是控制器中做数据过滤时可选的"选项")。
再看两个例子。POST 路由:
# config/routes.rb
Rails.application.routes.draw do
# ...
get "/products", to: "products#index"
post "/products", to: "products#create"
end
带**参数(parameter)**的路由:/products/:id 中的 :id 会捕获 URL 中的一段用于后续处理。访问 /products/1 时 :id 被设为 1,控制器动作即可据此查询并展示 ID=1 的 Product;参数不限于整数,例如博客可用 get "/blog/:title", to: "blog#show" 匹配 /blog/hello-world 并按标题查文章。
CRUD 路由与 resources
一个资源通常需要 4 类操作(增删改查 CRUD),展开对应 8 条典型路由:Index(列出全部)、New(渲染新建表单)、Create(处理新建表单)、Show(渲染单条记录)、Edit(渲染编辑表单)、Update full(处理整记录更新,通常 PUT)、Update partial(处理部分字段更新,通常 PATCH)、Destroy(删除记录)。手写如下:
# config/routes.rb
Rails.application.routes.draw do
# ...
get "/products", to: "products#index"
get "/products/new", to: "products#new"
post "/products", to: "products#create"
get "/products/:id", to: "products#show"
get "/products/:id/edit", to: "products#edit"
patch "/products/:id", to: "products#update"
put "/products/:id", to: "products#update"
delete "/products/:id", to: "products#destroy"
end
每次手写太过冗余,Rails 提供等价快捷写法,用一行替代上面全部:
# config/routes.rb
Rails.application.routes.draw do
# ...
resources :products
end
TIP:若不需要全部 CRUD 动作,可用
only:/except:精确指定,详见 routing 指南。
routes 命令
运行 bin/rails routes 可查看应用响应的全部路由:
Prefix Verb URI Pattern Controller#Action
products GET /products(.:format) products#index
POST /products(.:format) products#create
new_product GET /products/new(.:format) products#new
edit_product GET /products/:id/edit(.:format) products#edit
product GET /products/:id(.:format) products#show
PATCH /products/:id(.:format) products#update
PUT /products/:id(.:format) products#update
DELETE /products/:id(.:format) products#destroy
输出还包含健康检查等其他内置路由。注意最左列的 Prefix:它们对应生成 URL 的 helper(见后文)。
控制器与动作
用生成器创建带 index 动作的 ProductsController(路由已配好,用 --skip-routes 跳过生成):
$ bin/rails generate controller Products index --skip-routes
create app/controllers/products_controller.rb
invoke erb
create app/views/products
create app/views/products/index.html.erb
invoke test_unit
create test/controllers/products_controller_test.rb
invoke helper
create app/helpers/products_helper.rb
invoke test_unit
该命令生成:控制器本体、对应的 views 目录、动作视图、控制器测试文件,以及用于在视图中提取逻辑的 helper。控制器如下:
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
end
end
文件名 products_controller.rb 是类名 ProductsController 的下划线形式,这种约定使 Rails 免 require 自动加载代码。index 虽是空方法,Rails 默认仍会渲染同名模板 app/views/products/index.html.erb(初版内容是占位 HTML)。
发起请求:从浏览器到模板
重启 bin/rails server 后访问 http://localhost:3000/products:浏览器请求 /products → Rails 匹配 products#index → 调用空 index 动作 → 渲染同名模板并返回。再在 config/routes.rb 中把首页指到商品列表:
# config/routes.rb
Rails.application.routes.draw do
# ...
root "products#index"
resources :products
end
此后访问 http://localhost:3000 即渲染 Products#index。
实例变量与 ERB
Rails 用实例变量(@ 开头)在控制器与视图间共享数据。改造 index 动作:
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
@products = Product.all
end
end
在视图中用 ERB(Embedded Ruby) 动态生成 HTML。<%= %> 会执行 Ruby 并输出返回值;<% %> 只求值不输出。先用 debug helper 以 YAML 输出变量便于排查:
<%# app/views/products/index.html.erb %>
<%= debug @products %>
再把列表页改成循环输出全部商品名:
<%# app/views/products/index.html.erb %>
<h1>Products</h1>
<div id="products">
<% @products.each do |product| %>
<div>
<%= product.name %>
</div>
<% end %>
</div>
ERB 遍历 @products(ActiveRecord::Relation),为每个商品渲染一个包含名称的 <div>。
展示单个商品:Show 动作
resources :products 已生成 /products/:id → products#show。补充 show 动作:
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
end
show 用单数 @product(读取单条记录),index 用复数 @products。params 提供请求参数,此处即路由 :id:访问 /products/1 时 params 含 {id: 1},最终执行 Product.find(1)。按命名约定,show 对应视图 app/views/products/show.html.erb:
<%# app/views/products/show.html.erb %>
<h1><%= @product.name %></h1>
<%= link_to "Back", products_path %>
URL helpers 与 link_to
回到 bin/rails routes 输出的 Prefix 列——它对应 Ruby 中的 URL helper:
products_path生成"/products"products_url生成"http://localhost:3000/products"product_path(1)生成"/products/1"product_url(1)生成"http://localhost:3000/products/1"
_path 返回相对路径(浏览器认为针对当前域名),_url 返回含协议/主机/端口的完整 URL,后者在渲染"浏览器之外查看"的邮件时非常有用。link_to 接受显示文本与目标 path,生成干净的内联链接。把 index 页重构为 helper 写法:
<%# app/views/products/index.html.erb %>
<h1>Products</h1>
<div id="products">
<% @products.each do |product| %>
<div>
<%= link_to product.name, product_path(product.id) %>
</div>
<% end %>
</div>
创建商品:New 与 Create
创建需要两个动作:渲染表单的 new 与处理提交的 create:
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
def new
@product = Product.new
end
end
new 实例化一个空 Product 供表单填充。列表页顶部加"新建"入口:
<%= link_to "New product", new_product_path %>
创建 app/views/products/new.html.erb:
<%# app/views/products/new.html.erb %>
<h1>New product</h1>
<%= form_with model: @product do |form| %>
<div>
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
<%= link_to "Cancel", products_path %>
form_with helper 借助 form builder 处理 CSRF token、依据 model: 生成 URL、并让提交按钮文案贴合模型。浏览器"查看源代码"可见表单 HTML:
<form action="/products" accept-charset="UTF-8" method="post">
<input type="hidden" name="authenticity_token" value="UHQSKXCaFqy_aoK760zpSMUPy6TMnsLNgbPMABwN1zpW-Jx6k-2mISiF0ulZOINmfxPdg5xMyZqdxSW1UK-H-Q">
<div>
<label for="product_name">Name</label>
<input type="text" name="product[name]" id="product_name">
</div>
<div>
<input type="submit" name="commit" value="Create Product" data-disable-with="Create Product">
</div>
</form>
因为传入的是新 Product 实例,表单自动被配置为向 /products 发送 POST。在控制器中实现 create:
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
def new
@product = Product.new
end
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new, status: :unprocessable_entity
end
end
private
def product_params
params.expect(product: [ :name ])
end
end
Strong Parameters:参数白名单
product_params 即 strong parameters:它要求 params 中存在 :product 键,且其下仅允许 :name 一个字段,其他任何参数都会被忽略,从而防止恶意用户注入不该修改的属性。注意这里用的是 Rails 8.x 的 params.expect 新写法。
错误处理与重定向
@product.save 会执行校验。成功时 redirect_to @product——当传入 Active Record 对象时,Rails 会为它生成 show 路径(如 /products/2)。校验失败时执行 render :new(同控制器下渲染 app/views/products/new.html.erb),并把 HTTP 状态设为 422 Unprocessable Entity 通知浏览器本次 POST 失败;由于 @product 已赋值,表单会带数据回显。
编辑商品:Edit 与 Update,及 partial 抽取
编辑流程与新建几乎一致,区别是用 edit/update 动作:
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
# index/show/new/create ...
def edit
@product = Product.find(params[:id])
end
def update
@product = Product.find(params[:id])
if @product.update(product_params)
redirect_to @product
else
render :edit, status: :unprocessable_entity
end
end
private
def product_params
params.expect(product: [ :name ])
end
end
用 Partial 复用表单
新建与编辑表单几乎相同,可用 partial(局部模板) 复用。把表单抽到 app/views/products/_form.html.erb(下划线前缀表示 partial),并将实例变量替换为局部变量 product,顺便在表单内展示错误:
<%# app/views/products/_form.html.erb %>
<%= form_with model: product do |form| %>
<% if form.object.errors.any? %>
<p class="error"><%= form.object.errors.full_messages.first %></p>
<% end %>
<div>
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
TIP:使用局部变量使 partial 可在同一页面以不同值多次复用,渲染列表项时尤其有用。
new 视图改为渲染 partial:
<%# app/views/products/new.html.erb %>
<h1>New product</h1>
<%= render "form", product: @product %>
<%= link_to "Cancel", products_path %>
edit 视图几乎一模一样:
<%# app/views/products/edit.html.erb %>
<h1>Edit product</h1>
<%= render "form", product: @product %>
<%= link_to "Cancel", @product %>
详情页也补上编辑入口:
<%# app/views/products/show.html.erb %>
<h1><%= @product.name %></h1>
<%= link_to "Back", products_path %>
<%= link_to "Edit", edit_product_path(@product) %>
partial 的更多用法见 Action View 指南。
Before Actions:抽取共享代码
show、edit、update 都在执行 @product = Product.find(params[:id])。before_action 允许在动作执行之前运行共享代码,一次抽取、多处复用,是 DRY 哲学的又一实践:
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
before_action :set_product, only: %i[ show edit update ]
def index
@products = Product.all
end
def show
end
def new
@product = Product.new
end
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @product.update(product_params)
redirect_to @product
else
render :edit, status: :unprocessable_entity
end
end
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.expect(product: [ :name ])
end
end
删除商品:Destroy
最后实现删除。将 destroy 加入 before_action 名单,并实现动作:
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
before_action :set_product, only: %i[ show edit update destroy ]
# ...
def destroy
@product.destroy
redirect_to products_path
end
# ...
end
在 show 视图加删除按钮。这里用 button_to 而非 link_to——它会生成内含单按钮的表单,点击即提交 DELETE 请求:
<%# app/views/products/show.html.erb %>
<h1><%= @product.name %></h1>
<%= link_to "Back", products_path %>
<%= link_to "Edit", edit_product_path(@product) %>
<%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
turbo_confirm data 属性会指示 Turbo(见后文)在提交前弹出确认框。
加入身份认证
目前任何人都能改删商品,不安全。Rails 内置认证生成器,会创建 User 与 Session 模型以及登录所需的控制器/视图:
$ bin/rails generate authentication
随后迁移数据库以新增 User 与 Session 表:
$ bin/rails db:migrate
打开 console 创建一个用户(请替换为你自己的邮箱密码):
store(dev)> User.create! email_address: "you@example.org", password: "s3cr3t", password_confirmation: "s3cr3t"
重启 Rails 服务器以加载生成器引入的 bcrypt gem(用于安全地哈希密码)。之后访问任意页面都会要求登录——试试 http://localhost:3000/products/new,输入刚创建的邮箱密码即可通过,浏览器会缓存凭据供后续请求使用。
添加登出
在全局布局 app/views/layouts/application.html.erb 中加入导航与登出按钮(该布局放置每个页面都包含的头部/页脚等 HTML):
<%# app/views/layouts/application.html.erb %>
<!DOCTYPE html>
<html>
<%# ... %>
<body>
<nav>
<%= link_to "Home", root_path %>
<%= button_to "Log out", session_path, method: :delete if authenticated? %>
</nav>
<main>
<%= yield %>
</main>
</body>
</html>
仅当用户已认证时(authenticated?)才显示登出按钮;点击后向 session 路径发 DELETE 请求完成登出。
允许未认证访问
默认认证生成器会限制所有页面仅认证用户可访问。我们的商品列表/详情页应对所有人开放,在控制器中放行:
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
allow_unauthenticated_access only: %i[ index show ]
# ...
end
仅向认证用户展示操作链接
列表页的"新建"入口仅在登录后显示:
<%# app/views/products/index.html.erb %>
<%= link_to "New product", new_product_path if authenticated? %>
可在导航栏增加未登录时的登录链接:
<%# app/views/layouts/application.html.erb %>
<nav>
<%= link_to "Home", root_path %>
<%= button_to "Log out", session_path, method: :delete if authenticated? %>
<%= link_to "Login", new_session_path unless authenticated? %>
</nav>
同理可让详情页的 Edit/Delete 也只在认证后出现:
<%# app/views/products/show.html.erb %>
<h1><%= @product.name %></h1>
<%= link_to "Back", products_path %>
<% if authenticated? %>
<%= link_to "Edit", edit_product_path(@product) %>
<%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
<% end %>
用 Solid Cache 缓存页面片段
缓存页面局部能改善性能。Rails 默认随附 Solid Cache(基于数据库的后端缓存)。用 cache 方法缓存详情页标题区:
<%# app/views/products/show.html.erb %>
<% cache @product do %>
<h1><%= @product.name %></h1>
<% end %>
传入 @product 后,Rails 为商品生成唯一缓存键。Active Record 对象有 cache_key 方法(返回类似 "products/1" 的字符串),cache helper 再与模板 digest 合并构成该段 HTML 的唯一键。在开发环境启用缓存:
$ bin/rails dev:cache
首次访问 /products/2 时,服务器日志出现:
Read fragment views/products/show:a5a585f985894cd27c8b3d49bb81de3a/products/1-20240918154439539125 (1.6ms)
Write fragment views/products/show:a5a585f985894cd27c8b3d49bb81de3a/products/1-20240918154439539125 (4.0ms)
Read fragment 表示生成键并询问缓存是否存在;首次未命中故 Write fragment 生成并写入 HTML。刷新后不再有 Write fragment——第二次请求命中了缓存。当记录更新时 Rails 会改变缓存键,确保绝不渲染陈旧数据。详见 Caching with Rails。
Action Text:富文本字段
许多应用需要带嵌入(多媒体)的富文本,Rails 用 Action Text 开箱提供。先运行安装器:
$ bin/rails action_text:install
$ bundle install
$ bin/rails db:migrate
重启服务器加载新特性。给 Product 加富文本描述字段:
# app/models/product.rb
class Product < ApplicationRecord
has_rich_text :description
validates :name, presence: true
end
在表单 partial 中提交按钮前加富文本编辑区:
<%# app/views/products/_form.html.erb %>
<%= form_with model: product do |form| %>
<%# ... %>
<div>
<%= form.label :description, style: "display: block" %>
<%= form.rich_textarea :description %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
控制器同步放开新参数:
# app/controllers/products_controller.rb
def product_params
params.expect(product: [ :name, :description ])
end
详情页展示富文本(Rails 会安全渲染格式):
<%# app/views/products/show.html.erb %>
<% cache @product do %>
<h1><%= @product.name %></h1>
<%= @product.description %>
<% end %>
视图模板变化同样会改变缓存键,保证缓存与最新模板同步。新建商品并输入带粗体/斜体的描述即可在 show 页看到格式化文本,编辑时富文本也会完整保留在编辑器中。详见 Action Text Overview。
Active Storage:文件上传
Action Text 底层依赖 Active Storage——Rails 便捷的文件上传方案。可直接把图片拖入富文本编辑器,保存后即自动上传并渲染。也可以直接使用 Active Storage:给 Product 增加头图:
# app/models/product.rb
class Product < ApplicationRecord
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
end
表单提交按钮前加文件上传字段:
<%# app/views/products/_form.html.erb %>
<%= form_with model: product do |form| %>
<%# ... %>
<div>
<%= form.label :featured_image, style: "display: block" %>
<%= form.file_field :featured_image, accept: "image/*" %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
放开 :featured_image 参数:
# app/controllers/products_controller.rb
def product_params
params.expect(product: [ :name, :description, :featured_image ])
end
详情页顶部展示头图(仅在已附加时输出):
<%# app/views/products/show.html.erb %>
<%= image_tag @product.featured_image if @product.featured_image.attached? %>
国际化(I18n)
Rails 让应用翻译成其他语言变得容易。视图中的 translate/t helper 按名称查翻译并返回当前 locale 的文本。把 index 页标题改为翻译项:
<%# app/views/products/index.html.erb %>
<h1><%= t "hello" %></h1>
刷新后标题变为 Hello world——来自默认英文 locale 的 config/locales/en.yml:
# config/locales/en.yml
en:
hello: "Hello world"
新建西班牙语 locale 文件 config/locales/es.yml:
# config/locales/es.yml
es:
hello: "Hola mundo"
接着告知 Rails 使用哪个 locale:最简单的是在 URL 参数中查找。在 app/controllers/application_controller.rb 中:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
around_action :switch_locale
def switch_locale(&action)
locale = params[:locale] || I18n.default_locale
I18n.with_locale(locale, &action)
end
end
around_action 在每个请求运行:取 params[:locale],否则回退默认 locale;请求结束自动复位。验证:
- http://localhost:3000/products?locale=en → 英文
- http://localhost:3000/products?locale=es → 西班牙语
- 不带
locale参数 → 回退英文
再换用真实翻译取代 "Hello world"。注意 t ".title" 中标题前的点号:这是相对 locale 查找,自动把控制器与动作并入键名,即 en.products.index.title。相应更新两个 locale 文件:
# config/locales/en.yml
en:
hello: "Hello world"
products:
index:
title: "Products"
# config/locales/es.yml
es:
hello: "Hola mundo"
products:
index:
title: "Productos"
现在英文 locale 显示 "Products"、西班牙语显示 "Productos"。完整机制见 Internationalization (I18n)。
Action Mailer 与邮件通知
电商常见的功能是"缺货登记、到货提醒"。我们利用它系统学习 Action Mailer。
基础库存追踪
先给 Product 增加库存字段。Rails 能从命名 AddInventoryCountToProducts 推断目标表为 products(遵循 add_<columns>_to_<table> 约定),从而预填 add_column:
$ bin/rails generate migration AddInventoryCountToProducts inventory_count:integer
查看
bin/rails generate migration --help可了解更多命名约定。
打开生成的迁移,把默认值设为 0 以确保永不为空:
# db/migrate/<timestamp>_add_inventory_count_to_products.rb
class AddInventoryCountToProducts < ActiveRecord::Migration[8.2]
def change
add_column :products, :inventory_count, :integer, default: 0
end
end
运行 bin/rails db:migrate。将库存加入表单(用 number_field),并在控制器放开参数:
<%# app/views/products/_form.html.erb %>
<div>
<%= form.label :inventory_count, style: "display: block" %>
<%= form.number_field :inventory_count %>
</div>
# app/controllers/products_controller.rb
def product_params
params.expect(product: [ :name, :description, :featured_image, :inventory_count ])
end
模型上加校验确保库存不为负:
# app/models/product.rb
class Product < ApplicationRecord
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
end
订阅用户关联
生成 Subscriber 模型存储订阅邮箱并关联商品(product:belongs_to 声明一对多关系的一端;不指定 email 类型时 Rails 默认 string):
$ bin/rails generate model Subscriber product:belongs_to email
生成的迁移用 belongs_to 添加 product_id 外键列:
# db/migrate/<timestamp>_create_subscribers.rb
class CreateSubscribers < ActiveRecord::Migration[8.2]
def change
create_table :subscribers do |t|
t.belongs_to :product, null: false, foreign_key: true
t.string :email
t.timestamps
end
end
end
运行 bin/rails db:migrate。一个 Product 可以有多个 Subscriber,故在 Product 模型补上关联的另一端 has_many(dependent: :destroy 说明商品删除时级联删除订阅,并指导两张表如何联表查询):
# app/models/product.rb
class Product < ApplicationRecord
has_many :subscribers, dependent: :destroy
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
end
创建订阅控制器 app/controllers/subscribers_controller.rb:
# app/controllers/subscribers_controller.rb
class SubscribersController < ApplicationController
allow_unauthenticated_access
before_action :set_product
def create
@product.subscribers.where(subscriber_params).first_or_create
redirect_to @product, notice: "You are now subscribed."
end
private
def set_product
@product = Product.find(params[:product_id])
end
def subscriber_params
params.expect(subscriber: [ :email ])
end
end
redirect_to 的 notice: 参数设置 flash 消息——一种在控制器动作间传递临时数据的机制:放入 flash 的数据在下一次动作可用后即清除,常用于"重定向前设置提示"。在布局中展示 flash:
<%# app/views/layouts/application.html.erb %>
<body>
<div class="notice"><%= flash[:notice] %></div>
<div class="alert"><%= flash[:alert] %></div>
<%# ... %>
</body>
Flash 详细说明见 Action Controller Overview。
用嵌套路由让订阅归属于具体商品,在 config/routes.rb 中:
# config/routes.rb
Rails.application.routes.draw do
# ...
resources :products do
resources :subscribers, only: [ :create ]
end
end
新建库存展示 partial app/views/products/_inventory.html.erb:有货显示数量,缺货显示订阅表单:
<%# app/views/products/_inventory.html.erb %>
<% if product.inventory_count.positive? %>
<p><%= product.inventory_count %> in stock</p>
<% else %>
<p>Out of stock</p>
<p>Email me when available.</p>
<%= form_with model: [product, Subscriber.new] do |form| %>
<%= form.email_field :email, placeholder: "you@example.com", required: true %>
<%= form.submit "Submit" %>
<% end %>
<% end %>
然后在 show.html.erb 中 cache 块之后渲染该 partial:
<%# app/views/products/show.html.erb %>
<%= render "inventory", product: @product %>
发送到货邮件
Action Mailer 用于发送邮件。Mailer 类似"面向邮件的控制器":从数据库加载模型、施加业务逻辑、把数据传入模板生成邮件内容,但没有请求/响应循环。生成 mailer:
$ bin/rails g mailer Product in_stock
生成 app/mailers/product_mailer.rb,补全收件逻辑:
# app/mailers/product_mailer.rb
class ProductMailer < ApplicationMailer
# Subject 可在 config/locales/en.yml 的
# en.product_mailer.in_stock.subject 键下设置
def in_stock
@product = params[:product]
mail to: params[:subscriber].email
end
end
mailer 生成器同时产出 HTML 与纯文本两套邮件模板。HTML 版 app/views/product_mailer/in_stock.html.erb:
<%# app/views/product_mailer/in_stock.html.erb %>
<h1>Good news!</h1>
<p><%= link_to @product.name, product_url(@product) %> is back in stock.</p>
纯文本版 app/views/product_mailer/in_stock.text.erb:
<%# app/views/product_mailer/in_stock.text.erb %>
Good news!
<%= @product.name %> is back in stock.
<%= product_url(@product) %>
邮件中要使用 product_url 而非 product_path——邮件客户端需要完整 URL 才能在浏览器打开。在 console 测试发信:
store(dev)> product = Product.first
store(dev)> subscriber = product.subscribers.find_or_create_by(email: "subscriber@example.org")
store(dev)> ProductMailer.with(product: product, subscriber: subscriber).in_stock.deliver_later
日志会打印完整邮件内容(From: from@example.com、To: subscriber@example.com、HTML 与文本双部分、以及 ActionMailer::MailDeliveryJob 的异步投递记录)。
要让"库存从 0 变正数时自动触发邮件",可用 Active Record 回调:
# app/models/product.rb
class Product < ApplicationRecord
has_many :subscribers, dependent: :destroy
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
after_update_commit :notify_subscribers, if: :back_in_stock?
def back_in_stock?
inventory_count_previously_was.zero? && inventory_count.positive?
end
def notify_subscribers
subscribers.each do |subscriber|
ProductMailer.with(product: self, subscriber: subscriber).in_stock.deliver_later
end
end
end
after_update_commit 在数据库保存成功后触发;if: :back_in_stock? 限定仅当该方法返回 true 才运行。Active Record 会跟踪属性变更,inventory_count_previously_was 返回旧值,据此判断"之前为 0、现在为正"。notify_subscribers 借助关联查出该商品全部订阅者,并逐个把 in_stock 邮件投入后台队列。
用 Concern 重构
Product 模型的通知代码渐多,可抽成 ActiveSupport::Concern(Ruby 模块加一层语法糖,让模块中的行为并入类时更顺滑)。新建 app/models/product/notifications.rb:
# app/models/product/notifications.rb
module Product::Notifications
extend ActiveSupport::Concern
included do
has_many :subscribers, dependent: :destroy
after_update_commit :notify_subscribers, if: :back_in_stock?
end
def back_in_stock?
inventory_count_previously_was.zero? && inventory_count.positive?
end
def notify_subscribers
subscribers.each do |subscriber|
ProductMailer.with(product: self, subscriber: subscriber).in_stock.deliver_later
end
end
end
included 块内的代码在模块被 include 时如同写在类中一样执行;模块方法则成为类的实例方法。Product 模型随之简化:
# app/models/product.rb
class Product < ApplicationRecord
include Notifications
has_one_attached :featured_image
has_rich_text :description
validates :name, presence: true
validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
end
Concern 是组织特性代码的利器:功能多起来后按特性各抽一个自包含模块(如 Product::Notifications),还能在需要同样"订阅通知"能力的其他模型间复用。
退订链接
为邮件加上退订 URL。路由中新增顶层单数资源(resource :unsubscribe 用于匹配 /unsubscribe?token=xyz 这类无 ID 的路由):
# config/routes.rb
Rails.application.routes.draw do
# ...
resources :products do
resources :subscribers, only: [ :create ]
end
resource :unsubscribe, only: [ :show ]
end
Active Record 的 generates_token_for 能为不同用途生成唯一 token 查找记录。给 Subscriber 加退订 token:
# app/models/subscriber.rb
class Subscriber < ApplicationRecord
belongs_to :product
generates_token_for :unsubscribe
end
控制器先按 URL 中的 token 找订阅者,找到即销毁并重定向首页:
# app/controllers/unsubscribes_controller.rb
class UnsubscribesController < ApplicationController
allow_unauthenticated_access
before_action :set_subscriber
def show
@subscriber&.destroy
redirect_to root_path, notice: "Unsubscribed successfully."
end
private
def set_subscriber
@subscriber = Subscriber.find_by_token_for(:unsubscribe, params[:token])
end
end
把退订链接加进两套邮件模板(HTML 用 link_to,纯文本直接输出 URL):
<%# app/views/product_mailer/in_stock.html.erb %>
<h1>Good news!</h1>
<p><%= link_to @product.name, product_url(@product) %> is back in stock.</p>
<%= link_to "Unsubscribe", unsubscribe_url(token: params[:subscriber].generate_token_for(:unsubscribe)) %>
<%# app/views/product_mailer/in_stock.text.erb %>
Good news!
<%= @product.name %> is back in stock.
<%= product_url(@product) %>
Unsubscribe: <%= unsubscribe_url(token: params[:subscriber].generate_token_for(:unsubscribe)) %>
点击退订链接后记录即被删除;无效或过期的 token 会被控制器安全处理,不会抛错。
CSS 与 JavaScript
Propshaft:资产管线
Rails 的资产管线名为 Propshaft,负责把 CSS、JavaScript、图片等资产交付给浏览器;生产环境下它会为每个版本资产打指纹以支持缓存加速页面。修改 app/assets/stylesheets/application.css 设置字体与版式(导航右对齐、主区限宽、错误红/提示绿、商品图文并排等),并同步调整 show 视图的结构为 <section class="product"> + .product-info 两栏。完整 CSS 与本步视图结构请对照教程的 show.html.erb 最终形态实现,刷新即可看到效果。
Import Maps:无构建步骤的现代 JavaScript
Rails 默认用 import maps 管理 JavaScript——无需构建步骤即可书写现代 JS 模块。引脚(pin)配置在 config/importmap.rb:
# config/importmap.rb
# Pin npm packages by running ./bin/importmap
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "trix"
pin "@rails/actiontext", to: "actiontext.esm.js"
TIP:每个 pin 把 JS 包名(如
"@hotwired/turbo-rails")映射到具体文件/URL(如"turbo.min.js");pin_all_from把某目录下全部文件(如app/javascript/controllers)映射进一个命名空间(如"controllers")。注意 importmap 中已经出现了 Action Text 所需的 trix 与 actiontext 引脚。
Hotwire:Rails 默认的前端框架
import map 中这些 JS 属于 Hotwire——一个充分利用服务端生成 HTML 的前端框架,由三部分构成:
- Turbo:无需写任何自定义 JavaScript 即可处理导航、表单提交、页面组件与局部更新(你之前创建/编辑商品的表单就是 Turbo 驱动的)。
- Stimulus:当你需要自定义 JS 为页面加功能时,提供轻量框架。
- Native:把 Web 应用嵌入并渐进增强为混合移动应用。
目前你尚未手写任何 JS,但前端一直在使用 Hotwire。详见 Asset Pipeline 与 Working with JavaScript in Rails。
测试:用 Fixtures 与 Action Mailer 断言守护功能
Rails 自带健壮的测试体系。生成模型时会自动在 test/fixtures/ 下创建对应 fixtures 文件——运行测试前填充测试数据库的预定义数据,支持用易记名称访问记录。
更新 test/fixtures/products.yml:
# test/fixtures/products.yml
tshirt:
name: T-Shirt
inventory_count: 15
及 test/fixtures/subscribers.yml(fixture 间可直接按名字引用 product: tshirt,Rails 自动处理 ID 与关联):
# test/fixtures/subscribers.yml
david:
product: tshirt
email: david@example.org
chris:
product: tshirt
email: chris@example.org
在 test/models/product_test.rb 中编写"到货通知邮件"测试:
# test/models/product_test.rb
require "test_helper"
class ProductTest < ActiveSupport::TestCase
include ActionMailer::TestHelper
test "sends email notifications when back in stock" do
product = products(:tshirt)
# Set product out of stock
product.update(inventory_count: 0)
assert_emails 2 do
product.update(inventory_count: 99)
end
end
end
拆解测试逻辑:include ActionMailer::TestHelper 引入邮件监控能力;products(:tshirt) 通过 fixtures helper 取得对象(数据库 ID 每次运行可能不同,故按名字引用);先把库存置 0;assert_emails 2 断言块内恰好产生 2 封邮件——把库存改为 99 会触发 notify_subscribers 回调向两名订阅者发信。
运行单个测试文件或全量测试:
$ bin/rails test test/models/product_test.rb
# ...
1 runs, 2 assertions, 0 failures, 0 errors, 0 skips
再补全生成器产出的 mailer 测试 test/mailers/product_mailer_test.rb:
# test/mailers/product_mailer_test.rb
require "test_helper"
class ProductMailerTest < ActionMailer::TestCase
test "in_stock" do
mail = ProductMailer.with(product: products(:tshirt), subscriber: subscribers(:david)).in_stock
assert_equal "In stock", mail.subject
assert_equal [ "david@example.org" ], mail.to
assert_equal [ "from@example.com" ], mail.from
assert_match "Good news!", mail.body.encoded
end
end
全量运行 bin/rails test,两条测试 7 个断言全部通过。这可以作为你继续扩充覆盖率、构建完整测试套件的起点,更多内容见 Testing Rails Applications。
代码规范、安全审计与一键 CI
RuboCop:统一代码风格
Rails 附带 RuboCop 静态检查器保证格式一致:
$ bin/rubocop
# ...
53 files inspected, no offenses detected
自动修复违规可用 bin/rubocop --autocorrect(简写 -a)。
Brakeman:安全漏洞扫描
Rails 内置 Brakeman gem,可检出会话劫持、会话固定、不安全重定向等安全漏洞:
$ bin/brakeman
# ...
Controllers: 6
Models: 6
Templates: 15
Errors: 0
Security Warnings: 0
详细的安全加固见 Securing Rails Applications。
bin/ci:一站式持续集成
Rails 应用含 bin/ci 脚本,依次运行 setup、RuboCop、安全审计与测试等全部关键检查,步骤定义于 config/ci.rb,可自定义。脚本逐步骤打印 ✅/❌,任一步失败即以非零状态退出;CI 平台只需把流水线指向 bin/ci,保证本地与 CI 检查一致。本地运行:
$ bin/ci
# ...
✅ Setup passed in 2.11s
✅ Style: Ruby passed in 1.17s
✅ Continuous Integration passed in 8.91s
CI Steps DSL 示例
config/ci.rb 用一套 DSL 管理步骤,例如新增"禁止遗留 TODO"检查:
# config/ci.rb
CI.run do
step "Setup", "bin/setup --skip-server"
step "Style: Ruby", "bin/rubocop"
step "Check: No TODOs",
"if grep -r TODO app/; then exit 1; fi"
# ...
end
若在源码留下 # TODO 注释,bin/ci 会报错 ❌ 并给出具体文件;删掉注释后即通过 ✅。
部署到生产:Kamal + Docker + Solid Queue
使用 Kamal 部署
Rails 随附部署工具 Kamal:用 Docker 容器运行应用,零停机部署。Rails 默认带一份生产可用的 Dockerfile 供 Kamal 构建镜像(内含用 [Thruster] 压缩并高效服务生产资产),部署前需要准备:
- 一台运行 Ubuntu LTS、内存 ≥ 1GB 的服务器(Hetzner、DigitalOcean 等均提供);
- 一个 Docker Hub 账号及访问令牌,用于存放应用镜像。在 Docker Hub 创建名为
store的仓库。
编辑 config/deploy.yml,把 192.168.0.1 换成服务器 IP、your-user 换成你的 Docker Hub 用户名:
# config/deploy.yml
# Name of your application. Used to uniquely configure containers.
service: store
# Name of the container image.
image: your-user/store
# Deploy to these servers.
servers:
web:
- 192.168.0.1
# Credentials for your image host.
registry:
# Specify the registry server, if you're not using Docker Hub
# server: registry.digitalocean.com / ghcr.io / ...
username: your-user
在 proxy: 段可配置域名启用 SSL(DNS 指向服务器后,Kamal 会用 LetsEncrypt 自动签发证书):
# config/deploy.yml
proxy:
ssl: true
host: app.example.com
在 Docker 官网创建具备 Read & Write 权限的访问令牌,然后在终端导出供 Kamal 使用:
export KAMAL_REGISTRY_PASSWORD=your-access-token
首次部署用 setup 完成服务器初始化与部署:
$ bin/kamal setup
打开浏览器输入服务器 IP 即可看到 store 应用上线。之后每次改动推送到生产只需:
$ bin/kamal deploy
为生产环境添加用户
生产库尚无 User 记录。用 Kamal 打开生产 console:
$ bin/kamal console
store(prod)> User.create!(email_address: "you@example.org", password: "s3cr3t", password_confirmation: "s3cr3t")
之后即可用该邮箱密码在生产登录并管理商品。
Solid Queue:生产级后台任务
后台任务在独立进程中异步执行,避免打断用户体验——例如给一万名订阅者发到货邮件,就应放后台以免拖慢 HTTP 请求。开发环境 Rails 用 :async 队列适配器(内存存储,重启丢任务,不适合生产);生产环境使用 solid_queue:任务入库并由独立进程执行。Kamal 部署通过 SOLID_QUEUE_IN_PUMA: true 环境变量让 Puma 自动启停 Solid Queue 进程。
当 Action Mailer 的 deliver_later 发信时,邮件会交给 Active Job 后台发送:生产环境下任务可靠入库、失败自动重试、重启不丢失。
继续前进
恭喜你完成了第一个 Rails 应用的构建与部署!下一步建议:
- 跟随 Sign Up and Settings 教程 继续学习;
- 深入阅读 Active Record Basics、Layouts and Rendering、Testing Rails Applications、Debugging Rails Applications 与 Securing Rails Applications 等官方指南。
回顾本教程走完的路径:从 rails new 生成骨架、理解 MVC 与目录约定,到用 Active Record 建表建模、在 console 中做完整 CRUD,再到用路由/控制器/视图/partial 搭建 Web 界面,最后叠加认证、缓存、富文本、上传、国际化、邮件通知、测试、CI 并部署上线——这正是 Rails "约定优于配置 + DRY"哲学贯穿始终的一次完整实践。Happy building!
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00



