ТЕХНОЛОГИЯ ЭКСПЕДИА ГРУПП - ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ

Странный мир инструментов gRPC для Node.js, часть 2

Смело создавайте статически сгенерированный сервер JavaScript

Вы можете создать сервер gRPC в Node.js со статической генерацией кода, но с компромиссами, которые вы должны понимать. В предыдущем рассказе этой серии я объяснил экосистему инструментов gRPC для Node.js. Теперь я покажу вам, как с помощью этих инструментов создать статически сгенерированный сервер.

Образец приложения

Мы воспользуемся этими инструментами для создания простого примера приложения. Это приложение предоставляет услугу, моделирующую библиотеку, наполненную книгами. Его методы позволяют получить все книги и проверить книгу. Весь код, которым я поделюсь, можно найти здесь.

Схема следует структуре, рекомендованной Uber и применяемой их линтером protobuf, prototool. Обратите внимание, что если вы запускаете линтер в моем примере кода, prototool запускается с использованием образа Docker, поэтому вам потребуется установить Docker. Структура каталогов для схемы соответствует именам пакетов, используемых внутри схемы, в соответствии с рекомендациями Uber:

proto
├── com
│   └── rcbrown
│       └── grpc
│           └── v1
│               ├── library_api.proto
│               └── library_book.proto
├── package.json         # For linting scripts
└── prototool.yaml       # To configure rule exceptions

Сама схема проста, и я не буду ее подробно объяснять, поскольку сосредоточусь на инструментах. Использование сообщений запроса и ответа - еще одна рекомендация Uber.

Важно отметить, что в примере предполагается, что источник данных, лежащий в основе службы, возвращает идиоматические объекты JavaScript, соответствующие схеме protobuf. Обычно это происходит при использовании баз данных NoSQL, таких как Amazon DynamoDB или Mongo.

Генерация статического кода с protoc

Используйте protoc для создания артефактов как gRPC, так и protobuf, используя сценарий npm, подобный этому:

grpc_tools_node_protoc                                     \
    --proto_path=../../proto                               \
    --js_out=import_style=commonjs_strict,binary:generated \
    --grpc_out=generated                                   \
    ../../proto/com/rcbrown/grpc/v1/*.proto

Если вы не попросите commonjs, вы получите модули закрытия. Если это вам нравится, попробуйте, потому что у него есть некоторые дополнительные функции; Я выбираю CommonJs из-за его повсеместности.

Вы можете увидеть это на месте в package.json в моем образце. Он генерирует четыре файла: два _pb.js файла для сортировки объектов и два _grpc_pb.js файла, содержащих метаданные службы.

generated
└── com
    └── rcbrown
        └── grpc
            └── v1
                ├── library_api_grpc_pb.js
                ├── library_api_pb.js
                ├── library_book_grpc_pb.js
                └── library_book_pb.js

Вот несколько фрагментов из сгенерированного library_api_grpc_pb.js:

Так что на самом деле это касается только service и rpc частей прототипа. Вот несколько отрывков из library_api_pb.js:

Это касается всей сортировки и демаршалинга для операторов запроса / ответа message, определенных в library_api.proto.

Подобно library_api_pb.js, library_book_pb.js содержит код сортировки для операторов сообщений, которые определяют саму структуру данных библиотечной книги в library_book.proto. Он также содержит перечисление для статуса оформления заказа:

library_book_grpc_pb.js пусто, потому что library_book.proto не содержит операторов service или rpc.

Вы это заметили?

library_api_grpc_pb.js:
var com_rcbrown_grpc_v1_library_api_pb =
  require('../../../../com/rcbrown/grpc/v1/library_api_pb.js');
...
function serialize_com_rcbrown_grpc_v1_GetBooksRequest(arg) {
  if (!(arg instanceof
     com_rcbrown_grpc_v1_library_api_pb.GetBooksRequest)) {
...
----------------------------------------------------------------
library_api_pb.js:
proto.com.rcbrown.grpc.v1.GetBooksRequest = function(opt_data) {
...
goog.object.extend(exports, proto);  // Adds proto fields to export

Протобуф создал GetBooksRequest конструктор, который глубоко вложен, чтобы соответствовать структуре пакета прототипа. Но уровень gRPC ожидает, что GetBooksRequest будет на самом верху экспорта. Хотя они были получены в ходе одного цикла генерации кода, они не взаимодействуют друг с другом. Если вы построите сервер с ними, вы будете вознаграждены 13 INTERNAL: Cannot read property ‘deserializeBinary’ of undefined при вызове этой службы.

Пакеты с наказанием

В темном углу документации protobuf Google предупреждает, что сгенерированный код, использующий импорт CommonJS (в отличие от импорта Closure, единственной поддерживаемой опции), игнорирует директивы пакета. Это был не совсем мой опыт. Вы можете видеть выше, что protobuf уважал структуру пакета. Но сгенерированные *_grpc_pb.js файлов - нет. Они предполагают, что объектов protobuf не будет в пакете, поэтому они ссылаются на объекты, которых не будет во время выполнения. Единственное решение - не использовать пакеты с protoc и статической генерацией кода, что вынуждает нас отклоняться от рекомендаций Uber. Короче говоря,

Пакеты не работают!

В оставшейся части этой серии я буду использовать файлы .proto без директивы package и с плоской структурой каталогов:

proto
├── library_api.proto
├── library_book.proto
├── package.json
└── prototool.yaml

Написание сервера

Передайте сгенерированный дескриптор для службы и объект, содержащий функции-обработчики для ее конечных точек:

Каждый из методов обработчика должен преобразовывать входящие сообщения от объектов protobuf в обычные объекты JavaScript для передачи базовому объекту доступа к данным (DAO) и обратного процесса для возвращаемых значений. Эти преобразования используют код маршалинга / демаршалинга, сгенерированный в файлах _pb.js.

Обратите внимание, что оба обработчика предполагают, что DAO возвращает CheckoutStatus как строку, которая соответствует именам в перечислении.

Что может быть проще?

Java… Скрипт?

Надеюсь, последнее предложение вызвало у меня крик. Если вам это казалось простым, значит, вы прожили тяжелую жизнь.

Опытным разработчикам JavaScript следует недовольно использовать геттеры и сеттеры. Это стиль Java, применяемый к JavaScript. Это особенно актуально, потому что JavaScript уже давно поддерживает геттеры и сеттеры, которые выглядят как идиоматические обращения к полям, но сгенерированный код их не использует.

Такой код не добавляет ценности

Но это мелочь - больше беспокоит архитектура. Код обработчика очень тесно связан как с типами данных protobuf, так и с собственными типами данных, используемыми DAO. Изменения модели потребуют подробных обновлений в коде обработчика, и если это упустить, многие виды изменений приведут к молчаливым пропускам.

Если запросы или ответы содержат сложные графы объектов, этот код маршаллинга в обработчике будет весьма задействован. Это не очевидно из большинства примеров, которые вы найдете в Интернете (или, на самом деле, из моего), потому что они обычно используют тривиальные сообщения. Подобные платформы API в других экосистемах (например, Jersey для Java) автоматизируют этот вид работы. (Я не сравниваю gRPC с фреймворками JavaScript, такими как Express, потому что они обычно конвертируют в JSON, который является встроенным для JavaScript, что позволяет обойти эту проблему.)

Если вы обязуетесь использовать статически сгенерированный код gRPC Node.js, у вас будет очень подробный контроль над маршалингом, независимо от того, хотите вы этого или нет. На мой взгляд, такой код не добавляет ценности, и я стараюсь ее минимизировать.

Написание клиента

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

Мне нужно было написать mapCheckoutStatusToString, чтобы отобразить значения перечисления из целых чисел в строки; Мне не нужно было делать это для сервера, потому что DAO автоматически знал, какие строки использовать.

Доступны библиотеки для обещания вызовов, и часть кода маршаллинга можно было бы исключить, но я хотел сделать код как можно более простым и полностью видимым. Вы можете видеть, что маршаллинг составляет основную часть кода и похож на сервер.

Дорога малоизвестная

Вы видели некоторые недостатки при написании статически сгенерированного сервера.

  • Пакеты не работают
  • Вам нужно написать много идиосинкразического кода маршаллинга (хотя, если данные модели вашего клиента или уровня данных сильно отличаются от схемы protobuf, вы все равно будете писать аналогичный код)

Однако вы получаете легкую видимость кода, который вы должны написать. Если вам нужно написать такой код без статически сгенерированного кода,

const booksPb = getBooksResponsePb.getLibraryBooksList();

тогда вам будет очень трудно понять, что вы должны ввести после точки. В конце концов, library_books_list нет в .proto; это изобретено фреймворком. Достаточно легко открыть library_api_grpc_pb.js и посмотреть, что есть в наличии.

В следующей статье этой серии я покажу вам обработку .proto файлов во время выполнения, чтобы вы могли сравнить ее с этой.