引入gRPC
gRPC是Google开源的高性能RPC框架,它使用二进制Proto Buffer数据序列化替换JSON格式。本章我们会引入该框架,并且将在webview与vscode extension host之间消息通信中使用gRPC。
添加需要的以来包
package.json
...
"devDependencies": {
...
"globby": "^14.0.2",
"chalk": "5.6.2",
"grpc-tools": "^1.13.0",
"ts-proto": "^2.6.1"
},
"dependencies": {
...
"@grpc/grpc-js": "^1.9.15"
}
webview-ui/package.json
"dependencies": {
...
"uuid": "^9.0.1"
},
"devDependencies": {
...
"@types/uuid": "^9.0.8"
}
运行npm run install:all安装依赖
添加编译Proto Buffer的脚本
根目录下添加scripts目录,从cline目录拷贝以下四个mjs文件过来进行裁剪(目前我们不需要nice-grpc和host-bridge相关内容, 可以直接查看minicline中已经改好的mjs文件内容)。
在package.json中添加一个新的protos命令如下。
"scripts": {
...
"lint": "eslint src --ext ts",
"protos": "node scripts/build-proto.mjs"
},
运行npm run protos会有如下报错信息,不要紧,马上我们会添加第一个proto定义。
PS D:\git\minicline> npm run protos
> minicline@0.0.1 protos
> node scripts/build-proto.mjs
Missing input file.
Error generating TypeScript for proto files: Error: Command failed: D:\git\minicline\node_modules\grpc-tools\bin\protoc --proto_path="D:\git\minicline\proto" --plugin=protoc-gen-ts_proto="D:\git\minicline\node_modules\.bin\protoc-gen-ts_proto.cmd" --ts_proto_out="D:\git\minicline\src\shared\proto" --ts_proto_opt=env=node,esModuleInterop=true,outputServices=generic-definitions,outputIndex=true,useOptionals=none,useDate=false
at genericNodeError (node:internal/errors:983:15)
at wrappedFn (node:internal/errors:537:14)
at checkExecSyncError (node:child_process:882:11)
at execSync (node:child_process:954:15)
at tsProtoc (file:///D:/git/minicline/scripts/build-proto.mjs:67:3)
at compileProtos (file:///D:/git/minicline/scripts/build-proto.mjs:86:2)
at async main (file:///D:/git/minicline/scripts/build-proto.mjs:37:2) {
status: 1,
signal: null,
output: [ null, null, null ],
pid: 28476,
stdout: null,
stderr: null
}
编写第一个Proto Buffer模型
现在我们编写我们的第一个proto定义,在根目录下新建proto目录,在下边新建minicline子目录(前边我们添加的mjs脚本会递归遍历proto目录,编译所有*.proto文件),我们在下边新建一个weather.proto文件,定义我们的方法和消息体。
proto/minicline/weather.proto
syntax = "proto3";
package minicline;
service WeatherService {
rpc getWeather(GetWeatherRequest) returns (GetWeatherResponse);
}
message GetWeatherRequest {
string location = 1;
string unit = 2;
}
message GetWeatherResponse {
string skytext = 1;
string temperature = 2;
string degreeType = 3;
}
上边的定义很简单,我们首先定义了一个rpc服务getWeather,参数是GetWeatherRequest,返回值是GetWeatherResponse。 然后我们分别定义了参数和返回值的消息体。 参数内容分别是我们用到的location和unit,类型均为string。 返回值我们返回显示需要用到的skytext,temperature和degreeType,类型也均为string。
再次运行npm run protos,我们看到如下输出。
PS D:\git\minicline> npm run protos
> minicline@0.0.1 protos
> node scripts/build-proto.mjs
Compiling Protocol Buffers...
Processing 1 proto files from D:\git\minicline\proto
Generated ProtoBus files at:
- D:\git\minicline\webview-ui\src\services\grpc-client.ts
- D:\git\minicline\src\generated\hosts\vscode\protobus-service-types.ts
- D:\git\minicline\src\generated\hosts\vscode\protobus-services.ts
其中的webview-ui\src\services\grpc-client.ts是我们一会需要在react中调用的方法WeatherServiceClient.getWeather,内容如下
// GENERATED CODE -- DO NOT EDIT!
// Generated by scripts\generate-protobus-setup.mjs
import * as proto from "@shared/proto/index"
import { ProtoBusClient, Callbacks } from "./grpc-client-base"
export class WeatherServiceClient extends ProtoBusClient {
static override serviceName: string = "minicline.WeatherService"
static async getWeather(request: proto.minicline.GetWeatherRequest): Promise<proto.minicline.GetWeatherResponse> {
return this.makeUnaryRequest("getWeather", request, proto.minicline.GetWeatherRequest.toJSON, proto.minicline.GetWeatherResponse.fromJSON)
}
}
使用gRPC改写webview端
上边自动生成的代码webview-ui\src\services\grpc-client.ts还无法编译通过,里边引用了一个 ./grpc-client-base,现在让我们添加这个文件。
webview-ui/src/services/grpc-client-base.ts
import { v4 as uuidv4 } from "uuid";
import { vscode } from "../utilities/vscode";
const encodeMessage:Function = <T>(message: T, _encoder: (_: T) => unknown) => message;
const decodeMessage:Function = <T>(message: any, _decoder: (_: { [key: string]: any }) => T) => message;
...
export abstract class ProtoBusClient {
static serviceName: string;
static async makeUnaryRequest<TRequest, TResponse>(
methodName: string,
request: TRequest,
encodeRequest: (_: TRequest) => unknown,
decodeResponse: (_: { [key: string]: any }) => TResponse,
): Promise<TResponse> {
return new Promise((resolve, reject) => {
const requestId = uuidv4();
// Set up one-time listener for this specific request
const handleResponse = (event: MessageEvent) => {
const message = event.data;
if (message.type === "grpc_response" && message.grpc_response?.request_id === requestId) {
// Remove listener once we get our response
window.removeEventListener("message", handleResponse);
if (message.grpc_response.message) {
const response = decodeMessage(message.grpc_response.message, decodeResponse);
resolve(response);
}
...
}
}
window.addEventListener("message", handleResponse);
vscode.postMessage({
type: "grpc_request",
grpc_request: {
service: this.serviceName,
method: methodName,
message: encodeMessage(request, encodeRequest),
request_id: requestId
},
});
})
}
}
这个文件非常关键,是webview端和extension host端进行通信的基础,我们自动生成grpc-client.ts会调用makeUnaryRequest方法,这个方法主要干了下边两件事。
1. 会调用window.addEventListener("message", handleResponse),自动注册一个监听器,该监听器的注册函数在收到期待的消息响应后返回消息体,并且注销该监听器。
2. 向extension host发送一个类型为grpc_request的消息,其中包含extension host端需要处理该消息请求的服务及方法,还有具体请求的消息体内容。
接下来我们修改我们的react应用,使用自动生成的grpc-client.ts中的代码。
webview-ui/src/App.tsx
首先我们添加一个下边的函数调用WeatherServiceClient.getWeather, 我们使用react的useCallback封装我们的调用,在异步函数返回时显示结果。
const handleCheckWeatherMessage = useCallback(
async (locationValue: string, unitValue: string) => {
let messageSent = false;
const result:GetWeatherResponse = await WeatherServiceClient.getWeather(
GetWeatherRequest.create({
location: locationValue,
unit: unitValue
}),
);
displayWeatherData(result);
}, []
);
然后修改checkWeather函数如下,删掉之前的vscode.postMessage,改为调用上边的handleCheckWeatherMessage,因为vscode.postMessage已经改为在我们ProtoBusClient.makeUnaryRequest中调用了。
function checkWeather() {
const location = document.getElementById("location") as TextField;
const unit = document.getElementById("unit") as Dropdown;
handleCheckWeatherMessage(location.value, unit.value);
displayLoadingState();
}
下一步删掉我们之前定义的数据结构WeatherData,改为自动生成的GetWeatherResponse,并且修改对应的函数参数类型。
import { GetWeatherRequest, GetWeatherResponse } from "@shared/proto/minicline/weather";
function displayWeatherData(weatherData: GetWeatherResponse) {
}
function getWeatherSummary(weatherData: GetWeatherResponse) {
}
function getWeatherIcon(weatherData: GetWeatherResponse) {
}
最后,删除我们之前在程序启动时注册事件监听器,因为我们已经在调用ProtoBusClient.makeUnaryRequest时动态注册和注销事件监听器了。
执行npm run build:webview重新编译react应用
使用gRPC改写extension host端
现在我们从webview往extension host端发送的消息格式已经发生了变化,让我们修改下WeatherViewProvider._setWebviewMessageListener函数,先打印以下收到消息,确认webview端已经成功通过调用自动生成的代码发送了消息。
private _setWebviewMessageListener(webviewView: WebviewView) {
webviewView.webview.onDidReceiveMessage(
(message: any) => {
console.log("收到webview消息", message);
},
undefined,
this._disposables
);
}
重新启动插件,点击webview中的Check按钮,如果一切正常,在DEBUG CONSOLE中会看到如下图内容
接下来我们修改WeatherViewProvider处理新的消息体。
src/providers/WeatherViewProvider
import { WebviewMessage } from "@/shared/WebviewMessage";
import { ExtensionMessage } from "@/shared/ExtensionMessage";
import { Controller } from "@/core/controller";
import { handleGrpcRequest } from "@/core/controller/grpc-handler";
export class WeatherViewProvider implements WebviewViewProvider {
...
private webview?: WebviewView;
private controller?: Controller;
private _disposables: Disposable[] = [];
constructor(private readonly _extensionUri: Uri, private readonly _context: ExtensionContext) {
// Create controller with cache service
this.controller = new Controller(_context);
}
...
private _setWebviewMessageListener(webviewView: WebviewView) {
webviewView.webview.onDidReceiveMessage(
(message: any) => {
this.handleWebviewMessage(message);
},
undefined,
this._disposables
);
}
async handleWebviewMessage(message: WebviewMessage) {
const postMessageToWebview = (response: ExtensionMessage) => this.postMessageToWebview(response);
switch (message.type) {
case "grpc_request": {
if (message.grpc_request) {
if (this.controller) {
await handleGrpcRequest(this.controller, postMessageToWebview, message.grpc_request);
}
}
break;
}
default: {
console.error("Received unhandled WebviewMessage type:", JSON.stringify(message));
}
}
}
private async postMessageToWebview(message: ExtensionMessage): Promise<boolean | undefined> {
return this.webview?.webview.postMessage(message);
}
}
上边代码我们新的函数handleWebviewMessage处理webview端发送的请求,并且请求处理完毕后调用postMessageToWebview给webview发送消息,同时为了更好的区分webview和extension的消息,我们定义了数据结构WebviewMessage和ExtensionMessage。
处理gRPC的代码逻辑在grpc-handler中,文件内容如下.
src/core/controller/grpc-handler.ts
import { serviceHandlers } from "@/generated/hosts/vscode/protobus-services";
export type PostMessageToWebview = (message: ExtensionMessage) => Thenable<boolean | undefined>;
export async function handleGrpcRequest(
controller: Controller,
postMessageToWebview: PostMessageToWebview,
request: GrpcRequest,
): Promise<void> {
await handleUnaryRequest(controller, postMessageToWebview, request);
}
async function handleUnaryRequest(
controller: Controller,
postMessageToWebview: PostMessageToWebview,
request: GrpcRequest,
): Promise<void> {
try {
// Get the service handler from the config
const handler = getHandler(request.service, request.method);
// Handle unary request
const response = await handler(controller, request.message);
// Send response to the webview
await postMessageToWebview({
type: "grpc_response",
grpc_response: {
message: response,
request_id: request.request_id,
},
});
} catch (error) {
...
}
}
function getHandler(serviceName: string, methodName: string): any {
const serviceConfig = serviceHandlers[serviceName];
const handler = serviceConfig[methodName];
return handler;
}
函数handleUnaryRequest根据请求的service和method,找到本地对应的处理函数handler,将消息体的具体内容request.message交由该handler处理。
让我们查看**@/generated/hosts/vscode/protobus-services**中serviceHandlers的内容。
// Weather Service
import { getWeather } from "@core/controller/weather/getWeather"
const WeatherServiceHandlers: serviceTypes.WeatherServiceHandlers = {
getWeather: getWeather,
}
export const serviceHandlers: Record<string, any> = {
"minicline.WeatherService": WeatherServiceHandlers,
}
可以发现,当我们使用参数"minicline.WeatherService"和"getWeather"获取handler时,最终我们返回的是在@core/controller/weather/getWeather下的getWeather函数。所以,我们需要在src/core/controller/weather目录下到处一个名为getWeather的处理函数,让我们在该目录下新建一个getWeather.ts,具体内容如下。
src/core/controller/weather/getWeather.ts
import { GetWeatherRequest, GetWeatherResponse } from "@/shared/proto/minicline/weather";
import { Controller } from "..";
import * as weather from "weather-js";
export async function getWeather(_controller: Controller, request: GetWeatherRequest): Promise<GetWeatherResponse> {
return new Promise((resolve, reject) => {
weather.find({ search: request.location, degreeType: request.unit }, (err: any, result: any) => {
// Get the weather forecast results
const weatherForecast = result[0];
resolve(GetWeatherResponse.create({
skytext: weatherForecast.current.skytext,
temperature: weatherForecast.current.temperature,
degreeType: weatherForecast.location.degreetype
}));
});
});
}
大家可能注意到,在我们的代码里用到了一个Controller的类,这个类是cline里边的核心类,整个cline的生命周期就是围绕它而运行的,但是目前我们的代码没有使用到它任何功能,所以我们先在src/core/contorller/index.ts中新建一个空的类,内容如下
import * as vscode from "vscode";
export class Controller {
constructor(readonly context: vscode.ExtensionContext) {
}
}
再次运行我们的插件,点击Check按钮,webview将刷新显示天气情况,如下图所示。

总结
本章我们引入了gRPC,利用proto定义自动生成代码,同时在webview端和extension host端调用自动生成的方法和数据结构进行通信。下一章开始我们将添加新的proto定义,来和本地的ollama进行交互。