Railsのオートロードのためのプラグインの構造化

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

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

解決策があります!プラグインは標準の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 docs)。

  6. isolate_namespaceは、コアとプラグイン間での漏洩を防ぐのに役立ちます(:link: Rails docs)。

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"
  # define routes here
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