Node.js: помните практику рефакторинга http-сервисов с использованием gRPC

Node.js

задний план

В последнее время в процессе развития бизнеса обнаруживается, что поддержание деловой связи между несколькими службами одновременно требует больших затрат на разработку. Изучив код некоторых моих коллег, я обнаружил, что они обычно использовалиrpcдля создания службы, поэтому я решил попробовать использоватьgRPCРефакторинг сервиса. Вот некоторые основы проекта:

  • Поддерживайте несколько независимых сервисов одновременно.
  • Службы развертываются отдельно на нескольких машинах.
  • Между сервисами есть деловая связь (http).
  • каталог проекта
│   ├── controller
│   ├── service
│   ├── route
├── app.ts

анализ проблемы

Всякий раз, когда я пытаюсь добавить новую службу, мне нужно маршрутизировать, чтобы убедиться, что тип поля и имя точно совпадают, мне также нужно написатьAPIДокументируйте ограничения, а затем нужно написать дополнительный код для ввода и вывода формата данных. Наконец, при приеме изAPIответ, служба снова выполнит весь процесс в обратном порядке. Весь этот процесс выглядит так:Развитие идет медленно.

анализ спроса

  • Рефакторинг проводится с целью максимально не разрушить исходную структуру проекта и бизнес-код.
  • НапишиAPIВызовы нескольких служб могут быть ограничены одновременно.
  • Не нужно писать дополнительный код для ввода и вывода.

Процесс реализации

здесь сserverконец как пример. Во-первых, используйтеgRPCЗамените запись сервисной программы.

import service from 'controller';

// 加载proto文件
const grpcPkg = protoLoader.loadSync(file, loader);

function main() {
    const server = new grpc.Server();
    // 遍历grpcPkg,加载对应的service method
    bindServices(server, service, grpcPkg);
  
    server.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure());
    server.start();
}

согласно сrpcизФундаментальный, нам нужно написать.protoфайл для описания воздействия службыapi:

syntax = "proto3";

package test;

service Api {
  rpc index (Query) returns (Response) {};
  rpc show (Param) returns (Response) {};
  rpc update (Body) returns (Response) {};
  rpc create (Body) returns (Response) {};
  rpc del (Param) returns (Response) {};
}

message Response {
  int32 code = 1;
  string data = 2;
}

Вот делаю файлserviceЗаявленный метод такой же, какhttpУслугиcontrollerбыть последовательным

class Api {
  public index() {}
  public show() {}
  public update() {}
  public create() {}
  public del() {}
}

существуетbindService, мы можем перебрать загруженныйgrpcPkgи заявить об этомserviceа такжеcontrollerСопоставьте их один за другим.

  // 初始化方法
    for (const definition of getServiceNames(grpcPkg)) {
      const service = (service as any)[definition.name.toLowerCase()];
      !!service &&
        service.addService(
          // proto file中解析出的service
          definition.service.service,
          // 包装对应service
          await createService(service)
        );
    }

для того, чтобы гарантировать, что каждыйcontrollerМетод может точно иgrpcPkgЗагруженные методы должны соответствовать и могут использоваться во всехcontrollerВ общем, избегая конфликта одновременных методов, мы можем использоватьreflect-metadataсобиратьcontrollerметод, чтобы максимально избежать вторжения в первоначальный бизнес.

// decorator.ts
export const ROUTE_MAPPING_METADATA = 'MODULE_ROUTE_MAPPING';
export const ROUTE_METADATA = 'MODULE_ROUTE';
export const ROUTE_METADATA_PARAMS = 'MODULE_ROUTE_PARAMS';

export function Handler(params: any = {}) {
  return (target: any, methodName: string, descriptor: PropertyDescriptor) => {
    // 收集方法名
    const methods = Reflect.getMetadata(ROUTE_MAPPING_METADATA, target) || new Set<string>();
    methods.add(methodName);
    // 收集方法引用
    Reflect.defineMetadata(ROUTE_METADATA, descriptor.value, target, methodName);
    // 收集方法额外参数
    Reflect.defineMetadata(ROUTE_MAPPING_METADATA, methods, target);
    Reflect.defineMetadata(
      ROUTE_METADATA_PARAMS,
      params,
      target,
      methodName
    );
    return descriptor;
  };
}

// controller.ts
class Api {
  // rpc method
  @Handler()
  public index() {}
  
  // http method
  public show() {}
}

затем вaddService, мы можем получитьgrpcPkgсоответствующий методу вmethod, и это упаковка. Для достижения цели единого ввода и вывода.

function createService(controller: any) {
    const handlers: any = {};
    // 获取当前controller的方法集
    const methods: Set<string> = Reflect.getMetadata(ROUTE_MAPPING_METADATA, controller);
    for (const method of Array.from(methods)) {
      const handler = Reflect.getMetadata(ROUTE_METADATA, controller, method);
      if (!handler) continue;
      // 包装method
      const enhancer = createServiceMethod(
        handler
      );
      handlers[method] = enhancer;
    }
    // 返回方法集
    return handlers;
  }

Ну наконец тоprotoПроблемы с обслуживанием файлов, так как несколько служб могут использовать один и тот жеprotoдокумент. Здесь мы можем использоватьgit subtree, извлеките proto как подпроект для обслуживания.

использованная литература