阿多尼斯JS 折叠
在 JavaScript 中最简单、最直接的 IoC 容器
为什么这个项目存在?
许多现有的IoC容器的应用将这个概念走得太远并开始感觉更像Java. JavaScript 本质上没有所有的铃声和口哨;您需要像 PHP 或 Java 一样有类似的 IoC 容器好处.
因此,有了这个项目, 我活在JavaScript的特质之下, 却建造了一个容器, 可以帮助你创建松散的组合系统.
我已经解释了 使用IC容器的理由 在这个职位上。 最好先读一下这篇文章
说明: AdonisJS折叠由Laravel IoC容器所高度启发. 多亏了泰勒 想象了一个简单而强大的API.
项目目标
- 保持代码的视觉取悦。如果您已经使用过IoC容器的任何其他操作,您将自动找到
@adonisjs/fold易读而后行. - 很简单. JavaScript项目使用IoC容器有几个原因,所以不要建立任何人都不会使用或理解的特性.
- 为 JavaScript 构建并使用 TypeScript 改进 - 执行
@adonisjs/fold与香草JavaScript合作. 只是使用TypeScript时需要写出较少的代码. 由于它的装饰器元数据API.
使用量
从 npm 软件包注册处安装软件包 .
npm i @adonisjs/fold
完成后,您可以导入 Container 从软件包中创建一个实例。 大部分情况下,你会使用一个容器的一例.
import { Container } from '@adonisjs/fold'
const container = new Container()
上课
你可以通过调用 container.make 方法。 这种方法是同步的,因为它允许通过工厂功能来懒惰地加载依赖性(更在后来的工厂功能上).
class UserService {}
const service = await container.make(UserService)
assert(service instanceof UserService)
在前一个例子中 UserService 不存在任何依赖性;因此,集装箱可以直接加以说明.
现在,让我们看看一个例子,其中 UserService 需要数据库类实例.
class Database {}
class UserService {
static containerInjections = {
_constructor: {
dependencies: [Database],
},
}
constructor(db) {
this.db = db
}
}
const service = await container.make(UserService)
assert(service.db instanceof Database)
这个 static containerInjections 属性被容器要求知道在创建类的实例时要注入哪些值.
这种属性可以定义类方法(包括构造器)的依赖性. 依赖关系被定义为数组. 依存依存按数组内定义的相同顺序被注入.
你记得吗? 我说JavaScript没有Java或PHP那么强大. 这是典型的例子。 在其他语言中,您可以使用反射来查看类来进行注射,而在JavaScript中,您必须明确地告诉容器.
类型脚本到救援
等等,你可以使用装潢机,结合TypeScript的 发件人MetaData 选项进行反射。 您还需要安装 reflect-metadata 为了让 TypeScript 从您的类中提取元数据.
值得注意的是,TypeScript装饰器不像其他语言的反射API那么强大. 例如,在PHP中,可以使用接口进行反射. 在 TypeScript 中, 您不能 .
说到这里,让我们看看之前的例子,但这次在TypeScript中.
import { inject } from '@adonisjs/fold'
class Database {}
@inject()
class UserService {
constructor(db: Database) {
this.db = db
}
}
const service = await container.make(UserService)
assert(service.db instanceof Database)
这个 @inject 装饰者查看所有构造参数的类型,并定义 static containerInjections 幕后财产.
说明:以装饰为主的反射只能与具体值相配合,而不能与界面或类型相配合,因为在运行期间被移除.
以运行时间值制作课程
当呼叫 container.make 方法,您可以通过运行时值,这些值优先于 containerInjections 阵列.
例如 UserService 接受当前 HTTP 请求的一个实例作为第二段。 现在,当做一个实例 这个类,你可以手动通过.
import { inject } from '@adonisjs/fold'
import { Request } from '@adonisjs/core/src/Request'
class Database {}
@inject()
class UserService {
constructor(db: Database, request: Request) {
this.db = db
this.request = request
}
}
createServer((req) => {
const runtimeValues = [undefined, req]
const service = await container.make(UserService, runtimeValues)
assert(service.request === req)
})
在上述例子中:
- 容器将创建实例
Database分类,因为它被设定为undefined在运行时值阵列内. - 然而,对于第二个职位(即:
request),容器将使用req数值.
呼叫方法
也可以调用类方法自动查找/输入依赖性.
例如 UserService.find 方法需要数据库类实例。 这个 container.call 方法将查看 containerInjections 属性以查找要注入的值.
class Database {}
class UserService {
static containerInjections = {
find: {
dependencies: [Database],
},
}
async find(db) {
await db.select('*').from('users')
}
}
const service = await container.make(UserService)
await container.call(service, 'find')
TypeScript 工程可以重用相同的 @inject 装饰师.
class Database {}
class UserService {
@inject()
async find(db: Database) {
await db.select('*').from('users')
}
}
const service = await container.make(UserService)
await container.call(service, 'find')
这个 运行时间值 也得到以下支持: container.call 方法.
集装箱装订
在进行类例的同时,您还可以在容器内注册装订。 捆绑是简单的键值对.
- 钥匙可以是a
string页:1symbol或一个class constructor。 。 。 . - 当某人从容器中解析绑定时,该值是一个被引用的工厂函数.
const container = new Container()
container.bind('db', () => {
return new Database()
})
const db = await container.make('db')
assert(db instanceof Database)
以下是将类构造器与容器捆绑起来并使用工厂功能自行制造一个实例的例子.
container.bind(Database, () => {
return new Database()
})
Factory 函数参数
工厂收到以下三个论点.
- 这个
resolver参考文献。 解析器是引擎盖下用来解决依赖性的容器. 同样的例子也传到工厂,这样你就可以解决依赖性来构建阶级. - 在
container.make电话.
container.bind(Database, (resolver, runtimeValues) => {
return new Database()
})
什么时候使用工厂的功能?
我从框架创造者的角度回答这个问题。 我从来没有使用过 @inject 我班上的装饰器是用包裹运来的 相反,我用工厂的功能来定义他们的建筑逻辑,使班级对容器没有任何了解.
所以,如果你为AdonisJS创建软件包, 我强烈建议使用工厂功能。 离开 @inject 终端用户的装饰.
有条件的约束
使用 container.if 在解析时选择约束。 条件可能是同步的,也可能是同步的,每次解决了绑定时都会对其进行评价.
例如: 您正在向一小批用户推出一个新的支付网关 。 每个班级都要求 PaymentGateway 继续这样做,由集装箱决定它们得到什么执行.
container.bind(PaymentGateway, (resolver) => {
return resolver.make(StripePaymentGateway)
})
container
.if(async (resolver) => {
const ctx = await resolver.make(HttpContext)
return ctx.auth.user?.featureFlags.includes('new-payment') === true
})
.bind(PaymentGateway, (resolver) => resolver.make(BetaPaymentGateway))
您可以为同一密钥注册多个有条件的约束。 对条件按登记顺序进行评估,并采用第一次匹配。 当没有匹配时,容器会回落到常规绑定或其默认分辨率行为.
有条件单吨
建筑商还揭露了 singleton 方法。 碞钩 container.singleton,厂家函数只调用一次,其返回值被永久缓存.
每项决议都仍然评价这一状况。 只有工厂函数结果被缓存.
container
.if(() => env.get('SEARCH_DRIVER') === 'typesense')
.singleton(SearchService, (resolver) => resolver.make(TypesenseSearchService))
条件参数
一个条件接收执行解析器,运行时间值,以及请求绑定的母类.
container.if((resolver, runtimeValues, parent) => true)
这个 resolver 给予该解析器对本地值的条件访问。 本节上方的例子, HttpContext 可用,因为 AdonisJS 启动工具包将它按请求装入 ContainerBindingsMiddleware。 。 。 .
这个 parent 是装订品被注入的类别,或者 null 当直接使用 make. 使用它结合了上游和呼叫类.
container
.if((_, __, parent) => parent === CheckoutController && isBetaUser())
.bind(PaymentGateway, (resolver) => resolver.make(BetaPaymentGateway))
这个 runtimeValues 只有在直接使用 make 或者说 call. 为获得的类别解决的依赖性 undefined。 。 。 .
条件随处可见
对其具有约束力的密钥的每个解析都评价一个条件,而不只是在HTTP请求中. 同样的绑定在队列工人内部,Ace命令中也解决了,在启动时间,请求中不存在特定值.
防止他们缺席,否则,条件一旦达到从未注册的装订条件时就会出现.
container
.if(async (resolver) => {
if (!resolver.hasBinding(HttpContext)) {
return false
}
const ctx = await resolver.make(HttpContext)
return ctx.auth.user?.featureFlags.includes('new-payment') === true
})
.bind(PaymentGateway, (resolver) => resolver.make(BetaPaymentGateway))
决议令
容器按顺序尝试以下来源,然后返回第一个产生值的源.
| 顺序 | 来源 | 使用 | 适用于 |
|---|---|---|---|
| 1个 | 交换 | container.swap | 仅分类 |
| 2个 | 背景约束 | container.when().asksFor().provide() | 有父母的类 |
| 3个 | 解析器值 | resolver.bindValue | 所有绑定密钥 |
| 页:1 | 集装箱价值 | container.bindValue | 所有绑定密钥 |
| 页:1 | 有条件的约束 | container.if().bind() | 所有绑定密钥 |
| 6个 | 约束 | container.bind 编号 : container.singleton | 所有绑定密钥 |
| 第7条 | 阶级建筑 | —— 说 | 仅分类 |
两个后果值得知道。 交换会战胜有条件的束缚,所以在测试中假装一个类不管任何条件都可行. 价值观胜过有条件的约束 所以 bindValue 以同样的方式凌驾于有条件的约束之上.
有条件的约束和 hasBinding
container.hasBinding 返回时 true 用于只登记有条件约束的密钥,因为约束是真实注册的。 如果条件不匹配,它可能仍然无法解决.
container.if(() => false).bind('gateway', () => new BetaPaymentGateway())
container.hasBinding('gateway') // true
await container.make('gateway') // throws
错误点位于无法匹配的条件, 因此使用 container.bind 钥匙必须永远解决.
装订单吨
您可以使用 container.singleton 方法。 和那个一样 container.bind 方法,但厂家函数只调用一次,返回值被永久缓存.
container.singleton(Database, () => {
return new Database()
})
约束性价值
与工厂的功能并列,也可以将直接值绑定在容器上.
container.bindValue('router', router)
价值优先于工厂的功能。 因此,如果注册一个与工厂功能绑定同名的值,则该值将从容器中解决.
数值也可以在解析器级别上注册. 例如 Request 约束只针对一个孤立的解析器实例,而不针对整个容器.
const resolver = container.createResolver()
resolver.bindValue(Request, req)
await resolve.make(SomeClass)
别名
集装箱别名可以界定现有装订的别名。 其别名应为: string 或一个 symbol。 。 。 .
container.singleton(Database, () => {
return new Database()
})
container.alias('db', Database)
/**
* Make using the alias
*/
const db = await container.make('db')
assert.instanceOf(db, Database)
背景约束
背景约束允许您在特定类别上注册自定义依赖解决者,以进行特定依赖。 您将主要使用基于驱动程序执行的背景约束 .
例如: 你有一个 UserService (单位:千美元) BlogService 两人都需要一个 Drive 磁盘实例来写和读取文件 。 你想买吗 UserService 并使用本地磁盘驱动程序 BlogService 以使用 s3 磁盘驱动程序.
说明 可定义类别构造器的上下文约束,而不适用于集装箱约束
import { Disk } from '@adonisjs/core/driver'
class UserService {
constructor(disk: Disk) {}
}
import { Disk } from '@adonisjs/core/driver'
class BlogService {
constructor(disk: Disk) {}
}
现在,让我们用背景绑定 告诉容器,何时 UserService 需求 Disk 类,提供本地驱动磁盘.
container
.when(BlogService)
.asksFor(Disk)
.provide(() => drive.use('s3'))
container
.when(UserService)
.asksFor(Disk)
.provide(() => drive.use('local'))
B. 破坏执行工作
在使用容器解决依赖树时,你往往无法控制一个类的构造,因此在写作测试时无法互换/假冒其依赖.
例如 UsersController 需要实例 UserService 课.
@inject()
class UsersController {
constructor(service: UserService) {}
}
在下面的测试中,我们正在提出一个HTTP请求,将由该请求处理 UsersController然而,在测试范围内,我们无法控制控制器类的建设.
test('get all users', async ({ client }) => {
// I WANTED TO FAKE USER SERVICE FIRST?
const response = await client.get('users')
})
要使事情简单化,您可以告诉容器对一个特定的类构造器使用如下的互换执行.
test('get all users', async ({ client }) => {
class MyFakedService extends UserService {}
/**
* From now on, the container will return an instance
* of `MyFakedService`.
*/
container.swap(UserService, () => new MyFakedService())
const response = await client.get('users')
})
观察容器
你可以通过一个实例 事件编辑器 或者说 发射 ,用于在容器解决依赖性时倾听事件.
import { EventEmitter } from 'node:events'
const emitter = new EventEmitter()
emitter.on('container:resolved', ({ value, binding }) => {
// value is the resolved value
// binding name can be a mix of string, class constructor, or a symbol.
})
const container = new Container({ emitter })
集装箱钩子
如果您想要修改解析后的值, 您可以使用容器钩子, 然后再从中返回 make 方法.
- 每次从容器中解出捆绑时会叫起钩子.
- 单通装订只叫一次.
- 每次通过直接通过类构造器来构建一个类的实例时,钩子都会被呼叫.
说明: 钩子调回也可以是主动函数
container.resolving(Validator, (validator) => {
validate.rule('email', function () {})
})
集装箱供应商
集装箱提供商是静态功能,可以活在一个类别上,以解决类别构建者或特定类别方法的依赖性.
一次,你定义 containerProvider 在分类上,IoC容器将依赖它解决依赖性,而不使用默认提供者.
import { ContainerResolver } from '@adonisjs/fold'
import { ContainerProvider } from '@adonisjs/fold/types'
class UsersController {
static containerProvider: ContainerProvider = (
binding,
property,
resolver,
defaultProvider,
runtimeValues
) => {
console.log(binding === UserService)
console.log(this === UserService)
return defaultProvider(binding, property, resolver, runtimeValues)
}
}
我为什么要用定制供应商?
自定义提供者在创建一个类的实例时可能很方便,不足以正确构建该类.
举个例子 AdonisJS 路由模型绑定。随着路由模型绑定,您可以使用基于路由参数值的模型查询数据库,并在控制器内部注入模型实例.
import User from '#models/User'
import { bind } from '@adonisjs/route-model-binding'
class UsersController {
@bind()
public show(_, user: User) {}
}
现在,如果你使用 @inject 解决 User 模型,然后容器将只创建一个用户实例并还给你.
然而,在这种情况下,我们想要的不仅仅是创造一个模型的例子。 我们希望查阅数据库,并创建一个带有行值的示例.
这里 @bind 装潢师入相. 为了进行数据库查询,它将在数据库上注册一个自定义提供者 UsersController 课.
装订类型
如果您在 TypeScript 项目内使用容器, 您可以在创建容器实例时提前定义所有装订的类别 .
界定类型将确保 bind, (中文(简体) ), singleton, (中文(简体) ), bindValue 财务报告和审定财务报表 if().bind() 方法只接受已知的约束,并申明其类型.
class Route {}
class Database {}
type ContainerBindings = {
route: Route
db: Database
}
const container = new Container<ContainerBindings>()
// Fully typed
container.bind('route', () => new Route())
container.bind('db', () => new Database())
// Fully typed - db: Database
const db = await container.make('db')
常见错误
无法在"[类:xxxxx]"中注入"xxxx". 无法构建值
当您试图注入一个无法构建的值时, 会出现错误 。 常见的问题来源在 TypeScript 项目内, 当使用 interface 或一个 type 用于依赖性注射.
例如 User 是 TypeScript 类型,而容器无法从该类型中构造出运行时间值(类型在转录 TypeScript 代码后被删除).
因此,集装箱将提出例外 Cannot inject "[Function: Object]" in "[class: UsersController]". The value cannot be constructed。 。 。 .
type User = {
username: string
age: number
email: string
}
@inject()
class UsersController {
constructor(user: User)
}
模块表达式
在"AdonisJS"中,我们允许以以以字符串为主的模块表达形式来约束方法. 例如:
而不是进口和使用下列控制器:
import UsersController from '#controllers/users'
Route.get('users', (ctx) => {
return new UsersController().index(ctx)
})
您可以将控制器绑定如下:
Route.get('users', '#controllers/users.index')
我们为什么要这么做? 使用模块表达式有几个原因.
- 性能:懒惰的加载控制器确保我们保持应用程序启动过程的快速. 否则,我们将将所有控制器及其进口物拉入一个单一路径文件,这将影响应用程序的启动时间.
- Visual cluter: 想象在单一路由文件(或可能是2-3个不同的路由文件)内导入所有控制器,然后手动即时操作. 这在您的密码库中必然带来一些视觉杂乱( 相似的视觉杂乱是主观的) .
所以,如果我们使用 模块表达式 广泛分布于阿多尼斯JS生态系统. 我们抽象了将基于字符串的表达式解析成专门帮助者以重新使用和放松的逻辑.
假设
有一种强烈的假设,即每个使用模块表达的模块引用都会有一个 export default 输出一个类.
// Valid module for module expression
export default class UsersController {}
电话
这个 toCallable 方法返回一个在内部解析模块字符串表达式的函数,并返回一个像其他JavaScript函数一样可以引用的函数.
import { moduleExpression } from '@adonisjs/fold'
const fn = moduleExpression('#controllers/users.index', import.meta.url).toCallable()
// Later call it
const container = new Container()
const resolver = container.createResolver()
await fn(resolver, [ctx])
您也可以在创建可调用函数时通过容器实例.
const container = new Container()
const fn = moduleExpression('#controllers/users.index', import.meta.url).toCallable(container)
// Later call it
await fn([ctx])
手提电脑
这个 toHandleMethod 方法返回带有 handle 方法。 以主要区别 toCallable 财务报告和审定财务报表 toHandleMethod 返回输出
toHandleMethod返回时{ handle: fn }toCallable返回时fn
import { moduleExpression } from '@adonisjs/fold'
const handler = moduleExpression('#controllers/users.index', import.meta.url).toHandleMethod()
// Later call it
const container = new Container()
const resolver = container.createResolver()
await handler.handle(resolver, [ctx])
在创建手柄方法时也可以通过容器实例.
const container = new Container()
const handler = moduleExpression('#controllers/users.index', import.meta.url).toHandleMethod(
container
)
// Later call it
await handler.handle([ctx])
基准
以下是使用模块表达式时发生的性能损失的基准.
在Apple M1 iMac, 16GB上执行基准

handler: 在输出时调用控件方法toHandleMethod。 。 。 .callable: 调用函数toCallable方法.native:使用动态导入来懒地导入模块. 此变量不使用此软件包中的任何帮助 .inline: 当没有进行懒惰装载时. 该模块正在使用import关键词.
模块导入器
模块导入器类似于模块表达式. 然而,与其将导入路径定义为字符串,不如定义一个导入模块的函数.
import { moduleImporter } from '@adonisjs/fold'
const fn = moduleImporter(
() => import('#middleware/auth')
'handle'
).toCallable()
// Later call it
const container = new Container()
const resolver = container.createResolver()
await fn(resolver, [ctx])
创建处理方法对象
import { moduleImporter } from '@adonisjs/fold'
const handler = moduleImporter(
() => import('#middleware/auth')
'handle'
).toHandleMethod()
// Later call it
const container = new Container()
const resolver = container.createResolver()
await handler.handle(resolver, [ctx])
模块调用器
模块调用器类似于模块导入器. 然而,你不是懒惰地装入一个类,而是将类构造器传入这种方法.
import { moduleCaller } from '@adonisjs/fold'
class AuthMiddleware {
handle() {}
}
const fn = moduleCaller(AuthMiddleware, 'handle').toCallable()
// Later call it
const container = new Container()
const resolver = container.createResolver()
await fn(resolver, [ctx])
创建处理方法对象
import { moduleCaller } from '@adonisjs/fold'
class AuthMiddleware {
handle() {}
}
const handler = moduleImporter(AuthMiddleware, 'handle').toHandleMethod()
// Later call it
const container = new Container()
const resolver = container.createResolver()
await handler.handle(resolver, [ctx])
解析边框 参考文献
解析一个具有约束力的引用,并返回一个人类可读的名称。 具有约束力的提法可以是 魔法字符串, (中文(简体) ), 类参考,或一个 懒惰导入。AdonisJS在列出路线及其处理器时使用这种方法.
await parseBindingReference('#controllers/users_controller')
// { moduleNameOrPath: '#controllers/users_controller', method: 'handle' }
const UsersController = () => import('#controllers/users_controller')
await parseBindingReference([UsersController, 'index'])
// { moduleNameOrPath: '#controllers/users_controller', method: 'index' }
import UsersController from '#controllers/users_controller'
await parseBindingReference([UsersController, 'index'])
// { moduleNameOrPath: 'UsersController', method: 'index' }