是什么东西?

Protocol Buffers 是一种序列化数据结构的协议。对于透过管线(pipeline)或存储数据进行通信的程序开发上是很有用的。这个方法包含一个接口描述语言,描述一些数据结构,并提供程序工具根据这些描述产生代码,用于将这些数据结构产生或解析数据流。


浏览器端使用方法

安装工具库

1
npm i protobufjs

将后端接口开发提供的 .proto 文件转换成对应的 .js 文件,通过工具的命令 pbjs

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

执行后会将所有 proto 文件合并成一个 es6 格式的 js 文件,方便在具备打包环境的项目中使用。

封装请求函数

一般来说常用请求就是 postget

Protocol buffers 需要通过二进制数据进行传输,且需要通过 proto 文件中定义的数据格式来约束请求数据跟响应数据

因此我们在柯里化函数思路里可以考虑一个入参的流程

入参数据定义 -> 响应数据定义 -> 正式发送请求

有了流程我们开始设计第一个 入参数据的定义

1
2
3
4
5
6
7
8
9
10
/**
* @param {Object} protoRoot .proto文件转换成js的对象
* @param {String} reqMessageName 对应请求消息格式的名称 比如 model.CommonReq
*/
(protoRoot, reqMessageName) => {
let reqMessage;
if (reqMessageName) {
reqMessage = protoRoot.lookup(reqMessageName);
}
};

因为考虑到 get 请求可能并不需要入参因此根据 reqMessageName 来决定是否要描述入参数据

接下来开发 响应数据定义

1
2
3
4
5
6
7
/**
* 设置反馈消息
* @param {String} resMessageName 对应反馈消息格式的名称 比如 model.CommonRes
*/
return function(resMessageName) {
const resMessage = protoRoot.lookup(resMessageName);
};

最后就是正式的数据请求以及对反馈数据的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @param {String} method 请求方法
* @param {String} url 请求地址
* @param {Object} data 请求内容
*/
return function(method = 'get', url, data = {}) {
let message, sendData;
if (method.toLowerCase() === 'post') {
message = reqMessage.create(data);
sendData = reqMessage.encode(message).finish();
} else {
sendData = null;
const querySearch = useObjectQueryStringify(data);
url += querySearch === '' ? '' : `?${querySearch}`;
}

return new Promise((rs, rj) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.responseType = 'arraybuffer';
xhr.setRequestHeader('Content-Type', 'application/x-protobuf');
xhr.onload = function(response) {
const result = resMessage.toObject(
resMessage.decode(new Uint8Array(response.target.response)),
{
// 确保 byte 格式的会转换为 base64string
bytes: String
}
);
// 循环结果,将 base64string => string,忽略解码错误
for (let k in result) {
if (~toString.call(result[k]).indexOf('String')) {
try {
result[k] = atob(result[k]);
} catch (e) {}
}
}
rs(result);
};
xhr.onerror = function(error) {
rj(error);
};
xhr.send(sendData);
});
};

三个主要步骤都有了之后合并成一个 curry function

1
2
3
4
5
6
7
8
9
10
11
12
export default (protoRoot, reqMessageName) => {
let reqMessage;
if (reqMessageName) {
reqMessage = protoRoot.lookup(reqMessageName);
}
return function(resMessageName) {
const resMessage = protoRoot.lookup(resMessageName);
return function(method = 'get', url, data = {}) {
// 此处省略..
};
};
};

使用范例

先将上面的函数保存为 usePBRequest.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import protoRoot from './proto/proto.js';
import usePBRequest from './usePBRequest.js';

// get 请求
const commonReq = usePBRequest(protoRoot)('model.CommonRs');
commonReq('get', '/api/get1').then(resp => {
console.log(resp);
});
commonReq('get', '/api/get2').then(resp => {
console.log(resp);
});

// post 请求
const postReq = usePBRequest(protoRoot, 'model.CommonReq')(
// 请求格式声明
'model.CommonRs'
); // 响应格式声明

postReq('post', '/api/post1', { userId: 1, token: 'aaaa' }) // 发送数据
.then(resp => {
console.log(resp);
});

postReq('post', '/api/post2', { postId: 1 }) // 发送数据
.then(resp => {
console.log(resp);
});

更新函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import useObjectQueryStringify from './useObjectQueryStringify';

/**
* 独立一个创建请求数据函数用于嵌套
* @param {Object} data 请求内容
* @param {Object} { protoRoot: proto 转 js 后的对象, reqType: 请求格式描述 }
*/
export const createSendData = (data, { protoRoot, reqType }) => {
const reqMessage = protoRoot.lookup(reqType);
return reqMessage.encode(reqMessage.create(data)).finish();
};

/**
* 封装 protocol buffers 请求方法
* @param {Object} proto 转 js 后的对象
*/
export default protoRoot => ({ reqType, resType }) => {
/** reqType: 请求格式描述; resType: 响应格式描述 */
let resMessage;
resMessage = protoRoot.lookup(resType);

/**
* @param {String} method 请求方法
* @param {String} url 请求地址
* @param {Object} data 请求内容
*/
return function(method = 'get', url, data = {}) {
let sendData;
if (method.toLowerCase() === 'post') {
sendData = createSendData(data, { protoRoot, reqType });
} else {
sendData = null;
const querySearch = useObjectQueryStringify(data);
url += querySearch === '' ? '' : `?${querySearch}`;
}

return new Promise((rs, rj) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.responseType = 'arraybuffer';
xhr.setRequestHeader('Content-Type', 'application/x-protobuf');
xhr.onload = function(response) {
const result = resMessage.toObject(
resMessage.decode(new Uint8Array(response.target.response)),
{
// 确保 byte 格式的会转换为 base64string
bytes: String
}
);
// 循环结果,将 base64string => string,忽略解码错误
for (let k in result) {
if (~toString.call(result[k]).indexOf('String')) {
try {
result[k] = atob(result[k]);
} catch (e) {}
}
}
rs(result);
};
xhr.onerror = function(error) {
rj(error);
};
xhr.send(sendData);
});
};
};