首页
/ Ruby on Rails 生成器与模板全解:自定义 Generator、覆盖脚手架与应用模板实战

Ruby on Rails 生成器与模板全解:自定义 Generator、覆盖脚手架与应用模板实战

2026-09-07 13:16:06作者:凤尚柏Louis

Rails 生成器(Generators)与应用模板(Templates)是一套面向开发者的代码自动化基础设施:前者在既有应用中批量产出样板文件,后者在 rails new 时定制全新应用的结构。本文基于本仓库 generators.md 官方指南,结合 railties/lib/rails/generators 下的真实实现,系统讲解如何列出与剖析生成器、手写自定义生成器、理解其查找解析机制、通过 config.generators 覆盖内置脚手架,并最终用 Rails Generators API 编写可复用的应用模板。读完你将能独立为团队沉淀一套"生成器 + 模板"的标准化工程能力。


什么是 Rails 生成器

当运行 rails new myapp 时,你其实已经在使用一个 Rails 生成器(它由 rails 全局命令触发)。生成器会在你的应用中创建特定文件,实现样板代码的自动化;更多生成器则由应用内的 bin/rails generate 提供,例如 modelcontrollerscaffoldmigration 等。

$ rails new myapp
$ cd myapp
$ bin/rails generate
Usage:
  bin/rails generate GENERATOR [args] [options]

General options:
  -h, [--help]     # Print generator's options and usage
  -p, [--pretend]  # Run but do not make any changes
  -f, [--force]    # Overwrite files that already exist
  -s, [--skip]     # Skip files that already exist
  -q, [--quiet]    # Suppress status output

Please choose a generator below.

Rails:
  application_record
  authentication
  benchmark
  channel
  controller
  generator
  ...
SolidQueue:
  solid_queue:install

Stimulus:
  stimulus

TestUnit:
  test_unit:authentication
  test_unit:channel
  ...

这里有一个容易混淆的细节需要厘清:

  • rails new 使用通过 gem install rails 安装的全局 Rails 版本;
  • 应用目录内的 bin/rails 使用捆绑(bundle)在应用中的 Rails 版本,因此应用代码、Gemfile 里的 Rails 版本优先级高于全局安装版本。

无参数运行 bin/rails generate 会列出所有可用生成器及其使用说明。列表按命名空间分组输出——上面的输出里,"Rails:" 分组来自 railties/lib/rails/generators.rb 中的 sorted_groups 方法(它把非 rails 命名空间按冒号前缀归组、按字母排序),而 SolidQueueStimulusTestUnit 等分组则来自对应引擎或 Gem 提供的命名空间。

--pretend--help 探索生成器行为

在真正改动文件前,--pretend(或 -p)可以"彩排"一个生成器将要产生的全部改动:

$ bin/rails generate model product name:string --pretend
      invoke  active_record
      create    db/migrate/20260407190300_create_products.rb
      create    app/models/product.rb
      invoke    test_unit
      create      test/models/product_test.rb
      create      test/fixtures/products.yml

以上文件在 --pretend 模式下都不会真正创建。该参数非常适合对比相近生成器(例如 modelresource)的产物差异后再执行。其背后的运行时代码同样值得关注:上述 -h/-p/-f/-s/-q 一组通用选项正是在 railties/lib/rails/generators.rbhelp 方法中输出的帮助文本。

查看某个生成器的详细说明则使用 --help

$ bin/rails generate scaffold --help
Usage:
  bin/rails generate scaffold NAME [field[:type][:index] field[:type][:index]] [options]
...
Description:
    Scaffolds an entire resource, from model and migration to controller and
    views, along with a full test suite. The resource is ready to use as a
    starting point for your RESTful, resource-oriented application.
...
Examples:
    `bin/rails generate scaffold post`
    `bin/rails generate scaffold post title:string body:text published:boolean`
    `bin/rails generate scaffold purchase amount:decimal tracking_id:integer:uniq`
    `bin/rails generate scaffold user email:uniq password:digest`
...

--help 输出同时包含字段语法(field[:type][:index],其中 :uniq 会生成唯一索引)、用途说明与示例,是学习生成器最直接的资源。


创建你的第一个生成器

除了 Rails 内置生成器,你也可以构建自定义生成器。下面以"创建一个写入 config/initializers 的初始化文件"为例,先演示纯手工写法,再演示用 generator 生成器来生成生成器。

生成器构建在 [Thor][] 之上——Thor 负责命令行参数解析,并提供一整套文件操作 API。Thor 项目由 Rails 核心团队维护,其 Thor::GroupThor::Actions 是 Rails 生成器能力的底座(见 base.rb 开头对 require "thor/group" 的依赖)。

手工创建生成器

lib/generators 下新建文件 initializer_generator.rb

class InitializerGenerator < Rails::Generators::Base
  def create_initializer_file
    create_file "config/initializers/hello_generator.rb", <<~RUBY
      # Add hello_generator.rb file content here
    RUBY
  end
end

几点说明:

  • 根据文件名与类名,该生成器的名字是 initializer
  • 类继承自 [Rails::Generators::Base][];
  • 生成器被调用时,其中每个 public 方法会按定义顺序依次执行(这一行为源自 Thor::Group 的机制),本例只有一个 create_initializer_file
  • create_file 会在指定目标路径写入给定内容。

运行它:

$ bin/rails generate initializer

该命令会在 config/initializers 目录中创建一个名为 hello_generator.rb 的文件。在进入下一阶段前,先用 --help 查看描述:

$ bin/rails generate initializer --help

Rails 通常能从命名空间中推导出不错的描述(例如 ActiveRecord::Generators::ModelGenerator),但非命名空间的生成器就无能为力了。解决方案有二:

方案一:在生成器内调用 desc

class InitializerGenerator < Rails::Generators::Base
  desc "This generator creates a file at config/initializers"
  def create_initializer_file
    create_file "config/initializers/hello_generator.rb", <<~RUBY
      # Add hello_generator.rb file content here
    RUBY
  end
end

再次 bin/rails generate initializer --help 即可看到该描述。

方案二:在与生成器同目录下创建 USAGE 文件(下一步会演示)。

在实现层面,Base.desc 的读取逻辑正是"优先读取 source_root 上一级的 USAGE 文件(经 ERB 渲染),找不到时才使用默认描述"——这正是两种描述方案可以并存的原因。

用生成器来生成生成器

生成器本身也有一款元生成器,它会自动把文件放到正确位置。先删掉手写的 InitializerGenerator,再用 generator 命令重建:

$ rm lib/generators/initializer_generator.rb

$ bin/rails generate generator initializer
      create  lib/generators/initializer
      create  lib/generators/initializer/initializer_generator.rb
      create  lib/generators/initializer/USAGE
      create  lib/generators/initializer/templates
      invoke  test_unit
      create    test/lib/generators/initializer_generator_test.rb

生成的生成器长这样:

# lib/generators/initializer/initializer_generator.rb
class InitializerGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)
end

注意两点变化:

  1. 基类从 Rails::Generators::Base 换成了 [Rails::Generators::NamedBase][]。这意味着生成器期待至少一个参数——该参数是 initializer 的名字,并在代码中通过 name 访问。对应地,--help 用法会显示为:
$ bin/rails generate initializer NAME [options]

从源码看,Rails::Generators::NamedBase 通过 argument :name, type: :string 声明了这个必填参数,并在初始化时执行 assign_names!(name):它会按 /:: 拆分名字得到 class_path,末尾一段作为 file_name(见 named_base.rbassign_names!)。文档里的目标路径 config/initializers/#{file_name}.rb 正是由它驱动的。

  1. 生成了类方法 source_root,它指向模板文件目录。模板文件是生成器的"蓝图",默认位于刚创建的 lib/generators/initializer/templatessource_root 的默认实现是"在默认生成器根目录下寻找 templates/ 子目录"(见 base.rb 中的 default_source_root)。

为了理解模板机制,创建模板文件 lib/generators/initializer/templates/initializer.rb

# Add initialization content here

并把生成器改成"复制模板":

class InitializerGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)

  def copy_initializer_file
    copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
  end
end

运行:

$ bin/rails generate initializer core_extensions
      create  config/initializers/core_extensions.rb

$ cat config/initializers/core_extensions.rb
# Add initialization content here

copy_file 把模板内容复制到了 config/initializers/core_extensions.rb(目标路径中的 file_name 方法继承自 NamedBase,此处为 core_extensions)。Rails 在 lib/templates(详见下文"覆盖生成器模板")之外,还会把 Rails::Generators.templates_path 中配置的路径并入 source_paths(见 base.rb 的 inherited 钩子),这也是模板文件查找顺序的组成之一。

生成器的命令行选项:class_option

生成器可用 [class_option][] 支持命令行选项:

class InitializerGenerator < Rails::Generators::NamedBase
  class_option :scope, type: :string, default: "app"
end

现在可以带 --scope 调用:

$ bin/rails generate initializer theme --scope dashboard

这会把 --scope 的默认值 "app" 覆盖为 "dashboard"。选项值在生成器方法内通过 [options][] 读取:

def copy_initializer_file
  @scope = options["scope"]
  copy_file "initializer.rb", "config/initializers/#{@scope}/#{file_name}.rb"
end

于是 theme.rb 被生成到 config/initializers/dashboard/ 下。

值得指出,Rails 对 class_option 做了增强:Rails::Generators::Base.class_option 会先从 Rails::Generators.options(含各命名空间默认配置)与 Rails::Generators.aliases 中解析默认值与短别名(见 base.rb 中 class_option/default_for_option)。这正是一条连接"命令行选项"与"config.generators 默认配置"的隐式通道——你在应用配置中给出的默认值,会以 class option 默认值的形式出现在每个生成器中。


生成器解析(Generator Resolution)机制

当解析一个生成器名字时,Rails 会按多个候选文件名依次尝试加载。例如运行 bin/rails generate initializer core_extensions 时,Rails 按顺序尝试以下文件直到命中:

  • rails/generators/initializer/initializer_generator.rb
  • generators/initializer/initializer_generator.rb
  • rails/generators/initializer_generator.rb
  • generators/initializer_generator.rb

若全部未找到,则抛出错误。

这条查找规则在源码中体现为 generators.rb 的 file_lookup_paths:它把 {rails/generators,generators} 两个顶层目录与 **/*_generator.rb 组合成 glob,因此:

  • Rails 内置生成器落在 railties/lib/rails/generators/rails/*(对应候选路径中的 rails/generators/...);
  • 你应用/引擎的自定义生成器应放在 lib/generators/...(对应 generators/...)。

我们之所以把生成器放进应用的 lib/ 目录,是因为该目录位于 $LOAD_PATH(Ruby 加载文件时搜索的目录列表)中,Rails 才能找到并加载这些生成器文件。

补充原理$LOAD_PATH 由 Ruby 初始化,并在启动阶段由 Bundler 与 Rails 扩充。Bundler 加入每个 Gem 的 lib/ 目录,Rails 加入你应用的 lib/ 目录——这正是放在那里的生成器可被发现的原因。可以用 bin/rails runner 'puts $LOAD_PATH' 查看完整加载路径;也可以在 config/application.rb 中用 config.autoload_paths 追加修改。

除了文件级查找,还有一层命名空间级解析Rails::Generators.find_by_namespace(见 generators.rb)负责在已加载的子类集合里按 base:namename:contextrails:namename 等候选顺序命中类,并在未命中时触发 fallback(见下文"Fallbacks")。


覆盖内置生成器与模板

应用变大后,你可能想给生成的 controller 追加自定义方法、或改变生成视图的格式。这些都可以通过覆盖内置生成器与模板实现,配置入口是 [config.generators][]。

先近距离观察 scaffold 生成器是如何工作的:

$ bin/rails generate scaffold User name:string
      invoke  active_record
      create    db/migrate/20230518000000_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      create      app/views/users/_user.html.erb
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/users_controller_test.rb
      create      test/system/users_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder

从输出可以看出 scaffold 生成器会调用其他生成器(如 scaffold_controller),而这些生成器又会再调用别的生成器。尤其是 scaffold_controller 生成器调用了 helper 生成器。

在源码中,这种"链式调用"由 [hook_for] 声明实现:例如 scaffold_controller_generator.rb 中依次 hook_for :template_engine, as: :scaffoldhook_for :resource_routehook_for :test_framework, as: :scaffoldhook_for :helper, as: :scaffold(后者以复数 controller 名调用 helper)。hook_for 的实现细节见 base.rb:它声明一个 class option,并通过 invoke_from_option 在运行时解析并调用被钩住的生成器。

实战:用 my_helper 覆盖内置 helper 生成器

先创建新生成器 my_helper。由于我们覆盖的是 Rails 内置命名空间下的生成器,需要把它放在 lib/generators/rails 内,用 generator 命令生成骨架:

$ bin/rails generate generator rails/my_helper
      create  lib/generators/rails/my_helper
      create  lib/generators/rails/my_helper/my_helper_generator.rb
      create  lib/generators/rails/my_helper/USAGE
      create  lib/generators/rails/my_helper/templates
      invoke  test_unit
      create    test/lib/generators/rails/my_helper_generator_test.rb

编辑 my_helper_generator.rb

# lib/generators/rails/my_helper/my_helper_generator.rb
class Rails::MyHelperGenerator < Rails::Generators::NamedBase
  def create_helper_file
    create_file "app/helpers/#{file_name}_helper.rb", <<~RUBY
      module #{class_name}Helper
        # I'm helping!
      end
    RUBY
  end
end

然后在 config/application.rb 中通过 config.generators 让 Rails 使用 my_helper 取代内置 helper

config.generators do |g|
  g.helper :my_helper
end

再次运行 scaffold:

$ bin/rails generate scaffold Article body:text
      ...
      invoke  scaffold_controller
      ...
      invoke    my_helper
      create      app/helpers/articles_helper.rb
      ...

config.generators 背后的机制值得展开:它是 Rails::Configuration::Generators 的实例(见 railties/lib/rails/configuration.rb),通过 method_missing 支持 g.helper :my_helperg.test_framework ... 这类 DSL——非 rails 命名空间的调用会把值写入 @options[:rails][method] 并作为对应 hook 的默认类选项,最终影响 hook_for 的目标解析。这些默认配置保存在 Rails::Generators::DEFAULT_OPTIONSDEFAULT_ALIASES(见 generators.rb),启动时由 Rails::Generators.configure! 与应用配置合并。

为什么内置 helper 会多出 invoke test_unit 内置 helper 生成器默认不生成测试,但它通过 [hook_for] 预留了测试钩子(helper_generator.rbhook_for :test_framework)。你的 MyHelperGenerator 若也想保留该钩子,可以加入 hook_for :test_framework, as: :helper

用 Fallbacks 覆盖特定生成器

另一种覆盖方式是 fallbacks:当某命名空间下找不到匹配生成器时,允许它把请求委托给另一命名空间。

典型场景:想用自己的 my_test_unit:model 覆盖 test_unit:model,但不想替换 test_unit:controller 等其他生成器。与其在 my_test_unit 命名空间里实现所有生成器,不如配置 my_test_unit 对未显式定义的生成器回退到 test_unit

首先创建 my_test_unit:model 生成器,放于 lib/generators/my_test_unit/model/model_generator.rb

module MyTestUnit
  class ModelGenerator < Rails::Generators::NamedBase
    source_root File.expand_path("templates", __dir__)

    def do_different_stuff
      say "Doing different stuff..."
    end
  end
end

由于 my_test_unit 是自定义命名空间而非对 Rails 内置生成器的覆盖,应放在 lib/generators/my_test_unit/ 而非 lib/generators/rails/,Rails 会按常规加载路径找到它。下一步还需通过 config.generators 把它注册为 test_framework

接着配置 config.generators,既把测试框架切换为 my_test_unit,又声明 fallback:

config.generators do |g|
  g.test_framework :my_test_unit, fixture: false
  g.fallbacks[:my_test_unit] = :test_unit
end

现在运行 scaffold 会看到 test_unitmy_test_unit 替换,但只有 model 测试受影响

$ bin/rails generate scaffold Comment body:text
      invoke  active_record
      create    db/migrate/20230518000000_create_comments.rb
      create    app/models/comment.rb
      invoke    my_test_unit
    Doing different stuff...
      invoke  resource_route
       route    resources :comments
      invoke  scaffold_controller
      create    app/controllers/comments_controller.rb
      invoke    erb
      create      app/views/comments
      create      app/views/comments/index.html.erb
      create      app/views/comments/edit.html.erb
      create      app/views/comments/show.html.erb
      create      app/views/comments/new.html.erb
      create      app/views/comments/_form.html.erb
      create      app/views/comments/_comment.html.erb
      invoke    resource_route
      invoke    my_test_unit
      create      test/controllers/comments_controller_test.rb
      create      test/system/comments_test.rb
      invoke    helper
      create      app/helpers/comments_helper.rb
      invoke      my_test_unit
      invoke    jbuilder
      create      app/views/index.json.jbuilder
      create      app/views/show.json.jbuilder

注意:model 调用 my_test_unit 只打印了 "Doing different stuff..." 且未生成测试文件(自定义生成器没写文件);而 controller 与 helper 的 my_test_unit 调用则回退到 test_unit,因此 test/controllers/comments_controller_test.rb 照常生成。

Fallback 的运行时实现位于 generators.rb 的 invoke_fallbacks_for:命名空间解析失败后,会遍历 Rails::Generators.fallbacks[base] 中登记的目标命名空间再次尝试查找。另外,当你把默认测试框架改成 my_test_unit 后,test_unit:* 这类被隐藏的命名空间集合也会相应变化(见 hidden_namespaces,其中 h << "test_unit" if test.to_s != "test_unit" 会在默认框架非 test_unit 时隐藏 test_unit 以避免重复列表)。

覆盖生成器模板

解析生成器模板文件时,Rails 先在应用的 lib/templates/ 目录查找,找不到再回退到生成器自身的 source_root 目录。这意味着我们可以在 lib/templates/ 放置同名文件来覆盖 Rails 内置生成器使用的模板——例如覆盖 scaffold controller 模板scaffold 视图模板(后者真实存在于 railties 中,含 index.html.erb.ttedit.html.erb.tt 等)。

实践一下:创建 lib/templates/erb/scaffold/index.html.erb.tt

<%%= @<%= plural_table_name %>.count %> <%= human_name.pluralize %>

额外的 .tt 扩展名告诉 Rails:这是一个需要先由 Thor 处理的生成器模板(.tt 即 "thor template")。因为这是一个 ERB 模板去渲染另一个 ERB 模板,凡是想出现在"结果模板"里的 <% 都必须在"生成器模板"里转义为 <%%

现在运行内置 scaffold:

$ bin/rails generate scaffold Post title:string
      ...
      create      app/views/posts/index.html.erb
      ...

app/views/posts/index.html.erb 的内容变为:

<%= @posts.count %> Posts

模板的搜索路径在应用侧由 application/configuration.rb 维护(paths.add "lib/templates"),并把该目录并入 Rails::Generators.templates_path(见 generators.rb 的 configure!);内置 Rails 生成器子类继承时又会把这些路径并入 source_paths,从而形成"应用模板优先、gem 模板次之、生成器自带模板兜底"的查找链。


应用模板(Application Templates)

应用模板与生成器有本质区别:生成器向已有应用添加文件(model、view 等),模板则用于自动化新 Rails 应用的初始设置。模板本质上是 Ruby 脚本(通常命名为 template.rb),在新应用生成后立即定制它。

创建并使用模板

先看一个综合示例模板——询问用户是否安装 Devise、允许自定义用户模型名,并在 bundle install 后运行 Devise 生成器与迁移,最后做 git 提交:

# template.rb
if yes?("Would you like to install Devise?")
  gem "devise"
  devise_model = ask("What would you like the user model to be called?", default: "User")
end

after_bundle do
  if devise_model
    generate "devise:install"
    generate "devise", devise_model
    rails_command "db:migrate"
  end

  git add: ".", commit: %(-m 'Initial commit')
end

(Devise 是第三方 Gem,此处仅作为官方指南的示例角色引用,其本身不在本仓库内。)

-m 参数在创建应用时应用该模板:

$ rails new blog -m ~/template.rb

该命令会创建名为 blog 且预配置了 Devise 的新 Rails 应用。

模板同样可应用于已有应用,通过 app:template 任务,位置经 LOCATION 环境变量传入:

$ bin/rails app:template LOCATION=~/template.rb

模板不必存放在本地,也支持 URL:

$ rails new blog -m https://example.com/template.rb
$ bin/rails app:template LOCATION=https://example.com/template.rb

安全警告:执行第三方远程脚本务必谨慎。模板是纯 Ruby 脚本,很容易包含危害本机的代码(如下载病毒、删除文件、把私有文件上传到服务器)。

上面的 template.rb 用到了 after_bundlerails_command 等辅助方法,以及 yes? 这类交互方法——它们都属于 Rails Generators APIRails::Generators::Actions 模块)。


Rails Generators API

生成器与模板 Ruby 脚本通过一套 DSL 使用诸多辅助方法,这些方法定义在 Rails::Generators::Actions 中,底层文件操作继承自 Thor::Actions。本节每个小标题对应一个 API 方法,全部示例均可直接放进 template.rb。下方演示的"脚手架模型 → 迁移 → git 提交"是一个典型模板骨架:

# template.rb
generate(:scaffold, "person name:string")
route "root to: 'people#index'"
rails_command("db:migrate")

after_bundle do
  git :init
  git add: "."
  git commit: %Q{ -m 'Initial commit' }
end

add_source

向生成应用的 Gemfile 追加 source:

add_source "https://rubygems.org"

若给出代码块,块内的 gem 条目会被包进该 source 组。例如需要从 "http://gems.github.com" 引入 gem 时:

add_source "http://gems.github.com/" do
  gem "rspec-rails"
end

after_bundle

注册一个在 gems 完成 bundle 后执行的回调。例如 tailwindcss-railsdevise 的 "install" 命令应在对应 gem 被 bundle 之后运行:

# Install gems
after_bundle do
  # Install TailwindCSS
  rails_command "tailwindcss:install"

  # Install Devise
  generate "devise:install"
end

即使传入了 --skip-bundle,这些回调依然会执行。

environment

config/application.rbApplication 类中追加一行配置;若指定 options[:env],则该行被追加到 config/environments 下对应文件:

environment 'config.action_mailer.default_url_options = {host: "http://yourwebsite.example.com"}', env: "production"

上面这行会进入 config/environments/production.rb。源码中 environment 通过定位 class Application < Rails::ApplicationRails.application.configure do 两个哨兵行执行注入(见 actions.rb 的 environment),它还有一个别名 application

gem

向生成应用的 Gemfile 添加 gem 条目(只改 Gemfile,不安装)。依赖 devisetailwindcss-rails

gem "devise"
gem "tailwindcss-rails"

指定精确版本:

gem "devise", "~> 4.9.4"

在 gem 声明上方追加注释:

gem "devise", comment: "Add devise for authentication."

实现细节可参考 actions.rb 的 gem 方法,它还会把 git:branch: 等任意选项序列化进 Gemfile 声明。

gem_group

把 gem 条目包进分组。例如只在 developmenttest 组加载 rspec-rails

gem_group :development, :test do
  gem "rspec-rails"
end

generate

template.rb 内部调用另一个生成器:

generate(:scaffold, "person", "name:string", "address:text", "age:number")

内部实现是转调 rails_command "generate ..."(见 actions.rb 的 generate)。

git

用辅助方法执行任意 git 命令:

git :init
git add: "."
git commit: "-a -m 'Initial commit'"

支持符号或 Hash 两种形式(Hash 的每个键值对都会拼成一条 git cmd options 命令执行,见 actions.rb 的 git)。

initializer、vendor、lib、file

initializer 向生成应用的 config/initializers 添加初始化文件。例如在应用中提供 Object#not_nil?Object#not_blank?

initializer "not_methods.rb", <<-CODE
  class Object
    def not_nil?
      !nil?
    end

    def not_blank?
      !blank?
    end
  end
CODE

类似的,liblib/ 下创建文件,vendorvendor/ 下创建文件(三者的实现都收口到 create_file,见 actions.rb)。

另有 file 方法(create_file 的别名),接受相对 Rails.root 的路径,会自动创建所需的所有目录与文件:

file "app/components/foo.rb", <<-CODE
  class Foo
  end
CODE

上面会创建 app/components 目录并把 foo.rb 放进去。

rakefile

lib/tasks 下创建 Rake 文件:

rakefile("bootstrap.rake") do
  <<-TASK
    namespace :boot do
      task :strap do
        puts "I like boots!"
      end
    end
  TASK
end

生成 lib/tasks/bootstrap.rake,内含 boot:strap 任务。

run

执行任意命令。例如删除 README.rdoc

run "rm README.rdoc"

rails_command

在生成的应用中运行 Rails 命令。例如中途执行数据库迁移:

rails_command "db:migrate"

指定环境:

rails_command "db:migrate", env: "production"

要求失败即中止应用生成:

rails_command "db:migrate", abort_on_failure: true

源码里 rails_command 支持 :env:abort_on_failure:capture:sudo 等选项(见 actions.rb 的 rails_commandexecute_command,默认环境取 ENV["RAILS_ENV"] || "development");同族的 rake 方法则用于运行 Rake 任务。

route

config/routes.rb 追加条目。要让 PeopleController#index 成为默认首页:

route "root to: 'person#index'"

route 还支持 namespace: 关键字参数并把代码包进 namespace 块(见 actions.rb 的 route)。

本地文件系统相关方法

此外还有一批操作本地文件系统的辅助方法,如 copy_filecreate_fileinsert_into_filegsub_fileinside 等。inside 支持从指定目录运行命令——例如想把一份 edge Rails 源码软链进新应用:

inside("vendor") do
  run "ln -s ~/my-forks/rails rails"
end

用户交互:ask、yes?、no?

模板中也可以与用户交互:

ask 收集用户输入。让用户为新库命名:

lib_name = ask("What do you want to call the shiny library?")
lib_name << ".rb" unless lib_name.index(".rb")

lib lib_name, <<-CODE
  class Shiny
  end
CODE

yes? / no? 根据用户回答决定流程分支。例如询问是否执行迁移:

rails_command("db:migrate") if yes?("Run database migrations?")
# no? 与 yes? 语义相反

这些交互方法实现于 Thor::Shell::Basic(Rails 未重新实现,直接复用 Thor 能力),支持 default: 参数等选项。


测试生成器

Rails 为生成器测试提供了专用辅助方法,入口是 Rails::Generators::Testing::Behavior(源码见 railties/lib/rails/generators/testing/behavior.rb),其中最重要的是 run_generator。配合测试用例基类 Rails::Generators::TestCase(见 railties/lib/rails/generators/test_case.rb),典型测试结构如下(behavior.rb 文档注释中的示例):

class AppGeneratorTest < Rails::Generators::TestCase
  tests AppGenerator
  destination File.expand_path("../tmp", __dir__)
  setup :prepare_destination

  test "database.yml is not created when skipping Active Record" do
    run_generator %w(myapp --skip-active-record)
    assert_no_file "config/database.yml"
  end
end
  • tests AppGenerator 声明被测生成器;
  • destination ... 把生成文件写到临时目录,避免污染真实应用;
  • setup :prepare_destination 在每条用例前清空并重建目标目录;
  • 断言除了 assert_no_file,还有 Rails::Generators::Testing::Assertions(见 railties/lib/rails/generators/testing/assertions.rb)提供的一整套文件/内容断言。

如果针对生成器跑测试,需要设置 RAILS_LOG_TO_STDOUT=true 才能让调试工具正常工作:

RAILS_LOG_TO_STDOUT=true ./bin/test test/generators/actions_test.rb

原因是生成器测试经常用 FileUtils.cd 切换当前目录,而日志输出需要显式指向 stdout(behavior.rb 中注释亦说明了这一背景)。


总结与仓库导航

生成器与模板构成了一条完整的自动化链路:先用 bin/rails generate / --help / --pretend 探查能力,再用 Rails::Generators::Base(或 NamedBase)配合 source_root、模板文件与 class_option 写自定义生成器;理解"文件级 + 命名空间级"双层查找后,可通过 config.generators 的 hook 默认值覆盖生成器、用 fallbacks 做局部替换,或用 lib/templates/ 覆盖模板;最后借助 Rails::Generators::Actions 的 DSL 编写可复用应用模板,并用量产级生成器测试固化行为。

想在当前仓库继续深挖,可按以下路径对照源码阅读:

实际项目中,建议把高频覆盖(自定义 helper、定制的 scaffold 视图、统一的测试框架命名空间)沉淀为小团队共享的 gem 或模板仓库,再通过 -mapp:template LOCATION=... 在每次 rails new 时一键套用,从而让"脚手架"真正长成符合团队规范的应用起点。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
594
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
516
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388