gRPC-Web 踩坑記

     從張隊長的公衆號得知 gRPC-Web 發佈了,出於對 gRPC 的喜好,決定週末踩踩坑。
     從 https://github.com/grpc/grpc-dotnet 克隆了代碼下來,examples/Browser 這個項目就是 gRPC-Web 的例子。打開 Browser.sln 看一下目錄結構。
     整個解決方案只有 Server 這一個項目。javascript

先看 Protos 文件夾,裏面只有一個 greet.proto ,熟悉 gRPC 的都知道,是 gRPC 的接口定義。html

     文件中定義了 HelloRequest、HelloReply 消息體,Greeter 服務,及 Greeter服務的兩個方法 SayHello SayHellos前端

咱們再打開 Server.csproj 項目文件。java

     從圖中這句能看出,這個 proto 文件是從父級路徑 Link 過來的;並且做爲GrpcServices 設爲了服務器模式。有了這個設置,且引用了 Grpc.AspNetCore,那麼在生成的時候就會生成對應的類和接口。
     咱們來看 GreeterService.cs 文件,GreeterService 這個類是 gRPC 接口的實現。node

     SayHello 方法簡單地在傳入參數前面加了個 "Hello "返回了。SayHellos 是個返回服務端流的方法,gRPC-Web 支持服務端流,不支持客戶端流和雙向流。這個方法也簡單的返回了傳入參數,前邊加了 "Hello " 後面加了序號。webpack

     咱們再來看 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 是須要手動編寫的。git

     看格式咱們就知道,這個包是須要編譯的。(require 只有 nodejs 才支持)  index.js 首先引用了 工具生成的 js 包,而後實例化了一個 GreeterClient 服務。而後綁定了 sendInput 這個按鈕的點擊事件,github

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 文本框裏。web

     那麼這個 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。

相關文章
相關標籤/搜索