什么是Binding?
Binding译为“绑定”,下略。
Binding可以在一个实例化后的Context(上下文)中作为一个或多个对象的代表。一个Binding使用一个不重复的键作为地址,在Context中可以通过此地址获取到Binding所对应的值。
Binding的属性
一个Binding通常拥有以下属性:
- key(键): 在上下文中,每个
Binding都有一个用于标记自己的唯一键; - scope(范围): 在上下文中,用来控制如何创建或缓存
Binding的值; - tags(标签): 可以是
名称字符串或名称-值键值对,用来描述或注释一个Binding; - value(值): 每个
Binding必须设置一个可以解析出绑定的值的提供装置(比如,类、方法、常量等),这样Binding才可以被解析成一个常量或动态值;

如何创建一个Binding?
有几种方式可以创建一个Binding:
-
使用
Binding类的构造器import {Context, Binding} from '@loopback/core'; // 实例化一个上下文 const context = new Context(); // 实例化一个绑定,并将其'key'属性设置为'my-key' const binding = new Binding('my-key'); // 将绑定添加至上下文中 ctx.add(binding); -
使用
Binding类的.bind()静态方法import {Context, Binding} from '@loopback/core'; // 实例化一个上下文 const context = new Context(); // 实例化一个绑定,并将其'key'属性设置为'my-key' const binding = Binding.bind('my-key'); // 将绑定添加至上下文中 ctx.add(binding); -
使用
Context类的.bind()方法import {Context, Binding} from '@loopback/core'; // 实例化一个上下文 const context = new Context(); // 在上下文中添加一个绑定,并将其'key'属性设置为'my-key' context.bind('my-key');Note:
@loopback/core包会重复导出所有关于@loopback/context的公共API。为了保持写法上的一致,我们推荐使用@loopback/core包进行各种对象的引用,除非某个对象需要通过@loopback/context包显式引用. 下面的2行代码在作用上是相等的:import {inject} from '@loopback/context'; import {inject} from '@loopback/core';
</div>
如何设置一个Binding?
Binding类通过一套流畅的API提供了有关Binding的创建和配置的操作。
解析一个Binding的值的形式
Binding可以通过多种形式以解析出一个具体的值。具体如下:
常量形式(Constant)
适用场景:在解析一个Binding时,Binding的值是一个固定的值。
比如,Binding的值是一个字符串(String)、一个方法(Function)、一个对象(Object)、一个数组(Array)或者任何其他类型的值。
binding.to('my-value');
请注意,在常量形式下为了避免混淆,值的类型不能是Promise。
工厂形式(Factory Function)
适用场景:在解析一个Binding时,Binding的值是需要被动态计算的。
比如,Binding的值是当前系统时间、Binding的值是远程接口的返回值、Binding的值是远程数据库的数据等。
binding.toDynamicValue(() => 'my-value');
binding.toDynamicValue(() => new Date());
binding.toDynamicValue(() => Promise.resolve('my-value'));
工厂方法可以接收一个涵盖了上下文信息、绑定信息和解析选项信息的参数。
import {ValueFactory} from '@loopback/core';
// 现在可以通过工厂方法的第一个传参获取到解析时的相关信息了
const factory: ValueFactory<string> = resolutionCtx => {
return `Hello, ${resolutionCtx.context.name}#${
resolutionCtx.binding.key
} ${resolutionCtx.options.session?.getBindingPath()}`;
};
const b = ctx.bind('msg').toDynamicValue(factory);
通过 解构赋值(Destructuring assignment)
可以进一步简化并快速访问到context、binding和options等对象。
const factory: ValueFactory<string> = ({context, binding, options}) => {
return `Hello, ${context.name}#${
binding.key
} ${options.session?.getBindingPath()}`;
};
进阶用法:使用内置了静态方法value的类,该静态方法允许参数注入。(具体见提供器形式(Provider))
import {inject} from '@loopback/core';
class GreetingProvider {
static value(@inject('user') user: string) {
return `Hello, ${user}`;
}
}
const b = ctx.bind('msg').toDynamicValue(GreetingProvider);
类形式(Class)
适用场景:在解析一个Binding时,Binding的值是一个类的实例化对象。
比如,控制器(Controller)等。Binding的值可以是一个被实例化的类。依赖注入通常用于影响被注入依赖的类的内部成员对象身上。
class MyController {
constructor(@inject('my-options') private options: MyOptions) {
// ...
}
}
binding.toClass(MyController);
提供器形式(Provider)
适用场景:在解析一个Binding时,用于解析Binding的值的工厂方法需要用到依赖注入(Dependency Injection)。
提供器指的是,内置了value()方法的类,该方法可以在实例化后用来解析Binding的值。
class MyValueProvider implements Provider<string> {
constructor(@inject('my-options') private options: MyOptions) {
// ...
}
value() {
return this.options.defaultValue;
}
}
binding.toProvider(MyValueProvider);
提供器被当做一个内置了依赖注入功能的空壳,可以理解为是一个进阶版本的工厂方法。如果工厂方法没有使用到依赖注入,则直接使用普通的工厂方法和toDynamicValue()方法即可。
同族形式(Alias)
适用场景:在解析一个Binding时,此Binding的值的来源是另外一个Binding的值。
同族指的是,一个允许携带可选路径的键值,这个键值可以用来从另外一个Binding上解析出值。
比如,我们在设置Api Explorer(API浏览页面)的时候,需要用到RestServer对象的相关属性,我们可以创建一个Binding并将它通过.toAlias()方法设置为key为servers.RestServer.options#apiExplorer的Binding的同族。
// 创建`key`为`servers.RestServer.options`的绑定
ctx.bind('servers.RestServer.options').to({apiExplorer: {path: '/explorer'}});
// 创建`key`为`apiExplorer.options`的绑定
ctx
.bind('apiExplorer.options')
// 声明此绑定是`key`为`servers.RestServer.options`的绑定的同族
.toAlias('servers.RestServer.options#apiExplorer');
const apiExplorerOptions = await ctx.get('apiExplorer.options'); // => {path: '/explorer'}
可注入类形式(Injectable Class)
可注入类指的是一个被@injectable装饰的类,其类型可以如下:
- 一个类(Class)
- 一个提供器(Provider)
- 一个工厂方法(Factory Function)
toInjectable()方法是一种类似于toClass() / toProvider() / toDynamicValue()等方法的用来绑定值的快捷方法。@injectable装饰器可以将Binding的元数据添加到其中。
@injectable({scope: BindingScope.SINGLETON})
class MyController {
constructor(@inject('my-options') private options: MyOptions) {
// ...
}
}
binding.toInjectable(MyController);
如上所示,其代码等价于下面:
const binding = createBindingFromClass(MyController);
Note:
如果使用了binding.toClass(MyController)方法,则通过@injectable装饰器设置的Binding的Scope是不会生效的。
配置Binding的Socpe
Socpe译为“作用域”,下略。
一个Binding可以为多个请求提供解析值,通过Binding所在的Context对象的.get()和.getSync()方法,或者依赖注入。
在解析Binding的值时,Scope用来控制解析过程的策略。即可以返回一个新的值,或为从属于同一层级的Context的多个请求返回一个相同的值。
如下示例中,虽然Binding都是"my-key",但是value1变量和value2变量的值可能是相同的或不同的,这都取决于Binding的Scope是如何设置的。
const value1 = await ctx.get('my-key');
const value2 = ctx.getSync('my-key');
在相同的Context中解析一个Binding时,我们允许使用如下的Scope:
BindingScope.TRANSIENT(临时作用域,此项为默认值)BindingScope.CONTEXT(同级作用域)(已弃用)BindingScope.SINGLETON(常态作用域)BindingScope.APPLICATION(应用程序作用域)BindingScope.SERVER(服务作用域)BindingScope.REQUEST(请求作用域)
请参见BindingScope.
binding.inScope(BindingScope.SINGLETON);
Binding的Scope可以通过BindingScope枚举引用到。
配置正确的Scope
在配置Binding的Scope时,请先思考如下问题:
- 在解析
Binding的值时,是否有必要为每个请求都返回一个新的值? - 在解析
Binding的值时,是否解析值会得到保留,或是因请求不同而变化?
请注意,使用了Binding类.to()方法的Binding的值不会受到Scope的影响,如下所示:
ctx.bind('my-name').to('John Smith');
key属性为'my-name'的Binding的值将被永远解析为'John Smith'。
Scope只会影响到使用.toDynamicValue()、.toClass()和.toProvider()方法提供解析值的Binding。
假设我们需要满足一个需求:需要创建一个可以提供当前系统日期的Binding。
ctx
.bind('current-date')
.toDynamicValue(() => new Date());
const d1 = ctx.getSync('current-date');
const d2 = ctx.getSync('current-date');
// d1 !== d2
在上面的代码中,Binding的Scope的默认值为TRANSIENT,即临时作用域。
d1和d2的值都是来源于new Date()方法,并且都是通过ctx.getSync('current-date')方法解析出来的。两个不同的日期被分配到了d1和d2的值上,相当于每次解析Binding的值时,值都会成为解析时的系统日期。
ctx
.bind('current-date')
.toDynamicValue(() => new Date())
.inScope(BindingScope.SINGLETON);
const d1 = ctx.getSync<Date>('current-date');
const d2 = ctx.getSync<Date>('current-date');
// d1 === d2
在上面的代码中,Binding的Scope的值被设置为SINGLETON,即常态作用域。自然就可以推理出d1和d2的值都是相同的日期,也就是不符合我们的需求。
下面是Scope为SINGLETON的适用场景:
-
共享单个实例的状态
通过
Binding在多个使用者之间分享同一个实例的状态。// 声明一个全局计数器 export class GlobalCounter { public count = 0; } // 将全局计数器绑定至上下文中 ctx .bind('global-counter') .toClass(GlobalCounter) .inScope(BindingScope.SINGLETON); const c1: GlobalCounter = await ctx.get('global-counter'); c1.count++; console.log(c1.count); // `c1.count`的值是`1` const c2: GlobalCounter = await ctx.get('global-counter'); console.log(c2.count); // `c2.count`的值还是`1` -
共享单个实例的本体
若单个实例可以被共享,则避免创建多个实例。因为使用者无须保持或访问不同的实例的状态。比如,下面的
GreetingController类中,除了方法参数外,没有访问任何信息。通过一个GreetingController类的共享实例,可以重复调用其greet()方法,但是传参却可以是不同的(调用形式相当于c1.greet('John')或c1.greet('Jane'))。// 标记此类的`Scope`为`SINGLETON` @bind({scope: BindingScope.SINGLETON}) export class GreetingController { greet(name: string) { return `Hello, ${name}`; } }类似于
GreetingController类的控制器是使用SINGLETON的理想场景。因此在应用程序的上下文中,只有一个控制器的实例被创建,但是却可以分享给所有的请求。通过将Scope的值设置为SINGLETON,可以有效避免为每个请求重复创建GreetingController类的实例。// `createBindingFromClass()`方法听从`@bind`装饰器的配置,并将绑定的`Scope`设置为`SINGLETON` const binding = ctx.add(createBindingFromClass(GreetingController)); const c1 = ctx.getSync(binding.key); const c2 = ctx.getSync(binding.key); // c2和c1是同一个实例 c1.greet('John'); // 调用后得到'Hello, John' c2.greet('Jane'); // 调用后得到'Hello, Jane'
选择TRANSIENT:如果您不知道应该如何选择,则将其视为一个安全的默认值。
选择SINGLETON:如果您想为每个消费者不间断地提供一个相同的实例。
下面是一个需要从当前的请求对象中获取信息(用于获取http地址或记录日志等)的代码示例:
export class GreetingCurrentUserController {
@inject(SecurityBindings.USER)
private currentUserProfile: UserProfile;
greet() {
return `Hello, ${this.currentUserProfile.name}`;
}
}
如上所示,GreetingCurrentUserController内部的greet()方法的返回值取决于被注入的currentUserProfile属性对象。
因此在上面的例子中,我们需要将Scope设置为TRANSIENT。每次请求发生时,当前请求的用户信息UserProfile通过属性注入方式赋值于GreetingCurrentUserController的实例化对象的currentUserProfile属性之中。如果错误地将Scope设置为SINGLETON,则不同的请求发生时,用户信息可能是一样的。
export class SingletonGreetingCurrentUserController {
greet(@inject(SecurityBindings.USER) currentUserProfile: UserProfile) {
return `Hello, ${this.currentUserProfile.name}`;
}
}
如上所示,SingletonGreetingCurrentUserController内部的currentUserProfile属性对象被删除了,取而代之的是greet()方法中的@inject(SecurityBindings.USER) currentUserProfile: UserProfile方法注入。
因此在上面的例子中,我们需要将Scope设置为SINGLETON。每次请求发生时,当前请求的用户信息UserProfile通过方法注入方式赋值于SingletonGreetingCurrentUserController的实例化对象的greet()方法的第一个参数currentUserProfile之中。如果错误地将Scope设置为TRANSIENT,则不同的请求发生时,每次都会进行无意义的实例化行为。
ctx
.bind('controllers.SingletonGreetingCurrentUserController')
.toClass(SingletonGreetingCurrentUserController)
.inScope(BindingScope.SINGLETON);
如上所示,SingletonGreetingCurrentUserController的实例化对象是由拥有Binding的Context对象创建的。但是SingletonGreetingCurrentUserController的greet()方法仍然可以被携带了当前请求的用户信息的请求级别Context对象所调用。方法注入正是由不同的请求级别Context对象所实现的,不同于其他的Context对象(比如application的Context对象,仅用于将类实例化为单例)。
Note:
为了理解 @bind() 和 ctx.bind()之间的区别, 详见
Configure binding attributes for a class.
SINGLETON、CONTEXT和TRANSIENT有一些使用上的限制。假设一个经典的REST应用程序中拥有如下等级制度的Context:
// `Context`链: invocationCtx(调用级别Context) -> requestCtx(请求级别Context) -> serverCtx(服务级别Context) -> appCtx(应用程序级别Context)
appCtx
.bind('services.MyService')
.toClass(MyService)
// 将`MyService`类设置为`TRANSIENT`
// 并将其绑定到应用程序级别Context中
.inScope(BindingScope.TRANSIENT);
我们将Controllers(控制器)对象和Services(服务)对象的Scope设置为TRANSIENT,以便于在每次请求产生时都可以获得到一个新的实例化对象。但是,如果Controllers对象和Services对象是由Invocation Context(调用级别上下文)解析的(比如,Interceptors拦截器),那么一个新的实例化对象会被重复创建。
// 在中间件中
const serviceInst1 = requestCtx.get<MyService>('services.MyService');
// 在拦截器中
const serviceInst2 = invocationCtx.get<MyService>('services.MyService');
// 注意:serviceInst2 是一个新的实例化对象,并且不同于 serviceInst1
类似的事情可能还会发生在依赖注入中:
class MyMiddlewareProvider implements Provider<Middleware> {
// 依赖注入,并用于此中间件中
constructor(@inject('services.MyService') private myService) {}
}
class MyInterceptor {
// 依赖注入,并用于此拦截器中
constructor(@inject('services.MyService') private myService) {}
}
// 注意: 在同一个请求中,中间件和拦截器会收到两个不同的'MyService'的实例化对象
理想状态下,我们应该在requestCtx,即Request Context(请求级别上下文)的子节点中获得相同的MyService的实例化对象。但是实际情况可能会更糟糕,在requestCtx中解析一个Binding两次甚至会得到两个不同的MyService的实例化对象。
无论是SINGLETON还是CONTEXT都无法解决上面的问题。通常情况下,Controllers对象和Services对象是由Application Context(应用程序级别上下文)查询并解析的。继承于Component的组件类(比如,RestComponent组件)也会向Application Context贡献其自身的Binding,而不是向Server Context(服务器级别上下文)。在Scope为SINGLETON的情况下,我们可以从Application Context中获得MyService的相同的实例化对象。在Scope为CONTEXT的情况下,我们可以从任意Context中获得MyService的不同的实例化对象。下面是可以将Binding的解析过程变得更加准确的Scope:
BindingScope.APPLICATIONBindingScope.SERVERBindingScope.REQUEST
使用上面的Scope时,程序会从Context链中按照层级依次查找并匹配首个符合条件的Context。被解析的Binding的值会被缓存在同级别的Context中并被允许分享。这样就解决了上面的问题,同时确保了在同级别的Context中同一个值不会被解析多次。
Note:
在某些特殊场景中(比如,测试),在Context链中不会存在Scope为REQUEST的Context,解析过程会失败并转而在当前的Context中进行查找。这样方便于将REQUEST用于控制器或其他组件中。
// `Context`链: invocationCtx(调用级别Context) -> requestCtx(请求级别Context) -> serverCtx(服务级别Context) -> appCtx(应用程序级别Context)
appCtx
.bind('services.MyService')
.toClass(MyService)
// 将`MyService`类设置为`REQUEST`
// 并将其绑定到应用程序级别Context中
.inScope(BindingScope.REQUEST);
如上面的代码所示,在将Scope的改为REQUEST后,在MyMiddleware和MyInterceptor中,MyService将永远被解析为相同的实例对象。
根据Binding的Key和Scope在Context等级制结构中解析Binding
Binding的解析事件通常发生在使用ctx.get()、ctx.getSync()或binding.getValue(ctx)等方法的时候。当一个类被实例化或类中的一个方法被调用时,Binding的解析事件也伴随着依赖注入而发生。
在Context等级制结构中,解析一个Binding通常会涉及到很多的Context对象。根据不同的Context链和Binding的Scope的设置,解析过程所涉及到的Context对象也可能是不同的或相同的。
假设我们现在有如下的Context链:
import {Context} from '@loopback/core';
const appCtx = new Context('application');
appCtx.scope = BindingScope.APPLICATION;
const serverCtx = new Context(appCtx, 'server');
serverCtx.scope = BindingScope.SERVER;
const reqCtx = new Context(serverCtx, 'request');
reqCtx.scope = BindingScope.REQUEST;
-
所有者级Context
Binding直接通过Context.bind()或Context.add()方法添加到Context对象中。那么这个Context就可以称为是这个Binding的所有者级Context。现在让我们在
Context链中添加一些Binding:appCtx.bind('foo.app').to('app.bar'); serverCtx.bind('foo.server').to('server.bar');如上所示,
Binding的所有者级Context分别是:- ‘foo.app’: appCtx
- ‘foo.server’: serverCtx
-
目击者级Context
在
Context上直接显式地发起针对Binding的解析行为,或隐式地使用了依赖注入的Context。那么这个Context就可以称为是这个Binding的目击者级Context。const val1 = await reqCtx.get('foo.app'); const val2 = await reqCtx.get('foo.server');如上所示,
Binding的目击者级Context分别是:- ‘foo.app’: reqCtx
- ‘foo.server’: reqCtx
-
解析者级Context
被用来在
Context链中根据Binding的Key来查找或解析Binding的Context。那么这个Context就可以称为是这个Binding的解析者级Context。只有
解析者级Context本身和其祖先Context对解析过程可见。确定
解析者级Context通常按照如下流程:a. 依次在
目击者级Context及其祖先Context中根据Binding的Key进行查找并匹配第一个符合条件的Bindingb. 根据已经查找到的
Binding的Scope查找解析者级Context:CONTEXT / TRANSIENT:目击者级ContextSINGLETON:所有者级ContextAPPLICATION / SERVER / REQUEST: 依次在目击者级Context及其祖先Context中进行查找并匹配第一个符合条件的Context
import {generateUniqueId} from '@loopback/core'; appCtx.bind('foo').to('app.bar'); serverCtx .bind('foo') .toDynamicValue(() => `foo.server.${generateUniqueId()}`) .inScope(BindingScope.SERVER); serverCtx .bind('xyz') .toDynamicValue(() => `abc.server.${generateUniqueId()}`) .inScope(BindingScope.SINGLETON); // line 1 const val = await reqCtx.get('foo'); // line 2 const appVal = await appCtx.get('foo'); // line 3 const xyz = await reqCtx.get('xyz');如上所示:
代码行 解析出的 Binding的信息解析出的 Binding的解析者级Contextline 1 在 serverCtx中Key为foo的BindingserverCtxline 2 在 appCtx中Key为foo的BindingappCtxline 3 在 serverCtx中Key为xyz的BindingserverCtx对于依赖注入而言,
目击者级Context将会是声明了注入的类的Binding的解析者级Context。每次注入时,解析者级Context都会被查找并匹配出来。如果一个被注入的Binding对于解析者级Context是不可见的(比如,Binding的Key不存在或只存在于衍生对象中),则会抛出异常。
Note:
如果所有者级Context和解析者级Context是同一个对象,则Scope为APPLICATION / SERVER / REQUEST时,就等价于SINGLETON。如下所示,两个Binding实际上是一样的。
let count = 0;
appCtx
.bind('app.counter')
.toDynamicValue(() => count++)
.inScope(BindingScope.APPLICATION);
let count = 0;
appCtx
.bind('app.counter')
.toDynamicValue(() => count++)
.inScope(BindingScope.SINGLETON);