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。

 

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