總結前端走 gRPC 協議所遇到的坑

坑點一:ts-protoc-gen 不支持瀏覽器環境

首先你應該瞭解 ts-protoc-gen,它的目標是將編譯 .proto 文件所生成的文件夾包含 .js.d.ts 文件。vue

可是...node

<div class="tip">
請不要將 ts-protoc-gen 生成的代碼直接用在瀏覽器中
</div>git

由於當咱們直接使用以下代碼時:github

import { MyMessage } from "../generated/users_pb";

const msg = new MyMessage();
msg.setName("John Doe");

報錯:web

Uncaught ReferenceError: exports is not defined.

這個錯誤應該不陌生,exports 未定義,多見於瀏覽器環境直接使用 node 環境代碼,因此我翻看了一下 ts-protoc-gen 的源碼,直到發現了以下代碼:api

printer.printLn(`exports.${service.name} = ${service.name};`);

這裏的 exports 就已經說明一切了,這個庫生成的是運行在 node 環境的 CommonJS 規範代碼,而對於使用 Webpack4vue project 項目,也並不支持混合使用模塊系統,因此目前我想到了個臨時解決方案:promise

// 編譯文件導出方法和類時強制使用 `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 請求。bash

首先能夠先從 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));
相關文章
相關標籤/搜索