vue中使用protobuf的过程记录


Posted in Javascript onOctober 26, 2018

由于目前公司采用了ProtoBuf做前后端数据交互,进公司以来一直用的是公司大神写好的基础库,完全不了解底层是如何解析的,一旦报错只能求人,作为一只还算有钻研精神的猿,应该去了解一下底层的实现,在这里记录一下学习过程。

Protobuf简单介绍

Google Protocol Buffer(简称 Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。

有几个优点:

1.平台无关,语言无关,可扩展;
2.提供了友好的动态库,使用简单;
3.解析速度快,比对应的XML快约20-100倍;
4.序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。

个人感受: 前后端数据传输用json还是protobuf其实对开发来说没啥区别,protobuf最后还是要解析成json才能用。个人觉得比较好的几点是:

1.前后端都可以直接在项目中使用protobuf,不用再额外去定义model;
2.protobuf可以直接作为前后端数据和接口的文档,大大减少了沟通成本;

没有使用protobuf之前,后端语言定义的接口和字段,前端是不能直接使用的,前后端沟通往往需要维护一份接口文档,如果后端字段有改动,需要去修改文档并通知前端,有时候文档更新不及时或容易遗漏,沟通成本比较大。 使用protobuf后,protobuf文件由后端统一定义, protobuf直接可以作为文档 ,前端只需将protobuf文件拷贝进前端项目即可。如果后端字段有改动,只需通知前端更新protobuf文件即可,因为后端是直接使用了protobuf文件,因此protobuf文件一般是不会出现遗漏或错误的。长此以往,团队合作效率提升是明显的。

废话了一大堆,下面进入正题。 我这里讲的主要是在vue中的使用,是目前本人所在的公司项目实践,大家可以当做参考。

思路

前端中需要使用 protobuf.js 这个库来处理proto文件。

protobuf.js 提供了几种方式来处理proto。

protobuf.load("awesome.proto", function(err, root) {...})
protobuf.load("awesome.json", function(err, root) {...})

众所周知,vue项目build后生成的dist目录中只有html,css,js,images等资源,并不会有 .proto 文件的存在,因此需要用 protobuf.js 这个库将 *.proto 处理成 *.js 或 *.json ,然后再利用库提供的方法来解析数据,最后得到数据对象。

PS: 实践发现,转化为js文件会更好用一些,转化后的js文件直接在原型链上定义了一些方法,非常方便。因此后面将会是使用这种方法来解析proto。

预期目标

在项目中封装一个 request.js 模块,希望能像下面这样使用,调用api时只需指定请求和响应的model,然后传递请求参数,不需关心底层是如何解析proto的,api返回一个Promise对象:

// /api/student.js 定义接口的文件
import request from '@/lib/request'

// params是object类型的请求参数
// school.PBStudentListReq 是定义好的请求体model
// school.PBStudentListRsp 是定义好的响应model
// getStudentList 是接口名称
export function getStudentList (params) {
 const req = request.create('school.PBStudentListReq', params)
 return request('getStudentList', req, 'school.PBStudentListRsp')
}

// 在HelloWorld.vue中使用
import { getStudentList } from '@/api/student'
export default {
 name: 'HelloWorld',
 created () {

 },
 methods: {
 _getStudentList () {
  const req = {
  limit = 20,
  offset = 0
  }
  getStudentList(req).then((res) => {
  console.log(res)
  }).catch((res) => {
  console.error(res)
  })
 }
 }
}

准备工作

1.拿到一份定义好的proto文件。

虽然语法简单,但其实前端不用怎么关心如何写proto文件,一般都是由后端来定义和维护。在这里大家可以直接用一下我定义好的一份 demo 。

// User.proto
package framework;
syntax = "proto3";

message PBUser {
 uint64 user_id = 0;
 string name = 1;
 string mobile = 2;
}

// Class.proto
package school;
syntax = "proto3";

message PBClass {
 uint64 classId = 0;
 string name = 1;
}

// Student.proto
package school;
syntax = "proto3";

import "User.proto";
import "Class.proto";

message PBStudent {
 uint64 studentId = 0;
 PBUser user = 1;
 PBClass class = 2;
 PBStudentDegree degree = 3;
}

enum PBStudentDegree {
 PRIMARY = 0; // 小学生
 MIDDLE = 1; // 中学生
 SENIOR = 2; // 高中生
 COLLEGE = 3; // 大学生
}

message PBStudentListReq {
 uint32 offset = 1;
 uint32 limit = 2;
}

message PBStudentListRsp {
 repeated PBStudent list = 1;
}


// MessageType.proto
package framework;
syntax = "proto3";
// 公共请求体
message PBMessageRequest {
 uint32 type = 1;       // 消息类型
 bytes messageData = 2;      // 请求数据
 uint64 timestamp = 3;      // 客户端时间戳
 string version = 4;       // api版本号

 string token = 14;       // 用户登录后服务器返回的 token,用于登录校验
}

// 消息响应包
message PBMessageResponse {
 uint32 type = 3;       // 消息类型
 bytes messageData = 4;      // 返回数据

 uint32 resultCode = 6;      // 返回的结果码
 string resultInfo = 7;      // 返回的结果消息提示文本(用于错误提示)
}
// 所有的接口
enum PBMessageType {
 // 学生相关
 getStudentList = 0;       // 获取所有学生的列表, PBStudentListReq => PBStudentListRsp
}

其实不用去学习proto的语法都能一目了然。这里有两种命名空间 framework 和 school , PBStudent 引用了 PBUser ,可以认为 PBStudent 继承了 PBUser 。

一般来说,前后端需要统一约束一个请求model和响应model,比如请求中哪些字段是必须的,返回体中又有哪些字段,这里用 MessageType.proto 的 PBMessageRequest 来定义请求体所需字段, PBMessageResponse 定义为返回体的字段。

PBMessageType 是接口的枚举,后端所有的接口都写在这里,用注释表示具体请求参数和返回参数类型。比如这里只定义了一个接口 getStudentList 。

拿到后端提供的这份 *.proto 文件后,是不是已经可以基本了解到:有一个 getStudentList 的接口,请求参数是 PBStudentListReq ,返回的参数是 PBStudentListRsp 。

所以说proto文件可以直接作为前后端沟通的文档。

步骤

1.新建一个vue项目

同时添加安装 axios 和 protobufjs 。

# vue create vue-protobuf
# npm install axios protobufjs --save-dev

2.在 src 目录下新建一个 proto 目录,用来存放 *.proto 文件,并将写好的proto文件拷贝进去。

此时的项目目录和 package.json :

vue中使用protobuf的过程记录

3.将 *.proto 文件生成 src/proto/proto.js (重点)

protobufjs 提供了一个叫 pbjs 的工具,这是一个神器,根据参数不同可以打包成xx.json或xx.js文件。比如我们想打包成json文件,在根目录运行:

npx pbjs -t json src/proto/*.proto > src/proto/proto.json

可以在 src/proto 目录下生成一个proto.json文件,查看请点击这里。 之前说了:实践证明打包成js模块才是最好用的。我这里直接给出最终的命令

npx pbjs -t json-module -w commonjs -o src/proto/proto.js src/proto/*.proto

-w 参数可以指定打包js的包装器,这里用的是commonjs,详情请各位自己去看文档。运行命令后在src/proto目录下生成的 proto.js 。在chrome中 console.log(proto.js) 一下:

vue中使用protobuf的过程记录

可以发现,这个模块在原型链上定义了 load , lookup
等非常有用的api,这正是后面我们将会用到的。 为以后方便使用,我们将命令添加到package.json的script中:

"scripts": {
 "serve": "vue-cli-service serve",
 "build": "vue-cli-service build",
 "lint": "vue-cli-service lint",
 "proto": "pbjs -t json-module -w commonjs -o src/proto/proto.js src/proto/*.proto"
 },

以后更新proto文件后,只需要 npm run proto 即可重新生成最新的proto.js。

4. 封装request.js

在前面生成了proto.js文件后,就可以开始封装与后端交互的基础模块了。首先要知道,我们这里是用axios来发起http请求的。

整个流程:开始调用接口 -> request.js将数据变成二进制 -> 前端真正发起请求 -> 后端返回二进制的数据 -> request.js处理二进制数据 -> 获得数据对象。

可以说request.js相当于一个加密解密的中转站。在 src/lib 目录下添加一个 request.js 文件,开始开发:

既然我们的接口都是二进制的数据,所以需要设置axios的请求头,使用arraybuffer,如下:

import axios from 'axios'
const httpService = axios.create({
 timeout: 45000,
 method: 'post',
 headers: {
 'X-Requested-With': 'XMLHttpRequest',
 'Content-Type': 'application/octet-stream'
 },
 responseType: 'arraybuffer'
})

MessageType.proto 里面定义了与后端约定的接口枚举、请求体、响应体。发起请求前需要将所有的请求转换为二进制,下面是request.js的主函数

import protoRoot from '@/proto/proto'
import protobuf from 'protobufjs'
// 请求体message
const PBMessageRequest = protoRoot.lookup('framework.PBMessageRequest')
// 响应体的message
const PBMessageResponse = protoRoot.lookup('framework.PBMessageResponse')
const apiVersion = '1.0.0'
const token = 'my_token'
function getMessageTypeValue(msgType) {
 const PBMessageType = protoRoot.lookup('framework.PBMessageType')
 const ret = PBMessageType.values[msgType]
 return ret
}
/**
 * 
 * @param {*} msgType 接口名称
 * @param {*} requestBody 请求体参数
 * @param {*} responseType 返回值
 */
function request(msgType, requestBody, responseType) { 
 // 得到api的枚举值
 const _msgType = getMessageTypeValue(msgType)
 // 请求需要的数据
 const reqData = {
 timeStamp: new Date().getTime(),
 type: _msgType,
 version: apiVersion,
 messageData: requestBody,
 token: token
 }
}
 // 将对象序列化成请求体实例
 const req = PBMessageRequest.create(reqData)
 // 调用axios发起请求
 // 这里用到axios的配置项:transformRequest和transformResponse
 // transformRequest 发起请求时,调用transformRequest方法,目的是将req转换成二进制
 // transformResponse 对返回的数据进行处理,目的是将二进制转换成真正的json数据
 return httpService.post('/api', req, {
 transformRequest,
 transformResponse: transformResponseFactory(responseType)
 }).then(({data, status}) => {
 // 对请求做处理
 if (status !== 200) {
  const err = new Error('服务器异常')
  throw err
 }
 console.log(data)
 },(err) => {
 throw err
 })
}
// 将请求数据encode成二进制,encode是proto.js提供的方法
function transformRequest(data) {
 return PBMessageRequest.encode(data).finish()
}
function isArrayBuffer (obj) {
 return Object.prototype.toString.call(obj) === '[object ArrayBuffer]'
}
function transformResponseFactory(responseType) {
 return function transformResponse(rawResponse) {
 // 判断response是否是arrayBuffer
 if (rawResponse == null || !isArrayBuffer(rawResponse)) {
  return rawResponse
 }
 try {
  const buf = protobuf.util.newBuffer(rawResponse)
  // decode响应体
  const decodedResponse = PBMessageResponse.decode(buf)
  if (decodedResponse.messageData && responseType) {
  const model = protoRoot.lookup(responseType)
  decodedResponse.messageData = model.decode(decodedResponse.messageData)
  }
  return decodedResponse
 } catch (err) {
  return err
 }
 }
}
// 在request下添加一个方法,方便用于处理请求参数
request.create = function (protoName, obj) {
 const pbConstruct = protoRoot.lookup(protoName)
 return pbConstruct.encode(obj).finish()
}
// 将模块暴露出去
export default request

最后写好的具体代码请看: request.js 。 其中用到了 lookup() , encode() , finish() , decode() 等几个proto.js提供的方法。

5. 调用request.js

在.vue文件直接调用api前,我们一般不直接使用request.js来直接发起请求,而是将所有的接口再封装一层,因为直接使用request.js时要指定请求体,响应体等固定的值,多次使用会造成代码冗余。

我们习惯上在项目中将所有后端的接口放在 src/api 的目录下,如针对student的接口就放在 src/api/student.js 文件中,方便管理。 将 getStudentList 的接口写在 src/api/student.js 中

import request from '@/lib/request'
// params是object类型的请求参数
// school.PBStudentListReq 是定义好的请求体model
// school.PBStudentListRsp 是定义好的响应model
// getStudentList 是接口名称
export function getStudentList (params) {
 const req = request.create('PBStudentListReq', params)
 return request('getStudentList', req, 'school.PBStudentListRsp')
}
// 后面如果再添加接口直接以此类推
export function getStudentById (id) {
 // const req = ...
 // return request(...)
}

6. 在.vue中使用接口

需要哪个接口,就import哪个接口,返回的是Promise对象,非常方便。

<template>
 <div class="hello">
 <button @click="_getStudentList">获取学生列表</button>
 </div>
</template>
<script>
import { getStudentList } from '@/api/student'
export default {
 name: 'HelloWorld',
 methods: {
 _getStudentList () {
  const req = {
  limit: 20,
  offset: 0
  }
  getStudentList(req).then((res) => {
  console.log(res)
  }).catch((res) => {
  console.error(res)
  })
 }
 },
 created () {
 }
}
</script>
<style lang="scss">
</style>

总结

整个demo的代码:demo 。

前端使用的整个流程:

1. 将后端提供的所有的proto文件拷进 src/proto 文件夹
2. 运行 npm run proto 生成proto.js
3. 根据接口枚举在 src/api 下写接口
4. .vue 文件中使用接口。
(其中1和2可以合并在一起写一个自动化的脚本,每次更新只需运行一下这个脚本即可)。

写的比较??拢?谋室膊缓茫?蠹壹?隆?/p>

这个流程就是我感觉比较好的一个proto在前端的实践,可能并不是最好,如果在你们公司有其他更好的实践,欢迎大家一起交流分享。

后续

在vue中使用是需要打包成一个js模块来使用比较好(这是因为vue在生产环境中打包成只有html,css,js等文件)。但在某些场景,比如在Node环境中,一个Express的项目,生产环境中是允许出现 .proto 文件的,这时候可以采取 protobuf.js 提供的其他方法来动态解析proto,不再需要npm run proto这种操作了。

以上所述是小编给大家介绍的vue中使用protobuf的过程记录,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
node.js 一个简单的页面输出实现代码
Mar 07 Javascript
javascript实现TreeView 无刷新展开的实例代码
Jul 13 Javascript
js实现倒计时(距离结束还有)示例代码
Jul 24 Javascript
jQuery中even选择器的定义和用法
Dec 23 Javascript
js实现同一页面多个不同运动效果的方法
Apr 10 Javascript
Backbone.js 0.9.2 源码注释中文翻译版
Jun 25 Javascript
Webpack性能优化 DLL 用法详解
Aug 10 Javascript
JS实现去除数组中重复json的方法示例
Dec 21 Javascript
详解Vue中CSS样式穿透问题
Sep 12 Javascript
node爬取新型冠状病毒的疫情实时动态
Feb 06 Javascript
Vue2.0 ES6语法降级ES5的操作
Oct 30 Javascript
详细介绍Next.js脚手架完整搭建封装
Apr 26 Javascript
iview通过Dropdown(下拉菜单)实现的右键菜单
Oct 26 #Javascript
Javascript中弹窗confirm与prompt的区别
Oct 26 #Javascript
Phaser.js实现简单的跑酷游戏附源码下载
Oct 26 #Javascript
如何用Node写页面爬虫的工具集
Oct 26 #Javascript
Javascript中绑定click事件的四种方式介绍
Oct 26 #Javascript
使用webpack打包后的vue项目如何正确运行(express)
Oct 26 #Javascript
jQuery Ajax实现Select多级关联动态绑定数据的实例代码
Oct 26 #jQuery
You might like
PHP怎样调用MSSQL的存储过程
2006/10/09 PHP
ThinkPHP CURD方法之field方法详解
2014/06/18 PHP
PHP中使用数组指针函数操作数组示例
2014/11/19 PHP
php中文验证码实现方法
2015/06/18 PHP
php_pdo 预处理语句详解
2016/11/21 PHP
phpstudy默认不支持64位php的解决方法
2017/02/20 PHP
thinkPHP5.0框架简单配置作用域的方法
2017/03/17 PHP
基于laravel-admin 后台 列表标签背景的使用方法
2019/10/03 PHP
PHP Beanstalkd消息队列的安装与使用方法实例详解
2020/02/21 PHP
Mootools 1.2教程 同时进行多个形变动画
2009/09/15 Javascript
GRID拖拽行的实例代码
2013/07/18 Javascript
moment.js轻松实现获取当前日期是当年的第几周
2015/02/05 Javascript
基于jquery实现左右按钮点击的图片切换效果
2021/01/27 Javascript
js自定义回调函数
2015/12/13 Javascript
基于jQuery插件实现点击小图显示大图效果
2016/05/11 Javascript
JQuery 传送中文乱码问题的简单解决办法
2016/05/24 Javascript
JavaScript简单获取页面图片原始尺寸的方法
2016/06/21 Javascript
简单实现JavaScript弹幕效果
2020/08/27 Javascript
javascript 玩转Date对象(实例讲解)
2017/07/11 Javascript
完美解决手机浏览器顶部下拉出现网页源或刷新的问题
2017/11/30 Javascript
vue 使用ref 让父组件调用子组件的方法
2018/02/08 Javascript
js实现移动端轮播图
2020/12/21 Javascript
[01:34]2016国际邀请赛中国区预选赛IG战队教练采访
2016/06/27 DOTA
Python3基础之条件与循环控制实例解析
2014/08/13 Python
Python 查看list中是否含有某元素的方法
2018/06/27 Python
Python开发之Nginx+uWSGI+virtualenv多项目部署教程
2019/05/13 Python
python tkinter实现屏保程序
2019/07/30 Python
Python操作注册表详细步骤介绍
2020/02/05 Python
如何通过Python3和ssl实现加密通信功能
2020/05/09 Python
pycharm开发一个简单界面和通用mvc模板(操作方法图解)
2020/05/27 Python
巴西最大的体育用品商城:Netshoes巴西
2016/11/29 全球购物
外贸员简历中的自我评价
2014/03/04 职场文书
副董事长岗位职责
2014/04/02 职场文书
2014年社区计生工作总结
2014/11/18 职场文书
实习科室评语
2015/01/04 职场文书
Python Django 后台管理之后台模型属性详解
2021/04/25 Python