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 相关文章推荐
jQuery获取地址栏参数插件(模仿C#)
Oct 26 Javascript
jQuery获取和设置表单元素的方法
Feb 14 Javascript
用js的document.write输出的广告无阻塞加载的方法
Jun 05 Javascript
jQuery中not()方法用法实例
Jan 06 Javascript
JavaScript函数详解
Feb 27 Javascript
JavaScript实现非常简单实用的下拉菜单效果
Aug 27 Javascript
ECHO.js 纯javascript轻量级延迟加载的实例代码
May 24 Javascript
js实现从左向右滑动式轮播图效果
Jul 07 Javascript
分享ES6的7个实用技巧
Jan 18 Javascript
Vue.js实现开发购物车功能的方法详解
Feb 22 Javascript
在layer弹层layer.prompt中,修改placeholder的实现方法
Sep 27 Javascript
JavaScript实现抖音罗盘时钟
Oct 11 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
mysql 搜索之简单应用
2007/04/27 PHP
php目录管理函数小结
2008/09/10 PHP
php中用数组的方法设置cookies
2011/04/21 PHP
php实现建立多层级目录的方法
2014/07/19 PHP
理解php依赖注入和控制反转
2016/05/11 PHP
php fseek函数读取大文件两种方法
2016/10/12 PHP
JavaScript 空位补零实现代码
2010/02/26 Javascript
jcarousellite.js 基于Jquery的图片无缝滚动插件
2010/12/30 Javascript
扩展JS Date对象时间格式化功能的小例子
2013/12/02 Javascript
JavaScript DOM事件(笔记)
2015/04/08 Javascript
javascript常见数字进制转换实例分析
2016/04/21 Javascript
RGB和YUV 多媒体编程基础详细介绍
2016/11/04 Javascript
Vue 2.0+Vue-router构建一个简单的单页应用(附源码)
2017/03/14 Javascript
vue 监听屏幕高度的实例
2018/09/05 Javascript
从零开始封装自己的自定义Vue组件
2018/10/09 Javascript
react MPA 多页配置详解
2019/10/18 Javascript
vue中在vuex的actions中请求数据实例
2019/11/08 Javascript
VUE实现吸底按钮
2021/03/04 Vue.js
以911新闻为例演示Python实现数据可视化的教程
2015/04/23 Python
详谈Python高阶函数与函数装饰器(推荐)
2017/09/30 Python
分享一个简单的python读写文件脚本
2017/11/25 Python
Python装饰器用法实例总结
2018/02/07 Python
Python2包含中文报错的解决方法
2018/07/09 Python
pytorch常见的Tensor类型详解
2020/01/15 Python
python 装饰器的使用示例
2020/10/10 Python
一个非常简单好用的Python图形界面库(PysimpleGUI)
2020/12/28 Python
HTML5引入的新数组TypedArray介绍
2012/12/24 HTML / CSS
HTML5 video标签(播放器)学习笔记(二):播放控制
2015/04/24 HTML / CSS
关于html字符串正则判断和匹配的具体使用
2019/12/12 HTML / CSS
Lookfantastic美国/加拿大:英国知名美妆购物网站
2019/03/27 全球购物
纯净、自信、100%的羊绒服装:360Cashmere
2021/02/20 全球购物
服装创业计划书范文
2014/02/05 职场文书
中药专业毕业自荐书范文
2014/02/08 职场文书
学校交通安全责任书
2014/08/25 职场文书
交通事故赔偿协议书
2014/10/16 职场文书
Python实现随机生成迷宫并自动寻路
2021/06/13 Python