mcpField
R
routing

routing

github.com/thinkoodle开发7191 次安装8 周前更新
代码仓库

关于 routing

面向 Rails 开发者的路由技能,指导在 `config/routes.rb` 中设计精简的 RESTful 路由。内容涵盖资源嵌套、命名空间、约束、API 路由和路由测试。

主要功能

  1. 01按需生成 RESTful 资源路由
  2. 02使用浅层嵌套组织资源
  3. 03区分 namespace 与 scope
  4. 04定义成员路由和集合路由
  5. 05设置路由约束与 API 路由
  6. 06测试和排查路由

适用场景

  1. 01为控制器仅开放所需路由
  2. 02设计带父资源上下文的浅层嵌套路由
  3. 03为管理区或 API 版本设置命名空间
  4. 04验证路由映射并查找冲突

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/thinkoodle/rails-skills --skill routing

安装后在 Claude Code 中输入/routing确认已加载。

附带文件reference.md

详细介绍

Rails 路由专家

定义清晰、符合 RESTful 风格且精简的路由。额外路由会扩大不必要的攻击面,也会让阅读 routes.rb 的其他开发者感到困惑。

理念

  1. 默认使用 RESTful 风格 — 使用 resources 和 resource。自定义路由通常是问题的征兆。
  2. 尽量精简 — 始终使用 only: 或 except:,只开放必要的路由。
  3. 处处使用路径辅助方法 — 硬编码的 URL 字符串会在路由变更时失效。使用 _path 和 _url 辅助方法。
  4. 只进行浅层嵌套 — 资源嵌套不要超过 1 层;更深的嵌套会导致 URL 和辅助方法名称难以维护。
  5. 新增资源优于自定义操作 — 如果要添加很多自定义操作,就需要新建一个控制器。

适用场景

  • 在 config/routes.rb 中添加或修改路由
  • 在 namespace、scope 和 module 之间进行选择
  • 嵌套资源
  • 添加 member 或 collection 路由
  • 设置 API 版本控制
  • 调试路由冲突
  • 测试路由

说明

步骤 1:检查现有路由

添加新路由前先检查当前路由 — 重复和冲突会导致难以察觉的错误:

# Show all routes
bin/rails routes

# Filter by controller
bin/rails routes -c users

# Search by path or helper name
bin/rails routes -g admin

# Find unused routes (clean these up!)
bin/rails routes --unused
步骤 2:正确使用资源
resources(复数)— 用于集合

生成 7 条路由:index、show、new、create、edit、update、destroy。

# Generates all 7 routes — unused routes create dead endpoints and confuse devs
resources :articles

# Better — only generate what the controller actually implements
resources :articles, only: [:index, :show, :create]

思考:这个控制器实际实现了哪些操作? 只为这些操作添加路由。

resource(单数)— 用于单例资源

当每个用户或上下文中只有一个该资源时使用。不包含 index 操作。URL 中不包含 :id。

# Current user's profile — there's only one
resource :profile, only: [:show, :edit, :update]

# Current user's session
resource :session, only: [:new, :create, :destroy]

# App-wide dashboard
resource :dashboard, only: :show

⚠️ 重要:resource(单数)仍然会路由到复数形式的控制器。 resource :profile → ProfilesController(而不是 ProfileController)

路径辅助方法使用单数形式:profile_path(而不是 profiles_path)、edit_profile_path。

何时使用单数形式或复数形式:

  • 用户能拥有或查看多个资源吗?→ 使用 resources(复数)
  • 在当前上下文中是否恰好只有一个?→ 使用 resource(单数)
  • 是否适用 index 操作?→ 使用 resources(复数)
步骤 3:嵌套资源(最多 1 层)

当子资源只有在其父资源中才有意义时,进行嵌套。

# GOOD — 1 level deep
resources :articles do
  resources :comments, only: [:index, :create, :destroy]
end
# Avoid — 2+ levels creates unwieldy URLs and painful helper names
resources :users do
  resources :articles do
    resources :comments  # ← /users/1/articles/2/comments/3
  end
end

当子资源具有独立身份时,使用浅层嵌套:

# GOOD — collection routes nested, member routes flat
resources :articles do
  resources :comments, shallow: true
end
# Creates:
#   /articles/:article_id/comments     (index, create, new)
#   /comments/:id                      (show, edit, update, destroy)

这样,在创建或列出资源时可以获得父级上下文,而直接访问时则使用简洁的 URL。

嵌套路径辅助方法会包含父资源:

article_comments_path(@article)           # GET /articles/1/comments
article_comment_path(@article, @comment)  # GET /articles/1/comments/5
# With shallow:
comment_path(@comment)                    # GET /comments/5
步骤 4:Namespace、Scope 与 Module 的区别

这三者看起来相似,但行为各不相同——混淆使用会导致难以理解的路由错误。

namespace — 改变所有内容(路径、模块和辅助方法)
namespace :admin do
  resources :articles, only: [:index, :show]
end
  • 路径:/admin/articles
  • 控制器:Admin::ArticlesController(位于 app/controllers/admin/)
  • 辅助方法:admin_articles_path

适用场景: 管理面板、API 版本,以及使用独立控制器的不同区域。

scope module: — 仅改变模块(不改变路径)
scope module: :admin do
  resources :articles, only: [:index, :show]
end
  • 路径:/articles(没有前缀!)
  • 控制器:Admin::ArticlesController
  • 辅助方法:articles_path

适用场景: 将控制器整理到子目录中,但不改变 URL。

scope path: — 仅改变路径(不改变模块)
scope "/admin" do
  resources :articles, only: [:index, :show]
end
  • 路径:/admin/articles
  • 控制器:ArticlesController(没有模块!)
  • 辅助方法:articles_path

适用场景: 为 URL 添加前缀,但不使用单独的控制器命名空间。

快速参考
方法URL 前缀控制器模块辅助方法前缀
namespace :admin/adminAdmin::admin_
scope module: :admin无Admin::无
scope "/admin"/admin无无

不确定时,使用 namespace。 它的表达最明确,也最不容易引起意外。

步骤 5:Member 与 Collection 路由

当标准的 7 个 RESTful 操作之外还需要其他路由时使用。

Member 路由 — 针对一条特定记录执行操作(需要 :id)
resources :articles, only: [:index, :show] do
  member do
    patch :publish      # PATCH /articles/:id/publish
    patch :archive      # PATCH /articles/:id/archive
  end
end

生成:publish_article_path(@article)、archive_article_path(@article)

Collection 路由 — 针对整个集合执行操作(不需要 :id)
resources :articles, only: [:index, :show] do
  collection do
    get :search         # GET /articles/search
    get :drafts         # GET /articles/drafts
    delete :clear_all   # DELETE /articles/clear_all
  end
end

生成:search_articles_path、drafts_articles_path

简写(单一路由)
resources :photos do
  get :preview, on: :member       # GET /photos/:id/preview
  get :search, on: :collection    # GET /photos/search
end

⚠️ 停下——你真的需要自定义操作吗?

如果你要添加超过 2 个自定义操作,可能需要新建一个资源:

# BAD — too many custom actions on one resource
resources :articles do
  member do
    patch :publish
    patch :unpublish
    patch :archive
    patch :unarchive
    patch :feature
  end
end

# GOOD — extract a new resource
resources :articles, only: [:index, :show]
resources :article_publications, only: [:create, :destroy]  # publish/unpublish
resources :article_archives, only: [:create, :destroy]      # archive/unarchive
步骤 6:路由关注点(DRY 共享模式)

当多个资源共享相同的嵌套路由时:

concern :commentable do
  resources :comments, only: [:index, :create, :destroy]
end

concern :taggable do
  resources :tags, only: [:index, :create, :destroy]
end

resources :articles, concerns: [:commentable, :taggable]
resources :photos, concerns: [:commentable]
resources :videos, concerns: [:commentable, :taggable]
步骤 7:约束
段约束(验证参数)
resources :users, constraints: { id: /\d+/ }
请求约束(子域名、格式等)
constraints subdomain: "api" do
  namespace :api do
    resources :articles, only: [:index, :show]
  end
end
高级约束(自定义逻辑)
# Lambda
get "*path", to: "errors#not_found",
  constraints: lambda { |req| !req.path.start_with?("/admin") }

# Object (responds to matches?)
class ApiConstraint
  def matches?(request)
    request.headers["Accept"]&.include?("application/json")
  end
end

constraints ApiConstraint.new do
  resources :articles, only: [:index, :show]
end
步骤 8:根路由

始终定义根路由:

root "pages#home"

命名空间根路由:

namespace :admin do
  root "dashboard#show"  # /admin → Admin::DashboardController#show
end
步骤 9:重定向
# Simple redirect (301 by default)
get "/old-path", to: redirect("/new-path")

# With status code
get "/old-path", to: redirect("/new-path", status: 302)

# Dynamic redirect
get "/articles/:id", to: redirect("/posts/%{id}")
步骤 10:挂载引擎
mount Sidekiq::Web => "/sidekiq"
mount ActionCable.server => "/cable"
步骤 11:API 路由
namespace :api do
  namespace :v1 do
    resources :articles, only: [:index, :show, :create, :update, :destroy]
    resources :users, only: [:show]
  end
end

对于仅限 API 的控制器,使用 defaults: { format: :json }:

namespace :api, defaults: { format: :json } do
  namespace :v1 do
    resources :articles, only: [:index, :show]
  end
end
步骤 12:Direct 和 Resolve(自定义 URL 帮助方法)
direct — 创建自定义 URL 帮助方法
direct :homepage do
  "https://example.com"
end
# homepage_url => "https://example.com"

direct :cdn_image do |model|
  "https://cdn.example.com/#{model.image_path}"
end
# cdn_image_url(@photo) => "https://cdn.example.com/photos/1.jpg"
resolve — 自定义模型的多态路由

使用 resource(单数)搭配 form_with 时必须设置:

resource :basket, only: [:show, :update]
resolve("Basket") { [:basket] }

# Now form_with(model: @basket) generates /basket (not /baskets/:id)

路由文件组织

按如下方式排列 config/routes.rb:

Rails.application.routes.draw do
  # 1. Root
  root "pages#home"

  # 2. Authentication / sessions
  resource :session, only: [:new, :create, :destroy]
  resources :registrations, only: [:new, :create]

  # 3. Core resources (most used first)
  resources :articles do
    resources :comments, only: [:index, :create], shallow: true
  end

  # 4. Namespaced sections
  namespace :admin do
    root "dashboard#show"
    resources :users
  end

  namespace :api, defaults: { format: :json } do
    namespace :v1 do
      resources :articles, only: [:index, :show]
    end
  end

  # 5. Utility routes
  get "up", to: "rails/health#show", as: :rails_health_check

  # 6. Catch-all (if needed — put LAST)
  get "*path", to: "errors#not_found"
end

测试路由

# test/routing/articles_routing_test.rb
class ArticlesRoutingTest < ActionDispatch::IntegrationTest
  test "routes to articles#index" do
    assert_routing "/articles", controller: "articles", action: "index"
  end

  test "routes to articles#show" do
    assert_routing "/articles/1", controller: "articles", action: "show", id: "1"
  end

  test "generates correct path" do
    assert_generates "/articles/1", controller: "articles", action: "show", id: "1"
  end

  test "recognizes route" do
    assert_recognizes(
      { controller: "articles", action: "create" },
      { path: "/articles", method: :post }
    )
  end
end

反模式

  1. 需要 2 个路由时却生成全部 7 个路由 — 始终使用 only: 或 except:
  2. 嵌套层级超过 1 层 — 使用浅层嵌套或拆分资源来扁平化结构
  3. 硬编码路径 — 路由更改时,硬编码字符串会失效;请使用 article_path(@article),而不是 "/articles/#{@article.id}"
  4. 使用 match ... via: :all — 暴露所有 HTTP 动词会扩大攻击面;应明确指定
  5. 过多自定义成员/集合操作 — 改为提取一个新资源
  6. 混淆 namespace/scope/module — 查看步骤 4 中的表格
  7. 需要 resources 时却使用 resource — 单数形式表示单个对象,没有 index,也没有 :id
  8. 嵌套资源缺少 only:/except: — 嵌套路由很快就会膨胀
  9. 使用单数 resource 时忘记 resolve — 不设置它,form_with 将无法正常工作
  10. 路由顺序错误 — 更具体的路由应放在通用路由之前(Rails 从上到下匹配,匹配到第一个即生效)

调试

# See what a specific URL maps to
bin/rails routes -g "GET /articles"

# Check for conflicts
bin/rails routes | grep articles

# In rails console
Rails.application.routes.url_helpers.articles_path
app.edit_article_path(Article.first)

有关详细模式、示例和边缘情况,请参阅此技能目录中的 reference.md。

评论

支持 Markdown