Rails 如何在开发模式下重新加载源代码

在 Rails development 环境下,若更改了部分代码,只需要重新发起请求,就能看到最新代码的结果,不需要重启服务器。
下文简述 Rails 是如何做到这点的。

config/development.rb,也就是 development 的环境配置文件,我们可以看到不同于其他环境的一行:

1
2
3
4
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the webserver when you make code changes.
config.cache_classes = false

正如注释所说的,它会使得 Rails 在每次接收请求时都重新加载源代码。

初始化

我们先从 Rails 的初始化说起,以 Rails 4.2.6 为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
# rails/application/default_middleware_stack.rb
def build_stack
...
unless config.cache_classes
middleware.use ::ActionDispatch::Reloader, lambda { reload_dependencies? }
end
...
private
def reload_dependencies?
config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any?
end
end

当 config.cache_classses 为 true 时,middleware 会增加 ActionDispatch::Reloader,而这正是重新加载源代码的关键。
ps: middleware 可以通过 rake middleware 来查看。

处理请求

当服务器接收到请求时,中间件 ActionDispatch::Reloader 使用回调 prepare、cleanup 来实现重载源代码。其中 prepare 在处理请求前被调用,cleanup 在处理请求后被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ActionDispatch::Reloader
def initialize(app, condition=nil)
@app = app
@condition = condition || lambda { true }
@validated = true
end
def call(env)
@validated = @condition.call
prepare!
response = @app.call(env)
response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! }
response
rescue Exception
cleanup!
raise
end
def prepare! #:nodoc:
run_callbacks :prepare if validated?
end
def cleanup! #:nodoc:
run_callbacks :cleanup if validated?
ensure
@validated = true
end
private
def validated?
@validated
end
end

当 @validated 为 true 时会执行 prepare、cleanup 等回调。那么 @validated 的值是怎么得到的?
结合初始化的代码我们发现,@condition 其实就是创建 ActionDispatch::Reloader 时的参数 lambda { reload_dependencies? }。而 reload_dependencies? 具体代码如下。

1
2
3
def reload_dependencies?
config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any?
end

可以通过以下几种情况来分析
若 config.reload_clases_only_on_change 为 false,会执行回调。
若 config.reload_clases_only_on_change 为 true 且代码发生了变动(任一 reloader 调用 updated? 返回 true),会执行回调。
若 config.reload_clases_only_on_change 为 true 且代码未发生变动,不会执行回调。

回调

弄清楚了回调调用的时机,我们来继续研究回调的内容是什么。

Rails::Application::Finisher 负责结束 Rails 的初始化,它会给 ActionDispatch::Reloader 增加回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# rails/application/finisher.rb
initializer :set_clear_dependencies_hook, group: :all do
callback = lambda do
ActiveSupport::DescendantsTracker.clear
ActiveSupport::Dependencies.clear
end
if config.reload_classes_only_on_change
reloader = config.file_watcher.new(*watchable_args, &callback)
self.reloaders << reloader
# Prepend this callback to have autoloaded constants cleared before
# any other possible reloading, in case they need to autoload fresh
# constants.
ActionDispatch::Reloader.to_prepare(prepend: true) do
# In addition to changes detected by the file watcher, if routes
# or i18n have been updated we also need to clear constants,
# that's why we run #execute rather than #execute_if_updated, this
# callback has to clear autoloaded constants after any update.
reloader.execute
end
else
ActionDispatch::Reloader.to_cleanup(&callback)
end
end

当 config.reload_clases_only_on_change 为 true,会向 ActionDispatch::Reloader 的 prepare 添加回调,而 false 时会向 cleanup 添加回调。
该回调会清理所有的依赖,更确切地说,使用内置的 remove_const 清除所有加载的常量。
由于所有常量都被移除,ActiveSupport::Dependencies 使用 const_missing 并再次加载相关类,从而使得修改后的代码被加载。

总结

当 config.cache_class 为 false 时 middleware 会增加中间件 ActionDispatch::Reloader

当 config.reload_clases_only_on_change 为 true 时,Rails::Application::Finisher 会在 ActionDispatch::Reloader 增加 prepare 的回调。
请求处理前 Reloader 会根据代码是否更新来执行 prepare 回调,执行回调后会清理所有的依赖,ActiveSupport::Dependencies 使用 const_missing 并再次加载相关类,从而使得修改后的代码被加载。

当 config.reload_clases_only_on_change 为 false 时, Rails::Application::Finisher 会在 ActionDispatch::Reloader 增加 cleanup 的回调。Reloader 会在每次请求处理后执行回调清理所有的依赖。其他步骤类似。

关于更详细的步骤说明可以参考 How rails reloads your source code in development mode? ,关于 Rails 的 autoload 机制可以参考 Autoloading and Reloading Constants

参考