Ember 中的依赖注入

先设想这样一个场景:你正在写一个 Ember 的通用模块,它是一个 singleton 的模块,你觉得它不属于 MVC 的任何一层,但你想要在任意一层中都可以自由地获取并调用它。

注:这篇文章使用的 Ember 版本是 1.0 。

非依赖注入的做法

一个最典型的应用场景就是用户登录验证。也许你需要一个模块(暂叫 Session)来管理 sign in ,sign out 方法,记录当前用户登录状态。它是一个 singieton 的模块。我们首先可以这样做:

注:演示代码全部使用 coffeescript 。

# 假设全局命名空间叫 App
App = Em.Application.create()

# 定义 Session
App.Session = Em.Object.extend
  userId: null
  signIn: (data) -> # some code
  signOut: -> # some code

然后初始化一个实例,为了方便引用,把它放到 App 下面去。

# 初始化
App.session = App.Session.create()

这样我们就可以在任意地方使用 App.session 访问这个模块了,比如 Route 和 Controller 中:

# Route
App.IndexRoute = Em.Route.extend
  actions:
    signOut: ->
      App.session.signOut()
      
# Controller
App.IndexController = Em.Controller.extend
  actions:
    signIn: ->
      data = getProperties('username', 'password')
      App.session.signIn(data)

或者 template :

{{#if App.session.userId}}
  <div>You're logged in</div>
{{else}}
  <a>Login</a>
{{/if}}

除了需要自己管理 Session 初始化,和到处使用 App.session 比较丑之外,没别的缺点了。如果你满足了,请关掉这个页面,出门左拐吧……顺便把门带上……

对 Ember 而言,平时我们写代码只用定义模块就行了,Ember 会接管模块的初始化和销毁。所以上面的写法比较不 Ember way。我们不关心 Session 实例是存在 App.session 中还是 App.mySession 中,我只需要一个方法能够获取 Session 实例就行。这就是依赖注入做的事情。

依赖注入的做法

先看看依赖注入怎么代替 Session 实例化:

App.register 'session:main', App.Session, singleton: true
App.inject 'controller', 'session', 'session:main'
App.inject 'route', 'session', 'session:main'

在上面的代码中,我们用 register 方法把 App.Session 类注册为 session:main,你可以理解成一个名字,这个名字会在 inject 中使用。
然后我们用 inject 方法,把 session:main 注入成为所有 controller 和 route 的 session 属性。

然后我们就可以用 get 方法来读取 session 属性了。

App.IndexRoute = Em.Route.extend
  actions:
    signOut: ->
      @get('session').signOut()

Ember 的依赖注入大致就是这个思路,用 register 和 inject 注册模块,然后在被注入的模块中使用 get 方法访问。Ember Data 中的 store 就是用这样的方式注入到 route 和 controller 中的。

使用依赖注入还有个好处,只有被注入的模块才能访问 Session 实例,上面的例子就是所有 route 和 controller 。如果你想更严格的限制范围,比如只对 IndexRoute 注入,可以这样写:

App.inject 'route:index', 'session', 'session:main'

如果需要在 console 中调试的话,你可以用下面的方法获取 Session 实例:

App.__container__.lookup('session:main')

虽然 Ember 的文档并没有提及,但我想 Ember 内部已经使用依赖注入把我们定义的模块都 register 了。所以你也可以在 console 中访问 IndexRoute 的实例:

App.__container__.lookup('route:index')

依赖注入在 Ember 的官方文档中没有提及多少,只有 Ember 1.0 RC 发布时提了一下,然后就是 register 和 inject 两个方法的 API 说明。缺乏一个系统的用法,也不知道以后会不会改。所以这方面我就不详细写了。

一些其他依赖注入的写法

事先说明,以下方法我都不推荐。一是因为本质上它们还是使用的 App.register 和 App.inject 。但是更麻烦。二是有些写法已经过时了,但网上查资料还是能看到一堆。所以还是拿出来讲一讲,避免大家使用错的方式。

我查资料时,发现还有两种依赖注入的实现方式,一种是使用 ready 回调:

App = Em.Application.create
  ready: ->
    @register ...
    @inject ...

它是使用 this.register 来代替 App.register 。如果你调试一下,就会发现这里的 this 就是指向 App 。所以它们算是同一种写法。但我还是推荐 App.register 。因为 ready 回调好像只能在 App 初始化时写,这就限制了 registerinject 的使用范围。因为大部分情况是,我们因为要写自定义模块,所以才使用依赖注入的。

还有一种是使用 initializer 配合 containerregistertypeInjection

Em.Application.initializer
  name: 'session'
  initialize: (container, application) ->
    # 不推荐使用 typeInjection
    container.typeInjection('controller', 'session', 'session:main')
    
    # 推荐写法,这里的 application 就是你初始化的 App,而 container 就是 App.__container__
    application.inject('controller', 'session', 'session:main')

initializer 我了解不多,Ember 官方文档对这个也是一带而过。有兴趣的可以看看 Ember Application API。或者直接看源码。我不用是因为觉得对单纯使用 registerinject 来说,多套了一层有点麻烦。如果对 initializer 有了解的可以告诉我,谢谢!

typeInjection 这个方法在现在的版本中还能工作,但我非常不推荐使用 typeInjection。一是因为官方文档查不到说明,二是 App.inject 方法本身就包含了 typeInjection 的功能。
如果你读读 App.inject 的源码,会发现它内部就调用了 __container__.injection ,而 __container__.injection 内部会分情况调用 typeInjection (看名称中有没有 :)。

总结

学习 Ember 要注意两点,一是找文章一定要看日期,二是别人说的解决方案,自己一定要去试试。因为 Ember 更新太快,尤其是 1.0 正式版刚发布不久,以前的 RC 几个版本都有不小改动。而目前网络上还遗留着不少老文章。有些方法已经不适用了,但因为可以用,所以大家都还在用。总之,一切以官方文档为主,尤其是写了 API 说明的,基本改的可能性就很小了。

参考资料

Ember 1.0 RC
How and when to use Ember.Application register and inject methods?
Communication Between Controllers in Ember.js
Using Rails & Devise with Ember.js