如何在前端中使用protobuf(vue篇)
前言
由于目前公司采用了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) {...})
- 转化为JSON或js后使用,如
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
:
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)
一下:
可以发现,这个模块在原型链上定义了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是否是arrayBufferif (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可以合并在一起写一个自动化的脚本,每次更新只需运行一下这个脚本即可)。
写的比较啰嗦,文笔也不好,大家见谅。
这个流程就是我感觉比较好的一个proto在前端的实践,可能并不是最好,如果在你们公司有其他更好的实践,欢迎大家一起交流分享。
后续
在vue中使用是需要打包成一个js模块来使用比较好(这是因为vue在生产环境中打包成只有html,css,js等文件)。但在某些场景,比如在Node环境中,一个Express的项目,生产环境中是允许出现.proto
文件的,这时候可以采取protobuf.js
提供的其他方法来动态解析proto,不再需要npm run proto这种操作了。
后面有时间我会再写一篇在node端动态解析proto的记录。
如何在前端中使用protobuf(vue篇)相关推荐
- 前端面试总结(vue篇)
vue的优点 vue是个轻量级的框架,是一个构建数据的视图集合,大小只有几十Kb vue是组件化开发,适合多人开发 vue中的双向数据绑定更方便操作表单数据 因为vue是MVVM的框架,视图,数据,结 ...
- 第 5 节:前端面试指南 — Vue 篇(附面试题)
更新完「JavaScript篇」之后,忙完上周的送书活动,很多读者朋友都在微信催更「面试指南」系列的第 5 期 - vue篇. 前 4 期没看的同学,建议先看完再来看这一期的,传送门: 第 1 期:「 ...
- 2020前端最新面试题(vue篇)
2020前端最新面试题(vue篇) 由于疫情原因,原本每年的"金三银四"仿佛消失,随之而来的是找工作的压力,这里给要面试的小伙伴们总结了到目前为止我遇到的前端面试题,仅供参考哦,第 ...
- 【面试】前端面试之Vue篇
文章目录 前端面试之Vue篇 vue组件的核心选项 vue组件的通信方式有哪几种? vue如何操作dom节点? vue当数据更新是如何更新dom节点的? computed内想传入一个参数,要怎么实现? ...
- Vue React Angular之三国杀,web前端入坑第六篇 上
「 懒癌引发血案 」 目前前端技术栈发生了翻天覆地的变化,上篇刚写了只会jquery 要失业,再不学新的你就要被淘汰,虽然有点危言耸听,不过现实情况确实是这样. vue.react.angular对比 ...
- Vue、React、Angular之三国杀,web前端入坑第六篇(上)
「 懒癌引发血案 」 目前前端技术栈发生了翻天覆地的变化,上篇刚写了只会jquery 要失业,再不学新的你就要被淘汰,虽然有点危言耸听,不过现实情况确实是这样. vue.react.angular对比 ...
- flask 获取前端form内容_flask 项目中使用 bootstrapFileInput(进阶篇)
bootstrap 为 flask 使用人员提供了一个非常优美且有效的前端页面组件,但是完美之处还存在些许缺陷,比如文件的上传功能.而 bootstrap-fileinput 是基于 bootstra ...
- 前端开发中常用设计模式-总结篇
本文是向大家介绍前端开发中常用的设计模式,它使我们编写的代码更容易被复用,也更容易被人理解,并且保证代码的稳定可靠性. 1.什么是设计模式 通俗来讲,就是日常使用设计的一种惯性思维. 因为对应的这种思 ...
- VUE 前端中如何进行组件化开发?
1.前端里面常说的视图层是什么? 我们把HTML中的DOM就可以与其他的部分独立开来划分出一个层次,这个层次就叫做视图层. Vue 的核心库只关注视图层 图1: dom对象树结构 图2:DOM和CSS ...
- 前端面试高频精讲(二)Vue篇
watch 和 computed 的区别 watch 是监听,支持异步操作,内部有 immediate.deep.handle三个属性,当一条数据影响多条数据时,使用 watch ,常见的就是搜索 c ...
最新文章
- Per Johansson:经济学中自然实验和准实验 | 周日直播·因果科学读书会
- Java黑皮书课后题第2章:*2.22(金融应用:货币单位)改写程序清单2-10,解决将double转int可能会造成精度损失问题。以整数值作为输入,其最后两位代表的是美分币值
- 转载:程序员从初级到中级10个秘诀
- HUE集成Hbase
- 修改Maven本地仓库的位置 方法
- java注解 @SuppressWarnings注解用法
- luogu p1652 圆
- 浪潮成立Inspur DevOps Lab:以应用开发和平台服务打造开发者生态
- 如何在mac中使用downie下载视频?
- 2018年暑假第二周
- 为了方便远程登录写的简单expect脚本
- dwg格式的计算机图,看图纸(DWG文件浏览器)
- python面向对象实现算术编码
- 我国南北居民收入差距 正在扩大。
- c语言大小写字母变换,C语言学习:任意大小写字母转换
- Leetcode--Java--212. 单词搜索 II
- 论CTOR添加到11月BCH协议升级
- 说说jvm什么时候会回收垃圾
- linux查看当前文件和文件夹大小
- 70个JavaScript面试题集锦,内含解答,自测 JS 掌握程度