后端微服务云原生【免费下载链接】midway A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 项目地址https://gitcode.com/gh_mirrors/mi/midway点击查看免费下载导读在开发组件或业务服务时经常会遇到一个服务需要管理多个实例的场景例如 OSS 组件需要同时持有多个不同 Bucket 的客户端对象、HTTP 客户端需要按不同域名维护多个连接配置。Midway 为此抽象了ServiceFactory基类它负责统一的实例创建、配置合并、按名获取、默认实例代理与生命周期销毁。本文以官方文档 service_factory.md 为主线结合midwayjs/core源码serviceFactory.ts与测试用例完整演示如何用 ServiceFactory 实现一个支持多实例的 HTTP 客户端服务并深入解析其配置合并、优先级、注入装饰器与代理工具的底层原理。ServiceFactory 解决了什么问题ServiceFactory是midwayjs/core提供的一个抽象类位于 packages/core/src/common/serviceFactory.ts。当服务需要被多次实例化例如 OSS 组件创建多个 OSS 对象时如果为每个实例都编写一套接口代码会非常冗余。ServiceFactory 将创建实例、管理实例、按名获取三个职责收敛到一个基类中每个需要实现多实例的服务只需继承它并补齐少量抽象方法。编写一个多实例客户端服务通常包含三个部分创建客户端实例的方法客户端的配置服务类本身的实例化时机。以一个 HTTP 客户端为例最基本的形态是// 创建客户端配置 const config { baseUrl: , timeout: 1000 }; // 创建客户端实例的方法 const httpClient new HTTPClient(config);实现一个服务工厂类服务工厂在 Midway 中是普通的导出类作为服务成员通常放在src/service/httpServiceFactory.ts。它本身需要被容器托管因此要使用Provide()标注同时由于工厂是全局唯一的需要配合Scope(ScopeEnum.Singleton)声明单例作用域。1. 继承抽象类并实现两个抽象方法ServiceFactory带有一个泛型参数即工厂创建出的实例类型下面的HTTPClient。继承后必须实现两个方法createClient(config)接收单个实例的创建配置返回一个具体实例getName()返回该服务工厂的名称用于框架识别与日志输出。import { ServiceFactory, Provide, Scope, ScopeEnum } from midwayjs/core; Provide() Scope(ScopeEnum.Singleton) export class HTTPClientServiceFactory extends ServiceFactoryHTTPClient { // 创建单个实例 protected createClient(config: any): any { return new HTTPClient(config); } getName() { return httpClient; } }从源码看抽象方法的签名还额外接收第二个参数clientName并允许返回 PromiseserviceFactory.tspublic abstract getName(): string; protected abstract createClient( config, clientName ): PromiseT | void | (T | void);因此createClient内也可以执行异步的初始化如建立连接、加载凭据并返回 Promise工厂会自动处理同步与异步两种返回形态。2. 注入配置并在初始化阶段创建实例工厂需要一份完整的用户配置。以httpClient为配置键名在config.default.ts中声明// config.default.ts export const httpClient { // ... }随后通过Config(httpClient)注入并在Init()生命周期中调用基类提供的initClients方法批量创建实例import { ServiceFactory, Provide, Scope, ScopeEnum } from midwayjs/core; Provide() Scope(ScopeEnum.Singleton) export class HTTPClientServiceFactory extends ServiceFactoryHTTPClient { Config(httpClient) httpClientConfig; Init() async init() { await this.initClients(this.httpClientConfig); } protected createClient(config: any): any { // 创建实例 return new HTTPClient(config); } getName() { return httpClient; } }initClients是基类实现的方法它接收一份完整用户配置循环调用createClient创建对象并保存到内存中的clientsMap 中。并发初始化v4.0.0 起initClients支持第二个参数传入初始化选项通过concurrent字段控制是否并发初始化await this.initClients(this.httpClientConfig, { concurrent: true });concurrent默认值为false串行初始化这是为了向后兼容。如果确认所有实例的初始化互不干扰可以设为true以提升初始化速度。对应的源码实现在 serviceFactory.ts串行时逐个await createInstance并发时使用Promise.all同时启动所有实例的创建。真实组件 OSS 在 manager.ts 中就启用了并发初始化Init() async init() { await this.initClients(this.ossConfig, { concurrent: true, }); }3. 在生命周期中实例化服务类为了让使用者直接拿到工厂实例需要在组件或项目的生命周期中提前实例化服务类。一般放在Configuration的onReady中import { Configuration } from midwayjs/core; Configuration({ imports: [ // ... ] }) export class ContainerConfiguration { async onReady(container) { // 实例化服务类 await container.getAsync(HTTPClientServiceFactory); } }配置结构与获取实例createClient只定义了对象的创建方式还需要约定配置结构。ServiceFactory 的配置结构分为三部分默认配置所有对象可以复用的公共配置单个实例所需的配置多个实例所需的配置。默认配置default默认配置约定使用default属性。创建实例时普通实例配置与动态创建实例的配置都会与default配置合并// config.default.ts export const httpClient { default: { timeout: 3000 } }从源码看合并逻辑位于createInstanceserviceFactory.ts// options.default will be merge in to options.clients[id] config extend(true, {}, this.options[default], config);即创建每个实例时都会用深拷贝把options.default与实例自身配置合并实例配置覆盖默认配置。单个实例client使用client描述单个实例的结构创建时该对象会与default合并。通过无参的get()方法获取默认实例// config.default.ts export const httpClient { default: { timeout: 3000 }, client: { baseUrl: } }import { HTTPClientServiceFactory } from ./service/httpClientServiceFactory; import { join } from path; Provide() export class UserService { Inject() serviceFactory: HTTPClientServiceFactory; async invoke() { const httpClient this.serviceFactory.get(); } }get方法在基类中的默认参数就是defaultserviceFactory.ts所以无参调用等价于get(default)。多个实例clients使用clients配置多个实例每个 key 是一份独立的实例配置// config.default.ts export const httpClient { default: { timeout: 3000 }, clients: { aaa: { baseUrl: }, bbb: { baseUrl: } } }使用 key 获取对应实例import { HTTPClientServiceFactory } from ./service/httpClientServiceFactory; import { join } from path; Provide() export class UserService { Inject() serviceFactory: HTTPClientServiceFactory; async invoke() { const aaaInstance this.serviceFactory.get(aaa); // ... const bbbInstance this.serviceFactory.get(bbb); // ... } }基类还提供了has(id)判断实例是否存在、getClients()获取全部实例 Map、getClientKeys()获取全部实例名称serviceFactory.ts这些方法也全部收录在 IServiceFactory 接口 中。装饰器获取实例InjectClient从 v3.9.0 起ServiceFactory 增加了InjectClient装饰器方便在存在多个客户端时直接注入指定实例import { HTTPClientServiceFactory } from ./service/httpClientServiceFactory; import { join } from path; import { InjectClient } from midwayjs/core; Provide() export class UserService { InjectClient(HTTPClientServiceFactory, aaa) aaaInstance: HTTPClientServiceFactory; InjectClient(HTTPClientServiceFactory, bbb) bbbInstance: HTTPClientServiceFactory; async invoke() { // this.aaaInstance.xxx // this.bbbInstance.xxx //... } }InjectClient用于快速注入所有继承ServiceFactory的实现类的多个实例。其定义位于 inject.tsexport function InjectClient( serviceFactoryClz: new (...args) IServiceFactoryunknown, clientName?: string ) { return DecoratorManager.createCustomPropertyDecorator( FACTORY_SERVICE_CLIENT_KEY, { serviceFactoryClz, clientName, } ); }参数说明serviceFactoryClz必填ServiceFactory的派生类装饰器将从中查找实例clientName可选不填时默认查找配置中的defaultClientName指定的默认实例名装饰器的实际处理逻辑注册在 frameworkService.ts它从容器取出工厂实例用meta.clientName || factory.getDefaultClientName()确定要取的实例名实例存在则返回factory.get(clientName)若未设置任何名称会抛出MidwayParameterError提示Please set clientName or options.defaultClientName若名称不存在则提示ClientName(...) not found in ...。测试夹具 configuration.ts 中展示了省略clientName走默认实例、以及显式传入default1、custom1等多种用法。动态创建实例createInstance除配置静态声明外还可以通过基类的createInstance方法动态创建实例:::caution 注意这里的createClient指的是子类实现createClient本身不包含与default配置合并的逻辑。 :::import { HTTPClientServiceFactory } from ./service/httpClientServiceFactory; import { join } from path; Provide() export class UserService { Inject() serviceFactory: HTTPClientServiceFactory; async invoke() { // config 会与 options.default 合并 let customHttpClient await this.serviceFactory.createInstance({ baseUrl: xxxxx }, custom); // 传入名称后也可以从工厂中再次获取 customHttpClient this.serviceFactory.get(custom); } }createInstance的第一个参数是配置动态调用时可手动传入第二个参数是字符串名称。传入名称后创建的实例会被保存在内存中之后可通过get(name)再次取得。源码细节serviceFactory.ts同名实例已存在时会直接返回已有实例异步创建期间会记录到creatingClientsMap 防止重复创建实例创建完成后写入clientsMap 并从创建中的集合移除。测试用例serviceFactory.test.ts验证了createInstance({}, fff)之后instance.get(fff)与instance.has(fff)均成立。实例配置的合并逻辑实际运行时即使配置的是单个实例client工厂也会在内存中将其转换为clients结构。例如// config.default.ts export const httpClient { client: { baseUrl: } }在内存中会变成// config.default.ts export const httpClient { clients: { default: { baseUrl: } } }即自动多出一个名为default的默认实例服务工厂最终按clients结构初始化。这段转换逻辑在initClients开头serviceFactory.ts// merge options.client to options.clients[default] if (options.client) { options.clients options.clients || {}; options.clients[default] options.clients[default] || {}; extend(true, options.clients[default], options.client); }测试用例should test default clientserviceFactory.test.ts验证了仅配置client时instance.get(default)确实存在。默认实例代理可选如果每次使用都要通过serviceFactory获取默认实例会非常繁琐。对于最常用的默认实例可以提供一个代理类代理目标实例的所有方法import { ServiceFactory, MidwayCommonError, delegateTargetAllPrototypeMethod, Provide, Scope, ScopeEnum, Init } from midwayjs/core; //... export class HTTPClientServiceFactory extends ServiceFactoryHTTPClient { //... } // 下面是默认代理类 Provide() Scope(ScopeEnum.Singleton) export class HTTPClientService implements HTTPClient { Inject() private serviceFactory: HTTPClientServiceFactory; // 该属性用于保存实际实例 private instance: HTTPClient; Init() async init() { // 初始化阶段从工厂获取默认实例 this.instance this.serviceFactory.get( this.serviceFactory.getDefaultClientName() || default ); if (!this.instance) { throw new MidwayCommonError(http client default instance not found.); } } } // 下面代码正确继承了默认实例类的 ts 定义 // eslint-disable-next-line typescript-eslint/no-empty-interface export interface HTTPClientService extends HTTPClient { //empty } // 下面的代码将默认实例类的方法代理到实现类 delegateTargetAllPrototypeMethod(HTTPClientService, HTTPClient);有了以上代码使用者可以直接注入HTTPClientService使用而无需从HTTPClientServiceFactory获取默认实例import { HTTPClientService } from ./service/httpClientServiceFactory; import { join } from path; Provide() export class UserService { Inject() httpClientService: HTTPClientService; async invoke() { // this.httpClientService 即默认实例 } }delegateTargetAllPrototypeMethod是 Midway 提供的实例方法委托工具方法。三个委托工具的源码实现均位于 util/index.ts其行为差异如下工具方法作用delegateTargetAllPrototypeMethod委托目标所有原型方法包含整个原型链排除构造器和内部隐藏方法以_开头delegateTargetPrototypeMethod委托目标原型上的方法排除构造器和内部隐藏方法delegateTargetMethod仅委托代理目标上指定的方法从实现看代理的本质是向派生类原型写入同名函数函数内部调用this.instancename因此要求代理类持有instance属性指向真实实例。OSS 组件在 manager.ts 与 manager.ts 中同样使用了getDefaultClientName() || default的取值模式。修改默认实例名称默认实例的名称是default默认实例代理也是基于该实例进行内部代理。如果用户没有配置default实例或想修改默认实例可以通过配置指定// config.default.ts export const httpClient { clients: { default: { baseUrl: }, default2: { baseUrl: } }, defaultClientName: default2, }在默认实例代理中通过this.serviceFactory.getDefaultClientName()获取该值源码见 serviceFactory.tspublic getDefaultClientName(): string { return this.options[defaultClientName]; }测试用例should test default nameserviceFactory.test.ts验证了设置defaultClientName: abc后getDefaultClientName()返回abc。同时InjectClient省略第二个参数时也会优先使用defaultClientName作为查找目标frameworkService.ts。实例优先级从 v3.14.0 起服务工厂实例可以配置优先级属性。优先级分为三档L1、L2、L3分别对应高、中、低。定义位于 priorityManager.tsexport const DEFAULT_PRIORITY { L1: High, L2: Medium, L3: Low, };通过配置为不同实例指定优先级// config.default.ts import { DEFAULT_PRIORITY } from midwayjs/core; export default { httpClient: { clients: { default: { baseUrl: }, default2: { baseUrl: } }, clientPriority: { default: DEFAULT_PRIORITY.L1, default2: DEFAULT_PRIORITY.L2, } } }如果未设置默认优先级为中等即DEFAULT_PRIORITY.L2见 priorityManager.ts 的defaultPriority。注意在ServiceFactory源码中优先级配置的读取键是options.priorityserviceFactory.ts配置结构与ServiceFactoryConfigOption类型中的clientPriority键名保持一致见下文类型定义部分。ServiceFactory基类提供了若干方法用于判断优先级Provide() Scope(ScopeEnum.Singleton) export class HTTPClientService implements HTTPClient { Inject() private serviceFactory: HTTPClientServiceFactory; Init() async init() { // 获取优先级 this.serviceFactory.getClientPriority(default); // DEFAULT_PRIORITY.L2 // 判断优先级 this.serviceFactory.isHighPriority(default); this.serviceFactory.isMediumPriority(default); this.serviceFactory.isLowPriority(default); } }底层实现serviceFactory.ts委托给注入的MidwayPriorityManager完成判断逻辑非常简单直接isHighPriority等价于优先级等于L1isMediumPriority等价于等于L2isLowPriority等价于等于L3priorityManager.ts。测试用例serviceFactory.test.ts对高/中/低三种优先级以及未配置时的默认行为都做了完整验证。销毁服务类与实例服务工厂提供destroyClient方法当服务工厂类的stop方法被调用时会自动执行用于销毁实例。如果实例不需要销毁逻辑可以不实现Provide() Scope(ScopeEnum.Singleton) export class HTTPClientServiceFactory extends ServiceFactoryHTTPClient { // ... async destroyClient(client: HTTPClient, clientName: string) { // 销毁实例 } }除了实现destroyClient还需要在生命周期中显式调用stop方法import { Configuration } from midwayjs/core; Configuration({ imports: [ // ... ] }) export class ContainerConfiguration { private httpClientServiceFactory: HTTPClientServiceFactory; async onReady(container) { // 实例化服务类 this.httpClientServiceFactory await container.getAsync(HTTPClientServiceFactory); } async onStop() { // 销毁服务类 if (this.httpClientServiceFactory) { await this.httpClientServiceFactory.stop(); } } }stop在基类中的实现serviceFactory.ts会遍历clientsMap逐个调用destroyClient后清空 Mappublic async stop(): Promisevoid { for (const [name, value] of this.clients.entries()) { await this.destroyClient(value, name); } this.clients.clear(); }测试用例should test multi-clientsserviceFactory.test.ts验证了stop()之后实例的close()方法确实被调用isClose变为true确认了销毁链路是真实生效的。类型定义使用服务工厂时需要正确声明类型。Midway 提供ServiceFactoryConfigOption核心类型来帮助定义服务工厂配置其定义位于 interface.tsexport type BaseServiceFactoryConfigOptionOPTIONS PowerPartialOPTIONS { customClientClass?: any; }; export type ServiceFactoryConfigOptionOPTIONS { default?: BaseServiceFactoryConfigOptionOPTIONS; client?: BaseServiceFactoryConfigOptionOPTIONS; clients?: { [key: string]: BaseServiceFactoryConfigOptionOPTIONS; }; defaultClientName?: string; clientPriority?: { [key: string]: number; }; };可见该类型完整覆盖了前文介绍的default、client、clients、defaultClientName与clientPriority五个配置维度并且每个实例配置额外支持customClientClass字段允许自定义客户端实现类测试用例 serviceFactory.test.ts 验证了这一能力。实际使用中通过declare module扩展全局配置类型import { ServiceFactoryConfigOption } from midwayjs/core; // 定义 HTTPClient 配置 interface HTTPClientConfig { baseUrl: string; timeout?: number; } // 使用 ServiceFactoryConfigOption 定义配置 declare module midwayjs/core { interface MidwayConfig { httpClient?: ServiceFactoryConfigOptionHTTPClientConfig; } }MidwayConfig接口本身定义在 interface.ts其核心日志组件midwayLogger配置也直接复用了ServiceFactoryConfigOptionMidwayLoggerOptionsinterface.ts说明这是整个框架通用的配置类型基础设施。常见使用链路小结结合源码与文档一个完整的多实例服务工厂生命周期如下定义阶段继承ServiceFactoryT实现createClient(config, clientName)与getName()用Provide()Scope(ScopeEnum.Singleton)声明托管配置阶段在config.default.ts中以default公共默认、client单实例或clients多实例组织配置可选defaultClientName与clientPriority初始化阶段Init()中调用await this.initClients(config, { concurrent })将client折叠为clients.default后逐个/并发创建实例使用阶段通过factory.get(name)、InjectClient(FactoryClz, name)或默认代理类直接注入实例销毁阶段实现destroyClient在Configuration.onStop中调用factory.stop()统一销毁。进一步阅读官方英文文档service_factory.md中文对照文档service_factory.md核心实现serviceFactory.ts优先级实现priorityManager.ts类型定义interface.ts注入装饰器inject.ts、frameworkService.ts委托工具util/index.ts单元测试serviceFactory.test.ts生产实践OSS 组件manager.ts赞分享后端微服务云原生【免费下载链接】midway A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 项目地址https://gitcode.com/gh_mirrors/mi/midway点击查看免费下载相关推荐Langflow 后端服务抽象规范ServiceFactory 模式与服务层依赖方向详解Langflow 后端服务抽象规范ServiceFactory 模式与服务层依赖方向详解 本篇技术文章基于 Langflow 仓库中的后端代码评审规则 rep人工智能大模型AI AgentRAG后端前端MCP 服务工作流自动化Relay Resolver 返回类型详解标量、列表、客户端类型、服务端类型与抽象类型的实战指南Relay Resolver 返回类型详解标量、列表、客户端类型、服务端类型与抽象类型的实战指南 Relay Resolvers 允许在客户端用 JavaSc前端开发工具Midway 集成 etcd客户端封装、多实例管理与服务发现实战指南Midway 集成 etcd客户端封装、多实例管理与服务发现实战指南 etcd 是云原生架构中的重要基础组件由 CNCF 孵化管理既可作为微服务与 Kub后端微服务云原生上一篇微博记忆永久保存Speechless免费工具一键导出完整PDF备份方案下一篇ComfyUI IPAdapter终极指南从零开始掌握AI图像风格迁移创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?