☰
LoopBack 4 Context 全面解析:应用级 IoC 容器与依赖注入实战指南
2026/10/13 2:20:59 网站建设 项目流程
  • 后端
  • 微服务

【免费下载链接】loopback-next

LoopBack makes it easy to build modern API applications that require complex integrations.

项目地址:https://gitcode.com/gh_mirrors/lo/loopback-next
点击查看免费下载

导读

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

为什么它如此重要?

  1. 集中式/全局内存存储:通过 Context,你可以给 LoopBack 提供更多"信息",让应用中的其他依赖随时检索到它;
  2. 自动化的资源管理:通过 Dependency injection 与装饰器,LoopBack 能自动管理资源生命周期;
  3. 实时状态访问:你可以在任何时刻访问到最新、更新过的应用状态与请求状态。

包导出约定:@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 事件,可以在这类变动发生时执行回调。但直接监听事件有两个局限:

  1. 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'] });
  1. 事件监听器难以执行异步操作。

为此引入了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 链按以下规则上报:

  1. 检查当前 Context 对象是否注册了error监听器:若有,则在当前 Context 上发出error事件并结束;若没有,则重复步骤 1 尝试其父 Context;
  2. 若链上没有任何 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 绑定被增删。大多数场景下,我们无需为每个请求付出查找/解析的代价。

  • 创建视图的 APIcreateView()位于 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:RestServer
  • servers.RestServer.server1:$config:RestServerConfig
  • servers.RestServer.server2:RestServer
  • servers.RestServer.server2:$config:RestServerConfig

以下 API 用于强制/利用这一约定:

  1. ctx.configure('servers.RestServer.server1')⇒ 返回配置对应的绑定
  2. Binding.configure('servers.RestServer.server1')⇒ 为目标绑定创建配套的配置绑定
  3. ctx.getConfig('servers.RestServer.server1')⇒ 获取配置
  4. @config注入对应的配置
  5. @config.getter注入对应配置的 getter 函数
  6. @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.

项目地址:https://gitcode.com/gh_mirrors/lo/loopback-next
点击查看免费下载
上一篇:Super Productivity 免费开源时间管理:把一天忙完却没用上的时间找回来
下一篇:Switch大气层终极避坑指南:新手如何快速搭建稳定游戏环境?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询