Сучасна диджитал-освіта для дітей — безоплатне заняття в GoITeens ×
Mazda CX 5
×

gRPC-автогенерація Front-end-у

Привіт, мене звати Ярослав. Я працюю розробником у компанії Evrius. У цій статті розглянемо автогенерацію клієнт-серверної взаємодії на основі добре відомого прикладу, що зацікавить веброзробників.

Маленький ліричний відступ

Це вже п’ята моя стаття на DOU, і, звісно, кожну статтю після публікації я надсилав подивитися друзям і колегам, щоб отримати зворотний зв’язок.

Здебільшого статті друзям подобалися, але й частку критики вдалося здобути: так дізнався, що статті «сухі». І справді, статті схожі на мій код (так само мало коментарів) або на інструкцію, як доїхати від Києва до Львова й назад на велосипеді (знаю лише одного велотуриста, що так може).

Ця стаття теж буде інструкцією, та цього разу писатиму більше пояснень і думок.

Ще одна відмінність від уже написаних статей у тому, що раніше я розглядав завдання, які вже розв’язав, тому процес написання складався з підготовки прикладів коду й подальшого написання статті на основі вже готових прикладів. А в цій публікації мені ще самому треба буде розібратися з grpc-web і зробити інструкцію, яку зможу в майбутньому використовувати.

Чому автогенерація важлива

Автогенерація коду — це перекладання однотипної роботи на комп’ютер (як і має бути) або спосіб уникнення помилок-одруків під час копіювання коду (навіть досвідчені спеціалісти помиляються).

Процес розробки й однотипна робота

Коли тільки починаєш працювати на новій роботі, та ще й з новими технологіями, то є азарт, усе цікаво, працювати приємно й комфортно.

Звикаєш до інструментів: IDE, тестів, CI/CD, каркасу з командами для генерації шаблонів DB-міграцій, CRUD-генераторів і ApiDoc-генераторів.

Звикаєш до того, що процес розробки налаштовано і можна зосередитися на логіці. Звісно, є винятки. Для мене такими були валідації, які робив на стороні сервера, а потім копіював на клієнт, і API методи з моделями, які спершу робив на сервері й знову копіював на клієнт у браузері.

Так, процес розробки, як смуги на зебрі: білі — то цікава розробка нової логіки, а темні — однотипне копіювання вже наявної логічної структури й адаптування під нові критерії (зазвичай таке з радістю роблять початківці).

Правильні метафори

Щоб зрозуміло пояснити технологію, треба вибрати всім добре знайомий приклад.

Сайт з новинами LamerNews використовується як приклад для пояснення Redis-у.

У книжках про архітектуру я часто зустрічав приклади облікових систем. У цій статті для прикладу я виберу форум.

gRPC для міжсервісної взаємодії

gRPC — високопродуктивний каркас для взаємодії між сервісами, що дає можливість згенерувати код клієнта й сервера на основі файлів з розширенням .proto.

Код клієнта й сервера можна згенерувати для різних мов програмування. Proto-файли містять у собі опис повідомлень, що відправляються та отримуються у форматі protobuf. Якщо ви чуєте про protobuf уперше, то вважайте, що це альтернатива JSON-у.

Розгляньмо на основі прикладу про форум, який вигляд матиме proto-файл, де будуть методи для створення нової теми та редагування вже наявної:

syntax = "proto3";

service Forum {
    rpc CreateTopic (CreateTopicRequest) returns (UpdateTopicResponse);
    rpc UpdateTopic (UpdateTopicRequest) returns (UpdateTopicResponse);
}

message UpdateTopicResponse {
    bool success = 1;
    string error_message = 2;
}

message CreateTopicRequest {
    string title = 1;
    string description = 2;
}

message UpdateTopicRequest {
    uint32 code = 1;
    string title = 2;
    string description = 3;
}

Ідентифікатори-назви сервісу та повідомлень можуть бути довільними, індекси мають бути унікальними й використовуватися для серіалізації, і цей приклад може мати також інакший вигляд, застосовуючи одну структуру для створення та оновлення:

syntax = "proto3";

service ForumService {
    rpc TopicCreate (UpdateTopicRequest) returns (UpdateTopicResponse);
    rpc TopicUpdate (UpdateTopicRequest) returns (UpdateTopicResponse);
}

message UpdateTopicResponse {
    bool success = 1;
    string error_message = 2;
}

message UpdateTopicRequest {
    uint32 code = 1;
    string title = 2;
    string description = 3;
}

Або навіть так, виділивши дані в окреме повідомлення і використовуючи як спільний код:

syntax = "proto3";

service ForumService {
    rpc TopicCreate (CreateTopicRequest) returns (UpdateTopicResponse);
    rpc TopicUpdate (UpdateTopicRequest) returns (UpdateTopicResponse);
}

message UpdateTopicResponse {
    bool success = 1;
    string error_message = 2;
}

message Topic {
    string title = 1;
    string description = 2;
}

message CreateTopicRequest {
    Topic data = 1;
}

message UpdateTopicRequest {
    uint32 code = 1;
    Topic data = 2;
}

Я вибрав би перший або другий варіант, коли повідомлення мають мало полів, і третій — коли повідомлення вже має (чи ми сподіваємося, що воно матиме) багато полів.

На основі proto-файлу можна згенерувати повноцінний клієнт під багато платформ; це може бути мікросервіс, мобільний застосунок або ж клієнт у браузері.

Ця стаття саме про клієнт, що застосовуватиметься в браузері.

Як це працює

Ми написали proto-файл forum.proto, на основі якого, за допомогою інструменту protoc, згенерували моделі й інтерфейс; інтерфейс реалізували на сервері, і тепер сервер готовий до використання.

Далі ми копіюємо цей proto-файл forum.proto в репозиторій з Front-end-ом; на його основі за допомогою інструменту protoc і плагіна до нього protoc-gen-grpc-web генеруємо моделі та клієнт, готові до використання.

Коли нам треба буде додати нові поля в уже наявні повідомлення чи додати нові rpc-методи, ми оновимо proto-файл, згенеруємо на сервері код, реалізуємо нову логіку, так само скопіюємо proto-файл forum.proto в репозиторій з Front-end-ом і згенеруємо клієнт.

Таким чином Front-end-розробник матиме готовий до використання gRPC-клієнт, а подивитися proto-файл буде простіше, ніж API документацію.

При описі я зробив спрощення, коли писав про один proto-файл. Зазвичай це тека, де є service.proto, у якому підключаються файли з повідомленнями.

Завдання і технологічний стек

У цій статті я реалізую прототип форуму DOU, заради цікавості додам нових фіч; сервер буде на Go, зберігатиму в MongoDB, запускатиму через docker-compose, а на клієнті буде Vue.js і Webpack.

Якщо ви хочете самі розібратися з gRPC Web, то можете клонувати репозиторій github.com/grpc/grpc-web з простою інструкцією, як запустити:

docker-compose pull
docker-compose up
browse http://localhost:8081/echotest.html

AJAX-лічильник, від простого до складного

Перед тим, як розглядати приклад з gRPC, розгляньмо простіший.

Візьмемо для прикладу вебсторінку, на якій показуємо число запитів; число запитів отримуватимемо через AJAX, а в наступному прикладі замінимо AJAX на gRPC.

Маємо три файли: index.html для зображення вмісту, counter.js, що робить AJAX-запит, та main.go сервер на Go:

 
├── main.go
└── public
    ├── index.html
    └── js
        └── counter.js
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>AJAX counter example</title>
</head>
<body>
    <p>
        The page was viewed <span id="js-counter">0</span> times
    </p>
    <script src="js/counter.js"></script>
</body>
</html>
{
    fetch("/api/counter.json")
        .then(function (response) {
            return response.json();
        })
        .then(function (json) {
            document.getElementById("js-counter").innerHTML = json.count;
        })
        .catch(console.error);
}
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "sync/atomic"
)

type CounterResponse struct {
    Count uint32 `json:"count"`
}

func main() {
    var counter = uint32(0)

    http.Handle("/", http.FileServer(http.Dir("./public")))
    http.HandleFunc("/api/counter.json", func(w http.ResponseWriter, _ *http.Request) {
        var newCounter = atomic.AddUint32(&counter, 1)

        json.NewEncoder(w).Encode(&CounterResponse{
            Count: newCounter,
        })
    })

    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}

Якщо хочете перевірити приклад, треба мати вже встановлений Golang.

go run main

browse http://localhost:8080/

Код AJAX-прикладу доступний у репозиторії.

gRPC-лічильник

У цьому прикладі ми:

  1. Налаштуємо кодогенерацію серверної та клієнтської частини.
  2. Опишемо файл counter.proto, на основі якого згенеруємо код для клієнт-серверної взаємодії.
  3. На стороні сервера реалізуємо інтерфейс лічильника (інтерфейс згенерований через кодогенерацію).
  4. На стороні клієнта під’єднаємо згенерований клієнт і зберемо проєкт через Webpack.

Встановити protoc можна з офіційною інструкцією для Ubuntu (на момент написання статті найсвіжіша версія protoc 3.11.4):

PROTOC_ZIP=protoc-3.11.4-linux-x86_64.zip
curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.11.4/$PROTOC_ZIP
sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc
sudo unzip -o $PROTOC_ZIP -d /usr/local 'include/*'
rm -f $PROTOC_ZIP

Інструмента protoc досить для кодогенерації серверної частини на Go.

А ось для кодогенерації клієнтської частини на JavaScript потрібен плагін protoc-gen-grpc-web, що можна встановити на Ubuntu так:

curl -sSL https://github.com/grpc/grpc-web/releases/download/1.0.7/protoc-gen-grpc-web-1.0.7-linux-x86_64 -o /usr/local/bin/protoc-gen-grpc-web chmod +x /usr/local/bin/protoc-gen-grpc-web

Опишемо counter.proto:

syntax = "proto3";

package counter;

message Empty {

}

message Response {
  uint32 count = 1;
}

service CounterSomeServiceName {
  rpc CountSomeMethodName(Empty) returns (Response);
}

Файл counter.proto я розмістив у теці з довільною назвою protos:

 
~/go/src/gitlab.com/go-yp/grpc-counter
└── protos
    └── services
        └── counter
            └── counter.proto

Назвав CounterSomeServiceName і CountSomeMethodName, щоб було простіше побачити, які суфікси та префікси додаються після кодогенерації.

Згенеруємо код для серверної частини:

mkdir -p ./models
protoc -I . protos/services/counter/*.proto --go_out=plugins=grpc:models

Оскільки кодогенерацію ви будете запускати після оновлення proto-файлів, рекомендую зберігати в Makefile:

proto-server:
    mkdir -p ./models
    protoc -I . protos/services/counter/*.proto --go_out=plugins=grpc:models

make proto-server

Після кодогенерації proto-server отримаємо файл counter.pb.go:

~/go/src/gitlab.com/go-yp/grpc-counter
├── Makefile
├── protos
│   └── services
│       └── counter
│           └── counter.proto
└── models
    └── protos
        └── services
            └── counter
                └── counter.pb.go [+]

У файлі counter.pb.go буде згенерований код моделей Response і Empty та методи цих моделей:

// Code generated by protoc-gen-go. DO NOT EDIT.
// source: protos/services/counter/counter.proto

package counter

// ...

type Response struct {
    Count                uint32   `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"`
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

// ...

type Empty struct {
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

// ...

Методи моделей Response і Empty — звичайні обгортки для реалізації службових інтерфейсів серіалізації protobuf-у через рефлексію, тому я їх прибрав з прикладу.

Також у counter.pb.go нам буде цікавий інтерфейс сервісу та реєстрації:

package counter

// ...

type CounterSomeServiceNameServer interface {
    CountSomeMethodName(context.Context, *Empty) (*Response, error)
}

func RegisterCounterSomeServiceNameServer(s *grpc.Server, srv CounterSomeServiceNameServer) {
    // ...
}

Тепер реалізуємо інтерфейс CounterSomeServiceNameServer:

package main

import (
    "context"
    "gitlab.com/go-yp/grpc-counter/models/protos/services/counter"
    "sync/atomic"
)

type counterServer struct {
    count uint32
}

func (s *counterServer) CountSomeMethodName(context.Context, *counter.Empty) (*counter.Response, error) {
    var newCount = atomic.AddUint32(&s.count, 1)

    return &counter.Response{
        Count: newCount,
    }, nil
}

var _ counter.CounterSomeServiceNameServer = new(counterServer)

Реалізуємо й запустимо на 50551-порті gRPC-сервер (також залишимо static-server з прикладу про AJAX-лічильник):

package main

import (
    "context"
    "gitlab.com/go-yp/grpc-counter/models/protos/services/counter"
    "log"
    "net"
    "net/http"
    "sync/atomic"

    "google.golang.org/grpc"
)

type counterServer struct {
    count uint32
}

func (s *counterServer) CountSomeMethodName(context.Context, *counter.Empty) (*counter.Response, error) {
    var newCount = atomic.AddUint32(&s.count, 1)

    return &counter.Response{
        Count: newCount,
    }, nil
}

var (
    mainServer counter.CounterSomeServiceNameServer = new(counterServer)
)

func main() {
    go func() {
        lis, err := net.Listen("tcp", ":50551")
        if err != nil {
            log.Fatal(err)
        }
        defer lis.Close()

        grpcServer := grpc.NewServer()

        counter.RegisterCounterSomeServiceNameServer(grpcServer, mainServer)

        if err := grpcServer.Serve(lis); err != nil {
            log.Fatal(err)
        }
    }()

    http.Handle("/", http.FileServer(http.Dir("./public")))

    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}

Зробимо ініціалізацію Golang-проєкту й запустимо сервер:

go mod init
go run main.go
~/go/src/gitlab.com/go-yp/grpc-counter
├── go.mod  [+]
├── go.sum  [+]
├── main.go [+]
├── Makefile
├── protos
│   └── services
│       └── counter
│           └── counter.proto
└── models
    └── protos
        └── services
            └── counter
                └── counter.pb.go

gRPC-сервер чекає даних, переданих протоколом HTTP/2, а JavaScript-клієнт у браузері (який ми згенеруємо далі) передає дані протоколом HTTP 1.1; відповідно потрібен проксі, що зможе перетворити один протокол на інший.

Рекомендованим вирішенням є Envoy Proxy, про Envoy можна почитати в DevOps дайджесті або послухати доповідь Envoy as TCP proxy Олега Миколайченка.

Я хотів зробити клієнт-серверну взаємодію напряму — тому знайшов вирішення, як це здійснити, у статті Proxy gRPC-Web directly in your Go Server.

package main

import (
    "context"
    "gitlab.com/go-yp/grpc-counter/models/protos/services/counter"
    "golang.org/x/net/http2"
    "golang.org/x/net/http2/h2c"
    "log"
    "net/http"
    "sync/atomic"

    "github.com/improbable-eng/grpc-web/go/grpcweb"
    "google.golang.org/grpc"
)

type counterServer struct {
    count uint32
}

func (s *counterServer) CountSomeMethodName(context.Context, *counter.Empty) (*counter.Response, error) {
    var newCount = atomic.AddUint32(&s.count, 1)

    return &counter.Response{
        Count: newCount,
    }, nil
}

var (
    mainServer counter.CounterSomeServiceNameServer = new(counterServer)
)

func main() {
    go func() {
        grpcServer := grpc.NewServer()
        grpcWebServer := grpcweb.WrapServer(grpcServer)

        counter.RegisterCounterSomeServiceNameServer(grpcServer, mainServer)

        var handler = h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Access-Control-Allow-Origin", "*")
            w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
            w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-User-Agent, X-Grpc-Web")

            grpcWebServer.ServeHTTP(w, r)
        }), new(http2.Server))

        err := http.ListenAndServe(":50551", handler)
        if err != nil {
            log.Fatal(err)
        }
    }()

    http.Handle("/", http.FileServer(http.Dir("./public")))

    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}

gRPC-сервер успішно запускається, тепер залишилося згенерувати й під’єднати клієнт у браузері:

proto-client:
    mkdir -p ./client
    protoc -I . protos/services/counter/*.proto --js_out=import_style=commonjs,binary:client --grpc-web_out=import_style=commonjs,mode=grpcwebtext:client

make proto-client

~/go/src/gitlab.com/go-yp/grpc-counter
├── client
│   ├── app.js
│   └── protos
│       └── services
│           └── counter
│               ├── counter_grpc_web_pb.js [+]
│               └── counter_pb.js          [+]
├── go.mod
├── go.sum
├── main.go
├── Makefile
├── protos
│   └── services
│       └── counter
│           └── counter.proto
└── models
    └── protos
        └── services
            └── counter
                └── counter.pb.go

У файлі counter_pb.js будуть моделі та службові обгортки:

// source: protos/services/counter/counter.proto
// GENERATED CODE -- DO NOT EDIT!

var jspb = require('google-protobuf');

// ...

proto.counter.Empty = function(opt_data) {
  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};

// ...

proto.counter.Response = function(opt_data) {
  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};

// ...

У файлі counter_grpc_web_pb.js буде gRPC-клієнт:

// GENERATED CODE -- DO NOT EDIT!

const grpc = {};
grpc.web = require('grpc-web');

const proto = {};
proto.counter = require('./counter_pb.js');

// ...

proto.counter.CounterSomeServiceNameClient = function(hostname, credentials, options) {
  if (!options) options = {};
  options['format'] = 'text';

  this.client_ = new grpc.web.GrpcWebClientBase(options);

  this.hostname_ = hostname;
};

// ...

Цього досить, щоб зробити реалізацію в app.js, схожу на попередній AJAX-приклад:

const {Empty, Response} = require("./protos/services/counter/counter_pb");
const {CounterSomeServiceNameClient} = require("./protos/services/counter/counter_grpc_web_pb");

const app = new CounterSomeServiceNameClient("http://localhost:50551");

const request = new Empty();

app.countSomeMethodName(request, {}, (err, response) => {
    if (err) {
        console.error(err);

        return;
    }

    /** @type Response response */

    document.getElementById("js-counter").innerHTML = response.getCount();
});

І останні приготування package.json і webpack.config.js, щоб зібрати клієнтську частину:

{
  "name": "grpc-counter-example",
  "version": "0.1.0",
  "description": "gRPC counter example",
  "license": "MIT",
  "dependencies": {
    "grpc-web": "^1.0.0",
    "google-protobuf": "^3.6.1"
  },
  "devDependencies": {
    "@grpc/proto-loader": "^0.5.4",
    "webpack": "^4.16.5",
    "webpack-cli": "^3.1.0"
  },
  "scripts": {
    "build": "webpack --mode production"
  }
}
module.exports = {
    context: __dirname,

    entry: {
        app: './client/app'
    },

    output: {
        path: __dirname + '/public/js',
        filename: '[name].js'
    },
};
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>gRPC counter example</title>
</head>
<body>
    <p>
        The page was viewed <span id="js-counter">0</span> times
    </p>
    <script src="js/app.js"></script>
</body>
</html>
~/go/src/gitlab.com/go-yp/grpc-counter
├── [4.0K]  client
│   ├── [ 514]  app.js
│   └── [4.0K]  protos
│       └── [4.0K]  services
│           ├── [4.0K]  counter
│           │   ├── [3.5K]  counter_grpc_web_pb.js
│           │   └── [8.4K]  counter_pb.js
├── [ 386]  go.mod
├── [5.8K]  go.sum
├── [1.4K]  main.go
├── [ 887]  Makefile
├── [4.0K]  models
│   └── [4.0K]  protos
│       └── [4.0K]  services
│           └── [4.0K]  counter
│               └── [7.0K]  counter.pb.go
├── [ 382]  package.json
├── [4.0K]  protos
│   └── [4.0K]  services
│       ├── [4.0K]  counter
│       │   └── [ 187]  counter.proto
├── [4.0K]  public
│   ├── [ 257]  index.html
│   └── [4.0K]  js
│       ├── [282K]  app.js
└── [ 186]  webpack.config.js
npm i
npm run build

go run main

browse http://localhost:8080/

Готовий приклад можна побачити в репозиторії.

Серед мінусів — розмір app.js ~ 282 KB, у якому підключені лише gRPC- і protobuf-бібліотеки.

gRPC-прототип структури форуму

Зробімо трохи складніший приклад, щоб подивитися, який буде розмір app.js.

Завдання: треба зробити простий форум зі створенням теми, коментарями та модерацією.

Підготуємо proto-файл, що описує методи:

syntax = "proto3";

package forum;

import "protos/services/forum/topic.proto";

service AnonymousForum {
  rpc CreateTopic (CreateTopicRequest) returns (UpdateTopicResponse);
  rpc UpdateTopic (UpdateTopicRequest) returns (UpdateTopicResponse);
  rpc TopicList (Empty) returns (TopicListResponse);

  rpc AddComment (AddCommentRequest) returns (Empty);
  rpc Topic (TopicRequest) returns (FullTopicResponse);
}

service ModerationForum {
  rpc TopicList(Empty) returns (TopicListResponse);
  rpc TopicApprove(TopicRequest) returns (Empty);
  rpc TopicReject(TopicRequest) returns (Empty);

  rpc CommentList(Empty) returns (CommentListResponse);
  rpc CommentApprove(CommentRequest) returns (Empty);
  rpc CommentReject(CommentRequest) returns (Empty);
}

А такий вигляд матимуть моделі:

syntax = "proto3";

package forum;

message Empty {}

message UpdateTopicResponse {
  bool success = 1;
  string error_message = 2;
}

message CreateTopicRequest {
  string title = 1;
  string description = 2;
}

message UpdateTopicRequest {
  string id = 1;
  string title = 2;
  string description = 3;
}

message Topic {
  string id = 1;
  string title = 2;
  string description = 3;
}

message TopicListResponse {
  repeated Topic items = 1;
}

message TopicRequest {
  string id = 1;
}

message AddCommentRequest {
  string topic_id = 1;
  string username = 2;
  string text = 3;
}

message Comment {
  string id = 1;
  string username = 2;
  string text = 3;
}

message CommentListResponse {
  repeated Comment items = 1;
}

message CommentRequest {
  string id = 1;
}

message FullTopicResponse {
  string id = 1;
  string title = 2;
  string description = 3;
  repeated Comment items = 4;
}
└── protos
    └── services
        └── forum
            ├── anonymous.proto
            └── topic.proto

За аналогією з gRPC-лічильником згенерую клієнт:

protoc -I . protos/services/forum/*.proto --js_out=import_style=commonjs,binary:client --grpc-web_out=import_style=commonjs,mode=grpcwebtext:client

Під’єднаємо згенерований клієнт і використаємо його в прикладі:

const {CreateTopicRequest, UpdateTopicResponse} = require("./protos/services/forum/topic_pb");
const {AnonymousForum} = require("./protos/services/forum/anonymous_grpc_web_pb");

const app = new AnonymousForum("http://localhost:50551");

const request = new CreateTopicRequest();
request
    .setTitle("gRPC forum example")
    .setDescription("gRPC forum example");

app.addTopic(request, {}, (err, response) => {
    if (err) {
        console.error(err);

        return;
    }

    /** @type UpdateTopicResponse response */

    console.log(response);
});
├── [4.0K]  client
│   ├── [ 514]  app.js
│   └── [4.0K]  protos
│       └── [4.0K]  services
│           └── [4.0K]  forum
│               ├── [ 27K]  anonymous_grpc_web_pb.js
│               ├── [ 530]  anonymous_pb.js
│               └── [ 65K]  topic_pb.js
├── [1.0K]  Makefile
├── [ 382]  package.json
├── [4.0K]  protos
│   └── [4.0K]  services
│       └── [4.0K]  forum
│           ├── [ 755]  anonymous.proto
│           └── [ 898]  topic.proto
├── [4.0K]  public
│   └── [4.0K]  js
│       └── [310K]  forum-app.js
└── [ 229]  webpack.config.js

Бачимо, що розмір forum-app.js ~ 310 KB.

Далі буде

У цій статті я планував створити прототип форуму DOU зі збереженням у MongoDB, але стаття і так вийшла об’ємною; якщо сподобалася, то зроблю продовження.

Епілог

Одна із цілей написання — це те, що мені тема цікава, а шукаючи, бачив мало повноцінних прикладів; тому буду радий зустріти посилання на гарні приклади в коментарях.

Пишучи статтю, я зрозумів, що gRPC web добре підходить для розробки нових і складних проєктів.

Якщо прочитавши статтю, ви захотіли перевести вже наявну мікросервісну взаємодію з REST на gRPC, щоб було простіше розробляти нові методи й сервіси, то так, це доцільно.

Чи переводити вже наявну REST-взаємодію браузера й сервера на gRPC web? Ліпше подумайте, а чи справді вам це треба.

У майбутньому хочу писати кращі статті, тому буду радий коментарям про те, як і що міг би пояснити простіше.

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному8
LinkedIn

Схожі статті




18 коментарів

Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.

Gustavo Henrique написав схожу статтю про gRPC-Web gRPC-Web with Golang and VueJS: An alternative to REST and GraphQL з репозиторієм github.com/gustavohenrique/grpc-web-golang-vuejs на три місяці раніше

Глянь twirp (github.com/twitchtv/twirp) — трохи менше бойлєрплейта, підтримує http/1.1, і можна слати json, якщо дуже треба, мож сподобається

Насчьот REST, ну REST — це чисто відрижка 2000их, коли всі раптом захотіли заліпити все що можна в пачку HTTP-методів, для роботи з даними ще ок, але для складного RPC не годиться. Саме тому хоч SOAP і довго вмирав, але так досі і ще не вмер.

Для мене gRPC свого часу було десь як приблизно куля в лоб для SOAP :)
«Переписувати» нема сенсу, бо по великому рахунку REST диктує нормальну архітектуру, просто десь тісно, там де тісно завжди можна збоку присобачить хендлєр, вигоди переписування всього сумнівні, а от подібни хендлери — можна й переписати

Загалом grpc — досить сильна прив’язка клієнта до сервера, але мені подобається більше ніж REST. Нагенерував собі СДК для всіх клієнтів — і ніякої мороки з REST, хоча не завжди так краще. Так будуть казати що і REST так можна, але grpc якось суб’єктивно мені краще

Чудова стаття, розжовано так що моя мама зрозуміє, перечитаю ще всі твої статті, дякую :)
Замість атоміка можна було б всетаки range з канала, атомік тягне явою, але я не релігійний, просто помітив

Чудова стаття

Дякую :)

Замість атоміка можна було б всетаки range з канала

Поясни будь-ласка, бо всередині канал go/src/runtime/chan.go використовує atomic

Глянь twirp — трохи менше бойлєрплейта, підтримує http/1.1, і можна слати json, якщо дуже треба, мож сподобається

Тільки що глянув implementations in other languages:

This repo only has the official generators, which write out Go and Python code. For other languages, there are third-party generators available

є три генератори в JavaScript та TypeScript:

  1. github.com/larrymyers/protoc-gen-twirp_typescript
  2. github.com/thechriswalker/protoc-gen-twirp_js
  3. github.com/Xe/twirp-codegens
і з них тільки один активно підтримується github.com/larrymyers/protoc-gen-twirp_typescript, тому поки grpc-web надійніше
Так будуть казати що і REST так можна

Згоден, описувати Swagger ще те задоволення

Поясни будь-ласка

ну в каналах і горутінах же все го. одна з ідей цієї мови — мати легкоспоживну багатопоточність. Якщо закапуватись то все звісно десь викорстовує СAS і атоміки, але ідея гошки в тому щоб все це заховати, щоб голова не боліла. Але це така тема, можна ніч сперечатись

а про твірп сам дивись, просто в кількох проектах виручив, мені сподобалось, може й тобі припаде десь.

Замість атоміка можна було б всетаки range з канала

Як саме це реалізувати для counter-а? Бо atomic для простого counter-а найркаще підходить

(вже риторичне питання, зрозумів думку в цьому коментарі)

ну одна горутіна генерує послідовність і пише в канал, а хендлер просто читає з канала, я це мав на увазі, чисто технічно так — для простого каунтера підходить найкраще з точки зору скажем зберегти пару викликіів, але це і є якраз підхід яв, дотнетів і олдсукльної багатопоточності, як на мене, нє?

в Go теж практикують підхід збереження пару виклів як тут fastrand, принаймні на проекті перевіряли що швидше писати в канал чи в slice + mutex, вибрали slice + mutex для буферизації перед вставкою в ClickHouse

Це підхід optimization vs microbenchmark
Нема ніякого сенсу на кожному кутку зберігати пару інструкцій процесора для вебаплікухи, мати спагеті з купою локів, які ще потім розбери і відтестуй

Хто пише бази даних чи інше щось cpu-bound — хай, в таких областях

Я навіть придумав як тобі життя ускладнити вже, спробуй напиши такий каунтер але в римських цифрах ;) або не каунтер а прогресію якось, суть втому що тобі вже прийдеться десь залочитись і тримати лок, а канали якраз і дають локи без явних локів

всередині канал go/src/runtime/chan.go використовує atomic

в принципі я здається розумію звідки росте це питання

для каунтера це трохи котрінтуїтивно, бо каунтер простий тип і виглядає наче простіше просто атомік зразу і в принципі для просто каунтера ти прав. Але уяви що в тебе не просто каунтер, а складніша структура, може зі складною логікою, — відповідно тобі треба вже буде синхронізацію робити по структурі, там і м’ютекс підключати треба буде вже. Ідея гошки в тому щоб м’ютекси і всі подібні примітиви по максимуму замінити каналами, бо канали між горутінами дають синхронізацію і дають простіший для розуміння код

Якщо думаєш, що це просто, то на теми цих примітивів і атоміків написані реально тома головастих бородатих дядь, і саме тому мені так подобається Го — бо прийшов Пайк, і зробив всьо простіше борща

Тоді зрозумів, так згоден що канали зрозуміліші для синхронізації між горутинами

Юзав grpc-typescript «в продакшоні».
Прикольна штука, але були недоліки:
— На той час grpc streams лікали пам’ять, тому від них довелось відмовитись.
— Неможливо дебажить через бінарний протокол. Треба впилювать кастомний логгінг.
— Неможливо користувать сторонні клієнти (curl, postman).

Для автогенерації клієнтів можу порекомандувати класний варіант: генерувати сваггерфайл з гошних структур, а з сваггерфайлу генерить клієнти під потрібну мову. Потім ще можна публікувати, якщо апі вийде в паблік.

— Неможливо користувать сторонні клієнти (curl, postman).

Для информации, может Вам будет полезно в работе:
для решения задачи обращения к grpc серверу есть такой инструмент:

github.com/fullstorydev/grpcurl

Что-то вроде curl для grpc, конечно не аналогично, но в работе очень полезно

Про curl уже написали, а вместо postman — Bloom RPC github.com/uw-labs/bloomrpc

Юзав grpc-typescript «в продакшоні»

Зараз вже теж хочу генерувати в TS, бо на JS підказує всі можливі методи, але TS вказаний як експериментальний:

import_style=typescript: (Experimental) The service stub will be generated in TypeScript.

Чи були якісь проблеми з gRPC-web-TypeScript у робочому коді?

Пишучи статтю, я зрозумів, що gRPC web добре підходить для розробки нових і складних проєктів.

Расскажите и нам чем подходит)
Чем лучше/хуже реста/графкуэля?

Порівнюючу з REST-ом я можу в proto-файлах спроектувати весь сервіс, обсудити з командою, а вже потім займатись реалізацією інтерфейсу, без прописування маршрутизації і моделей, бо все вже буде згенеровано автоматично

З GraphQL мені складно порівнювати бо рідко зустрічав його на проектам, але генератор для коду в TypeScript знайшов graphql-code-generator.com

Підписатись на коментарі