takeaway

[gRPC] Web request TS encapsulation (hereinafter referred to as “encapsulation”) article provides a version of the gRPC encapsulation, although it can be used in practice, but there are still two imperfect places. New RequestClass() is used in the Services declaration layer to handle incoming parameters of a request whose type is object. The second is that the requestClass must be passed in at the Services layer, which in theory could be omitted.

This paper is committed to solve the above two problems, to achieve [gRPC] encapsulation front-end network request core idea – TS version requirements of the declaration layer minimalist effect. However, the implementation method uses AST processing tools to perform secondary processing on protoc generated code, so it has a certain technical complexity. It’s also a good time to learn about AST tools, but to keep the focus of this article, AST processing will be a separate article.

background

As mentioned in the introduction, there are two imperfections in the Encapsulation scheme: the handling of complex type inputs and the need to pass in a requestClass. This is still a bit abstract, so let’s go straight to the code:

Suppose the helloWorld.proto file in Encapsulation has the following additions:

// Add Student type
message Student {
  string name = 1;
  int32 age = 2;
}

// Add Student to HelloRequest
message HelloRequest {
  string name = 1;
  Student student_info = 2;
}
Copy the code

HelloRequest has one more parameter, student_info, which is a complex type (object). At this point, the code for the Services layer will become:

import { grpcPromise, client } from "./request";
import { HelloRequest, Student } from "./helloworld_pb.js";

export const sayHello = (data: HelloRequest.AsObject) = > {
  /* Handle Student start */
  const{ studentInfo, ... rest } = data;const { name = "", age = 0 } = data.studentInfo ?? {};
  const student = new Student();
  student.setName(name);
  student.setAge(age);
  /* Handle Student end */
  return grpcPromise({
    method: "sayHello".requestClass: HelloRequest,
    data: { ...rest, studentInfo: student },
  });
};
Copy the code

There’s a whole bunch of code dealing with Student, which is very inelegant and not perfect. This is the handling of the complex type input mentioned above.

In addition, you have to introduce HelloRequest here and pass it in as a parameter to grpcPromise, which is also inelegant. In theory, if you pass in sayHello, you’ll get the message from its HelloRequest class. This is the second problem mentioned above: the requestClass must be passed in.

To solve these two problems, the JS/TS files generated by protoc must be processed again, using AST processing tools. There is a certain amount of complexity, but how to implement it is detailed in my other article GoGoCode – handling AST as easily as Jquery. For the sake of consistency, this article assumes that the above problems have been solved and focuses on what a perfect version of wrapped code would look like.

Assuming that

The following assumptions are the goals of the GoGoCode article:

  1. On the parameter instance (Message)getXXXClassMethod can be obtainedXXXClass, as in the previous examplegetStudentInfoClass()To be able to returnStudentClass;
  2. On the Service instancegetXXXParamsClassMethod can be obtainedXXXMethod called in the example abovegetSayHelloParamsClass()To be able to returnHelloRequestClass.

The body of the

If the above assumptions are met, services.ts will meet the requirements in The Idea article:

import { grpcPromise } from "./request";
import { HelloRequest } from "./helloworld_pb.js";

// No need to handle Student; You don't need to pass in HelloRequest;
// The helloRequest. AsObject here is used only as ts
export const sayHello = (data: HelloRequest.AsObject) = >
  grpcPromise({ method: "sayHello", data });
Copy the code

It’s elegant. It’s perfect. Now comes the big part, how to modify the request.ts code.

Processing of complex type inputs

Because you get the Student class by calling the getStudentInfoClass method, the code above that handles JSON to Student Instance can be abstracted into Request.ts. In addition, it is natural to assume that this is a typical recursive scenario, so the key is to implement the recursive function, directly to the code:

const transJson2ClassInstance = <C>(
  className: ConcreteClass<C>,
  data: Record<string.any>
) = > {
  const result = new className(); // abstracted as parameters
  Object.entries(data).forEach(([key, val]) = > {
    const method = upperFirst(camelCase(key));
    const setMethod = `set${method}` as keyof C;
    const setFunc = result[setMethod];
    if (typeof setFunc === "function") {
      if (typeof val === "object"&&!Array.isArray(val)) {
        // Call getXXXClass to get the XXX class
        const subClassName = result[`get${method}Class` as keyof C]();
        setFunc.call(result, transJson2ClassInstance(subClassName, val));
      } else{ setFunc.call(result, val); }}});return result;
};
Copy the code

The requestClass must be passed in

The next step is to get the HelloRequest class according to sayHello and pass it in as the initial argument to transJson2ClassInstance. With the matting above, it is easier to understand here. Call getSayHelloParamsClass directly:

interface GrpcPromiseParams<M extends ClientMethod> {
  method: M;
  // requestClass: ConcreteClass
      
       >; Don't need the
      
  data: Partial<ReturnType<RequestClass<M>["toObject"] > >; metadata? : Metadata; }export constgrpcPromise = <M extends ClientMethod>( params: GrpcPromiseParams<M> ) => { const { method, data, metadata = {} } = params; // Call getXXXParamsClass const getClassMethod = 'get${upperFirst(method)}ParamsClass'; const requestClass = client[getClassMethod]() as ConcreteClass< RequestClass<M> >; const request = transJson2ClassInstance(requestClass, data); const result = client[method](request as any, metadata) .then((res) => res.toObject()) .catch((err) => console.log(err)) as GrpcPromiseReturn<M>; return result; };Copy the code

The main logic is to call getXXXParamsClass and transJson2ClassInstance to generate the request. This has reached a relatively perfect state, at least the declaration layer and call layer has been very simple to use.

PS: This part of the logic will be concatenated string, this weak logic is difficult to handle TS, so it is appropriate to ignore @ts-ignore, at least this layer does not affect the user layer.

conclusion

This article was inspired by a colleague who studied the source code for Protoc in order to achieve this effect, and also achieved the above effect. But that leaves us with a customized version of the Protoc. However, his study gave the author inspiration, since there are many restrictions to change the source code, then the use of scripts for secondary processing of products can always it, so there is this article.

Recently, I proposed a vision for the team: extreme, innovative, no boundaries. My understanding of it is:

Acme is the premise of innovation, innovation is the result of acme; Borderless can search for optimal solution in a larger range and is a catalyst for innovation.

In this case, my colleague went for the extreme first before thinking of modifying the Protoc source code. As a front-end to modify C code, dare to think, do not set boundaries. The final result can be said to be the only one in the industry, certainly can be considered innovation. But also really brought benefits for the team, just think, if the team is such a good comrade, the team fighting will be how strong.

In intense competition, we often find that the winning system goes to absurd extremes in maximizing or minimizing one or more variables. — Poor Charlie’s Book