总结前端走|总结前端走 gRPC 协议所遇到的坑

坑点一:ts-protoc-gen 不支持浏览器环境 首先你应该了解 ts-protoc-gen,它的目标是将编译 .proto 文件所生成的文件夹包含 .js.d.ts 文件。
【总结前端走|总结前端走 gRPC 协议所遇到的坑】但是...

请不要将 ts-protoc-gen 生成的代码直接用在浏览器中

因为当我们直接使用如下代码时:

import { MyMessage } from "../generated/users_pb"; const msg = new MyMessage(); msg.setName("John Doe");

报错:
Uncaught ReferenceError: exports is not defined.

这个错误应该不陌生,exports 未定义,多见于浏览器环境直接使用 node 环境代码,所以我翻看了一下 ts-protoc-gen 的源码,直到发现了如下代码:
printer.printLn(`exports.${service.name} = ${service.name}; `);

这里的 exports 就已经说明一切了,这个库生成的是运行在 node 环境的 CommonJS 规范代码,而对于使用 Webpack4vue project 项目,也并不支持混合使用模块系统,所以目前我想到了个临时解决方案:
// 编译文件导出方法和类时强制使用 `es module` // src/service/grpcweb.ts printer.printLn(`var ${service.name} = (function () {`); // line 251 //|| //|| //\/ printer.printLn(`export var ${service.name} = (function () {`); printer.printLn(`exports.${service.name} = ${service.name}; `); // line 270 //|| //|| //\/ // delete.printLn(`function ${service.name}Client(serviceHost, options) {`) // line 286 //|| //|| //\/ .printLn(`export function ${service.name}Client(serviceHost, options) {`)printer.printLn(`exports.${service.name}Client = ${service.name}Client; `); // line 304 //|| //|| //\/ // delete

这个方法可以暂时解决 Uncaught ReferenceError: exports is not defined. 的问题。
坑点二:grpc-web-client 所提供的方法只支持回调函数
import {grpc} from "grpc-web-client"; // Import code-generated data structures. import {BookService} from "./generated/proto/examplecom/library/book_service_pb_service"; import {GetBookRequest} from "./generated/proto/examplecom/library/book_service_pb"; const getBookRequest = new GetBookRequest(); getBookRequest.setIsbn(60929871); grpc.unary(BookService.GetBook, { request: getBookRequest, host: host, onEnd: res => { const { status, statusMessage, headers, message, trailers } = res; if (status === grpc.Code.OK && message) { console.log("all ok. got book: ", message.toObject()); } } });

以上是官方给出的例子,发送一个标准请求。看到 callback 和一堆引入的文件的时候,我瞬间整个人就不好了,遂开始琢磨如何二次封装 gRPC 请求。
首先可以先从 callback 函数转成 Promise 下手:
// callbackToPromise.js const promiseFunc = new Promise((resolve, reject) => { grpc.unary(BookService.GetBook, { request: getBookRequest, host: host, onEnd: res => { const { status, statusMessage, message } = res; if (status === grpc.Code.OK && message) { resolve(res) } else { reject(res); } } }); }); return promiseFunc;

我们还可以再各这个请求加上超时限制(折腾一下准没错):
// callbackToPromise.js return utils.fetchTimeout(promiseFunc, 2000).catch(err => { // 设置 2000 ms 超时 if (err.code === 'TIMEOUT') { // 提示超时 } }); // utils.js /** * fetch 超时 helper * * @param {Function} fetchPromise fetch 方法 * @param {Number} timeout 超时时间 * @returns Promise */ function fetchTimeout (fetchPromise, timeout) { let abortFunc = null; const abortPromise = new Promise((resolve, reject) => { abortFunc = () => { reject({ code: 'TIMEOUT', msg: 'TIMEOUT' }); }; }); const abortablePromise = Promise.race([ fetchPromise, abortPromise ]); setTimeout(() => { abortFunc(path); }, timeout); return abortablePromise; }

这样 callback 函数专成 Promise 就完成了。
其次我们需要将 grpc-web-client 目标文件引入和回调函数的封装分割开来,这样也有利于之后代码的维护:
// user.js/** * 根据用户 ID 查询用户信息 * * @param {String} publicId 用户 ID */ export function queryUserDetails (publicId) { const queryUserDetailsRequest = new QueryUserDetailsRequest(); queryUserDetailsRequest.setUserPublicId(publicId); const config = { request: queryUserDetailsRequest, headers: { ...headers, ...makeAuthorizationHeader(utils.getToken()) } }; return createRequest(Dashboard.QueryUserDetails, config, transformQueryUserDetailsValue); }

// api.config.js /** * 创建请求 * * @param {Object} service service function * @param {Object} config 配置项 * @param {Function} transformValue 响应数据体转换 * @returns Promise */ export function createRequest (service, config, transformValue) { const promiseFunc = new Promise((resolve, reject) => { ProgressBar.start(); grpc.unary(service, { request: config.request, host: DASHBOARD_API, metadata: new grpc.Metadata(config.headers), onEnd: (res) => { const { status, statusMessage, message } = res; if (status === grpc.Code.OK && message) { ProgressBar.finish(); resolve((transformValue && transformValue(message.toObject())) || message.toObject()); // 在这里我们可以运行数据转化函数 } else if (status === grpc.Code.Unauthenticated) { ProgressBar.fatal(); errorHandler.showNotice(grpc.Code[status], statusMessage); router.push({ name: 'unauthenticated', path: '/403' }); reject(res); } else { ProgressBar.fatal(); errorHandler.showNotice(grpc.Code[status], statusMessage); reject(res); } } }); }); return utils.fetchTimeout(promiseFunc, `${service.service.serviceName}.${service.methodName}`, TIMEOUT).catch(err => { if (err.code === 'TIMEOUT') { const { code, msg } = err; ProgressBar.fatal(); errorHandler.showNotice(code, msg); } }); }

代码很简单,经过以上两步骤,我们就可以如下轻松加愉快的去请求数据了:
import { queryUserDetails } from '@/api/user'; queryUserDetails(publicId) .then(res => console.log(res)) .catch(res => console.log(res));

    推荐阅读