Railsの自動読み込みのためのプラグインの構造化

多くのプラグインでは、plugin.rb 内に多くのクラス定義を含めたり、require_relative を使用して Ruby ファイルを読み込んだりします。これは機能しますが、いくつかの欠点があります。

  1. 開発中の変更の自動リロードができない。変更にはサーバーの完全な再起動が必要。
  2. require の呼び出しを正しい順序にするのが難しい場合がある。
  3. after_initialize ブロックの外で require されている場合、他の自動ロードされたクラス/モジュールが利用できない可能性がある。

解決策があります!プラグインは標準の Rails 自動ロードシステムを利用できます。新しいプラグインの場合、必要なものはすべて plugin-skeleton に定義されています。このトピックでは、既存のプラグインを適応させ、設定を拡張する方法を説明します。

1. プラグインのモジュールと Rails::Engine を定義する

plugin.rb で、一意の PLUGIN_NAME を持つプラグインのモジュールを定義し、これから作成するエンジンファイルを読み込む require_relative 行を追加します。

# name: my-plugin-name
# ...

module ::MyPluginModule
  PLUGIN_NAME = "my-plugin-name"
end

require_relative "lib/my_plugin_module/engine"

次に {plugin}/lib/my_plugin_module/engine.rb を作成します。

module ::MyPluginModule
  class Engine < ::Rails::Engine
    engine_name PLUGIN_NAME
    isolate_namespace MyPluginModule
  end
end

注意すべき重要な点:

  1. plugin.rb では、ルート名前空間に定義するためにモジュール名の前に :: を含める必要があります(そうしないと、Plugin::Instance の下に定義されます)。

  2. require_relative "lib/.../engine" は、after_initialize ブロック内ではなく、plugin.rb ファイルのルートに配置する必要があります。

  3. エンジンを lib/ の下の独自のファイルに配置することが重要です。plugin.rb ファイルで直接定義しても機能しません。(Rails は lib/ ディレクトリの存在を使用してエンジンのルートを決定します)

  4. ファイルパスには、Zeitwerk のルール に従ってモジュール名を含める必要があります。

  5. engine_name は、Rake タスクのプレフィックスとして、またエンジンによって定義されたルートに使用されます(:link: Rails ドキュメント)。

  6. isolate_namespace は、コアとプラグインの間で名前が漏洩するのを防ぐのに役立ちます(:link: Rails ドキュメント)。

2. 正しいディレクトリ構造で Ruby ファイルを定義する

エンジンは、{plugin}/app/{type}/* のすべてのファイルを自動ロードするようになります。たとえば、コントローラーを定義できます。

{plugin}/app/controllers/my_plugin_module/examples_controller.rb

module ::MyPluginModule
  class ExamplesController < ::ApplicationController
    requires_plugin PLUGIN_NAME

    def index
      render json: { hello: "world" }
    end
  end
end

これにより、Rails が ::MyPluginModule::MyController にアクセスしようとするたびに自動的にロードされるようになります。テストするには、Rails コンソールからそのクラスにアクセスしてみてください。

自動ロードが正しく機能するためには、ファイルパスは Zeitwerk によって定義されたルール に従って、完全なモジュール/クラス階層と一致する必要があります。

3. プラグインのエンジンにルートを定義する

{plugin}/config/routes.rb ファイルを作成します。

MyPluginModule::Engine.routes.draw do
  get "/examples" => "examples#index"
  # ルートをここに定義
end

Discourse::Application.routes.draw do
  mount ::MyPluginModule::Engine, at: "my-plugin"
end

このファイルはエンジンによって自動的にロードされ、サーバーを再起動せずに変更が有効になります。この場合、コントローラーアクションは /my-plugin/examples.json で利用可能になります。

4. 追加の自動ロードパスを追加する

場合によっては、自動ロード可能な Ruby ファイルの追加ディレクトリを導入したいことがあります。最も一般的な例は、プラグインの lib/ ディレクトリです。

エンジンの自動ロードパスに lib/ を追加するように、エンジン定義を変更します。

class Engine < ::Rails::Engine
  engine_name PLUGIN_NAME
  isolate_namespace MyPluginModule
  config.autoload_paths << File.join(config.root, "lib")
end

これで、次のような lib モジュールを定義できます。

{plugin}/lib/my_plugin_module/some_lib_module.rb

module ::MyPluginModule::SomeLibModule
end

そして、::MyPluginModule::SomeLibModule への参照は、このファイルからモジュールを自動的にロードするようになります。

5. 利益!

これらのファイルはすべて、明示的な require 呼び出しなしで自動的にロードされるようになります。変更は Rails によって自動的に検出され、サーバーを再起動することなくインプレースでリロードされます。


このドキュメントはバージョン管理されています - 変更は github で提案してください。

「いいね!」 17

デビッド、このドキュメントのきっかけは何ですか? 上流の変更ですか?

自動リロードを実現するために特定の構造を維持することを心配したことは一度もありませんでしたが、それとも適切な構造に偶然従っていただけでしょうか?…

「いいね!」 1

変更なし - いくつかのプラグイン(例:anonymous-moderatorsclient-performance)で使用している構造を文書化するだけで、より多くのプラグインで使用したいと考えています。

OPの先頭にある3つの欠点を受け入れるなら、どの構造でも機能します。この構造に従えば、それらの問題は解決され、(開発者体験ははるかにスムーズになるはずです!)

「いいね!」 2

はい、自動リロードは非常に望ましいです。

明確化とドキュメントをありがとうございます!

「いいね!」 1

明確にしておきますが、「自動読み込み」とは、Railsが(Zeitwerkを介して)実行中のプロセスに変更されたコードを、再起動なしでほぼ瞬時に取り込む機能のことです。plugin.rbファイルにloadまたはrequire呼び出しがある場合、そのプラグインはほぼ確実に自動読み込みを使用していません。

別個に、Discourseには「再読み込み不可」のRubyファイルの変更を検出し、サーバー全体を自動的に再起動するヘルパーがあります。これには数秒かかります。このシステムがトリガーされると、コンソールに次のようなものが表示されます。

[DEV]: 自動読み込みされないファイルが編集されました。サーバーを再起動しています...
       - plugins/discourse-follow/plugin.rb
「いいね!」 2

ああ!それが私が欠けていた区別でした、ありがとう!自動再起動だけで満足していましたが、これは本当にクールです!

「いいね!」 2

デビッド様

興味深い問題が発生し、解決したと考えていますが、コメントするお時間はありますでしょうか?

/lib/locations 内のプラグインモジュールで定数が一度だけ定義されていました。

再構築のたびに、以下のエラー警告が表示されていました。

image

warning: already initialized constant Locations::REQUEST_PARTS
warning: already previous definition of REQUEST_PARTS was here

これを plugin.rb に移動すると、問題が解決するようです。

しかし、なぜでしょうか?

私の見解では、plugin.rb は一度だけ評価されますが、/lib/plugin-module 内のものは複数回評価される可能性があるということでしょうか?しかし、なぜ他のコードについては苦情が来ないのでしょうか?

「いいね!」 1

はい、その通りです。

Rails/Zeitwerk がコードをリロードするときは、remove_const :Blah を実行してから、関連する名前でファイルをロードします。そのため、lib/locations/geocode.rb ファイルの場合、オートローダーは次のような処理を行います。

Locations.remove_const :Geocode
load "lib/locations/geocode.rb"

これが、Geocode 定数が既に定義されているというエラーが発生しない理由です。Rails/Zeitwerk は、ファイルをリロードする前に自動的に定数を削除します。

しかし、オートローダーは、あなたのファイルが Locations::REQUEST_PARTS も定義していることを知る方法がないため、ロード前に remove_const を実行しませんでした。

そのため、geocode.rb ファイルに定数を保持したい場合は、REQUEST_PARTS を Geocode クラス内 (Locations::Geocode::REQUEST_PARTS) に移動させることができます。

しかし、Locations::REQUEST_PARTS として定数を保持したい場合は、plugin.rb に移動するのが理にかなっていると思います。または、完全に自動ロード可能にしたい場合は、REQUEST_PARTS 定数を含む module ::Locations 定義全体を、lib/locations.rb のような別のファイルに移動できる可能性があります。

「いいね!」 3

それは完全に理にかなっています、ありがとう!

他に何かあるかもしれませんか?

たとえば、次のように plugin.rb でこれらのファイルを明示的にロードする必要がないと言っても公正でしょうか。

after_initialize do
  %w(
    ../app/models/location_country_default_site_setting.rb
    ../app/models/location_geocoding_language_site_setting.rb
    ../app/models/locations/user_location.rb
    ../app/models/locations/topic_location.rb
**    ../lib/locations/user_location_process.rb**
**    ../lib/locations/topic_location_process.rb**
**    ../lib/locations/country.rb**
**    ../lib/locations/geocode.rb**
**    ../lib/locations/helper.rb**
**    ../lib/locations/map.rb**
    ../lib/users_map.rb
    ../app/serializers/locations/geo_location_serializer.rb
    ../app/controllers/locations/geocode_controller.rb
    ../app/controllers/locations/users_map_controller.rb
  ).each do |path|
    load File.expand_path(path, __FILE__)
  end

これらは、オートローディングによっても処理される構造内にあるためですか?

/lib がオートローディング設定に含まれているので、手動で load する必要はないはずです :100:

「いいね!」 2

コードの単純化は誰だって好きですよね!:chefs_kiss:

「いいね!」 1