takeaway

This paper will be based on GRPC – Web encapsulation of an easy to call method, will be like [GRPC] encapsulation of the core idea of front-end network request – TS version of the same encapsulation, declaration, call three layers. Finally, the call layer is completely consistent, and the packaging layer is unified to deal with formatting, exceptions and other logic.

As it is only demo stage, there may be some details not taken into account, but I believe that the overall idea and basic code has been relatively complete, and it should be the only copy that can be found in the public network. If you find defects in the application process, you can modify the compatibility. Also welcome to leave a message to help the author make up logic.

PS: If you don’t know what gRPC is, you can read gRPC for 5 minutes.

The target

  1. Browser usagegRPCMeans of communication;
  2. Centralized management of common logic such as exceptions, cross-domain, cache, etc., does not need to write every call;
  3. The underlying implementation is completely decoupled from the calling code. That is, when the underlying framework (GRPC-Web, GRPC-JS, or even AXIos, FETCH) is replaced, the calling code of the usage layer does not need to be modified at all;
  4. You can easily use TS when calling.

Row of pit

  1. The @vue/ CLI generated project typescript version 4.1.6 reports errors at runtime. However, VSCode is used to encode version 4.4.3 without any error. It is not yet clear which version will work at the lowest level, but 4.4.3 will definitely work. So if you want to use the practices in this article, make sure that the TS version of your project meets the requirements;

  2. GRPC returns in two formats. One is an ordinary Promise, which can be used as then, catch and other methods. The other is the ReadableStream format, which uses.on(‘data’, callback) to listen for changes to the data stream, which is used in a completely different way. In this article, we will only encapsulate services in the Promise format because they are more common.

  3. It’s a bit of a hassle to write your own demo. Follow the grPC-Web HelloWorld tutorial to complete the running of the server, envoy proxy, and client.

    • Need to install:docker,protoc,protoc-gen-grpc-web;
    • Then follow the tutorial to successfully launch the server and envoy proxy, noting that you may need to change the port number in the proxy configuration file;
    • Since the client needs to write TS, it needs to build its own. The author used@vue/cli, encountered the TS version problem, after the upgrade is barely solved;
    • Copy the proto file to the client directory, and run the protoc command line to generate the required TS, JS files;
    • Even modify the proto file appropriately to achieve2The two formats mentioned are separated for the purpose of convenient encapsulation.

Say these absolutely not want to discourage you, and even strongly suggest that you build a demo, because it seems troublesome, in fact, this is the fastest way to get familiar with gRPC from zero. This is just to give you a sense of what to expect. It’s going to take you a whole day. Maybe you’re going to hit a hole that wasn’t mentioned above.

The body of the

To prepare

Using the code provided by grPC-Web HelloWorld, the server and proxy can just follow the steps (brace yourself, this step may not be as smooth), and we’ll focus on the client-side code implementation. In addition, as mentioned above, we will make a slight change to the proto file to separate the two return types as follows:

helloworld.proto

// helloworld.proto

syntax = "proto3";

package helloworld;

service Greeter {
  // unary call
  rpc SayHello(HelloRequest) returns (HelloReply);
}

service Stream {
  // server streaming call
  rpc SayRepeatHello(RepeatHelloRequest) returns (stream HelloReply);
}

message HelloRequest {
  string name = 1;
}

message RepeatHelloRequest {
  string name = 1;
  int32 count = 2;
}

message HelloReply {
  string message = 1;
}
Copy the code

PS: Since only sayHello will be used for the time being, the server-side code and proto file will not need to be modified.

The original implementation

Let’s take a look at the non-abstract, direct call code. More intuitive, for the follow-up abstract foreshadowing, or jump too big.

import { GreeterPromiseClient } from "./helloworld_grpc_web_pb.js";

const client = new GreeterPromiseClient("http://localhost:8080");

function sayHello(name: string) {
  const request = new HelloRequest();
  request.setName(name);
  return client
    .sayHello(request)
    .then((res) = > res.toObject())
    .catch((err) = > {
      console.log(err);
    });
}

sayHello('world').then(res= > {
  console.log(res.message); // 'Hello! world'
});
Copy the code

The client

Index. Ts – call

There is no difference from normal RESTful wrapped calls.

import { sayHello } from "./services";

sayHello({ name: "world" }).then((res) = > {
  console.log(res.message); // 'Hello! World 'and has a friendly TS prompt
});
Copy the code

Services. Ts – statement

Due to the nature of gRPC, this part of the code inevitably depends on the JS and TS generated by proto files.

import { grpcPromise } from "./request";
/* Need to rely on proto-generated classes, inevitably */
import { HelloRequest } from "./helloworld_pb.js";

export const sayHello = (data: HelloRequest.AsObject) = > {
  return grpcPromise({
    method: "sayHello".requestClass: HelloRequest,
    data,
  });
};
Copy the code

Request. Ts – encapsulation

The main part is coming, the TS of this place is very complicated, please prepare yourself. If you can’t understand it, take it and use it. This is complicated for two main reasons:

First, there is the.d.ts file generated from the proto file, and this file cannot be moved. So it’s not like RESTful, where TS starts from scratch and we declare it ourselves. We don’t have absolute control over that.

In addition, in pursuit of the extreme, the use of the declaration layer shows that with client (common to all projects) and Method, the types of requestClass and data can theoretically be inferred. If you have method, you know the method. If you know method, you know the input parameter. If you know method, you know the input parameter. And it’s true, but you can see how complicated it is.

Combining the above two factors, there are many “ribs” statements that need to be deduced from the statements in.d.ts before we can use them happily. As you can see from the code below, there are even six levels of nested TS inference, and two helper methods that you have to write yourself. I have made the notes as detailed as possible. After all, these notes are probably written for myself in a few months…

import { Metadata } from "grpc-web";
import { upperFirst, camelCase } from "lodash-es";
import { GreeterPromiseClient } from "./helloworld_grpc_web_pb.js";

export const client = new GreeterPromiseClient("http://localhost:8080");

/** GrPC-Web plugin is enabled when the browser is installed, it can be more convenient to see GRPC requests, not necessary. * const enableDevTools = window.__GRPCWEB_DEVTOOLS__; * if (typeof enableDevTools === "function") { * enableDevTools([client]); *} * /

/** * auxiliary: Used for classes that have no constructor but can be new. Deduce its type from the instance T after new. * if const instance: InstanceType = new InstanceClass(); * then typeof InstanceClass === ConcreteClass
      
       ; * /
      
interface ConcreteClass<T> {
  new (): T;
}

/** * auxiliary: Deduce the input generic from the Promise type. * if type A = Promise; * Then UnPromise === B; * /
type UnPromise<T extends Promise<unknown>> = T extends Promise<infer U>
  ? U
  : never;

/** * All method names of the client instance */
type ClientMethod = keyof GreeterPromiseClient;

/** * Reverses the request type based on the (first) input parameter of the method in the client instance. * /
type RequestClass<M extends ClientMethod> = Parameters<
  GreeterPromiseClient[M]
>[0];

/** * requestClass () {ConcreteClass (); * Backflow logic of data type: return value of toObject method in request instance. * /
interface GrpcPromiseParams<M extends ClientMethod> {
  method: M;
  requestClass: ConcreteClass<RequestClass<M>>;
  data: Partial<ReturnType<RequestClass<M>["toObject"] > >; metadata? : Metadata; }/** * The toObject method is called at the end, so returning ReturnType<
      
       > will not work, * you need to UnPromise and then get the toObject ReturnType and wrap it with the Promise. * /
      [m]>
type GrpcPromiseReturn<M extends ClientMethod> = Promise<
  ReturnType<UnPromise<ReturnType<GreeterPromiseClient[M]>>["toObject"] > >;export constgrpcPromise = <M extends ClientMethod>( params: GrpcPromiseParams<M> ) => { const { requestClass, method, data, metadata = {} } = params; const request = new requestClass(); Object.entries(data).forEach(([key, val]) => { const setFunc = request[`set${upperFirst(camelCase(key))}` as keyof RequestClass<M>]; If (typeof setFunc === "function") {// Notice: If (typeof setFunc === "function") {// Notice: If (typeof setFunc === "function") {// Notice: setFunc.call(request, val); }}); const result = client[method](request as any, metadata) .then((res) => res.toObject()) .catch((err) => { console.log(err); // other code }) as GrpcPromiseReturn<M>; return result; };Copy the code

conclusion

To tell you the truth, the difficulty of writing this part of the code is beyond the original estimate, fortunately, so far the effect is ok, to achieve the goal set before. But there is room for further refinement. Such as

  • Let’s abstract it a little bit furtherGreeterPromiseClientIs also removed as a generic input;
  • Logic such as exception handling and initial configuration can also be removed and passed in as parameters.
  • Then addReadableStreamType declarations and so on.

This is no doubt more perfect, and can be packaged separately as a package open source. But the cost of doing this will increase a lot, and the most important thing for us to do engineering is to measure the cost performance. At present, the research results can be better applied in the project, so we first throw out for reference. Further abstraction will be determined according to energy and time, if you are interested, welcome to communicate.

Finally, after writing this part, the author felt that his TS level has improved significantly. For example, he finally understood the meaning of some infer and never. Have to sigh TS profound, seemingly so a few simple keywords, actually can have so many magical changes, really is the road to simple, their own way is still long.

Do the hard things and you’ll get it. The more noisy, the more lonely; The more lonely, the more rich. – general

Bao Jianfeng from honed out, plum blossom incense from the bitter cold. — A Cautionary Tale