gRPC-Web 踩坑记

     从张队长的公众号得知 gRPC-Web 发布了,出于对 gRPC 的喜爱,决定周末踩踩坑。
     从 https://github.com/grpc/grpc-dotnet 克隆了代码下来,examples/Browser 这个项目就是 gRPC-Web 的例子。打开 Browser.sln 看一下目录结构。
     整个解决方案只有 Server 这一个项目。


     先看 Protos 文件夹,里面只有一个 greet.proto ,熟悉 gRPC 的都知道,是 gRPC 的接口定义。

     文件中定义了 HelloRequest、HelloReply 消息体,Greeter 服务,及 Greeter服务的两个方法 SayHello SayHellos

我们再打开 Server.csproj 项目文件。

     从图中这句能看出,这个 proto 文件是从父级路径 Link 过来的;而且作为GrpcServices 设为了服务器模式。有了这个设置,且引用了 Grpc.AspNetCore,那么在生成的时候就会生成对应的类和接口。
     我们来看 GreeterService.cs 文件,GreeterService 这个类是 gRPC 接口的实现。

     SayHello 方法简单地在传入参数前面加了个 "Hello "返回了。SayHellos 是个返回服务端流的方法,gRPC-Web 支持服务端流,不支持客户端流和双向流。这个方法也简单的返回了传入参数,前边加了 "Hello " 后面加了序号。

     我们再来看 wwwroot/Scripts 文件夹,里面有 greet_grpc_web_pb.js、greet_pb.js、index.js 三个JS文件。根据官方文档能够得知,greet_grpc_web_pb.js、greet_pb.js 是使用工具根据 greet.proto 生成的 js 包,就像生成的C#类一样。
index.js 是需要手动编写的。

     看格式我们就知道,这个包是需要编译的。(require 只有 nodejs 才支持)  index.js 首先引用了 工具生成的 js 包,然后实例化了一个 GreeterClient 服务。然后绑定了 sendInput 这个按钮的点击事件,

sendInput.onclick = function () {
    var request = new HelloRequest();
    request.setName(nameInput.value);

    client.sayHello(request, {}, (err, response) => {
        resultText.innerHTML = htmlEscape(response.getMessage());
    });
};

     在事件中,先实例化了 HelloRequest 对象 request,设置 name 值,然后调用了服务的 sayHello 方法, request 作为入参,在回调方法里把出参放到 resultText 文本框里。

     那么这个 index.js 是什么时候编译的呢?我们看回 Server.csproj 项目文件。

     这两个扩展编译项,其实功能是差不多的,一个是在“生成(Build)”之前执行,一个是在计算发布文件后执行。功能都是使用 npm 去编译 index.js。编译后会在 wwwroot 文件夹生成 dist/main.js 文件。这个文件是可以在 html 里直接引用、运行的。
     我们再来看 wwwroot/index.html 这里有简单的几个控件,还引用了编译好的 dist/main.js 文件。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>gRPC-Web ASP.NET Core example</title>
</head>
<body>
    <h2>gRPC-Web ASP.NET Core example</h2>
    <div>
        <span>Hello:</span>
        <input id="name" type="text" />
        <input id="send" type="button" value="Send unary" />
        <input id="stream" type="button" value="Start server stream" />
    </div>
    <div>
        <p id="result"></p>
    </div>

    <script type="text/javascript" src="./dist/main.js"></script>
</body>
</html>

     我们 F5 跑起来一下。

     现在流程我们差不多清楚了,那么动动手吧,改造一个 WebApi 项目,试一试是否好用。

     要改造的项目是公司的一个模板项目,有一些基本的框架,有一个读取数据库的列表页。今天改造的目的,是把这个列表页的获取列表数据的方式,由 WebApi 改为 gRPC.Web。
     先引用 Grpc 包,Grpc.AspNetCore  Grpc.AspNetCore.Web

     然后编写 proto 文件。我写了一个 common.proto 文件,代码如下:

syntax = "proto3";
option csharp_namespace = "你的项目的命名空间";

package CIGProtos;

// The greeting service definition.
service CommonRpc {
  // Sends a greeting
  rpc CallApi (RequestMessage) returns (ResponseMessage);
}

message RequestMessage {
  string ApiUrl = 1;
  string ApiParam = 2;
}

message ResponseMessage {
  string ApiResult = 1;
}

     这个 CommonRpc 的目的是代替 WebApi,所以格式上尽量能替换掉现有 WebApi。入参为 RequestMessage,一个 ApiUrl 用于替换现有 WebApi 的地址,ApiParam 替换现有 WebApi 的 Json 参数。出参为 ResponseMessage 是一个 Json 字符串。
CallApi 是调用服务的方法。
     然后我们来实现 gRPC 的接口,新建 CommonRpcService 类,继承 CommonRpc.CommonRpcBase。代码不放了,这个类的执行流程大概是,把服务层的所有类的所有方法,都反射出来,放到一个列表里,然后提供 CallApi 方法,根据参数里的 ApiUrl 属性对方法进行匹配,匹配到了,就从 serviceProvider 里找到实例,然后把参数里的 ApiParam 反序列化为方法参数的类型,进行调用;调用后把出参序列化后返回。

     然后我们打开 Startup.cs 在 ConfigureServices 节加上 services.AddGrpc();
在 Configure 节加上 app.UseGrpcWeb(); 及 endpoints.MapGrpcService<CommonRpcService>().EnableGrpcWeb();

     这样我们服务端就已经写好了,下面是客户端。

     注意,对于使用 nodejs 作为前端的项目,不在本文讨论范围内,其实也只是比本文的方法少了一些步骤。
     我们首先需要根据 common.proto 生成 两个JS文件。这一步需要下载两个工具:
https://github.com/grpc/grpc-web/releases  我下载的是 protoc-gen-grpc-web-1.1.0-windows-x86_64.exe
https://github.com/protocolbuffers/protobuf/releases  注意这第2个,一大堆包,一不小心就下载错了。应该下载 最后面的 protoc-3.12.3-win64.zip 这样的 .zip 前是 操作系统的。

     下载后,放在同一个文件夹下,如果不放在同一个文件夹,需要设置 PATH 环境变量。把 common.proto 也放在些文件夹下,执行命令:
protoc common.proto --js_out=import_style=commonjs:.\ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:.\
     然后就会生成两个 js 文件:common_pb.js  common_grpc_web_pb.js 。然后我们需要编写 index.js ,叫别的名也可。这一步骤对于非前端人员有一个难点,就是如何让前端 javascript 能调用到包里的方法,我就卡在这里两个小时,才查到可以使用把对象附加到 window 的方法。这个方法以前也用过,只不过没想到可以用在这里。 另外,要注意生成的两个 js 文件里的名称和大小写,比如,CommonRpc 要加一个 Client,RequestMessage 的设置ApiUrl 的设置器叫 setApiurl;建议要和生成的两个 js 对照一下。index.js 文件内容如下:

const { RequestMessage, ResponseMessage } = require('./common_pb.js');
const { CommonRpcClient } = require('./common_grpc_web_pb.js');

var CallApi = function (apiUrl, apiParam, callBack) {
    var client = new CommonRpcClient(window.location.origin);
    var request = new RequestMessage();
    request.setApiurl(apiUrl);
    request.setApiparam(apiParam);

    client.callApi(request, {}, (err, response) => {
        callBack(JSON.parse(response.getApiresult()));
    });
}
var model = {
    CallApi: CallApi
};
/**
 * @constructor
 * */
var mygrpc = function () {
    return model;
}
window.mygrpc = mygrpc;

     这个文件我也放在 wwwroot/Scripts 文件夹里。然后我们可以生成一下这个项目,让他执行 npm 的编译操作,当然也可以手动执行一下命令。在 wwwroot 文件夹执行  npm install 然后再执行 npx webpack scripts/index.js ,和生成效果是一样的。
     然后在页面里引用一下生成的 dist/main.js ,再来改页面上的代码,之前的代码为:

var that = this;
$.post("/控制器名/GetPageList",
   this.Query,
   function (data) {
      if (data.result) {
          that.tableData = data.listdata;
          that.pager.total = data.total;
       } else {
           that.$message.error("出错了:" + data.Message);
       }
   });

     页面框架用的 Vue。Query 是查询的信息:包含关键字、分页页数等;那么只需要改为:

var that = this;
new mygrpc().CallApi("/服务名/GetPageList",
   this.Query,
   function (data) {
      if (data.result) {
          that.tableData = data.listdata;
          that.pager.total = data.total;
       } else {
           that.$message.error("出错了:" + data.Message);
       }
   });

     不出意外,就已经能跑起来了。
     好了,还是很简单的吧。
     有几点想总结一下:
1,gRPC-Web 不是 gRPC ,没有利用 Http2。
2,gRPC 不能和 Controller 放在同一项目,但 gRPC-Web 可以。
3,gRPC-Web 提供了一种代替 WebApi 的选择,后台项目之间的调用推荐 gRPC 不推荐 gRPC-Web。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章