坑点一:ts-protoc-gen
不支持浏览器环境
首先你应该了解 ts-protoc-gen,它的目标是将编译 .proto
文件所生成的文件夹包含 .js
和 .d.ts
文件。
但是...
请不要将
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
规范代码,而对于使用 Webpack4
的 vue 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));