5

Rails7 的Zeitwerk模式解惑

 1 year ago
source link: http://xfyuan.github.io/2022/11/rails7-zeitwerk-mode/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Rails7 的Zeitwerk模式解惑

Written by Mr.Z on 21 Nov 2022
Rails7 的Zeitwerk模式解惑

本文已获得原作者(Athira KadampattaSupriya Laxman Medankar)和 Kiprosh 授权许可进行翻译。原文详细讲述了 Rails 7 中新的 Zeitwerk 自动加载模式。

【正文如下】

Rails 中传统的 autoloader 很有帮助,但仍然有一些瑕疵造成自动加载偶尔会出毛病。为了解决这个问题, Xavier Noria 在 Rails 6 的这个 PR 中提出了 zeitwerk 模式并使其可配置使用。Rails 7 则更进一步,zeitwerk 完全替代了传统的 autoloader。

本文中,我们会看看传统的自动加载会碰到的问题,以及 Zeitwerk 模式如何解决的。(你可以阅读这篇文章 来理解 Rails 的 autoloader 是怎样工作的)。

How classic autoloading works?

起初,Rails 使用的是在 Active Support 中称作 Classic Autoloading 的实现来作为 autoloader,一直持续到 Rails 6。

Classic Autoloading 依赖的是 Ruby 的常量查找。要解析一个常量,会首先在所定义的类的词法域中查找,然后在其祖先链中查找。如果该常量未找到,const_missing方法就会被 Ruby 调用。Rails 覆写了 Ruby 的const_missing 方法,并使用autoload_paths 根据惯例约定来解析常量。

How zeitwerk autoloading works?

新引入的 Zeitwerk Mode 则不依赖 Ruby 的常量查找。

相反,它利用的是 Ruby 的 Module#autoload 方法提前告知 Ruby 哪个文件将定义一个特定常量,而不需要立即加载该文件。

https://blog.kiprosh.com/content/images/2022/09/Rails-autoload-how-it-works.png

Common Problems resolved by zeitwerk mode

传统模式存在许多问题,但都已被 zeitwerk 模式解决了。这其中,我们会看看三个不同的陷阱,每个都带有示例。

1、When Constants aren’t Missed

假设我们有如下 model 结构:

# course.rb
class Course
  def initialize
    puts "From Course"
  end
end

# mit_university/course.rb
module MitUniversity
  class Course
    def initialize
      puts "From MitUniversity::Course"
    end
  end
end

# mit_university/engineering.rb
module MitUniversity
  class Engineering
    def initialize
      @course = Course.new
    end
  end
end

With Classic Mode

Loading development environment (Rails 5.2.7.1)
2.7.5 :001 > Course.new
From Course
 => #<Course:0x0000563bfa029810>
2.7.5 :002 > MitUniversity::Engineering.new
From Course
 => #<MitUniversity::Engineering:0x0000563bf9e7ab40 @course=#<Course:0x0000563bf9e7aaf0>>

这里,由于我们在调用MitUniversity::Course之前调用了Course,Ruby 的常量查找就已经在内存中自动加载了Course,所以如果我们想要为MitUniversity::Engineering创建一个对象时,它就会引用到已在内存中被自动加载的Course,而不去搜索MitUniversity::Course了。这让自动加载依赖于常量被调用的顺序。

With Zeitwerk Mode

Loading development environment (Rails 7.0.3)
2.7.5 :001 > Course.new
From Course
 => #<Course:0x00005615a9707fa0>
2.7.5 :002 > MitUniversity::Engineering.new
From MitUniversity::Course
 => #<MitUniversity::Engineering:0x00005615ae41d290 @course=#<MitUniversity::Course:0x00005615ae40b270>>
2.7.5 :003 >

因为 zeitwerk 模式为所有常量定义了autoload_path,它已经知道了到哪里去查找哪个常量。所以尽管首先初始化Course,但在MitUniversity::Engineering类中调用时,它仍然如期望的那样加载了MitUniversity::Course

2、Autoloading within Singleton Classes

这是一个关于 Singleton 类方法的类似问题,已经被 zeitwerk 模式所解决。例子如下所示:

# mit_university/course.rb
module MitUniversity
  class Course
    def initialize
      puts "From MitUniversity::Course"
    end
  end
end

# mit_university/engineering.rb
module MitUniversity
  class Engineering
    class << self
      def details
        Course.new
      end
    end
  end
end

With classic mode

如果我们在调用MitUniversity::Course之前调用 MitUniversity::Engineering.details,它将会抛出uninitialized constant Course的错误。这是由于,当自动加载被触发时,Rails 只去检查顶层命名空间,因为 singleton 类是匿名的,所以 Rails 不会知道嵌套的MitUniversity

Loading development environment (Rails 5.2.7.1)
2.7.5 :001 > MitUniversity::Engineering.details
Traceback (most recent call last):
        2: from (irb):3
        1: from app/models/mit_university/engineering.rb:5:in `details'
NameError (uninitialized constant Course)
2.7.5 :002 > MitUniversity::Course
 => MitUniversity::Course
2.7.5 :003 > MitUniversity::Engineering.details
From MitUniversity::Course
 => #<MitUniversity::Course:0x0000560cabb27c18>

With zeitwerk mode

Zeitwerk 模式则不会抛出任何错误,并且即使之前没有自动加载它也能载入该常量。

Loading development environment (Rails 7.0.3)
2.7.5 :001 > MitUniversity::Engineering.details
From MitUniversity::Course
 => #<MitUniversity::Course:0x0000559ebba13dd0>

3、Autoloading and Single-table Inheritance (STI)

假设我们有如下 Single-table Inheritance (STI) 的 model 已定义:

class Polygon < ApplicationRecord
end

class Triangle < Polygon
end

class Rectangle < Polygon
end

class Square < Rectangle
end

Square继承自 Rectangle,所以当我们调用Rectangle.all时,结果必须包含Polygon类型的Square以及Rectangle

With classic mode

然而,当我们调用Rectangle.all时,并不能在结果中看到Square记录。我们可以看到所生成的 SQL 查询中并未包含Square

Loading development environment (Rails 5.2.7.1)
 2.7.5 :001 > Rectangle.all
  Rectangle Load (0.4ms)  SELECT `polygons`.* FROM `polygons` WHERE `polygons`.`type` = 'Rectangle' /* loading for inspect */ LIMIT 11
 => #<ActiveRecord::Relation [#<Rectangle id: 1, area: 100.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:00:50.705523000 +0000", updated_at: "2022-08-28 14:00:50.705523000 +0000">, #<Rectangle id: 2, area: 200.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:01:09.543825000 +0000", updated_at: "2022-08-28 14:01:09.543825000 +0000">]>
 2.7.5 :002 > Square
 => Square(id: integer, area: float, type: string, type_id: integer, created_at: datetime, updated_at: datetime)
 2.7.5 :003 > Rectangle.all
  Rectangle Load (0.9ms)  SELECT `polygons`.* FROM `polygons` WHERE `polygons`.`type` IN ('Rectangle', 'Square') /* loading for inspect */ LIMIT 11
 => #<ActiveRecord::Relation [#<Rectangle id: 1, area: 100.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:00:50.705523000 +0000", updated_at: "2022-08-28 14:00:50.705523000 +0000">, #<Rectangle id: 2, area: 200.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:01:09.543825000 +0000", updated_at: "2022-08-28 14:01:09.543825000 +0000">, #<Square id: 5, area: 250.0, type: "Square", type_id: nil, created_at: "2022-08-28 14:01:52.165141000 +0000", updated_at: "2022-08-28 14:01:52.165141000 +0000">]>

要解决这个问题,我们不得不在rectangle.rb文件底部加上require_dependency 'square'

# app/models/rectangle.rb
class Rectangle < Polygon
end
require_dependency 'square'

With zeitwerk mode

由于在 zeitwerk 模式中,Square已被自动加载进来,我们就无需添加require_dependency 'square'这一行了:

Loading development environment (Rails 7.0.3)
2.7.5 :001 > Rectangle.all
  Rectangle Load (0.9ms)  SELECT `polygons`.* FROM `polygons` WHERE `polygons`.`type` IN ('Rectangle', 'Square') /* loading for inspect */ LIMIT 11
 => #<ActiveRecord::Relation [#<Rectangle id: 1, area: 100.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:00:50.705523000 +0000", updated_at: "2022-08-28 14:00:50.705523000 +0000">, #<Rectangle id: 2, area: 200.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:01:09.543825000 +0000", updated_at: "2022-08-28 14:01:09.543825000 +0000">, #<Square id: 5, area: 250.0, type: "Square", type_id: nil, created_at: "2022-08-28 14:01:52.165141000 +0000", updated_at: "2022-08-28 14:01:52.165141000 +0000">]>

Conclusion

对于 Rails 7,Zeitwerk 已经成为默认模式,而传统模式已不可用了。这是一个很有影响的变化,改进了 Rails 中常量自动加载的方式,解决了诸多如上所述的传统模式带来的问题。

References


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK