Added multiple proxies interoperability

This commit is contained in:
Stanley Cheung 2018-08-03 23:49:02 -07:00
parent 712d78461e
commit a9aa7dffb0
17 changed files with 270 additions and 37 deletions

View File

@ -93,6 +93,9 @@ plugin:
example: plugin
cd "$(ROOT_DIR)"/net/grpc/gateway/examples/echo && make
standalone-proxy: package
cd "$(ROOT_DIR)"/net/grpc/gateway/examples/echo && make standalone-proxy
echo_server:
cd "$(ROOT_DIR)"/net/grpc/gateway/examples/echo && make echo_server

View File

@ -28,13 +28,13 @@ Try gRPC-Web and run a quick Echo example from the browser!
From the repo root directory:
```sh
$ docker-compose up
$ docker-compose up echo-server envoy commonjs-client-example
```
Open a browser tab, and inspect
```
http://localhost/net/grpc/gateway/examples/echo/echotest.html
http://localhost:8081/echo_commonjs_test.html
```
## How it works
@ -99,3 +99,24 @@ stream.on('data', function(response) {
console.log(response.getMessage());
});
```
## Proxy interoperability
Multiple proxies supports the gRPC-Web protocol. Currently, the default proxy
is [Envoy](https://www.envoyproxy.io).
```
$ docker-compose up echo-server envoy commonjs-client-example
```
An alternative is to build Nginx that comes with this repository.
```
$ docker-compose up echo-server nginx commonjs-client-example
```
Finally, you can also try this [gRPC-Web Go Proxy](https://github.com/improbable-eng/grpc-web/tree/master/go/grpcwebproxy).
```
$ docker-compose up echo-server grpcwebproxy binary-client-example
```

View File

@ -23,6 +23,24 @@ services:
- "8080:8080"
links:
- echo-server
grpcwebproxy:
build:
context: ./
dockerfile: ./net/grpc/gateway/docker/grpcwebproxy/Dockerfile
image: grpc-web:grpcwebproxy
ports:
- "8080:8080"
links:
- echo-server
nginx:
build:
context: ./
dockerfile: ./net/grpc/gateway/docker/nginx/Dockerfile
image: grpc-web:nginx
ports:
- "8080:8080"
links:
- echo-server
static-assets:
build:
context: ./
@ -41,3 +59,12 @@ services:
image: grpc-web:commonjs-client-example
ports:
- "8081:8081"
binary-client-example:
build:
context: ./
dockerfile: ./net/grpc/gateway/docker/binary_client_example/Dockerfile
depends_on:
- prereqs
image: grpc-web:binary-client-example
ports:
- "8081:8081"

18
etc/localhost.crt Normal file
View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC9jCCAd4CCQCfXxHXagE8mjANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJV
UzELMAkGA1UECAwCQ0ExITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0
ZDAeFw0xODA4MDMyMTE2NDdaFw0yMTA1MjMyMTE2NDdaMD0xCzAJBgNVBAYTAlVT
MQswCQYDVQQIDAJDQTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2/MlKj+OtIgJm/7DywOR
POypfvGHXqHTpg/ZbZgflx2vMwgoAhdun2e//AlssouStadnkevPEr+uFfxkEzH3
80iYDtcZKXY8E6692hFrp7hKnA7gcBbb7ZQ1FwG/SfKLtLcderLcQb51P7IsQkfh
nB8hSosV9nHhdfVtsMW7L/caqB5lUHIbRsHhSw3+hzg0r0HuKxXd5HlyRXzf9cQX
4xc5B8Ldxo3QmXDOUHDw9quuxvUn5VWppWCGn2J+f9L/5iwgciApbiMBv/CkiVrt
iYwZY+TZY5u8lmL4FtLd2tj2vNXl5ESWcL1SRGSiaYmxX1B5rg4fSAAXmcNOzZHo
8wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCHko8eFag++9knWV8KlRRi+IdGeatU
TejdBVnCPFc7sJf1lkUQSb0mZMv0QEO51aXGVvU46pIjTAwtzcVgPc6ZHqcZY4r3
xscrmECThbhsEQCHqDD55OB2a06bx+ylfbBnLh+F18W+/rI+HlRxSBGclyfVto1P
aCuYvYc0qKK90Ft1joZh1tXpho/D52B4CTa0Ax/5UqSVjTt0uPDhkCZJKnoENVgh
6hF8ehYTC6Kf6ZtbB6+GuaLXf6F96CROLifW219qxrKmGbMyJXolOxLatufnWwwv
Hw7z1FUzulJUkSRmgPJ9hFeyTjCS1BJ18glFjleLykYOtQi8kvnpFm6C
-----END CERTIFICATE-----

27
etc/localhost.key Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA2/MlKj+OtIgJm/7DywORPOypfvGHXqHTpg/ZbZgflx2vMwgo
Ahdun2e//AlssouStadnkevPEr+uFfxkEzH380iYDtcZKXY8E6692hFrp7hKnA7g
cBbb7ZQ1FwG/SfKLtLcderLcQb51P7IsQkfhnB8hSosV9nHhdfVtsMW7L/caqB5l
UHIbRsHhSw3+hzg0r0HuKxXd5HlyRXzf9cQX4xc5B8Ldxo3QmXDOUHDw9quuxvUn
5VWppWCGn2J+f9L/5iwgciApbiMBv/CkiVrtiYwZY+TZY5u8lmL4FtLd2tj2vNXl
5ESWcL1SRGSiaYmxX1B5rg4fSAAXmcNOzZHo8wIDAQABAoIBAFPnJL5BEIb9fezr
+nRvH/BFt0KdkC4hPUOTuDV+Wk6jHDozWk+x8JkOUsYqMjTJ2WVCPtgDRDK6vAXX
CbXo0dUUVC0VEJwoZjJ77iBJlO+d9ZgidKtNjQfMCZSFLhtfUrvVPoGXyT2rEb8C
kK+YDBAqL+DnvbENMBx3SyirxQQ+YetAUSxiZagtjKlax1bhXF/JCj816ezDqOzR
ZVx4MJiJg3oD+zKlwP+IaFlANIuW2W7+LbNzPpdXER4gafRzyjRy5ksO9NFO11Bu
2srJbAMZEr0MCEBjf5rD7CPuvhTTEcgk6CNPsIEt8zzMSZUMeS0xaIv0W1FKiw+R
BENntsECgYEA84fSMRApKaweeBrthPKR1ucnv3EHxn1l1mz45bp+2euHB6jUul9D
629mMv8J9PVdcPF5ck7YpCjslWm6oOhtKMemuwJm61cRF44Lz7jtLm3zNJMGhpbh
6Q0GoIVcyMrW79BODbEIU5SlWp0Usyjo/4CZEP5adho1JNTDHCyvEVUCgYEA5zY6
tbfu+YYgICBnaRkGgdCRBxMurxdPrgbwvrKSMerYE9fufpgvZc5wwx02rJX0psuo
JNGLAkPJyimZCYhY/hxWwXQX8X4BhkKK2aFyBMaDIBA0h+unOwTxHrebQNprot3h
YVT8+tfZf8bGPl+dBs3Qf+WOESjRSyO9jr5BMScCgYBfI4mPF1Qtbot8unBeRvGI
tkeF999kwOp/CZV3EhOqiOP4rxFkOgFrwdp4Q8CdDRpTHFMov/rMrxw2BtcdM5Ap
pU3Ss06H1DzeKeUdYo5uXA/uUx3yiJF7HVagcVldLDkp+QP1P1sUY/bxXnqOv4W/
A3tI80Vd7EEkwWXz5NUD/QKBgGXhYXFdQTI2RcWiQa7v1gwxqRYi/7krXnLioAaH
jR/tyZTE21RxHsGPe+Sd5M+brBgrOUYwBz7SPAKW3dZzfDNMrXXFAB/rVCSjAafw
Gdu81V61hVA3KJM7FDxiz0h+dltnxb4rwuWNY0uIfSZS31B2NF+G+VjaUY74irhx
YSyVAoGANrn70s5+cJDWcmDhaFNU/J/X2Q8GgTyBRd02FIvuv7BXgd9TZ7bUVTws
1nsQCCEVJqj4ksddVRq33NvsnjrgetGYe1LGl3uLakqaXd7iJXFCice3ZuFFrp9o
Iq2sUgG+9K8WFqNhANRKBVd32IpQzjIMAAJSbuG3EFZDLZJqxDs=
-----END RSA PRIVATE KEY-----

View File

@ -165,7 +165,7 @@ void PrintFileHeader(Printer* printer, const std::map<string, string>& vars) {
}
void PrintServiceConstructor(Printer* printer,
const std::map<string, string>& vars) {
std::map<string, string>& vars) {
printer->Print(
vars,
"/**\n"
@ -181,6 +181,14 @@ void PrintServiceConstructor(Printer* printer,
" /**\n"
" * @private @const {!grpc.web.$mode$ClientBase} The client\n"
" */\n"
" if (!options) options = {};\n");
if (vars["mode"] == GetModeVar(Mode::GRPCWEB)) {
printer->Print(
vars,
" options['format'] = '$format$';\n\n");
}
printer->Print(
vars,
" this.client_ = new grpc.web.$mode$ClientBase(options);\n\n"
" /**\n"
" * @private @const {string} The hostname\n"
@ -357,8 +365,9 @@ class GrpcCodeGenerator : public CodeGenerator {
vars["mode"] = GetModeVar(Mode::OP);
} else if (mode == "base64") {
vars["mode"] = GetModeVar(Mode::GATEWAY);
} else if (mode == "grpcweb") {
} else if (mode == "grpcweb" || mode == "grpcwebtext") {
vars["mode"] = GetModeVar(Mode::GRPCWEB);
vars["format"] = (mode == "grpcweb") ? "binary" : "text";
} else if (mode == "jspb") {
vars["mode"] = GetModeVar(Mode::OPJSPB);
} else if (mode == "frameworks") {

View File

@ -43,6 +43,13 @@ const googCrypt = goog.require('goog.crypt.base64');
* @implements {AbstractClientBase}
*/
const GrpcWebClientBase = function(opt_options) {
/**
* @const
* @private {string}
*/
this.format_ =
goog.getObjectByName('format', opt_options) || "text";
/**
* @const
* @private {boolean}
@ -58,8 +65,6 @@ const GrpcWebClientBase = function(opt_options) {
GrpcWebClientBase.prototype.rpcCall = function(
method, request, metadata, methodInfo, callback) {
var xhr = this.newXhr_();
var serialized = methodInfo.requestSerializeFn(request);
xhr.headers.addAll(metadata);
var genericTransportInterface = {
xhr: xhr,
@ -89,18 +94,28 @@ GrpcWebClientBase.prototype.rpcCall = function(
}
});
xhr.headers.set('Content-Type', 'application/grpc-web-text');
xhr.headers.addAll(metadata);
if (this.format_ == "text") {
xhr.headers.set('Content-Type', 'application/grpc-web-text');
xhr.headers.set('Accept', 'application/grpc-web-text');
} else {
xhr.headers.set('Content-Type', 'application/grpc-web+proto');
}
xhr.headers.set('X-User-Agent', 'grpc-web-javascript/0.1');
xhr.headers.set('Accept', 'application/grpc-web-text');
var payload = this.encodeRequest_(serialized);
payload = googCrypt.encodeByteArray(payload);
xhr.headers.set('X-Grpc-Web', '1');
if (this.suppressCorsPreflight_) {
var headerObject = xhr.headers.toObject();
xhr.headers.clear();
method = GrpcWebClientBase.setCorsOverride_(method, headerObject);
}
var serialized = methodInfo.requestSerializeFn(request);
var payload = this.encodeRequest_(serialized);
if (this.format_ == "text") {
payload = googCrypt.encodeByteArray(payload);
} else if (this.format_ == "binary") {
xhr.setResponseType(XhrIo.ResponseType.ARRAY_BUFFER);
}
xhr.send(method, 'POST', payload);
return;
};
@ -112,8 +127,6 @@ GrpcWebClientBase.prototype.rpcCall = function(
GrpcWebClientBase.prototype.serverStreaming = function(
method, request, metadata, methodInfo) {
var xhr = this.newXhr_();
var serialized = methodInfo.requestSerializeFn(request);
xhr.headers.addAll(metadata);
var genericTransportInterface = {
xhr: xhr,
@ -121,18 +134,28 @@ GrpcWebClientBase.prototype.serverStreaming = function(
var stream = new GrpcWebClientReadableStream(genericTransportInterface);
stream.setResponseDeserializeFn(methodInfo.responseDeserializeFn);
xhr.headers.set('Content-Type', 'application/grpc-web-text');
xhr.headers.addAll(metadata);
if (this.format_ == "text") {
xhr.headers.set('Content-Type', 'application/grpc-web-text');
xhr.headers.set('Accept', 'application/grpc-web-text');
} else {
xhr.headers.set('Content-Type', 'application/grpc-web+proto');
}
xhr.headers.set('X-User-Agent', 'grpc-web-javascript/0.1');
xhr.headers.set('Accept', 'application/grpc-web-text');
var payload = this.encodeRequest_(serialized);
payload = googCrypt.encodeByteArray(payload);
xhr.headers.set('X-Grpc-Web', '1');
if (this.suppressCorsPreflight_) {
var headerObject = xhr.headers.toObject();
xhr.headers.clear();
method = GrpcWebClientBase.setCorsOverride_(method, headerObject);
}
var serialized = methodInfo.requestSerializeFn(request);
var payload = this.encodeRequest_(serialized);
if (this.format_ == "text") {
payload = googCrypt.encodeByteArray(payload);
} else if (this.format_ == "binary") {
xhr.setResponseType(XhrIo.ResponseType.ARRAY_BUFFER);
}
xhr.send(method, 'POST', payload);
return stream;

View File

@ -40,6 +40,7 @@ const XhrIo = goog.require('goog.net.XhrIo');
const XmlHttp = goog.require('goog.net.XmlHttp');
const events = goog.require('goog.events');
const googCrypt = goog.require('goog.crypt.base64');
const googString = goog.require('goog.string');
const {GenericTransportInterface} = goog.require('grpc.web.GenericTransportInterface');
const {Status} = goog.require('grpc.web.Status');
@ -114,18 +115,26 @@ const GrpcWebClientReadableStream = function(genericTransportInterface) {
var self = this;
events.listen(this.xhr_, EventType.READY_STATE_CHANGE,
function(e) {
var FrameType = GrpcWebStreamParser.FrameType;
var contentType = self.xhr_.getStreamingResponseHeader('Content-Type');
if (!contentType) return;
contentType = contentType.toLowerCase();
var responseText = self.xhr_.getResponseText();
var newPos = responseText.length - responseText.length % 4;
var newData = responseText.substr(self.pos_, newPos - self.pos_);
if (newData.length == 0) return;
self.pos_ = newPos;
var byteSource = googCrypt.decodeStringToUint8Array(newData);
if (googString.startsWith(contentType, 'application/grpc-web-text')) {
var responseText = self.xhr_.getResponseText();
var newPos = responseText.length - responseText.length % 4;
var newData = responseText.substr(self.pos_, newPos - self.pos_);
if (newData.length == 0) return;
self.pos_ = newPos;
var byteSource = googCrypt.decodeStringToUint8Array(newData);
} else if (googString.startsWith(contentType, 'application/grpc')) {
var byteSource = new Uint8Array(self.xhr_.getResponse());
} else {
return;
}
var messages = self.parser_.parse([].slice.call(byteSource));
if (!messages) return;
var FrameType = GrpcWebStreamParser.FrameType;
for (var i = 0; i < messages.length; i++) {
if (FrameType.DATA in messages[i]) {
var data = messages[i][FrameType.DATA];

View File

@ -0,0 +1,40 @@
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM grpc-web:prereqs
ARG EXAMPLE_DIR=/github/grpc-web/net/grpc/gateway/examples/echo
RUN cd /github/grpc-web/packages/grpc-web && \
rm -rf node_modules && \
npm install && \
npm run build && \
npm link
RUN protoc -I=$EXAMPLE_DIR echo.proto \
--plugin=protoc-gen-grpc-web=\
/github/grpc-web/javascript/net/grpc/web/protoc-gen-grpc-web \
--js_out=import_style=commonjs:\
$EXAMPLE_DIR/commonjs-example \
--grpc-web_out=import_style=commonjs,mode=grpcweb,out=echo_grpc_pb.js:\
$EXAMPLE_DIR/commonjs-example
RUN cd $EXAMPLE_DIR/commonjs-example && \
rm -rf node_modules && \
npm install && \
npm link grpc-web && \
./node_modules/.bin/browserify client.js > out.js
EXPOSE 8081
CMD ["nginx"]

View File

@ -16,17 +16,24 @@ FROM grpc-web:prereqs
ARG EXAMPLE_DIR=/github/grpc-web/net/grpc/gateway/examples/echo
RUN cd /github/grpc-web/packages/grpc-web && \
rm -rf node_modules && \
npm install && \
npm run build && \
npm link
RUN protoc -I=$EXAMPLE_DIR echo.proto \
--plugin=protoc-gen-grpc-web=\
/github/grpc-web/javascript/net/grpc/web/protoc-gen-grpc-web \
--js_out=import_style=commonjs:\
$EXAMPLE_DIR/commonjs-example \
--grpc-web_out=import_style=commonjs,mode=grpcweb,out=echo_grpc_pb.js:\
--grpc-web_out=import_style=commonjs,mode=grpcwebtext,out=echo_grpc_pb.js:\
$EXAMPLE_DIR/commonjs-example
RUN cd $EXAMPLE_DIR/commonjs-example && \
rm -rf node_modules && \
npm install && \
npm link grpc-web && \
./node_modules/.bin/browserify client.js > out.js
EXPOSE 8081

View File

@ -0,0 +1,20 @@
FROM golang:alpine
RUN apk add --no-cache git ca-certificates && \
rm -rf /var/lib/apt/lists/*
RUN go get -v -u github.com/golang/dep/cmd/dep
RUN go get -v github.com/improbable-eng/grpc-web/go/grpcwebproxy ; exit 0
RUN cd src/github.com/improbable-eng/grpc-web && dep ensure
RUN go get -v -u github.com/improbable-eng/grpc-web/go/grpcwebproxy
ADD ./etc/localhost.crt /etc
ADD ./etc/localhost.key /etc
ENTRYPOINT [ "/bin/sh", "-c", "exec /go/bin/grpcwebproxy \
--backend_addr=echo-server:9090 \
--server_bind_address=0.0.0.0 \
--server_http_debug_port=8080 \
--run_http_server=true \
--server_tls_cert_file=/etc/localhost.crt \
--server_tls_key_file=/etc/localhost.key " ]

View File

@ -0,0 +1,21 @@
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM grpc-web:prereqs
RUN cd /github/grpc-web && \
make standalone-proxy
EXPOSE 8080
CMD ["/github/grpc-web/gConnector/nginx.sh"]

View File

@ -25,6 +25,7 @@ RUN apt-get update && apt-get install -y \
default-jdk \
default-jre \
libtool \
libpcre3-dev \
libssl-dev \
make \
nginx \
@ -33,9 +34,11 @@ RUN apt-get update && apt-get install -y \
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
apt-get install -y nodejs
RUN git clone https://github.com/grpc/grpc-web /github/grpc-web
COPY . /github/grpc-web
RUN cd /github/grpc-web && \
rm -rf third_party && \
git checkout third_party && \
./scripts/init_submodules.sh
RUN cd /github/grpc-web/third_party/grpc && \

View File

@ -41,6 +41,11 @@ package:
cp run_example.sh $(ROOT_DIR)/gConnector
zip -r $(ROOT_DIR)/gConnector.zip $(ROOT_DIR)/gConnector/*
standalone-proxy:
mkdir -p $(ROOT_DIR)/gConnector/$(EXAMPLES_PATH)
cp nginx.conf $(ROOT_DIR)/gConnector/conf
zip -r $(ROOT_DIR)/gConnector.zip $(ROOT_DIR)/gConnector/*
echo_server: echo.pb.o echo.grpc.pb.o echo_server.o echo_service_impl.o
$(CXX) $^ $(LDFLAGS) -o $@
@ -86,7 +91,7 @@ proto-js:
$(PROTOS_PATH)/protos/pair.proto
$(PROTOC) -I=. --js_out=$(JS_IMPORT_STYLE):$(OUT_DIR) ./echo.proto
$(PROTOC) -I=. --plugin=protoc-gen-grpc-web=$(GRPC_WEB_PLUGIN_PATH) \
--grpc-web_out=out=$(OUT_DIR)/echo.grpc.pb.js,mode=grpcweb:. ./echo.proto
--grpc-web_out=out=$(OUT_DIR)/echo.grpc.pb.js,mode=grpcwebtext:. ./echo.proto
install:
mkdir -p $(HTML_DIR)/$(EXAMPLES_PATH)

View File

@ -21,11 +21,11 @@ http {
root /var/www/html;
}
location / {
grpc_pass localhost:9090;
grpc_pass echo-server:9090;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Transfer-Encoding,Custom-Header-1,X-Accept-Content-Transfer-Encoding,X-Accept-Response-Streaming,X-User-Agent';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Transfer-Encoding,Custom-Header-1,X-Accept-Content-Transfer-Encoding,X-Accept-Response-Streaming,X-User-Agent,X-Grpc-Web';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
@ -34,7 +34,7 @@ http {
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Transfer-Encoding,Custom-Header-1,X-Accept-Content-Transfer-Encoding,X-Accept-Response-Streaming,X-User-Agent';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Transfer-Encoding,Custom-Header-1,X-Accept-Content-Transfer-Encoding,X-Accept-Response-Streaming,X-User-Agent,X-Grpc-Web';
add_header 'Access-Control-Expose-Headers' 'Content-Transfer-Encoding';
}
}

View File

@ -99,13 +99,13 @@ To generate the client stub JS file, now run this command:
```sh
$ protoc -I=. --plugin=protoc-gen-grpc-web=<path to plugin> \
--grpc-web_out=out=echo.grpc.pb.js,mode=grpcweb:. ./echo.proto
--grpc-web_out=out=echo.grpc.pb.js,mode=grpcwebtext:. ./echo.proto
```
The format for the `--grpc-web_out` param is
```sh
--grpc-web_out=out=<filename>,mode=grpcweb:<output dir>
--grpc-web_out=out=<filename>,mode=grpcwebtext:<output dir>
```
Our command generates the client stub in the file `echo.grpc.pb.js`.

View File

@ -38,7 +38,7 @@ This example is using the `echo.proto` file from the [Echo Example](https://gith
2. Generate your client with `protoc` and the `protoc-gen-grpc-web` plugin. Make sure you set the import_style for both `js_out` and `grpc-web_out` to **commonjs**. It is also important that both your js and grpc-web output to the same directory.
```shell
protoc echo.proto --js_out=import_style=commonjs:generated --grpc-web_out=import_style=commonjs,mode=grpcweb,out=echo_grpc_pb.js:generated
protoc echo.proto --js_out=import_style=commonjs:generated --grpc-web_out=import_style=commonjs,mode=grpcwebtext,out=echo_grpc_pb.js:generated
```
3. Start using your generated client!