在 development 环境下,修改项目代码,不用重启服务器,刷新页面就能看到代码变化。为什么?是用 load
重新加载了代码吗?——没那么简单。
下面在 Rails-3.2.13 代码里找到它的实现。
首先,这和 Rails 加载常量的方式有关。 ActiveSupport::Dependencies::ModuleConstMissing
模块重写了 Module#const_missing
方法。当程序遇到未定义的常量时,就会调用到该方法。const_missing
是类方法,所以应该定义在类的类 Class
上,而 Module
是 Class
的父类。在重写的 const_missing
方法里,会根据常量名,按照路径的惯例去加载对应的文件,当找到后,就加载文件,并且把常量保存起来(常量保存在 ActiveSupport::Dependencies.autoloaded_constants
,加载过的文件路径保存在 ActiveSupport::Dependencies.history
)。当需要时,调用 ActiveSupport::Dependencies.clear
移除已经加载过的常量(可参考 http://blog.yuaz.net/archives/415 ,很多人认为 Rails 加载类的方式,用到了 autoload
http://ruby-doc.org/core-1.9.3/Module.html#method-i-autoload ,是不对的)。
所以,Rails 可以用 ActiveSupport::Dependencies.clear
在某个时刻清除加载的类和模块的。
在代码里搜 ActiveSupport::Dependencies.clear
,找到 railties/lib/rails/application/finisher.rb
里可以看到,有如下的初始化步骤。这里定义了一个 lambda
# Set app reload just after the finisher hook to ensure
# paths added in the hook are still loaded.
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
# We need to set a to_prepare callback regardless of the reloader result, i.e.
# models should be reloaded if any of the reloaders (i18n, routes) were updated.
ActionDispatch::Reloader.to_prepare(:prepend => true){ reloader.execute }
else
ActionDispatch::Reloader.to_cleanup(&callback)
end
end
然后将其传给 ActionDispatch::Reloader.to_prepare
或 ActionDispatch::Reloader.to_cleanup
。打开其源码 actionpack/lib/action_dispatch/middleware/reloader.rb
看到 ActionDispatch::Reloader
是一个 rack middleware,在其 ActionDispatch::Reloader#call
方法里,会在每次请求前( ActionDispatch::Reloader#prepare!
)和请求后( ActionDispatch::Reloader#cleanup!
),调用到初始化步骤里的 callback,此时就移除了已经加载过的常量。这样每次请求前或者后,就会移除以及加载的常量;对代码的改动,就会立即生效。
那么,为什么在 development 环境下,会重新加载代码,在 production 下不会呢?
搜索 ActionDispatch::Reloader
关键字,可以在 railties/lib/rails/application.rb
找到
unless config.cache_classes
app = self
middleware.use ::ActionDispatch::Reloader, lambda { app.reload_dependencies? }
end
在此处看到,有个开关 config.cache_classes
。在 Rails 生成的环境配置里, config/environments/development.rb
里有 config.cache_classes = false
,config/environments/production.rb
里有 config.cache_classes = true
。所以,只有在 development 环境下才会使用 ::ActionDispatch::Reloader
。因此,production 下修改代码,不会生效,必须重启。
最终的结论就是:在 development 下 Rails 使用了 ActionDispatch::Reloader
这个 middleware,在每次请求时调用 ActiveSupport::Dependencies.clear
来移除已经加载的类和模块;这样在程序用到他们时,就会重新加载。
另外,在 production 环境下,Rails 并不是在用到这些常量时,才去通过 const_missing 的方式加载。而是在初始化的时候,用 eager_load! 的方式,把 Rails.configuration.eager_load_paths
里的文件都加载(默认是 app 的若干目录,可见 railties/lib/rails/engine/configuration.rb 里的 Rails::Engine::Configuration#paths
,这样也是为了线程安全吧)。