Page Contents

什么是Binding?

Binding译为“绑定”,下略。

Binding可以在一个实例化后的Context(上下文)中作为一个或多个对象的代表。一个Binding使用一个不重复的键作为地址,在Context中可以通过此地址获取到Binding所对应的值。

Binding的属性

一个Binding通常拥有以下属性:

  • key(键): 在上下文中,每个Binding都有一个用于标记自己的唯一键;
  • scope(范围): 在上下文中,用来控制如何创建或缓存Binding的值;
  • tags(标签): 可以是名称字符串或名称-值键值对,用来描述或注释一个Binding
  • value(值): 每个Binding必须设置一个可以解析出绑定的值的提供装置(比如,类、方法、常量等),这样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');
    

</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) 可以进一步简化并快速访问到contextbindingoptions等对象。

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()方法设置为keyservers.RestServer.options#apiExplorerBinding的同族。

// 创建`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);

配置BindingSocpe

Socpe译为“作用域”,下略。

一个Binding可以为多个请求提供解析值,通过Binding所在的Context对象的.get().getSync()方法,或者依赖注入。 在解析Binding的值时,Scope用来控制解析过程的策略。即可以返回一个新的值,或为从属于同一层级的Context的多个请求返回一个相同的值。
如下示例中,虽然Binding都是"my-key",但是value1变量和value2变量的值可能是相同的或不同的,这都取决于BindingScope是如何设置的。

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);

BindingScope可以通过BindingScope枚举引用到。

配置正确的Scope

在配置BindingScope时,请先思考如下问题:

  1. 在解析Binding的值时,是否有必要为每个请求都返回一个新的值?
  2. 在解析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

在上面的代码中,BindingScope的默认值为TRANSIENT,即临时作用域。 d1d2的值都是来源于new Date()方法,并且都是通过ctx.getSync('current-date')方法解析出来的。两个不同的日期被分配到了d1d2的值上,相当于每次解析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

在上面的代码中,BindingScope的值被设置为SINGLETON,即常态作用域。自然就可以推理出d1d2的值都是相同的日期,也就是不符合我们的需求。

下面是ScopeSINGLETON的适用场景:

  1. 共享单个实例的状态

    通过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`
    
  2. 共享单个实例的本体

    若单个实例可以被共享,则避免创建多个实例。因为使用者无须保持或访问不同的实例的状态。比如,下面的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的实例化对象是由拥有BindingContext对象创建的。但是SingletonGreetingCurrentUserControllergreet()方法仍然可以被携带了当前请求的用户信息的请求级别Context对象所调用。方法注入正是由不同的请求级别Context对象所实现的,不同于其他的Context对象(比如applicationContext对象,仅用于将类实例化为单例)。

SINGLETONCONTEXTTRANSIENT有一些使用上的限制。假设一个经典的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(服务器级别上下文)。在ScopeSINGLETON的情况下,我们可以从Application Context中获得MyService的相同的实例化对象。在ScopeCONTEXT的情况下,我们可以从任意Context中获得MyService的不同的实例化对象。下面是可以将Binding的解析过程变得更加准确的Scope

  • BindingScope.APPLICATION
  • BindingScope.SERVER
  • BindingScope.REQUEST

使用上面的Scope时,程序会从Context链中按照层级依次查找并匹配首个符合条件的Context。被解析的Binding的值会被缓存在同级别的Context中并被允许分享。这样就解决了上面的问题,同时确保了在同级别的Context中同一个值不会被解析多次。

// `Context`链: invocationCtx(调用级别Context) -> requestCtx(请求级别Context) -> serverCtx(服务级别Context) -> appCtx(应用程序级别Context)
appCtx
  .bind('services.MyService')
  .toClass(MyService)
   // 将`MyService`类设置为`REQUEST`
   // 并将其绑定到应用程序级别Context中
  .inScope(BindingScope.REQUEST);

如上面的代码所示,在将Scope的改为REQUEST后,在MyMiddlewareMyInterceptor中,MyService将永远被解析为相同的实例对象。

根据Binding的Key和Scope在Context等级制结构中解析Binding

Binding的解析事件通常发生在使用ctx.get()ctx.getSync()binding.getValue(ctx)等方法的时候。当一个类被实例化或类中的一个方法被调用时,Binding的解析事件也伴随着依赖注入而发生。

Context等级制结构中,解析一个Binding通常会涉及到很多的Context对象。根据不同的Context链和BindingScope的设置,解析过程所涉及到的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;
  1. 所有者级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
  2. 目击者级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
  3. 解析者级Context

    被用来在Context链中根据BindingKey来查找或解析BindingContext。那么这个Context就可以称为是这个Binding解析者级Context

    只有解析者级Context本身和其祖先Context对解析过程可见。

    确定解析者级Context通常按照如下流程:

    a. 依次在目击者级Context及其祖先Context中根据BindingKey进行查找并匹配第一个符合条件的Binding

    b. 根据已经查找到的BindingScope查找解析者级Context

    • CONTEXT / TRANSIENT目击者级Context
    • SINGLETON所有者级Context
    • APPLICATION / 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解析者级Context
    line 1 serverCtxKeyfooBinding serverCtx
    line 2 appCtxKeyfooBinding appCtx
    line 3 serverCtxKeyxyzBinding serverCtx

    对于依赖注入而言,目击者级Context将会是声明了注入的类的Binding解析者级Context。每次注入时,解析者级Context都会被查找并匹配出来。如果一个被注入的Binding对于解析者级Context是不可见的(比如,BindingKey不存在或只存在于衍生对象中),则会抛出异常。