内容简介:Web gRPC 是 gRPC 在 Web 上的一个适配实现。关于他的介绍以及为什么要用 gRPC 就不在这解释了,如果你决定使用 Web gRPC,并且正在寻找前端的库和解决方案,看一看这篇文章,应该会有所帮助。gRPC 的使用方案有很多,每种方案方法都有各自的特点,也有各自的优缺点。接下来会列举三种接入方案
Web gRPC 是 gRPC 在 Web 上的一个适配实现。关于他的介绍以及为什么要用 gRPC 就不在这解释了,如果你决定使用 Web gRPC,并且正在寻找前端的库和解决方案,看一看这篇文章,应该会有所帮助。
gRPC 的使用方案有很多,每种方案方法都有各自的特点,也有各自的优缺点。
接下来会列举三种接入方案
- google-protobuf + grpc-web-client
- grpc-web (最近发布)
- protobufjs + webpack loader + grpc-web-client + polyfill (目前在用)
1. google-protobuf + grpc-web-client
google-protobuf 是 google 提供的 protobuf 文件的编译工具,可以将 protobuf 编译成各种语言,我们用它来编译成 js 文件。
grpc-web-client 则可以执行 google-protobuf 生成的 js,调用远程 rpc 服务。
使用步骤
- 编译文件
protoc --js_out=import_style=commonjs,binary:. messages.proto base.proto 复制代码
- 引入 js 代码
import {grpc} from "grpc-web-client"; // Import code-generated data structures. import {BookService} from "../_proto/examplecom/library/book_service_pb_service"; import {QueryBooksRequest, Book, GetBookRequest} from "../_proto/examplecom/library/book_service_pb"; 复制代码
- 创建请求对象
const queryBooksRequest = new QueryBooksRequest(); queryBooksRequest.setAuthorPrefix("Geor"); 复制代码
- 执行 grpc 方法调用服务
grpc.invoke(BookService.QueryBooks, { request: queryBooksRequest, host: "https://example.com", onMessage: (message: Book) => { console.log("got book: ", message.toObject()); }, onEnd: (code: grpc.Code, msg: string | undefined, trailers: grpc.Metadata) => { if (code == grpc.Code.OK) { console.log("all ok") } else { console.log("hit an error", code, msg, trailers); } } }); 复制代码
封装代码
封装 invoke 方法
封装 grpc.invoke
方法,一方面可以统一处理 host,header,错误,增加 log 等
另一方面可以改造成 Promise,方便调用
/** * @classdesc GrpcClient * grpc客户端 */ class GrpcClient { constructor(config) { this.config = extend({}, DEFAULT_CONFIG, config || {}) } /** * 执行grpc方法调用 * @param methodDescriptor 方法定义描述对象 * @param params 请求参数对象 * @return {Promise} */ invoke(methodDescriptor, params = {}) { let host = this.config.baseURL let RequestType = methodDescriptor.requestType || Empty let request = params.$request || new RequestType(), headers = {} let url = host + '/' + methodDescriptor.service.serviceName + '/' + methodDescriptor.methodName return new Promise((resolve, reject) => { // eslint-disable-next-line no-console this.config.debug && console.log('[Grpc.Request]:', url, request.toObject()) grpc.invoke(methodDescriptor, { headers, request, host, onMessage: (message) => { resolve(message) }, onEnd: (code, message, trailers) => { if (code !== grpc.Code.OK) { message = message || grpc.Code[code] || '' const err = new Error() extend(err, { code, message, trailers }) return reject(err) } }, }) }).then((message) => { // eslint-disable-next-line no-console this.config.debug && console.log('[Grpc.Response]:', url, message.toObject()) return message }).catch((error) => { // eslint-disable-next-line no-console console.error('[Grpc.Error]:', url, error) // eslint-disable-next-line no-console if (error.code) { Log.sentryLog.writeExLog('[Error Code]: ' + error.code + ' [Error Message]: ' + decodeURI(error.message), '[Grpc.Error]:' + url, 'error', { 'net': 'grpc' }) } else { Log.sentryLog.writeExLog('[Error Message]: ' + decodeURI(error.message), '[Grpc.Error]:' + url, 'warning', { 'net': 'grpc' }) } return Promise.reject(error) }) } } export default GrpcClient 复制代码
集中管理请求方法
按功能模块,将每个模块的 rpc 方法集中到一个文件,方便管理和与界面解耦
export function queryBook(request) { return grpcApi.invoke(BookService.QueryBooks) } export function otherMethod(request) { return grpcApi.invoke(BookService.OtherRpcMethod) } 复制代码
1. grpc-web
grpc-web 是 gRPC 官方发布的解决方案 ,他的实现思路是:
先把 proto 文件编译成 js 代码,然后引入 js 代码,调用提供好的 grpc 方法
使用步骤
- 编译文件
$ protoc -I=$DIR echo.proto \ --js_out=import_style=commonjs:generated \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:generated 复制代码
- 引用编译后代码
const {EchoServiceClient} = require('./generated/echo_grpc_web_pb.js'); const {EchoRequest} = require('./generated/echo_pb.js'); 复制代码
- 创建客户端
const client = new EchoServiceClient('localhost:8080'); 复制代码
- 创建请求对象
const request = new EchoRequest(); request.setMessage('Hello World!'); 复制代码
- 执行方法
const metadata = {'custom-header-1': 'value1'}; client.echo(request, metadata, (err, response) => { // ... }); 复制代码
小结
总体思路上,与第一种类似,都是先编译再使用编译后的 js,request 对象丢需要通过 new 和 set 来进行组装。区别在于编译后的 js 内置了请求方法,不需要另外的库来调用方法。
3. protobufjs + webpack loader + grpc-web-client + polyfill
区别于前两种,这种方法可以省去手动编译的步骤和严格创建 request 对象的操作,使用起来更“动态”。
实现思路
利用 webpack loader 在 webpack 构建期间编译,编译的结果虽然是 js,但是 js 中并不是 proto 对应的 class,而是引入 protobufjs 和解析包装对象的过程。实际解析在运行时执行,返回 protobufjs 的 root 对象
通过 prototype 追加方法的方式增加 service 方法,返回可直接执行 rpc 方法的对象,具体的执行方法依赖于 grpc-web-client,由于 protobufjs 可以将普通对象直接转换成 request 对象,所以方法直接接收普通对象,内部转换
创造一种路径格式 import Service from '##service?some.package.SomeService'
利用 babel 插件,分析 import 语法,在 protobuf 目录中搜索定义此 service 的文件,修改成
import real_path_of_service_proto from 'real/path/of/service.proto' const Service = real_path_of_service_proto.service() 复制代码
使用步骤
- 引入 service
import Service from '##service?some.package.SomeService' 复制代码
- 执行方法
Service.someMethod({ propA: 1, propB: 2 }).then((response)=>{ // invoke susscess } , (error)=> { // error }) 复制代码
实现代码
- loader
const loaderUtils = require('loader-utils') const protobuf = require('protobufjs') const path = require('path') module.exports = function (content) { const { root, raw, comment } = loaderUtils.getOptions(this) || {} let imports = '', json = '{}', importArray = '[]' try { // 编译期解析协议, 寻找 import 依赖 const result = protobuf.parse(content, { alternateCommentMode: !!comment, }) // 引入依赖 imports = result.imports ? result.imports.map((p, i) => `import root$${i} from '${path.join(root, p)}'`).join('\n') : '' importArray = result.imports ? '[' + result.imports.map((p, i) => `root$${i}`).join(',') + ']' : '[]' // json 直接输出到编译后代码中 json = JSON.stringify(JSON.stringify(result.root.toJSON({ keepComments: !!comment }))) } catch (e) { // warning } return `import protobuf from 'protobufjs' import { build } from '${require('path').join(__dirname, './dist/web-grpc')}' ${imports} var json = JSON.parse(${json}) var root = protobuf.Root.fromJSON(json) root._json = json ${raw ? `root._raw = ${JSON.stringify(content)}` : ''} build(root, ${importArray}) export default root` } 复制代码
代码倒数第4行 build,负责将依赖的 proto 模块追加到当前 root 对象中,单独放在其他文件是为了节省编译后的代码尺寸
这是 build 的代码,递归可以用栈优化,由于这部分性能影响太小,暂时忽略
exports.build = (root, importArray) => { root._imports = importArray let used = [] // 递归寻找依赖内容 function collect(root) { if (used.indexOf(root) !== -1) { return } used.push(root) root._imports.forEach(collect) } collect(root) // 添加到 root 中 used.forEach(function (r) { if (r !== root) { root.addJSON(r._json.nested) } }) } 复制代码
- polyfill
polyfill 的目的是简化执行 grpc 的用法
import protobuf from 'protobufjs' import extend from 'extend' import _ from 'lodash' import Client from './grpc-client' // 获取完整 name const fullName = (namespace) => { let ret = [] while (namespace) { if (namespace.name) { ret.unshift(namespace.name) } namespace = namespace.parent } return ret.join('.') } export const init = (config) => { const api = new Client(config) extend(protobuf.Root.prototype, { // 增加获取 service 方法 service(serviceName, extendConfig) { let Service = this.lookupService(serviceName) let extendApi if (extendConfig) { let newConfig if (typeof extendConfig === 'function') { newConfig = extendConfig(_.clone(config)) } else { newConfig = extend({}, config, extendConfig) } extendApi = new Client(newConfig) } else { extendApi = api } let service = Service.create((method, requestData, callback) => { method.service = { serviceName: fullName(method.parent) } method.methodName = method.name // 兼容 grpc-web-client 处理 method.responseType = { deserializeBinary(data) { return method.resolvedResponseType.decode(data) }, } extendApi.invoke(method, { // 兼容 grpc-web-client 处理 toObject() { return method.resolvedRequestType.decode(requestData) }, // 兼容 grpc-web-client 处理 serializeBinary() { return requestData }, }).catch((err) => { callback(err) }) }) // 方法改成小写开头, request 去掉非空限制,使用起来更贴近前端习惯 _.forEach(Service.methods, (m, name) => { let methodName = name[0].toLowerCase() + name.slice(1) let serviceMethod = service[methodName] service[methodName] = function method(request) { if (!request) { request = {} } return serviceMethod.apply(this, [request]) } service[name] = service[methodName] }) return service }, // 增加过去枚举方法 enum(enumName) { let Enum = this.lookupEnum(enumName) let ret = {} for (let k in Enum.values) { if (Enum.values.hasOwnProperty(k)) { let key = k.toUpperCase() let value = Enum.values[k] ret[key] = value ret[k] = value ret[value] = k } } return ret }, }) } 复制代码
Client 是 方案1 中整理出来的 GrpcClient
- babel-plugin
首先遍历所有 proto 文件创建字典
exports.scanProto = (rootPath) => { let list = glob.sync(path.join(rootPath, '**/*.proto')) let collections = {} const collect = (type, name, fullName, node, file) => { if (type !== 'Service' && type !== 'Enum' && type !== 'Type') { return } let typeMap = collections[type]; if (!typeMap) { typeMap = {} collections[type] = typeMap } if (typeMap[fullName]) { console.error(fullName + 'duplicated') } typeMap[fullName] = { type, name, fullName, node, file } } list.forEach(p => { try { const content = fs.readFileSync(p, 'utf8') let curNode = protobuf.parse(content).root const dealWithNode = (protoNode) => { collect(protoNode.constructor.name, protoNode.name, fullName(protoNode), protoNode, p) if (protoNode.nested) { Object.keys(protoNode.nested).forEach(key => dealWithNode(protoNode.nested[key])) } } dealWithNode(curNode) } catch (e) { // console.warn(`[warning] parse ${p} failed!`, e.message) } }) return collections } 复制代码
然后替换代码中的 import 声明和 require 方法
module.exports = ({ types: t }) => { let collections return { visitor: { // 拦截 import 表达式 ImportDeclaration(path, { opts }) { if (!collections) { let config = isDev ? opts.develop : opts.production collections = scanProto(config['proto-base']) } const { node } = path const { value } = node.source if (value.indexOf('##') !== 0) { return } let [type, query] = value.split('?') if (type.toLowerCase() !== '##service' && type.toLowerCase() !== '##enum') { return } let methodType = type.toLowerCase().slice(2) let service = collections[methodType[0].toUpperCase() + methodType.slice(1)][query] if (!service) { return } let importName = '' node.specifiers.forEach((spec) => { if (t.isImportDefaultSpecifier(spec)) { importName = spec.local.name } }) let defaultName = addDefault(path, resolve(service.file), { nameHint: methodType + '_' + query.replace(/\./g, '_') }) const identifier = t.identifier(importName) let d = t.variableDeclarator(identifier, t.callExpression(t.memberExpression(defaultName, t.identifier(methodType)), [t.stringLiteral(query)])) let v = t.variableDeclaration('const', [d]) let statement = [] statement.push(v) path.insertAfter(statement) path.remove() }, // 拦截 require 方法 CallExpression(path, { ops }) { const { node } = path if (node.callee.name !== 'require' || node.arguments.length !== 1) { return } let sourceName = node.arguments[0].value let [type, query] = sourceName.split('?') if (type.toLowerCase() !== '##service' && type.toLowerCase() !== '##enum') { return } let methodType = type.toLowerCase().slice(2) let service = collections[methodType[0].toUpperCase() + methodType.slice(1)][query] if (!service) { return } const newCall = t.callExpression(node.callee, [t.stringLiteral(resolve(service.file))]) path.replaceWith(t.callExpression(t.memberExpression(newCall, t.identifier(methodType)), [t.stringLiteral(query)])) }, }, } } 复制代码
通过 ##service
和 ##enum
匹配要替换的代码,进行替换
import Service from '##service?some.package.SomeService' 复制代码
替换成
import real_path_of_service_proto from 'real/path/of/service.proto' const Service = real_path_of_service_proto.service() 复制代码
;
import SomeEnum from '##enum?some.package.SomeEnum' 复制代码
替换成
import real_path_of_service_proto from 'real/path/of/service.proto' const SomeEnum = real_path_of_service_proto.enum() 复制代码
。
最后在项目的最开始执行 polyfill,保证在执行 proto 的时候有对应的 service 和 enum 方法
import { init } from './polyfill' init(config) 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。