Cordis利用动态创建子Context的方式构建具有树形层次化结构的Context Tree进而为插件提供的独立的执行环境。貌似很完美地为插件解决了上下文隔离问题但是还有一个问题得解决由于Cordis总是将服务注册到根Context上意味着以此为根的整棵Context Tree注册的插件使用的同名服务都是同一个实例如何让服务执行的时候通过提取与插件关联的上下文信息而不是使用与自身绑定的上下文。1. 有状态服务实例复用的困境对于一个无状态的服务对象来说它可以毫无风险地被复用。但是一旦让服务实例拥有了状态复用的时候就要小心了。以如下这个演示程序为例服务类型SingletonService定义了一个ctx字段绑定一个创建的Context对象我们定义了一个名为foobar的属性返回ctx的foobar字段如果没有值就返回N/A。import{Context}fromdeepseek-ai/cordisclassSingletonService{getfoobar(){returnthis.ctx?.foobar??N/A}ctx:ContextnewContext()}declaremoduledeepseek-ai/cordis{interfaceContext{service:SingletonService foobar:string,}}constcontextnewContext();context.provide(service,newSingletonService())constsubContext1context.isolate(child1)Object.defineProperty(subContext1,foobar,{get(){returnfoo}});subContext1.plugin(ctxconsole.log([Plugin1] ctx.service.foobar ${ctx.service.foobar}));constsubContext2context.isolate(child2);Object.defineProperty(subContext2,foobar,{get(){returnbar}});subContext2.plugin(ctxconsole.log([Plugin2] ctx.service.foobar ${ctx.service.foobar}));我们调用provide方法将SingletonService对象注册到创建的根Context上。然后创建了两个子Context并分别定义了foobar字段对应返回值分别为foo和bar。我们在这两个子Context上分别注册了插件并让插件输出当前上下文的ctx.service.foobar的值。 由于SingletonService对象的ctx字段是一个完全独立的Context所以它的foobar访字段应该永远返回 N/A如下的输出也体现了这一点。[Plugin1]ctx.service.foobarN/A[Plugin2]ctx.service.foobarN/A但是我们希望SingletonService的ctx字段能够与当前执行的插件所在Context绑定。按照我们对上面程序的理解这貌似是要完成一件无中生有的事情感觉无解。实则不然我们看看Cordis是如何解决这个问题的。2. 基于代理的字段解析让同一个对象的某个字段动态绑定当前Context源于Context内部采用了基于代理的动态解析机制。从如下的代码可以看出Context的构造函数返回的并不是this而是一个将它作为目标对象的代理。这是Cordis最精妙的设计这意味着所有对Context所有成员的访问都会经过这个Proxy对象。至于这个Proxy究竟服务返回字段访问请求全部实现在作为拦截处理器的ReflectService.handler静态字段上。不过这不是本篇文章关注的内容我们会在本系列后续文章中对此进行详细介绍。在这里我们只需要知道在整Cordis体系内针对Context成员的访问都由ReflectService.handler控制。exportclassContext{constructor(){...constselfnewProxythis(this,ReflectService.handler)...returnself}}3. symbols.tracker和Tracker 接口Cordis针对这个问题的解决需要使用到如下这个名为Tracker的接口。exportinterfaceTracker{associate?:stringproperty?:stringnoShadow?:boolean}三个字段说明如下associate用于开启跨命名空间的动态重定向property用来指定当前Context绑定到对象的哪个字段上noShadow: 决定在依赖注入入口处是否要强行保留最外层的影子上下文说到这里你肯定还是很晕我们会通过具体的实例对此作进一步解释。在此之前我们还需要了解另一个通过symbols.tracker标识的Symbol服务对象利用此Symbol从自己的成员中提取Tracker来确定重定向策略。也就是说我们需要创建一个Tracker对象作为服务对象通过symbols.tracker标识的字段。4. 利用Tracker.property 动态绑定上下文对于我们演示实例来说其实我们的目的明确让SingletonService对象的ctx字段直接与当前的Context绑定在一起。所以我们需要按照如下的方式为SingletonService额外添加一个由symbols.tracker标识的字段并将作为字段值的Tracker对象的property字段设置为当前上下文绑定的目标字段ctx就可以。import{Context,Tracker,symbols}fromdeepseek-ai/cordisclassSingletonService{[symbols.tracker]:Tracker{property:ctx}getfoobar(){returnthis.ctx?.foobar??N/A}ctx:ContextnewContext()}输出[Plugin1]ctx.service.foobarfoo[Plugin2]ctx.service.foobarbar5. 利用Tracker.associate关联字段成员Tracker的property解决的是当前Context绑定到服务对象的哪个成员上而associate刚好相反解决是服务对象如何与当前Context的某个成员进行关联。具体例子当某个服务以名称service注册到Context上 服务对象携带的Tracker对象将associate设置为foo。假设当前Context通过变量ctx表示 我们可以通过ctx.service得到这个服务对象的代理。如果我们访问ctx.service.bar并且此时ctx具有一个名为foo.bar的成员该成员的值将作为返回值。下面的程序很好的演示了这一点import{Context,Tracker}fromdeepseek-ai/cordisclassFooService{[symbols.tracker]:Tracker{associate:foo}}classBarService{[symbols.tracker]:Tracker{associate:bar}}declaremoduledeepseek-ai/cordis{interfaceContext{foo:{baz:string},bar:{baz:string},}}constcontextnewContext();context.provide(foo,newFooService());context.provide(bar,newBarService());context.provide(foo.baz,ctx.foo.baz);context.provide(bar.baz,ctx.bar.baz);console.log(context.foo.baz);console.log(context.bar.baz);输出ctx.foo.baz ctx.bar.baz6. 关于noShadow对于Tracker的noShadow字段的作用和用法我到目前没有看到明确的说明文档只有从源代码去寻找答案。根据目前提供的源代码我发现如下是唯一能够验证noShadow效果的方式。如代码所示我们定义了一个名为FoobarService的服务类型并在构造函数中创建了一个Tracker对象作为symbols.tracker字段。Tracker的property设置成ctx意味着将当前Context绑定到ctx字段上noshadow则来源于指定的参数。FoobarService的invoke方法会依次输入如下的内容当前的Tracker当前Context(this.ctx)的foobar字段值如果symbols.shadow存在则输出其foobar字段值否则输出undefined。import{Context,symbols,Tracker}fromdeepseek-ai/cordisdeclaremoduledeepseek-ai/cordis{interfaceContext{shadowService:FoobarService,noShadowService:FoobarService,foobar:string}}classFoobarService{invoke(){consttranker:Trackerthis[symbols.tracker];console.log(\ntranker: property${tranker.property}, noShadow${tranker.noShadow});console.log(this.ctx.foobar ${this.ctx.foobar});constshadow:Contextthis.ctx[symbols.shadow];if(shadow){console.log(this.ctx[symbols.shadow].foobar ${shadow.foobar});}else{console.log(this.ctx[symbols.shadow] ${shadow});}}constructor(privatectx:Context,noShadow:boolean){this[symbols.tracker]{property:ctx,noShadow:noShadow}}}constcontextnewContext()Object.defineProperty(context,foobar,{value:foo,writable:true});(contextasany)[shadowService]newFoobarService(context,false);(contextasany)[noShadowService]newFoobarService(context,true);constsubConextcontext.extend();;Object.defineProperty(subConext,foobar,{value:bar,writable:true});subConext.shadowService.invoke()subConext.noShadowService.invoke()process.stdin.resume();我们创建了一个根Context并将其foobar字段设置为foo。然后基于这个Context对象和不同的noShadow值创建了FoobarService并将它们作为当前Conext的shadowService和noShadowService字段值。我们最终通过extend方法创建了一个子Context并将foobar字段修改为bar。我们最终从子Context利用shadowService和noShadowService字段提取FoobarService对象并调用其invoke方法。tranker: propertyctx, noShadowfalse this.ctx.foobar bar this.ctx[symbols.shadow].foobar foo tranker: propertyctx, noShadowtrue this.ctx.foobar bar this.ctx[symbols.shadow] undefined从上面的输出可以看出只有在默认noshadowfalse的情况下才能通过symbols.shadow得到服务对象真正绑定的Context。
阅读完成 · 觉得有帮助?