概述
在翼文档的需求中,后端提议采用proto进行前后端数据的传输,其考虑有两点,其一是数据不是明文传输(二进制),从某种程度上增加安全性以及传输速率,其二便是proto文件可以直接作为前后端接口文档,提高开发效率和减少沟通成本。本文以axios + protobufjs作为基础,讲述如何在生产中使用。
流程
-
前端首先借助protobufjs生成proto.js文件
-
将请求体进行编码,生成二进制请求体,而本次需求,后端需要前端将二进制再进行转换成base64,所以需要多一步二进制转base64的操作
-
axios发送post请求,并通过transformRequest将第二步编码后的数据交给axios
-
后端接收请求并处理,将处理结果以二进制或者base64的形式返回给axios,通过transformResponse进行解码
-
页面根据json数据进行展示或者交互
proto文件
proto文件必须指定请求体的数据结构以及返回体的数据结构,一般由后端定义,常用在微服务中。但是通过protobufjs,我们纯前端也可以使用。
base.proto文件:
syntax = "proto3";
// 包名,很重要
package framework;
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
option java_multiple_files = true;
option java_outer_classname = "BaseProto";
option java_package = "com.ctg.doc.portal.protobuf.generated";
// 通用的请求数据
message CommonParam {
optional uint64 userId = 1;
optional string loginToken = 2;
optional uint32 clientId = 3;
optional string deviceId = 4;
}
// 通用的返回数据
message Response {
uint32 resultCode = 1;
optional bytes data = 2;
optional string errorMsg = 3;
}
user.proto文件:
syntax = "proto3";
package framework;
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
import "base.proto"; // 引入通用的proto文件
option java_multiple_files = true;
option java_outer_classname = "PortalAccountProto";
option java_package = "com.ctg.doc.portal.protobuf.generated";
// 查询用户的请求数据
messageQueryUserInfoRequest {
CommonParam commonParam = 1;
}
// 用户信息返回数据(data部分)
message UserInfoData {
uint64 userId = 1;
string userName = 2;
string headUrl = 3;
string mobile = 4;
}
// 登录的请求数据
message LoginRequest {
CommonParam commonParam = 1;
string mobile = 2;
string password = 3;
}
// 登录的返回数据(data部分)
message LoginData {
UserInfoData userInfo = 1;
string token = 2;
}
上面的proto文件相信有计算机基础的都看得懂,基本就是对请求数据或者响应数据进行定义,其值是多少我们不用关心,只要了解属性便可。
所以我们在请求的时候,需要定义好三个最主要的元素
-
请求url,这个也看后端,如果后端把所有的请求放在一个url处理,然后通过增加一个请求名来区分不同的请求的话则不需要。本次每个请求一个url,因此需要定义url
-
请求体,例如上面的LoginRequest
-
响应体,例如上面的Response以及LoginData,这里的LoginData就是Response的登录返回data定义
步骤
处理proto文件
处理proto文件我们需要借助protobufjs来处理,它会帮我们基于*.proto文件生成proto.js文件或者proto.json文件,我们就是通过proto.js或者proto.json进行数据的序列化和反序列化的,本文使用proto.js。
安装依赖
npm install protobufjs
// OR
pnpm install protobufjs
// OR
yarn add protobufjs
生成proto.js文件
在package.json中,配置脚本,在运行或者打包的时候,需要先执行这个脚本生成proto.js
"scripts": {
"proto": "pbjs -t json-module -w commonjs -o src/proto/proto.js src/proto/*.proto"
}
编码
import protoRoot from 'xxx/proto' // 引入proto.js文件
function addPrefix(requestType: string) {
// 注意这里的framework就是proto文件定义的包名
return 'framework.' + resquestType
}
// 将请求数据encode成二进制,然后再转为base64
function transformRequestFactory(requestType: string) {
const PBRequest = protoRoot.lookupType(addPrefix(requestType))
return function (data: any) {
console.log('params', data)
return btoa(String.fromCharCode(...PBRequest.encode(data).finish())) // base64
// return PBRequest.encode(data).finish() // 二进制
}
}
解码
import protoRoot from 'xxx/proto' // 引入proto.js文件
function base64ToBuffer(base64: string) {
const binary = atob(base64)
const arrayBuffer = new ArrayBuffer(binary.length)
const uint8 = new Uint8Array(arrayBuffer)
for (let i = 0; i < uint8.length; i++) {
uint8[i] = binary.charCodeAt(i)
}
return uint8
}
function transformResponseFactory(responseData?: string) {
return function transformResponse(rawResponse: string, _: AxiosHeaders, status?: number) {
if (status !== 200) {
return
}
try {
// decode响应体
const buf = base64ToBuffer(rawResponse)
// 这里的Response和上面base.proto定义的通用响应体保持一致
const PBResponse = protoRoot.lookupType(addPrefix('Response'))
const decodedResponse = PBResponse.decode(buf)
const result = PBResponse.toObject(decodedResponse, {
defaults: true
})
// 上面定义的base.proto的通用返回体中data的定义是个二进制,所以我们需要对data再进行一次解码
if (result.data && responseData) {
const model = protoRoot.lookupType(addPrefix(responseData))
result.data =model.decode(result.data).toJSON()
}
return result
} catch (error) {
Promise.reject(error)
}
}
}
封装axios
import axios, { AxiosHeaders } from 'axios'
const httpService = axios.create({
timeout: 45000,
method: 'post',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/octet-stream' // important
},
// responseType: 'arraybuffer', // 根据需要打开,如果你想让axios自动转为arraybuffer,或者后端本来就是返回二进制,这可以打开
baseURL: process.env.VUE_APP_API_PREFIX
})
function request<T = any>(url: string, requestType: string, responseData?: string, params?: { [key: string]: any }) {
if (!url.startsWith('/')) {
url += '/'
}
const PBRequest = protoRoot.lookupType(addPrefix(requestType))
const reqData = {
commonParam: {
userId: localStorage.getItem('uid') || '',
loginToken: localStorage.getItem('token') || '',
clientId: 1
},
...(params || {})
}
// 将对象序列化成请求体实例
const req = PBRequest.create(reqData)
return new Promise<BaseResponse<T>>((resolve, reject) => {
httpService
.post<T>(url, req, {
transformRequest: transformRequestFactory(requestType),
transformResponse: transformResponseFactory(responseData)
})
.then(
({ data }: any) => {
// 可以根据data.resultCode进行业务异常的通用处理,比如业务繁忙(限流导致),服务器异常等等
resolve(data)
},
error => {
const { response } = error
if (response?.status !== 200) {
const msg = '请求异常'
message.error(msg)
const error = {
msg,
code: SERVER_FAILED_CODE
}
return reject(error)
}
reject(error)
}
)
})
}
编写接口
export function login(params: { mobile: string; password: string }) {
return request<LoginResponse>('/account/login/mobilePassword', 'LoginRequest', 'LoginData', params)
}
总结
我们可以通过protobufjs来实现用proto文件进行前后端的数据传输,其一般为二进制的方式进行传输,传输的大小和安全性比传统的优越,且后端不需要额外编写接口文档,提高开发效率和沟通成本。
但也有缺点,由于请求体和响应体是二进制的格式,所以调试或者定位问题的时候会比较麻烦,而且mock数据也会相对麻烦点。在开发模式下,我们可以通console.log打印出来,但是线上环境就行不通,会某种程度上造成定位问题效率低。
整体上来说,肯定是利大于弊。