前端 JS/TS 呼叫 ASP.NET Core gRPC-Web
前言
在上兩篇文章中,介紹了ASP.NET Core 中的 gRPC-Web 實現 和 在 Blazor WebAssembly 中使用 gRPC-Web,實現了 Blazor WebAssembly 呼叫 ASP.NET Core gRPC-Web。雖然 ASP.NET Core 中的 gRPC-Web 實現目前還是試驗性專案,但是鑑於它在生態上的重大意義,說不定我們很快就能在正式版本中使用。
雖然 Blazor WebAssembly 現在已經是 .NET 進軍前端的大熱門,但有同學說,只介紹了 Blazor WebAssembly 的呼叫方法還不夠呀,現在比較常用的還是 JS/TS 前端,那麼本篇,我就介紹一下在前端 JS/TS 中呼叫 ASP.NET Core gRPC-Web。
其實 gRPC-Web 專案本身,就是為 JS/TS 提供 gRPC 能力的,讓不支援 HTTP/2 的客戶端和服務端也能使用 gRPC 的大部分特性。gRPC-Web 專案提供了一個 protoc CLI 外掛,可用於把 proto 協議檔案轉換為 JS/TS 語言可匯入的對應 gRPC 服務的客戶端,還生成了 .d.ts
檔案來支援 Typescript。
示例
接下來,我就來展示一下,用 Visual Studio 自帶的 ASP.NET Core + Angular 模板建立的專案,把原來的 WebApi 呼叫改造成 gRPC-Web 呼叫。
本示例基於 .NET Core 3.1,請安裝好最新的 .NET Core SDK 和 Visual Studio 2019。
建立專案
開啟 Visual Studio 2019,建立新專案 -> 選擇"ASP.NET Core Web 應用程式" -> 填寫專案名 -> 選擇 "Angular" 專案模板。如圖:
我們就是用這個專案,把 fetch-data 頁面獲取資料的方式修改為 gRPC-Web 。
新增 gRPC proto 檔案
在專案中新建一個目錄 Protos
,建立檔案 weather.proto
:
syntax = "proto3"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; option csharp_namespace = "AspNetCoreGrpcWeb"; package WeatherForecast; service WeatherForecasts { rpc GetWeather (google.protobuf.Empty) returns (WeatherReply); } message WeatherReply { repeated WeatherForecast forecasts = 1; } message WeatherForecast { google.protobuf.Timestamp dateTimeStamp = 1; int32 temperatureC = 2; int32 TemperatureF = 3; string summary = 4; }
可以看到 proto 中匯入了官方庫的其他 proto 檔案,它們是編譯用的輔助檔案。對於.NET Core 專案,可以通過引用 Google.Protobuf
這個包引入。
修改 ASP.NET Core 服務端
我們先修改服務端,讓 ASP.NET Core 提供 gRPC-Web 服務。
由於 gRPC-Web 包還沒有釋出到 NuGet.org,現在你需要新增一個臨時的包管理源來獲得 nightly 預覽。在你的解決方案的根目錄下新增NuGet.config
檔案:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="gRPC-nightly" value="https://grpc.jfrog.io/grpc/api/nuget/v3/grpc-nuget-dev" />
</packageSources>
</configuration>
再新增必要的 Nuget 包引用:
<PackageReference Include="Grpc.AspNetCore" Version="2.27.0-dev202001100801" />
<PackageReference Include="Grpc.AspNetCore.Web" Version="2.27.0-dev202001100801" />
接著,修改原來的 WeatherForecastController
改為 WeatherForecastService
:
public class WeatherForecastsService : WeatherForecasts.WeatherForecastsBase
{
private static readonly string[] Summaries = {
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public override Task<WeatherReply> GetWeather(Empty request, ServerCallContext context)
{
var reply = new WeatherReply();
var rng = new Random();
reply.Forecasts.Add(Enumerable.Range(1, 5).Select(index =>
{
var temperatureC = rng.Next(-20, 55);
return new WeatherForecast
{
DateTimeStamp = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(index)),
TemperatureC = temperatureC,
TemperatureF = 32 + (int)(temperatureC / 0.5556),
Summary = Summaries[rng.Next(Summaries.Length)]
};
}));
return Task.FromResult(reply);
}
}
現在,在你的伺服器的 Startup.cs
檔案中,修改 ConfigureServices
以新增以下行:
services.AddGrpc();
注意:如果你只打算公開 gRPC 服務,你可能不再需要 MVC 控制器,在這種情況下,你可以從下面刪除services.AddMvc()
和 endpoints.MapDefaultControllerRoute()
。
只是在 app.AddRouting();
的下面新增以下內容,它會處理將傳入的 gRPC-web 請求對映到服務端,使其看起來像 gRPC 請求:
app.UseGrpcWeb();
最後,在app.UseEndpoints
語句塊中註冊你的 gRPC-Web 服務類,並在該語句塊的頂部使用以下程式碼行:
endpoints.MapGrpcService<WeatherForecastsService>().EnableGrpcWeb();
就這樣,你的 gRPC-Web 服務端已經準備好了!
修改 Angular 專案
專案的 ClientApp
目錄中,就是 Angular 的專案檔案,使用 ASP.NET Core 託管,是這個專案模板約定的。
我們需要先通過 proto
生成需要的 js 檔案。
安裝 npm 包
執行命令:
npm i protoc google-protobuf ts-protoc-gen @improbable-eng/grpc-web -s
生成 js 檔案
在 ClientApp 目錄下建立目錄 proto
,再執行命令:
./node_modules/protoc/protoc/bin/protoc --plugin="protoc-gen-ts=.\node_modules\.bin\protoc-gen-ts.cmd" --js_out="import_style=commonjs,binary:./../Protos" --ts_out="service=grpc-web:src/app/proto" -I ./../Protos ../Protos/*.proto
可以在proto
目錄中看到生成了 4 個檔案(每個 proto 會生成 4 個)
weather_pb_service.js
: 包含了 rpc 呼叫客戶端WeatherForecastsClient
。weather_pb.js
: 包含了傳輸物件WeatherForecast
。這個檔案在原 proto 的目錄下,需要手動移過來。- 兩個
*.d.ts
檔案是對應以上兩個檔案的型別描述,用於 TS。
修改 fetch-data 頁面元件
接下來,需要引用生成的檔案,建立一個 WeatherForecastsClient
來呼叫 gRPC-Web 服務端。
fetch-data.component.ts
import { Component } from '@angular/core'; import { WeatherForecast } from '../proto/weather_pb'; import { WeatherForecastsClient } from '../proto/weather_pb_service'; import { Empty } from 'google-protobuf/google/protobuf/empty_pb'; @Component({ selector: 'app-fetch-data', templateUrl: './fetch-data.component.html', }) export class FetchDataComponent { public forecasts: WeatherForecast[]; private client: WeatherForecastsClient; constructor() { this.client = new WeatherForecastsClient('https://localhost:5001'); this.client.getWeather(new Empty(), (error, reply) => { if (error) { console.error(error); } this.forecasts = reply.getForecastsList(); }); } }
fetch-data.component.html
<h1 id="tableLabel">Weather forecast</h1> <p>This component demonstrates fetching data from the server.</p> <p *ngIf="!forecasts"><em>Loading...</em></p> <table class="table table-striped" aria-labelledby="tableLabel" *ngIf="forecasts"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> <tr *ngFor="let forecast of forecasts"> <td>{{ forecast.getDatetimestamp().toDate() }}</td> <td>{{ forecast.getTemperaturec() }}</td> <td>{{ forecast.getTemperaturef() }}</td> <td>{{ forecast.getSummary() }}</td> </tr> </tbody> </table>
可以看到:
- 建立
WeatherForecastsClient
物件需要傳入服務端的 HostName,要注意不要用/
字尾。 - 生成出來的
WeatherForecast
型別包含getter/setter
, 而WeatherForecast.AsObject
類才是值物件,可以直接訪問屬性值,需要呼叫.toObject()
方法進行轉換。 datetimestamp
屬性的型別proto.google.protobuf.Timestamp
是 protobuf 裡的關鍵字,呼叫toDate()
方法可轉換為 TS 的Date
型別。
執行專案
完事具備,我們可以執行專案了。訪問 https://localhost:5001/fetch-data
,就可以看到前端是通過 gRPC-Web 獲取資料了。
總結
可以看出,要在前端 JS/TS 使用 gRPC-Web 雖然在開發工具上沒有 Blazor WebAssembly 方便,但是從 proto
生成客戶端之後,前端的 TS 程式碼就能直接獲得強型別的呼叫方法和路由,很簡單地得到 gRPC 帶來的好處。另外,gRPC-Web 專案本身已經 GA,所以我們可以先在後端使用它的 gRPC 代理,而前端可以放心大膽地在我們的生產專案中使用它。等到 ASP.NET Core 正式支援 gRPC-Web 後,就可以不需要代理了,比其他平臺和語言都更有優勢。
本示例的原始碼已釋出到 Github:https://github.com/ElderJames/AspNetCoreGrpc