Angular und gRPC Web
gRPC hat bisher vor allem im Backend oder bei der direkten Kommunikation zwischen Anwendungen auf dem Endgrät und dem Backend eine Rolle gespielt.
Aufgrund der zunehmenden Rolle von Browseranwendungen sollte gRPC auch in clientseitigen Webanwendungen genutzt werden können.
Das zugehörige Projekt ist gRPC-Web: https://github.com/grpc/grpc-web
In diesem Beitrag wurde die Erstellung eines Spring Boot gRPC Backends demonstriert. Nun get es darum, das Backend in einer Angular Webanwendung mit gRPC-Web anzubinden.
Der erste Schritt ist die Erzeugung der JavaScript oder TypeScript Objekte und Client-Bindings. Falls TypeScript eine Option im Projekt darstellt, sollte darauf zurückgegriffen werden: Durch die Typinformationen ist die Werkzeugunterstützung, beispielsweise in IDEs oder bei statischer Codeanalyse auf potentielle Fehler deutlich besser.
TypeScript Protobuf Bindings
Aus Protobuf Definitionen lassen sich sowohl TypeScript Interfaces, als auch eine grpc-web Zugriffsschicht für die Nutzung in Browser Anwendungen generieren. Am einfachsten erfolgt dies mit einem Docker Image, in dem die benötigten Werkzeuge enthalten sind.
# docker build -t trion/grpc-web-builder .
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y \
automake \
build-essential \
git \
libtool \
make && \
apt-get clean; rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/grpc/grpc-web /github/grpc-web && \
cd /github/grpc-web && \
./scripts/init_submodules.sh && \
cd /github/grpc-web/third_party/grpc && \
make && make install && \
cd /github/grpc-web/third_party/grpc/third_party/protobuf && \
make install && \
cd /github/grpc-web && \
make install-plugin && \
cd / && rm -rf /github
ENV MAKEFLAGS=-j4 import_style=commonjs grpc_web_import_style=commonjs mode=grpcwebtext protofile=echo.proto output=/protofile/generated
VOLUME /protofile
CMD rm -rf $output && \
mkdir $output && \
protoc \
-I=/protofile \
/protofile/$protofile \
--js_out=import_style=$import_style:$output \
--grpc-web_out=import_style=$grpc_web_import_style,mode=$mode:$output
Alternativ kann natürlich ein fertiges Docker Image verwendet werden, oder die Werkzeuge nativ installiert werden. Nun können die Protocol Buffer Definitionen übersetzt werden. Bei Verwendung des Docker Image werden die notwendigen Einstellungen am einfachsten mit Umgebungsvariablen definiert. Dabei wird festgelegt, ob JavaScript oder TypeScript Code erzeugt werden soll, wo sich die Protobuf Beschreibung findet, und welches Transportprotokoll verwendet werden soll. Aktuell wird lediglich grpcwebtext und kein rein binäres Transportformat unterstützt.
$ docker run --rm -u $(id -u) \
-v "$PWD:/protofile" \
-e protofile=Greeter.proto \
-e mode=grpcwebtext \
-e grpc_web_import_style=typescript \
trion/grpc-web-builder
Als Ergebnis finden sich dann im generated
Ordner folgende Dateien
-
Greeter_pb.d.ts
-
Greeter_pb.js
-
GreeterServiceClientPb.ts
Die ersten beiden sind JavaScript und zugehörige TypeScript Informationen zu den Objekten, der GreeterServiceClientPb.ts
ist der Client für den tatsächlichen Zugriff über gRPC Web.
Mit den so erzeugten Dateien kann nun in einem Angular Projekt gearbeitet werden.
Angular Anwendung
Bei der Erstellung der Angular Anwendung kann ebenfalls auf ein Docker Image zurückgegriffen werden, um die Installation von lokalen Werkzeugen zu vermeiden. Alternativ kann natürlich mit Angular-CLI oder einem Angular Seed-Projekt gearbeitet werden.
$ docker run --rm -u $(id -u)\
-v $PWD:/app \
trion/ng-cli ng new GrpcDemo
CREATE GrpcDemo/README.md (1025 bytes)
CREATE GrpcDemo/angular.json (3786 bytes)
...
Um mit Protocol Buffers zu arbeiten, muss die Abhängigkeit google-protobuf
und für gRPC Web die Abhängigkeit grpc-web
ergänzt werden.
Das geschieht wie gewohnt mit npm
- auch hier, falls gewünscht, mit einem Docker Image.
$ cd GrpcDemo
$ docker run --rm -u $(id -u)\
-v $PWD:/app \
trion/ng-cli npm install grpc-web
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: [email protected] (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for [email protected]: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
+ [email protected]
added 1 package from 1 contributor and audited 39129 packages in 7.842s
found 0 vulnerabilities
$ docker run --rm -u $(id -u)\
-v $PWD:/app \
trion/ng-cli npm i google-protobuf
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: [email protected] (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for [email protected]: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
+ [email protected]
added 1 package from 1 contributor and audited 39130 packages in 7.217s
found 0 vulnerabilities
Die Infrastruktur der Angular Anwendung für gRPC-Web ist damit eingerichtet.
Nun fehlt noch eine Einbindung.
In Angular werden sogenannte Services verwendet, um Funktionalität - wie den Zugriff auf eine Remote-API - bereitzustellen, und Components
in der View Schicht zur Darstellung und Interaktion.
Im nächsten Schritt wird daher ein Greeting
-Service und eine Greeter
-Component erzeugt.
Bei Angular kann der erforderliche Code und die Einbindung in das Angular Modul komfortabel durch Angular-CLI erfolgen.
$ docker run --rm -u $(id -u)\
-v $PWD:/app \
trion/ng-cli ng g s greeter
CREATE src/app/greeter.service.spec.ts (338 bytes)
CREATE src/app/greeter.service.ts (136 bytes)
$ docker run --rm -u $(id -u)\
-v $PWD:/app \
trion/ng-cli ng g c greeter
CREATE src/app/greeter/greeter.component.css (0 bytes)
CREATE src/app/greeter/greeter.component.html (26 bytes)
CREATE src/app/greeter/greeter.component.spec.ts (635 bytes)
CREATE src/app/greeter/greeter.component.ts (273 bytes)
UPDATE src/app/app.module.ts (400 bytes)
Verwendung gRPC in Angular
Die mit grpc-web-builder erzeugten Sourcen werden in die Angular Anwendung nach src/app
kopiert.
Dazu kann ruhig der generated
Ordner als solches genutzt werden.
Anschließend können die generierten Klasen im Angular Service regulär importiert werden.
Im Service wird dann auch konfiguriert, wo der gRPC Web Endpunkt angesprochen werden kann.
Natürlich ist hier eine dynamischere Konfiguration, je nach Umebung, als Erweiterung denkbar möglich.
Für das Beispiel wird auf einen festen Port auf localhost
zugegriffen.
import { Injectable } from '@angular/core';
import { Greeting, Person } from './generated/Greeter_pb';
import { GreeterServiceClient } from './generated/GreeterServiceClientPb';
import { Error, Status } from 'grpc-web';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class GreeterService {
private greeterServiceClient: GreeterServiceClient;
constructor() {
this.greeterServiceClient =
new GreeterServiceClient('http://localhost:8090', null, null);
}
callGreet(firstName: string, lastName: string) {
const person = new Person();
person.setFirstName(firstName);
person.setLastName(lastName);
const obs = new Observable<string>(s => {
const call = this.greeterServiceClient.greet(person, null,
(err: Error, greeting: Greeting) => {
s.next(greeting.getMessage());
});
call.on('status', (status: Status) => {
// handle status code
});
});
return obs;
}
}
In der Angular Komponente kann der so erzeugte Angular gRPC Service nun angebunden werden. Die Beispielkomponente erhält ein einfaches Eingabefeld und einen Knopf, um den den gRPC Greeting Dienst Aufruf zu starten.
import { Component } from '@angular/core';
import {GreeterService} from '../greeter.service';
import {Observable} from 'rxjs';
@Component({
selector: 'app-greeter',
template: `
First name: <input type="text" #firstName>
Last name: <input type="text" #lastName>
<button (click)="greet(firstName.value, lastName.value)">Greet!</button>
Greeting: {{greeting | async }}`
})
export class GreeterComponent {
private greeting: Observable<string>;
constructor(private greeterService: GreeterService) { }
greet(firstName: string, lastName: string) {
this.greeting = this.greeterService.callGreet(firstName, lastName);
}
}
Damit die nun erzeugte Komponente auch angezeigt wird, wird diese in dem app.component.html
Template als <app-greeter></app-greeter>
eingebunden.
Anschließend kann mit ng serve
die Angular Anwendung lokal gestartet werden.
$ docker run --rm -u $(id -u)\
-v $PWD:/app \
-p 4200:4200 \
trion/ng-cli ng serve --host 0.0.0.0
Ein Zugriff auf den gRPC Dienst ist jedoch noch nicht direkt möglich: Das gRPC-Web Protokoll ist aktuell noch nicht in den typischen Frameworks integriert.
gRPC Web Gateway Proxy für Spring Boot
Damit ein Zugriff von Web Clients via gRPC Web auf einen gRPC Dienst möglich ist, bedarf es aktuell noch einem zusätzlichen Gateway-Proxy. Diesem obliegt die Konvertierung von gRPC-Web Aufrufen auf das reguläre gRPC Protokoll. Es ist davon auszugehen, dass in absehbarer Zeit eine direkte Unterstützung des gRPC-Web Protokolls in populären Frameworks, wie Spring, implementiert wird.
Als Gateway-Proxy eignet sich beispielsweise Envoy, der vor allem im Kubernetes Umfeld als Teil von Istio Popularität erlangt hat. Envoy bringt gRPC-Web Support von Haus aus mit, lediglich eine Konfiguration zum Mapping von Eingangsparametern auf das richtige gRPC Backend muss vorgenommen werden.
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 8091 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8090 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: greeting_service }
cors:
allow_origin: ["*"]
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web
max_age: "1728000"
expose_headers: grpc-status,grpc-message
enabled: true
http_filters:
- name: envoy.grpc_web
- name: envoy.cors
- name: envoy.router
clusters:
- name: greeting_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
hosts: [{ socket_address: { address: 127.0.0.1, port_value: 6565 }}]
Am einfachsten lässt sich Envoy als Docker Container starten.
Wichtig ist im Test dabei die Konfiguration --net=host
, damit der Zugriff auf die Spring-Boot Anwendung ermöglicht wird, die auf localhost
läuft.
localhost
$ docker run --rm --net=host \ -v $PWD/envoy-config.yaml:/envoy-config.yaml \ envoyproxy/envoy \ /usr/local/bin/envoy -c /envoy-config.yaml [main] [source/server/server.cc:206] initializing epoch 0 (hot restart version=10.200.16384.127.options=capacity=16384, num_slots=8209 hash=228984379728933363 size=2654312) [main] [source/server/server.cc:208] statically linked extensions: [main] [source/server/server.cc:210] access_loggers: envoy.file_access_log,envoy.http_grpc_access_log [main] [source/server/server.cc:213] filters.http: envoy.buffer,envoy.cors,envoy.ext_authz,envoy.fault,envoy.filters.http.header_to_metadata,envoy.filters.http.jwt_authn,envoy.filters.http.rbac,envoy.grpc_http1_bridge,envoy.grpc_json_transcoder,envoy.grpc_web,envoy.gzip,envoy.health_check,envoy.http_dynamo_filter,envoy.ip_tagging,envoy.lua,envoy.rate_limit,envoy.router,envoy.squash [main] [source/server/server.cc:216] filters.listener: envoy.listener.original_dst,envoy.listener.proxy_protocol,envoy.listener.tls_inspector [main] [source/server/server.cc:219] filters.network: envoy.client_ssl_auth,envoy.echo,envoy.ext_authz,envoy.filters.network.dubbo_proxy,envoy.filters.network.rbac,envoy.filters.network.sni_cluster,envoy.filters.network.thrift_proxy,envoy.http_connection_manager,envoy.mongo_proxy,envoy.ratelimit,envoy.redis_proxy,envoy.tcp_proxy [main] [source/server/server.cc:221] stat_sinks: envoy.dog_statsd,envoy.metrics_service,envoy.stat_sinks.hystrix,envoy.statsd [main] [source/server/server.cc:223] tracers: envoy.dynamic.ot,envoy.lightstep,envoy.tracers.datadog,envoy.zipkin [main] [source/server/server.cc:226] transport_sockets.downstream: envoy.transport_sockets.alts,envoy.transport_sockets.capture,raw_buffer,tls [main] [source/server/server.cc:229] transport_sockets.upstream: envoy.transport_sockets.alts,envoy.transport_sockets.capture,raw_buffer,tls [main] [source/server/server.cc:271] admin address: 0.0.0.0:8091 [config] [source/server/configuration_impl.cc:51] loading 0 static secret(s) [config] [source/server/configuration_impl.cc:57] loading 1 cluster(s) [upstream] [source/common/upstream/cluster_manager_impl.cc:136] cm init: all clusters initialized [config] [source/server/configuration_impl.cc:62] loading 1 listener(s) [config] [source/server/configuration_impl.cc:95] loading tracing configuration [config] [source/server/configuration_impl.cc:115] loading stats sink configuration [main] [source/server/server.cc:429] all clusters initialized. initializing init manager [config] [source/server/listener_manager_impl.cc:908] all dependencies initialized. starting workers [main] [source/server/server.cc:457] starting main dispatch loop
Die Spring Boot Anwendung, die den gRPC Dienst bereitstellt, wird im Beispiel über Maven gestartet. (Details zur Anwendung finden sich in In diesem Beitrag zu gRPC mit Spring Boot.)
$ ./mvwn spring-boot:run [INFO] Scanning for projects... [INFO] ------------------------------------------------------------------------ [INFO] Detecting the operating system and CPU architecture [INFO] ------------------------------------------------------------------------ [INFO] os.detected.name: linux [INFO] os.detected.arch: x86_64 [INFO] os.detected.version: 4.18 [INFO] os.detected.version.major: 4 [INFO] os.detected.version.minor: 18 [INFO] os.detected.release: ubuntu [INFO] os.detected.release.version: 18.10 [INFO] os.detected.release.like.ubuntu: true [INFO] os.detected.release.like.debian: true [INFO] os.detected.classifier: linux-x86_64 [INFO] [INFO] -------------------------< de.trion:grpcdemo >-------------------------- [INFO] Building grpc-demo 1.0.0.CI-SNAPSHOT [INFO] --------------------------------[ jar ]--------------------------------- [INFO] ...
Nun kann ein Aufruf der Angular Anwendung über http://localhost:4200
erfolgen.
Nachdem durch die Eingabefelder ein Vorname und Nachname eingegeben wurde, kann der Greeting Dienst aufgerufen werden.
Um sich ein Bild der übermittelten Daten zu machen, kann im Browser die Netzwerkkonsole geöffnet werden.
Ein Beispiel für die Person "Demo User" bekommt als Antwort folgende gRPC Payload: QUFBQUFCSUtFRWhsYkd4dk9pQkVaVzF2
SUZWelpYST1nQUFBQUE5bmNuQmpMWE4wWVhSMWN6b3dEUW89
.
Fazit und gRPC Beispiel Projekt
Das gRPC-Web Projekt ist ein deutlicher Schritt nach vorne und eröffnet neue Architekturoptionen. In diesem Beitrag wurde gezeigt, wie komfortabel die Einbindung in eine Angular Anwendung aussehen kann und dank TypeScript und Protobuf ein typsicherer Umgang mit der API möglich ist. Leider unterstützt gRPC-Web derzeit noch kein Streaming, so dass lediglich Request-Response Abläufe abgebildet werden können. Außerdem fehlt noch die direkte Unterstützung für gRPC-Web in den Anwendungsframeworks, so dass auf ein Gateway-Proxy zurückgegriffen werden muss.
Zu den Themen Spring Boot, Angular und Cloud Architektur bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:
Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.