- 后端
- 微服务
【免费下载链接】loopback-next
LoopBack makes it easy to build modern API applications that require complex integrations.
导读
Context是 LoopBack 4(即loopback-next仓库中的下一代框架)的核心基础设施:它既是应用全局状态的注册中心,也是一个完整的 IoC(Inversion of Control,控制反转)容器,负责管理应用中的配置、状态、依赖与类实例。本文以官方文档 Context.md 为主体骨架,结合@loopback/context包源码,系统讲解 Context 的三级层次结构(应用级 / Server 级 / 请求级)、绑定与取值 API、@inject依赖注入、上下文事件与异步观察者(ContextObserver)、动态扩展跟踪(ContextView),以及"约定优于配置"的@config.*配置注入体系。读完本文,你将掌握如何在 LoopBack 4 中创建与组织 Context 层级、绑定/解绑任意资源、通过装饰器实现依赖注入与动态配置,从而构建可维护、可扩展的 REST 应用。
什么是 Context?
根据官方文档 Context.md,Context 在 LoopBack 4 中承担四个核心角色:
- 应用全部状态与依赖的抽象:所有配置、状态、依赖、类实例都收拢在 Context 中;
- 全局注册中心:应用中"任何东西/所有东西"(配置、状态、依赖、类等)的统一存储;
- IoC 容器:一个用于向代码中注入依赖的 控制反转 容器;
- 统一管理手段:LoopBack 借此自动"管理"资源,并让你随时获得最新、实时的应用与请求状态。
// 来自 docs/site/Context.md 的示意图说明 import {Context} from '@loopback/core'; const rootCtx = new Context('root-ctx'); // 无父级 const serverCtx = new Context(rootCtx, 'server-ctx'); // rootCtx 为父级 const reqCtx = new Context(serverCtx); // 无显式名称,自动生成唯一 id在源码层面,Context类继承自 Node.js 的EventEmitter,内部以Map<string, Binding>作为注册表,并维护一个父级引用与标签索引器:
- 类定义与构造函数见 packages/context/src/context.ts;
- 当名称未提供时,
generateName()会调用generateUniqueId()生成唯一标识(见 context.ts); - 构造函数默认
setMaxListeners(Infinity),以避免子 Context 增多时触发 Node.js 的 "memory leak" 监听器警告(见 context.ts)。
为什么它如此重要?
- 集中式/全局内存存储:通过 Context,你可以给 LoopBack 提供更多"信息",让应用中的其他依赖随时检索到它;
- 自动化的资源管理:通过 Dependency injection 与装饰器,LoopBack 能自动管理资源生命周期;
- 实时状态访问:你可以在任何时刻访问到最新、更新过的应用状态与请求状态。
包导出约定:@loopback/core 与 @loopback/context
官方文档特别强调:@loopback/core会重新导出@loopback/context的全部公开 API。为保持一致,LoopBack 模块与应用中推荐统一从@loopback/core导入。下面两种写法完全等价:
import {inject} from '@loopback/context'; import {inject} from '@loopback/core'; // 推荐LoopBack 的 Context 体系允许无限数量的 Context 实例存在,每个实例都可以有一个父 Context。一个典型的应用通常拥有三个"层级"的 Context:应用级(application-level)、Server 级(server-level)与请求级(request-level)。
应用级 Context(全局)
应用级 Context 存储应用在进程存活期间的全部初始与变更状态,一般在应用创建时配置(运行时也可修改)。
import {Application} from '@loopback/core'; // 请注意:Application 继承自 Context const app = new Application(); // `app` 本身就是一个 "Context" class MyController {} app.controller(MyController);上面的例子中,app.controller(...)将MyController注册进了应用级 Context(app即 Context)。
源码印证:Application类的定义正是export class Application extends Context implements LifeCycleObserver(见 packages/core/src/application.ts)。controller()方法内部调用createBindingFromClass生成绑定(默认命名空间CoreBindings.CONTROLLERS、类型标签CoreTags.CONTROLLER、作用域BindingScope.TRANSIENT),再通过this.add(binding)注册进 Context(见 application.ts)。也就是说,app.controller(MyController)本质上就是一次 Context 绑定操作,返回的Binding对象还可以继续链式调用(例如.lock()锁定)。
Server 级 Context
Server 级 Context:
- 是应用级 Context 的子级;
- 持有某个特定 Server 实例专属的配置。
应用通常包含一个或多个 Server 实例,它们都以应用级 Context 为父级。这意味着:定义在应用上的绑定对 Server 同样可见,除非你在 Server 实例上直接替换了这些绑定。
以@loopback/rest的RestServer为例:它负责在某个端口上启动 HTTP/S 服务并为 REST API 定义路由。要为RestServer设置端口绑定,需要把RestBindings.PORT绑定到一个数字(该键在 packages/rest/src/keys.ts 中定义为BindingKey.create<number>('rest.port'))。
我们可以有选择性地为某些 Server 实例重新绑定该值,从而改变它们使用的端口:
// src/application.ts async start() { // publicApi 将使用 443 端口,因为它从 app 继承了该绑定 app.bind(RestBindings.PORT).to(443); const publicApi = await app.getServer<RestServer>('public'); const privateApi = await app.getServer<RestServer>('private'); // privateApi 将被绑定到 8080 privateApi.bind(RestBindings.PORT).to(8080); await super.start(); }这里的关键机制是绑定继承:子 Context 在自身注册表中找不到绑定键时,会沿父链向上查找。源码中isBound()与getOwnerContext()的递归实现印证了这一点(见 context.ts)。
请求级 Context(request)
仍以@loopback/rest为例,每个传入的请求都会动态创建一个请求级 Context,它:
- 为每个到来的服务端请求动态创建;
- 扩展应用级 Context,让你在请求/响应生命周期内访问应用级依赖;
- 响应发送完毕后即被垃圾回收,以优化内存管理。
import {DefaultSequence, RestBindings, RequestContext} from '@loopback/rest'; class MySequence extends DefaultSequence { async handle(context: RequestContext) { // RequestContext 为方便与性能提供了 request/response 属性, // 但它们在 Context 中同样可取 const req = await this.ctx.get(RestBindings.Http.REQUEST); const res = await this.ctx.get(RestBindings.Http.RESPONSE); this.send(res, `hello ${req.query.name}`); } }this.ctx在 Sequence 中始终可用;- 它允许你实时(在需要的瞬间)组合应用级资源与请求级资源来构造响应。
整体三层 Context 层次结构如下图所示:
从 Context 中存取条目:Binding 与 BindingKey
Context 中的条目以**键(key)**索引,并绑定到一个BoundValue(绑定值)。BindingKey本质上是一个字符串,用于查找与之关联的存储值。例如:
// app 级别 const app = new Application(); app.bind('hello').to('world'); // BindingKey='hello', BoundValue='world' console.log(app.getSync<string>('hello')); // => 'world'这里我们把字符串'world'绑定到键'hello';通过getSync传入BindingKey即可取回当初绑定的BoundValue(当然还可以做更高级的事——例如实例化你的类)。
- 把
BoundValue注册进 Context 的过程称为绑定(binding),详见 Binding.md; - 可供使用的绑定函数完整列表参见 Context API 文档。
源码层面,bind()创建Binding对象并调用add()入注册表(见 context.ts);add()会检测同名绑定是否已锁定(locked),若已锁定则抛出Cannot rebind key ... to a locked binding,随后写入registry并依次发出unbind、bind事件(见 context.ts)。getSync与异步get支持key#property语法读取绑定对象的深层属性(见 context.ts)。
依赖注入:@inject 装饰器
应用实例化/启动时,开发者会把大量配置放进 Context;而 Context 在运行时提供访问这些依赖的途径。底层方式包括app.getSync或前面 Sequence 示例中的this.ctx.get。
但在使用类时,LoopBack 提供了更优雅的途径——@inject装饰器:
import {inject, Application} from '@loopback/core'; const app = new Application(); app.bind('defaultName').to('John'); export class HelloController { constructor(@inject('defaultName') private name: string) {} greet(name?: string) { return `Hello ${name || this.name}`; } }注意,我们在构造函数中直接"凭空"使用defaultName。Context 允许 LoopBack 在运行时把必要信息注入进来,即使你在编写 Controller 时并不知道该值。上述代码运行时会打印Hello John。
两点重要说明:
- 源码中
inject()定义于 packages/context/src/inject.ts,并提供inject.getter、inject.setter、inject.binding、inject.tag、inject.view、inject.context等命名空间变体; - 官方文档同时提醒:
@inject装饰器目前无法利用绑定键关联的值类型信息,因此 TypeScript 编译器不会校验注入目标(如构造参数)声明的类型是否能被绑定值赋值。请结合 Dependency injection 进一步阅读。
Context 元数据与"语法糖"装饰器
借助@loopback/rest提供的装饰器,可以把上面的例子注册为GET /greet路由:
class HelloController { // 告诉 LoopBack 你希望这个控制器方法 // 在 GET /greet 路由上可用 @get('/greet') greet( // 告诉 LoopBack 你希望 name 参数 // 作为字符串从查询串中接收 @param.query.string('name') name: string, ) { return `Hello ${name}`; } }这些"语法糖"装饰器以元数据的形式向 LoopBack 传达你的意图,使你能快速搭建应用,而不必手写所有附加逻辑。它们与 Context 的关系在于:装饰器收集的元数据最终都会转换为 Context 中的绑定(如路由绑定、参数绑定),再由 REST 层的RestServer在运行时解析。
Context 事件
Context实例可以发出以下事件:
bind:新的绑定被加入 Context 时发出。binding:新加入的绑定对象context:该绑定对象的所属 Context
unbind:已有绑定被移出 Context 时发出binding:被移除的绑定对象context:该绑定对象的所属 Context
error:观察者在通知过程中抛出错误时发出err:抛出的错误对象
bind/unbind事件的类型定义如下(见 packages/context/src/context-event.ts):
/** * 上下文发出的事件 */ export type ContextEvent = { /** * 发出事件的源上下文 */ context: Context; /** * 被添加/移除/更新的绑定 */ binding: Readonly<Binding<unknown>>; /** * 事件类型 */ type: string; // 'bind' 或 'unbind' };值得注意的规则:
- 当已有绑定键被新值替换时,先为旧绑定发出
unbind,再为新绑定发出bind; - 如果 Context 有父级,且当前 Context 中不存在该绑定键,则父级的绑定事件会在此 Context 上重发;
- 事件监听器需符合如下签名:
/** * 将 `Context` 作为事件发射器的同步事件监听器 */ export type ContextEventListener = (event: ContextEvent) => void;- 默认情况下 Context 对象的
maxListeners被设为Infinity,以避免内存泄漏警告(对应 GitHub issue #4363);可以按需重置:
ctx.setMaxListeners(128);Context 观察者(ContextObserver)
绑定可以在 Context 中增删。通过监听 Context 事件,可以在这类变动发生时执行回调。但直接监听事件有两个局限:
bind事件发出时,绑定对象可能尚未完全配置。例如:
const ctx = new Context(); ctx .bind('foo') .to('foo-value') .tag('foo-tag'); ctx.on('bind', ({binding}) => { console.log(binding.tagNames); // 返回空数组 [] });ctx.bind被调用时即发出bind事件,它无法控制随后在新绑定对象上执行的链式调用.to('foo-value').tag('foo-tag')。因此监听器拿到的绑定对象只有绑定键被填充。
解决办法是先构造好绑定,再添加进 Context:
const ctx = new Context(); const binding = Binding.create('foo') .to('foo-value') .tag('foo-tag'); ctx.add(binding); ctx.on('bind', ({binding}) => { console.log(binding.tagMap); // 返回 ['foo-tag'] });- 事件监听器难以执行异步操作。
为此引入了ContextObserver及对应的 Context API(类型定义见 packages/context/src/context-observer.ts):
/** * 监听 `bind`、`unbind` 或其他事件 * @param eventType - Context 事件类型 * @param binding - 作为事件源的绑定 * @param context - 绑定事件所属的 Context 对象 */ export type ContextObserverFn = ( eventType: ContextEventType, binding: Readonly<Binding<unknown>>, context: Context, ) => ValueOrPromise<void>; /** * Context 的 bind/unbind 事件观察者 */ export interface ContextObserver { /** * 可选的绑定过滤函数。若未提供,将通知该监听器所有绑定事件。 */ filter?: BindingFilter; /** * 监听 `bind`、`unbind` 或其他事件 * @param eventType - Context 事件类型 * @param binding - 作为事件源的绑定 */ observe: ContextObserverFn; } /** * Context 事件观察者类型——`ContextObserver` 实例或函数 */ export type ContextEventObserver = ContextObserver | ContextObserverFn;如果不需要filter,可以直接使用ContextObserverFn。
请注意ContextEventObserver与ContextEventListener的区别:
ContextEventListener是同步的,在事件发出时立即被调用(在emit返回之前);ContextEventObserver是异步的,在事件发出后被通知队列调用(在emit返回之后)。
观察者注册 API
subscribe(observer: ContextEventObserver):向 Context 链(包括其祖先)添加上下文事件观察者;unsubscribe(observer: ContextEventObserver):从 Context 链移除上下文事件观察者;close():关闭 Context 并释放其对链上其他对象的引用。注意:子 Context 会向父 Context 注册事件监听器,因此若子 Context 需要被回收,必须调用close()以避免内存泄漏。
要异步响应 Context 事件,需实现ContextObserver接口,或提供一个ContextObserverFn并注册到 Context 上。例如:
const app = new Context('app'); const server = new Context(app, 'server'); const observer: ContextObserver = { // 只关心被标记为 `foo` 的绑定 filter: binding => binding.tagMap.foo != null, observe(event: ContextEventType, binding: Readonly<Binding<unknown>>) { if (event === 'bind') { console.log('bind: %s', binding.key); // ... 执行异步操作 } else if (event === 'unbind') { console.log('unbind: %s', binding.key); // ... 执行异步操作 } }, }; server.subscribe(observer); server.bind('foo-server').to('foo-value').tag('foo'); app.bind('foo-app').to('foo-value').tag('foo'); // 将打印以下消息: // bind: foo-server // bind: foo-app注意:观察者订阅某个 Context 时,会注册到链上的所有 Context。上述示例中,观察者同时加入了server与app两个 Context,从而能收到链上任一 Context 的绑定增删通知。
异步通知的调度规则:
- 观察者在 Promise 微任务队列 的下一轮被调用;
- 注册多个异步观察者时,对同一个事件按串行顺序通知;
- 同一事件循环 tick 内发出多个绑定事件且存在异步观察者时,事件被排队,观察者按事件顺序依次收到通知。
源码佐证:订阅管理由ContextSubscriptionManager实现(见 packages/context/src/context-subscription.ts),其close()负责清理订阅;完整的观察者行为验收测试见 context-event-observer.acceptance.ts,其中使用filterByTag('controller')跟踪控制器绑定增删。
观察者错误处理
官方建议ContextEventObserver实现不要在代码中抛出错误。若观察者抛出错误,错误将沿 Context 链按以下规则上报:
- 检查当前 Context 对象是否注册了
error监听器:若有,则在当前 Context 上发出error事件并结束;若没有,则重复步骤 1 尝试其父 Context; - 若链上没有任何 Context 对象注册
error监听器,则在当前 Context 上发出error事件,此时 Node.js 进程会异常退出(参见 Node.js 事件机制的error事件说明)。
ContextView:动态跟踪绑定列表
Context 中的绑定可能随时增删。通常一个工件(尤其是扩展点)希望持续跟踪其他工件(扩展)。例如RestServer需要知道 controller 类或其他处理器贡献的路由;应用启动后又加入新的 controller 时,新路由会被绑定进应用 Context——理想情况下RestServer应无需重启就能感知这些新路由。
为此引入了ContextObserver接口与ContextView类:用BindingFilter函数筛选、用可选的BindingComparator函数排序,动态观察 Context 链中匹配的绑定列表。
import {Context, ContextView} from '@loopback/core'; // 建立一个 Context 链 const appCtx = new Context('app'); const serverCtx = new Context(appCtx, 'server'); // server -> app // 定义一个绑定过滤器,选中带 `controller` 标签的绑定 const controllerFilter = binding => binding.tagMap.controller != null; // 观察带 `controller` 标签的绑定 const view = serverCtx.createView(controllerFilter); // 还没有任何控制器 await view.values(); // 返回 [] // 在 server Context 绑定 Controller1 serverCtx .bind('controllers.Controller1') .toClass(Controller1) .tag('controller'); // 解析出 Controller1 的实例 await view.values(); // 返回 [一个 Controller1 的实例]; // 在 app Context 绑定 Controller2 appCtx.bind('controllers.Controller2').toClass(Controller2).tag('controller'); // 解析出 Controller1 与 Controller2 的实例 await view.values(); // 返回 [一个 Controller1 的实例, 一个 Controller2 的实例]; // 解绑 Controller2 appCtx.unbind('controllers.Controller2'); // Controller2 的实例消失 await view.values(); // 返回 [一个 Controller1 的实例];ContextView的关键优势在于:它缓存解析结果,直到匹配过滤器的 Context 绑定被增删。大多数场景下,我们无需为每个请求付出查找/解析的代价。
- 创建视图的 API
createView()位于 context.ts,内部构造ContextView并自动调用view.open()开始监听; ContextView的observe()会在收到bind/unbind事件时刷新缓存并转发事件,values()通过process.nextTick等待事件通知完成后再解析(见 context-view.ts)。
要充分利用"活"的扩展列表,扩展点(如RoutingTable)应:
- 持有对应全部
routes(扩展)的ContextView实例,每个请求调用values()重新匹配实时路由;或 - 自身实现为
ContextObserver,通过listen()在路由变化时重建路由。
若你的依赖需要跟随 Context 获取匹配某过滤器的值,请使用@inject.view进行依赖注入(其注入对象为ContextView<Counter>这类视图)。
ContextView 事件
ContextView对象可以发出以下事件:
bind:有绑定被加入视图时unbind:有绑定被移出视图时refresh:绑定增删导致视图被刷新时resolve:缓存值被解析并更新时close:视图被关闭(停止观察 Context 事件)时
这些事件可用于更新除ContextView自身所观察值之外的其他状态/缓存值,例如:
class MyController { private _total: number | undefined = undefined; constructor( @inject.view(filterByTag('counter')) private taggedAsFoo: ContextView<Counter>, ) { // 视图刷新时使 `_total` 缓存失效 taggedAsFoo.on('refresh', () => { this._total = undefined; }); } async total() { if (this._total != null) return this._total; // 计算所有计数器的总和 const counters = await this.taggedAsFoo.values(); let result = 0; for (const c of counters) { result += c.value; } this._total = result; return this._total; } }约定优于配置:@config.* 配置注入
为了让 Context 中的绑定项可配置,LoopBack 引入了一系列约定与 API。我们把绑定项在 Context 中的配置视为一种依赖,与其他形式的依赖一样可以被解析和注入。例如RestServer可以用RestServerConfig配置。
先看一个传统(硬编码键)示例:
export class RestServer { constructor( @inject(CoreBindings.APPLICATION_INSTANCE) app: Application, @inject(RestBindings.CONFIG, {optional: true}) config: RestServerConfig = {}, ) { // ... } // ... }配置(RestServerConfig)本身是 Context 中的一个绑定(RestBindings.CONFIG),独立于RestServer的绑定。缺点很明显:需要为配置维护不同的绑定键,且硬编码的配置键导致无法让多个RestServer实例拥有不同配置(如protocol或port)。
为解决这些问题,LoopBack 为"期待配置的项"引入了配套绑定。例如:
servers.RestServer.server1:RestServerservers.RestServer.server1:$config:RestServerConfigservers.RestServer.server2:RestServerservers.RestServer.server2:$config:RestServerConfig
以下 API 用于强制/利用这一约定:
ctx.configure('servers.RestServer.server1')⇒ 返回配置对应的绑定Binding.configure('servers.RestServer.server1')⇒ 为目标绑定创建配套的配置绑定ctx.getConfig('servers.RestServer.server1')⇒ 获取配置@config注入对应的配置@config.getter注入对应配置的 getter 函数@config.view注入对应配置的ContextView
RestServer现在可以用@config注入当前RestServer绑定的配置:
export class RestServer { constructor( @inject(CoreBindings.APPLICATION_INSTANCE) app: Application, @config() config: RestServerConfig = {}, ) { // ... } // ... }propertyPath 深层属性注入
@config.*装饰器可接受可选的propertyPath参数,使配置值可以取绑定值的深层属性。例如@config('port')会把RestServerConfig.port注入目标:
export class MyRestServer { constructor( @config('host') host: string, @config('port') port: number, ) { // ... } // ... }fromBinding:从其他绑定解析配置
@config.*还支持从当前绑定之外的另一个绑定解析配置:
import {config, CoreBindings} from '@loopback/core'; export class MyRestServer { constructor( // 从应用配置注入 `rest.host` @config({ fromBinding: CoreBindings.APPLICATION_INSTANCE, propertyPath: 'rest.host', }) host: string, // 从应用配置注入 `rest.port` @config({ fromBinding: CoreBindings.APPLICATION_INSTANCE, propertyPath: 'rest.port', }) port: number, ) { // ... } // ... }源码中ConfigInjectionMetadata接口正式定义了propertyPath与fromBinding两个元数据字段(见 packages/context/src/inject-config.ts),config()默认以optional: true解析配置。
通过 context.configure() 提供配置
现在可以用context.configure()为目标绑定提供配置:
const appCtx = new Context(); appCtx.bind('servers.RestServer.server1').toClass(RestServer); appCtx .configure('servers.RestServer.server1') .to({protocol: 'https', port: 473}); appCtx.bind('servers.RestServer.server2').toClass(RestServer); appCtx.configure('servers.RestServer.server2').to({protocol: 'http', port: 80});@config.*与@inject.*的本质区别在于:@config.*基于当前应用该装饰器的绑定来注入配置,无需硬编码绑定键。这也使得同一个类(如RestServer)可以绑定到不同键、配以不同配置——正如上面的代码片段所示。
底层实现:ctx.configure()会构造一个:$config后缀的配置绑定并add()入 Context(见 context.ts);配置解析默认交给DefaultConfigurationResolver,其getConfigAsValueOrPromise()默认以optional: true解析(见 packages/context/src/binding-config.ts),也允许通过ContextBindings.CONFIGURATION_RESOLVER注入自定义的ConfigurationResolver(见 context.ts)。
配置的默认行为与可选性
所有配置访问器/注入器(如ctx.getConfig、@config)默认将配置绑定视为可选:若未绑定配置则返回undefined。这与ctx.get、@inject不同——后两者要求绑定必须存在,找不到时抛出错误。该行为可通过ResolutionOptions.optional标志自定义。
支持动态变更的配置
某些配置设计为可动态改变,例如应用的日志级别。为此引入@config.getter,始终获取配置的最新值:
export class Logger { @config.getter() private getLevel: Getter<string>; async log(level: string, message: string) { const currentLevel = await getLevel(); if (shouldLog(level, currentLevel)) { // ... } } }源码中config.getter通过resolveAsGetterFromConfig注入一个 getter 函数(见 inject-config.ts);而config.view则注入ContextView,对配置变化同样保持响应。配置体系的验收测试可参考 binding-config.acceptance.ts。
小结
- Context 是 LoopBack 4 的 IoC 容器与全局注册中心,内部以
Map<string, Binding>实现,继承自EventEmitter(packages/context/src/context.ts); - 三级层次:应用级 → Server 级 → 请求级,子 Context 继承父级绑定,可选择性覆盖;
- 绑定与取值:
ctx.bind(key).to(value)/ctx.get(key)/ctx.getSync(key),键支持#property深层路径; - 依赖注入:
@inject及其命名空间变体在运行时解析构造参数; - 事件与观察者:
bind/unbind/error事件 + 同步ContextEventListener+ 异步ContextObserver(通过subscribe/unsubscribe/close管理,注意子 Context 回收需调用close()); - 动态扩展:
ContextView+BindingFilter缓存式跟踪匹配绑定,@inject.view注入视图,支撑RestServer热感知新增控制器路由; - 约定式配置:
:$config配套绑定 +ctx.configure()+@config/@config.getter/@config.view,默认可选、支持propertyPath与fromBinding,实现多实例差异化配置与运行期动态配置。
官方还提供了概念图以帮助理解:
若需进一步深入,可继续阅读仓库内的 Binding.md、Dependency-injection.md 与@inject.view文档,并结合@loopback/context包的 单元/验收测试 验证各 API 的实际行为。
- 后端
- 微服务
【免费下载链接】loopback-next
LoopBack makes it easy to build modern API applications that require complex integrations.
相关推荐
LoopBack 4 Context Explorer 实战:可视化 Context 层级、Binding 与依赖注入关系
LoopBack 4 Context Explorer 实战:可视化 Context 层级、Binding 与依赖注入关系 @loopback/context
后端微服务LoopBack 4 核心概念全解:Application、Context 与依赖注入架构剖析
LoopBack 4 核心概念全解:Application、Context 与依赖注入架构剖析 LoopBack 4 将典型 API 与微服务应用中差异化的职责
后端微服务TypeGraphQL 依赖注入实战:接入 IoC 容器与实现请求级 Scoped 容器
TypeGraphQL 依赖注入实战:接入 IoC 容器与实现请求级 Scoped 容器 导读 本篇指南围绕 TypeGraphQL 的依赖注入(Depende
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考