Веб-gRPC — это адаптированная реализация gRPC в Интернете. Я не буду объяснять его введение и почему использовать gRPC здесь, если вы решите использовать веб-gRPC и ищете интерфейсные библиотеки и решения, взгляните на эту статью, она должна быть полезной.
Существует множество способов использования gRPC, и каждый метод решения имеет свои особенности, свои преимущества и недостатки.
Далее будут перечислены три схемы доступа
- google-protobuf + grpc-web-client
- grpc-web (недавно выпущенный)
- protobufjs + загрузчик webpack + grpc-web-client + polyfill (в настоящее время используется)
1. google-protobuf + grpc-web-client
google-protobufЭто инструмент компиляции файлов protobuf, предоставляемый Google, который может компилировать protobuf на различные языки, и мы используем его для компиляции файлов js.
grpc-web-clientЗатем вы можете выполнить js, сгенерированный google-protobuf, и вызвать удаленную службу 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);
}
}
});
код пакета
Оберните метод вызова
упаковкаgrpc.invoke
метод, с одной стороны, он может единообразно обрабатывать хост, заголовок, ошибку, журнал увеличения и т. д.
С другой стороны, его можно преобразовать в 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)
}
2. grpc-web
grpc-webЭто решение официально выпущено gRPC, Его идея реализации такова:
Сначала скомпилируйте прото-файл в код 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.Объект запроса нужно собрать через новый и установить. Разница в том, что скомпилированный js имеет встроенный метод запроса и не нуждается в другой библиотеке для вызова метода.
3. protobufjs + webpack loader + grpc-web-client + polyfill
В отличие от первых двух, этот метод позволяет сэкономить этапы ручной компиляции и операции строгого создания объекта запроса, а также является более «динамичным» в использовании.
Реализовать идеи
Используйте загрузчик веб-пакета для компиляции во время создания веб-пакета.Хотя скомпилированный результат — это js, js — это не класс, соответствующий proto, а процесс введения protobufjs и разбора объекта упаковки. Фактический синтаксический анализ выполняется во время выполнения, возвращая корневой объект protobufjs.
Метод службы добавляется путем добавления метода к прототипу, и возвращается объект, который может напрямую выполнять метод rpc. Конкретный метод выполнения зависит от grpc-web-client. Поскольку protobufjs может напрямую преобразовывать обычные объекты в объекты запроса, метод напрямую получает обычные объекты.Внутреннее преобразование
создать формат путиimport Service from '##service?some.package.SomeService'
Используйте подключаемый модуль babel для анализа синтаксиса импорта, найдите в каталоге protobuf файл, определяющий эту службу, и измените его на
import real_path_of_service_proto from 'real/path/of/service.proto'
const Service = real_path_of_service_proto.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`
}
Четвертая строчка снизу кода, build, отвечает за добавление зависимого модуля proto к текущему корневому объекту, а размещение его в других файлах отдельно — для сохранения размера скомпилированного кода
Это код сборки. Рекурсия может быть оптимизирована с помощью стека. Поскольку влияние этой части на производительность слишком мало, на данный момент она будет проигнорирована.
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
Цель полифилла — упростить использование выполнения 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
},
})
}
Клиентплан 1GrpcClient разобрался в
- babel-plugin
Сначала просмотрите все прото-файлы, чтобы создать словарь.
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
}
Затем замените оператор импорта и метод 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()
.
Наконец, выполните полифилл в начале проекта, чтобы убедиться, что при выполнении прототипа существуют соответствующие методы службы и перечисления.
import { init } from './polyfill'
init(config)
Суммировать
В следующей статье будут проанализированы преимущества и недостатки этих трех методов, приглашаю всех обратить внимание