NestJS — Node.js للمؤسسات

الخدمات المصغّرة باستخدام gRPC

16 دقيقة الدرس 39 من 48

الخدمات المصغّرة باستخدام gRPC

gRPC إطار عمل RPC عالي الأداء ومفتوح المصدر طوّرته Google. يستخدم Protocol Buffers (protobuf) لغةً لتعريف الواجهات وتنسيقًا للنقل عبر الشبكة، مما يمنحك عقودًا موثوقة النوع بشكل كامل، وتسلسلًا ثنائيًا كفؤًا، ودعمًا أصليًا للبث ثنائي الاتجاه — كل ذلك ضروري لتواصل الخدمات المصغّرة في بيئات الإنتاج.

لماذا gRPC بدلًا من REST؟

  • عقود محكمة الأنواع — ملف .proto هو المصدر الوحيد للحقيقة؛ يُولَّد الخادم والعميل منه، مما يزيل أي اختلاف بينهما.
  • تسلسل ثنائي — يُعدّ protobuf أكثر إيجازًا بـ 3–10 أضعاف وأسرع تحليلًا من JSON.
  • دعم البث — بث الخادم، والعميل، وثنائي الاتجاه كلها متوفّرة كمواطنين من الدرجة الأولى.
  • توليد الكود — تُولَّد أنواع TypeScript تلقائيًا، مما يمنع فئات كاملة من الأخطاء في وقت التشغيل.

تعريف خدمة protobuf

يبدأ كل شيء بملف .proto. يُعلِن عن الرسائل (أشكال البيانات) والخدمة (دوال RPC). ضعه في مجلد proto/ مشترك في جذر المستودع حتى يتمكّن الخادم والعميل من الإشارة إلى الملف نفسه:

// proto/hero.proto syntax = "proto3"; package hero; service HeroService { rpc FindOne (HeroById) returns (Hero); rpc FindAll (Empty) returns (stream Hero); } message HeroById { int32 id = 1; } message Hero { int32 id = 1; string name = 2; } message Empty {}
تُستخدم أرقام الحقول لا أسماؤها عبر الشبكة. لا تغيّر أبدًا رقم حقل موجود في خدمة منشورة — فذلك سيُفسد البيانات القادمة من العملاء القديمة بصمت. يمكنك إضافة حقول جديدة بأرقام جديدة؛ وسيتجاهل العملاء القدامى الحقول غير المعروفة.

إعداد ناقل gRPC في NestJS

ثبّت الحزم المطلوبة، ثم اضبط main.ts لتشغيل خدمة مصغّرة بناقل gRPC:

npm install @nestjs/microservices @grpc/grpc-js @grpc/proto-loader
// main.ts import { NestFactory } from '@nestjs/core'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { join } from 'path'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, { transport: Transport.GRPC, options: { package: 'hero', protoPath: join(__dirname, '../proto/hero.proto'), url: '0.0.0.0:5000', }, }); await app.listen(); } bootstrap();

تنفيذ معالجات gRPC باستخدام @GrpcMethod

استخدم المزيّن @GrpcMethod() لربط دالة في المتحكّم بتعريف RPC. يجب أن يطابق اسم الدالة اسم RPC في ملف .proto (أو يُمرَّر كوسيط ثانٍ):

import { Controller } from '@nestjs/common'; import { GrpcMethod } from '@nestjs/microservices'; interface Hero { id: number; name: string } interface HeroById { id: number } @Controller() export class HeroController { private readonly heroes: Hero[] = [ { id: 1, name: 'Lancelot' }, { id: 2, name: 'Percival' }, ]; // استدعاء أحادي — طلب واحد، استجابة واحدة @GrpcMethod('HeroService', 'FindOne') findOne(data: HeroById): Hero { return this.heroes.find(h => h.id === data.id)!; } }

الاستدعاءات الأحادية مقابل البث

  • أحادي (Unary) — طلب واحد واستجابة واحدة (مثل HTTP المعتاد). يُعالَج بدالة عادية تُعيد قيمة أو Promise.
  • بث الخادم — طلب واحد وسيل من الاستجابات. أعِد Observable من المعالج؛ سيُصدر NestJS كل قيمة كرسالة protobuf منفصلة.
  • بث العميل / ثنائي الاتجاه — اقبل Observable كوسيط أول. أنماط متقدّمة لسيناريوهات الوقت الفعلي.
import { Observable, of } from 'rxjs'; import { GrpcStreamMethod } from '@nestjs/microservices'; // مثال على بث الخادم @GrpcMethod('HeroService', 'FindAll') findAll(_: {}): Observable<Hero> { return of(...this.heroes); // يُصدر كل بطل كرسالة منفصلة }
استخدم Observable من RxJS للبث. يتكامل ناقل gRPC في NestJS مباشرةً مع RxJS: إعادة Observable من معالج @GrpcMethod يحوّله تلقائيًا إلى تدفق بث gRPC من جانب الخادم، مما يُبقي كودك نظيفًا وقابلًا للاختبار.

استدعاء خدمة gRPC من عميل NestJS

أدخِل عميل gRPC عبر @Inject() بعد تسجيله كمزوِّد في ClientsModule. يمنحك وكيل العميل كعبًا موثوق النوع يطابق واجهة ملف .proto:

// في الوحدة المستهلِكة ClientsModule.register([{ name: 'HERO_PACKAGE', transport: Transport.GRPC, options: { package: 'hero', protoPath: join(__dirname, '../proto/hero.proto'), url: 'localhost:5000', }, }]); // في الخدمة المستهلِكة constructor(@Inject('HERO_PACKAGE') private client: ClientGrpc) {} onModuleInit() { this.heroService = this.client.getService<HeroService>('HeroService'); } getHero(id: number): Observable<Hero> { return this.heroService.findOne({ id }); }
ولّد أنواع TypeScript من ملف proto. كتابة الواجهات يدويًا بما يطابق الـ proto عُرضة للأخطاء. استخدم ts-proto أو protoc مع إضافة TypeScript لتوليد أنواع دقيقة وإبقائها متزامنة كلّما تغيّر الـ proto.

الخلاصة

تتّبع الخدمات المصغّرة عبر gRPC في NestJS نمطًا واضحًا: عرّف العقد في ملف .proto، واضبط ناقل Transport.GRPC في main.ts، وزيّن المعالجات بـ @GrpcMethod(). تُعيد الاستدعاءات الأحادية قيمًا عادية أو Promises؛ وتُعيد استدعاءات بث الخادم Observable من RxJS. تسجّل العملاء مزوّدًا في ClientsModule وتستدعي الخدمة البعيدة بأمان تام من حيث الأنواع. يضمن عقد protobuf أن كلا الطرفين يتحدّثان اللغة ذاتها — حتى عبر لغات برمجة مختلفة.