Rails路由高级特性 #

一、路由Concerns #

1.1 什么是Concerns #

Concerns是可复用的路由模块,可以在多个资源之间共享路由配置。

1.2 定义Concerns #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 定义可复用的路由模块
  concern :commentable do
    resources :comments, only: [:index, :create, :destroy]
  end
  
  concern :likable do
    resources :likes, only: [:create, :destroy]
  end
  
  concern :searchable do
    collection do
      get 'search'
    end
  end
  
  concern :paginatable do
    get 'page/:page', action: :index, on: :collection
  end
end

1.3 使用Concerns #

ruby
# config/routes.rb
Rails.application.routes.draw do
  concern :commentable do
    resources :comments, only: [:create, :destroy]
  end
  
  concern :likable do
    resources :likes, only: [:create, :destroy]
  end
  
  # 使用多个concerns
  resources :articles, concerns: [:commentable, :likable]
  resources :photos, concerns: [:commentable, :likable]
  resources :videos, concerns: [:commentable]
end

1.4 Concerns嵌套 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  concern :commentable do
    resources :comments do
      concerns :likable
    end
  end
  
  resources :articles, concerns: :commentable
end

二、挂载引擎 #

2.1 挂载Rack应用 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 挂载Sidekiq Web界面
  mount Sidekiq::Web => '/sidekiq'
  
  # 挂载Grape API
  mount API => '/api'
  
  # 挂载Rails引擎
  mount Blazer::Engine => '/blazer'
end

2.2 挂载带约束 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 只允许管理员访问
  constraints AdminConstraint.new do
    mount Sidekiq::Web => '/sidekiq'
    mount Blazer::Engine => '/blazer'
  end
  
  # 子域名约束
  constraints subdomain: 'admin' do
    mount RailsAdmin::Engine => '/admin'
  end
end

2.3 常见挂载示例 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # Sidekiq监控
  mount Sidekiq::Web => '/sidekiq'
  
  # 管理后台
  mount RailsAdmin::Engine => '/admin', as: 'rails_admin'
  
  # API文档
  mount Rswag::Api::Engine => '/api-docs'
  mount Rswag::Ui::Engine => '/api-docs'
  
  # 性能监控
  mount Bullet::Engine => '/bullet' if Rails.env.development?
  
  # 健康检查
  mount OkComputer::Engine => '/health'
end

三、路由优先级 #

3.1 路由匹配顺序 #

Rails按照定义顺序匹配路由,第一个匹配的路由会被使用。

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 更具体的路由应该放在前面
  
  # 1. 特定路由
  get 'articles/popular', to: 'articles#popular'
  
  # 2. 动态路由
  get 'articles/:id', to: 'articles#show'
  
  # 3. 通配符路由
  get 'articles/*path', to: 'articles#catch_all'
end

3.2 路由冲突解决 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 错误:动态路由会先匹配
  # get 'articles/:id', to: 'articles#show'
  # get 'articles/popular', to: 'articles#popular'
  
  # 正确:特定路由在前
  get 'articles/popular', to: 'articles#popular'
  get 'articles/:id', to: 'articles#show', constraints: { id: /\d+/ }
end

四、路由格式 #

4.1 默认格式 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 设置默认格式
  resources :articles, defaults: { format: :json }
  
  # 命名空间默认格式
  namespace :api, defaults: { format: :json } do
    resources :articles
  end
end

4.2 格式约束 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 只允许特定格式
  resources :articles, constraints: { format: /(html|json|xml)/ }
  
  # 禁止格式后缀
  resources :articles, format: false
end

4.3 格式变体 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 支持多种格式
  resources :articles do
    member do
      get 'download', constraints: { format: /(pdf|doc)/ }
    end
  end
end

五、路由优化 #

5.1 路由缓存 #

ruby
# config/environments/production.rb
Rails.application.configure do
  # 启用路由缓存
  config.action_dispatch.railties_order_engine_routes = true
end

5.2 减少路由数量 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 不推荐:生成所有路由
  resources :articles
  
  # 推荐:只生成需要的路由
  resources :articles, only: [:index, :show]
  
  # 使用concerns复用路由
  concern :readable do
    get 'read', on: :member
  end
  
  resources :articles, concerns: :readable
  resources :books, concerns: :readable
end

5.3 延迟加载 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 开发环境延迟加载
  if Rails.env.development?
    mount LetterOpenerWeb::Engine => '/letter_opener'
  end
end

六、路由助手方法 #

6.1 自定义助手方法 #

ruby
# app/helpers/route_helper.rb
module RouteHelper
  def article_permalink(article)
    article_path(article, anchor: "comment-#{article.last_comment_id}")
  end
  
  def user_profile_path(user)
    user_path(user.username.downcase)
  end
end

6.2 路由助手选项 #

erb
<!-- 路径助手选项 -->
<%= link_to '文章', article_path(@article, anchor: 'comments') %>
<%= link_to '文章', article_path(@article, format: :json) %>
<%= link_to '文章', article_path(@article, only_path: false) %>

<!-- URL参数 -->
<%= link_to '搜索', articles_path(q: 'rails', page: 2) %>

七、路由国际化 #

7.1 多语言路由 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  scope '(:locale)', locale: /en|zh|ja/ do
    resources :articles
    root 'home#index'
  end
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_locale
  
  private
  
  def set_locale
    I18n.locale = params[:locale] || I18n.default_locale
  end
  
  def default_url_options
    { locale: I18n.locale }
  end
end

7.2 本地化路径 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  localized do
    get 'about', to: 'pages#about', as: :about
    get 'contact', to: 'pages#contact', as: :contact
  end
end

# config/locales/routes/en.yml
en:
  routes:
    about: 'about'
    contact: 'contact'

# config/locales/routes/zh.yml
zh:
  routes:
    about: 'guanyu'
    contact: 'lianxi'

八、路由测试 #

8.1 路由测试示例 #

ruby
# test/integration/routes_test.rb
require 'test_helper'

class RoutesTest < ActionDispatch::IntegrationTest
  test 'should route to articles' do
    assert_routing '/articles', controller: 'articles', action: 'index'
  end
  
  test 'should route to article' do
    assert_routing '/articles/1', controller: 'articles', action: 'show', id: '1'
  end
  
  test 'should route to new article' do
    assert_routing '/articles/new', controller: 'articles', action: 'new'
  end
  
  test 'should route nested resource' do
    assert_routing '/articles/1/comments', 
      controller: 'comments', action: 'index', article_id: '1'
  end
  
  test 'should recognize admin routes' do
    assert_routing '/admin/users', 
      controller: 'admin/users', action: 'index'
  end
end

8.2 测试助手方法 #

ruby
# test/helpers/routes_helper_test.rb
require 'test_helper'

class RoutesHelperTest < ActionView::TestCase
  test 'should generate article permalink' do
    article = articles(:one)
    article.stubs(last_comment_id: 5)
    
    expected = "/articles/#{article.id}#comment-5"
    assert_equal expected, article_permalink(article)
  end
end

九、路由调试 #

9.1 路由调试工具 #

bash
# 查看所有路由
rails routes

# 过滤路由
rails routes | grep articles

# 扩展格式
rails routes -E

# 只显示特定控制器
rails routes -c ArticlesController

# 只显示特定路径
rails routes -g articles

9.2 控制台调试 #

ruby
# Rails控制台
rails console

# 检查路由助手
app.articles_path
# => "/articles"

app.article_path(1)
# => "/articles/1"

# 检查URL生成
Rails.application.routes.url_helpers.articles_path
# => "/articles"

9.3 视图调试 #

erb
<!-- 显示当前路由信息 -->
<%= debug(params) %>

<!-- 显示所有路由 -->
<% Rails.application.routes.routes.each do |route| %>
  <%= route.path.spec.to_s %>
<% end %>

十、高级路由模式 #

10.1 多租户路由 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  constraints subdomain: /.+/ do
    get '/', to: 'tenants#show'
    resources :articles
  end
end

# app/controllers/tenants_controller.rb
class TenantsController < ApplicationController
  def show
    @tenant = Tenant.find_by(subdomain: request.subdomain)
    render :not_found unless @tenant
  end
end

10.2 API版本控制 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :articles
    end
    
    namespace :v2 do
      resources :articles
    end
  end
  
  # 或使用约束
  scope module: :api do
    scope module: :v1, constraints: ApiConstraint.new(version: 1) do
      resources :articles
    end
    
    scope module: :v2, constraints: ApiConstraint.new(version: 2) do
      resources :articles
    end
  end
end

10.3 动态路由 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 从数据库加载路由
  Page.all.each do |page|
    get page.slug, to: 'pages#show', defaults: { page_id: page.id }
  end
end

# 更好的做法:使用约束
get '*slug', to: 'pages#show', constraints: PageConstraint.new

# lib/constraints/page_constraint.rb
class PageConstraint
  def matches?(request)
    Page.exists?(slug: request.params[:slug])
  end
end

十一、最佳实践 #

11.1 路由组织 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 1. 根路由
  root 'home#index'
  
  # 2. 公开资源
  resources :articles, only: [:index, :show]
  
  # 3. 认证路由
  devise_for :users
  
  # 4. 用户资源
  resources :users, only: [:show] do
    resources :articles, only: [:index]
  end
  
  # 5. 管理后台
  namespace :admin do
    root 'dashboard#index'
    resources :users
    resources :articles
  end
  
  # 6. API路由
  namespace :api, defaults: { format: :json } do
    namespace :v1 do
      resources :articles
    end
  end
  
  # 7. 挂载引擎
  mount Sidekiq::Web => '/sidekiq'
end

11.2 性能优化 #

优化点 说明
减少路由数量 只生成需要的路由
使用concerns 复用路由配置
路由缓存 生产环境启用缓存
避免深层嵌套 限制嵌套层级
使用约束 减少不必要的匹配

11.3 安全考虑 #

ruby
# config/routes.rb
Rails.application.routes.draw do
  # 限制管理路由访问
  constraints AdminConstraint.new do
    namespace :admin do
      resources :users
    end
  end
  
  # 限制API访问
  constraints ApiConstraint.new do
    namespace :api do
      resources :articles
    end
  end
end

十二、总结 #

12.1 核心要点 #

要点 说明
Concerns 可复用路由模块
挂载引擎 集成第三方应用
路由优先级 定义顺序影响匹配
路由优化 减少数量、使用缓存
国际化 多语言路由支持

12.2 下一步 #

现在你已经掌握了路由高级特性,接下来让我们学习 控制器基础,深入了解Rails的控制器系统!

最后更新:2026-03-28