IoC 容器
介绍
在理解控制反转( IoC )容器的使用和好处之前,我们需要回过头来了解大型代码库所面临的依赖关系管理问题。
无用的抽象
经常会遇到这样的情况,你必须为库创建无用的抽象来管理其生命周期。
例如,为了确保数据库只连接一次,你可以将所有数据库设置代码移动到它自己的文件中(例如 lib/database.js ),然后在应用程序中导入:
const knex = require('knex')
const connection = knex({
client: 'mysql',
connection: {}
})
module.exports = connection
现在,不需要直接使用 knex ,你可以在任何需要的地方使用 lib/database.js 。
这对于单个依赖项来说没有问题,但是随着应用程序的增长,你会发现代码库中有许多这样的文件在增长,这并不理想。
依赖关系管理
大型代码库面临的最大问题之一是依赖关系的管理。
由于依赖项彼此不了解,开发人员必须以某种方式将它们连接在一起。
让我们以存储在 redis 数据库中的会话为例:
class Session {
constructor (redis) {
// needs Redis instance
}
}
class Redis {
constructor (config) {
// needs Config instance
}
}
class Config {
constructor (configDirectory) {
// needs config directory path
}
}
可以看到,会话类依赖于 Redis 类, Redis 类依赖于配置类,等等。
当使用会话类时,我们必须正确地构建它的依赖关系:
const config = new Config(configDirectory)
const redis = new Redis(config)
const session = new Session(redis)
由于依赖项列表可能会根据项目的需求而增加,你可以很快想象这个顺序实例化过程将如何开始失控!
这就是 IoC 容器发挥作用的地方,它负责为你解决依赖关系。
痛苦的测试
当不使用 IoC 容器时,你必须想出不同的方法来模拟依赖关系,或者依赖于像 sinonjs 这样的库。
使用 IoC 容器时,创建 fakes 很简单,因为所有依赖项都是从 IoC 容器解析的,而不是直接从文件系统解析的。
绑定依赖关系
假设我们想要 Redis 在 IoC 容器中绑定库,确保它知道如何编写自己。
IoC 容器其实没什么,控制模块之间的依赖关系是一个相当简单的想法,但是开辟了一个全新的世界。 第一步是创建 Redis 类,并将所有依赖项定义为构造参数:
class Redis {
constructor (Config) {
const redisConfig = Config.get('redis')
// connect to redis server
}
}
module.exports = Redis
请注意, Config 是构造函数依赖项,而不是 require 语句。
接下来,让我们将 Redis 类绑定到 IoC 容器 My/Redis :
const { ioc } = require('@adonisjs/fold')
const Redis = require('./Redis')
ioc.bind('My/Redis', function (app) {
const Config = app.use('Adonis/Src/Config')
return new Redis(Config)
})
然后我们可以像这样使用 My/Redis 绑定:
const redis = ioc.use('My/Redis')
该 ioc.bind 方法有两个参数:
绑定的名称(例如 My/Redis )
每次访问绑定时都会执行一个工厂函数,返回绑定的最终值
由于我们使用的是 IoC 容器,因此我们会提取任何现有的绑定(例如 Config )并将其传递给 Redis 类。
最后,我们返回一个新的实例Redis,已配置并可供使用。
单例模式
我们刚刚创建的绑定 My/Redis 存在一个这样的问题:
每次我们从 IoC 容器中获取它时,它返回一个新 Redis 实例,然后都会创建一个新的 Redis 服务器连接。
为了解决这个问题,IoC 容器允许你使用单例模式:
ioc.singleton('My/Redis', function (app) {
const Config = app.use('Adonis/Src/Config')
return new Redis(Config)
})
相比于 ioc.bind ,ioc.singleton 会缓存它的第一个返回值,当再次使用 My/Redis 的时候,仍将返回上一个 Redis 实例。
解决依赖关系
只需调用 ioc.use 方法并为其指定命名空间即可:
const redis = ioc.use('My/Redis')
也可以使用 use 全局方法:
const redis = use('My/Redis')
从 IoC 容器解析依赖关系时执行的步骤是:
寻找已注册的 Fake 。
接下来,找到实际的绑定。
查找别名,如果找到,重复该绑定过程。
解析为自动加载路径。
回退到 Node.js 原生的 require 方法。
别名
由于 IoC 容器绑定必须是唯一的,我们使用以下模式来绑定名称: Project/Scope/Module 。
以 Adonis/Src/Config 为例:
Adonis 是项目名称(也可能是你公司的名称)
Src 是 Scope ,因为这个绑定是核心的一部分(对于自己的包,我们使用 Addon 关键字)
Config 是实际的模块名称
由于有时很难记住并输入完整的命名空间,因此 IoC 容器允许你为它们定义别名。
别名是在 start/app.js 文件 aliases 对象中定义的。
AdonisJs 预设了一些别名,如 Route ,View ,Model 等。但是,你可以覆盖它们,如下所示。
aliases: {
MyRoute: 'Adonis/Src/Route'
}
const Route = use('MyRoute')
自动加载
你还可以定义一个由 IoC 容器自动加载的目录,而不仅仅是将依赖项绑定到 IoC 容器。
不用担心,它不会从目录中加载所有文件,而是将目录路径视为依赖项解析过程的一部分。
例如,AdonisJs 的 app 的目录定义为 App 命名空间,这意味着你可以 require app 目录中的所有文件而不需要写路径。
例如:
class FooService {
}
module.exports = FooService
可以被导入
const Foo = use('App/Services/Foo')
如果没有自动加载,就不得不像这样导入它: require('../../Services/Foo') 。
因此,将自动加载视为一种更可读,更一致的方式来处理文件。
FAQ's
- 是否必须绑定 IoC 容器内的所有内容?
不需要。当你想要将 library/module 的设置抽象为它自己的东西时,才应该使用IoC容器绑定。此外,当你想要分发依赖关系并希望它们与 AdonisJs 生态系统一起时,请考虑使用服务提供者。
- 如何模拟绑定?
因为 AdonisJs 允许你实现 fakes ,所以不需要模拟绑定。了解更多关于fakes。
- 如何将npm模块包装为服务提供者?
服务提供者是完整的指南。
← Request 生命周期 服务提供者 →