Rails 7 controller decorator uninitialized constant error only in production 只

Rails 7 controller decorator uninitialised constant error in production only

我收到以下错误 zeitwerk/loader/helpers.rb:95:in const_get': uninitialized constant Controllers::BasePublicDecorator (NameError) 这是使用 rails c -e production 的本地生产控制台中的错误,但不是完美运行的开发中的问题。

在引擎 CcsCms::PublicTheme 中,我有一个装饰器用于扩展另一个 CcsCms::Core 引擎的控制器,正是这个装饰器导致了错误。

public_theme/app/decorators/decorators/controllers/base_public_decorator.rb

CcsCms::BasePublicController.class_eval do
  before_action :set_theme #ensure that @current_theme is available for the
                          #header in all public views

  private

    def set_theme
      @current_theme = CcsCms::PublicTheme::Theme.current_theme
    end
end

此功能在开发中运行良好,但在生产中失败并出现如下错误

我试图在 CcsCms::Core 引擎中装饰的控制器是 CcsCms::BasePublicController.rb

module CcsCms
  class BasePublicController < ApplicationController
    layout "ccs_cms/layouts/public"

    protected
      def authorize
      end
  end
end

在我尝试使用装饰器的主题引擎中,我有一个定义核心引擎的 Gemfile,如下所示

gem 'ccs_cms_core', path: '../core'

在 ccs_cms_public_theme.gemspec 我需要核心引擎作为依赖项

  spec.add_dependency "ccs_cms_core"

在 engine.rb 中,我需要核心引擎并在 config.to_prepare do 块中加载装饰器路径

require "deface"
require 'ccs_cms_admin_dashboard'
require 'ccs_cms_custom_page'
require 'ccs_cms_core'
require 'css_menu'
#require 'tinymce-rails'
require 'delayed_job_active_record'
require 'daemons'
require 'sprockets/railtie'
require 'sassc-rails'

module CcsCms
  module PublicTheme
    class Engine < ::Rails::Engine
      isolate_namespace CcsCms::PublicTheme
      paths["app/views"] << "app/views/ccs_cms/public_theme"

      initializer "ccs_cms.assets.precompile" do |app|
        app.config.assets.precompile += %w( public_theme_manifest.js )
      end

      initializer :assets do |config|
        Rails.application.config.assets.paths << root.join("")
      end

      initializer :append_migrations do |app|
        unless app.root.to_s.match?(root.to_s)
          config.paths['db/migrate'].expanded.each do |p|
            app.config.paths['db/migrate'] << p
          end
        end
      end

      initializer :active_job_setup do |app|
        app.config.active_job.queue_adapter = :delayed_job
      end

      config.to_prepare do
        Dir.glob(Engine.root.join("app", "decorators", "**", "*_decorator*.rb")) do |c|
          Rails.configuration.cache_classes ? require(c) : load(c)
        end
      end

      config.generators do |g|
        g.test_framework :rspec,
          fixtures: false,
          request: false,
          view_specs: false,
          helper_specs: false,
          controller_specs: false,
          routing_specs: false
        g.fixture_replacement :factory_bot
        g.factory_bot dir: 'spec/factories'
      end

    end
  end
end

鉴于我的装饰器的名称与它从核心引擎装饰的控制器同名,但带有 .decorator 扩展名,我很确定一切都正确连接,如前所述,这在开发中完美运行但是由于此错误,我无法在生产环境中启动 rails 控制台。 似乎 class_eval 不知何故失败了,我只能认为这可能是一个路径问题,但我无法弄清楚

更新 经过相当大的学习曲线,非常感谢@debugger 评论和@Xavier Noria 很明显,我的问题归结为 Zeitworks 自动加载功能

Rails 指南 here 对我来说有一个有趣且有吸引力的解决方案

Another use case are engines decorating framework classes:

initializer "decorate ActionController::Base" do  
> ActiveSupport.on_load(:action_controller_base) do
>     include MyDecoration   end end

There, the module object stored in MyDecoration by the time the initializer runs becomes an ancestor of ActionController::Base, and reloading MyDecoration is pointless, it won't affect that ancestor chain.

但也许这不是正确的解决方案,我再次未能使其适用于以下

  initializer "decorate CcsCms::BasePublicController" do
    ActiveSupport.on_load(:ccs_cms_base_public_controller) do
      include CcsCms::BasePublicDecorator
    end
  end

生成以下错误

zeitwerk/loader/callbacks.rb:25:in `on_file_autoloaded': expected file /home/jamie/Development/rails/comtech/r7/ccs_cms/engines/public_theme/app/decorators/controllers/ccs_cms/base_public_decorator.rb to define constant Controllers::CcsCms::BasePublicDecorator, but didn't (Zeitwerk::NameError)

所以回到提供的解决方案 here,再次感谢下面的回答 我尝试了以下最终有效的方法

  config.to_prepare do
    overrides = Engine.root.join("app", "decorators")
    Rails.autoloaders.main.ignore(overrides)
    p = Engine.root.join("app", "decorators")
    loader = Zeitwerk::Loader.for_gem
    loader.ignore(p)
    Dir.glob(Engine.root.join("app", "decorators", "**", "*_decorator*.rb")) do |c|
      Rails.configuration.cache_classes ? require(c) : load(c)
    end
  end

这里的问题是延迟加载时,没有人引用名为 ...::BasePublicDecorator 的常量。但是,Zeitwerk 期望在该文件中定义该常量,并且在急切加载时发现不匹配。

解决方案是将自动加载器配置为忽略装饰器,因为您正在处理它们的加载,并且因为它们没有在名称后定义常量。 This documentation 有一个例子。它需要适应您的引擎,但您会看到这个想法。

为了完整起见,我还要说明一下,在Zeitwerk中,预加载是递归的const_get,而不是递归的require。这是为了确保如果您访问常量,加载成功或失败在两种模式下始终如一(而且效率更高)。递归 const_get 仍然通过 Module#autoload 发出 require 调用,如果你 运行 一个用于某些文件的幂等性也适用,但 Zeitwerk 检测到预期的常量无论如何都没有定义,这是错误情况。