uniapp 微信小程序使用protobuf(http请求)

5,366 阅读4分钟

前言

之前一直是搞Android开发,最近公司要上小程序,前后端数据交互是用的protobuf,js啥的都已经还给老师了,反正是边学边搞吧!这里选择了uniapp作为微信小程序的开发,看了uniapp官网的介绍,感觉挺吊的。

第一步:protobuf的编解码

先准备好proto文件

syntax = "proto3";

package common;

message ErrorInfo {
    fixed32 errorCode = 1; //错误码
    bytes errorMessage = 2; //错误描述信息
}

message AwesomeMessage{
    fixed32 version     = 1; 
    fixed32 app         = 2; 
    fixed32 server      = 3;
    fixed32 servant     = 4; 
    bytes data          = 9;
}

protobuf的编解码就要用到protobufjs

进入你的项目目录安装protobufjs,这里顺便把axios也安装了(后来发现axios在微信小程序里用不了)前端http请求基本都是用的axios

npm install axios protobufjs --save

生成proto的js文件,在这里我们用static-module 来生成js文件,和json-module有啥区别我目前也不知道。目前发现用json-module,使用lookup和lookuptype在微信小程序中报错,h5可以正常编解码。

npx pbjs -t static-module -w commonjs -o proto/bundle.js proto/*.proto

接下来是使用这个bundle.js来进行编解码

import {common} from '../../proto/bundle.js';

let errinfo = common.ErrorInfo.create({
	errorCode: 0,
	errorMessage:"success"
})

//把ErrorInfo对象 编码成Uint8Array (browser) or Buffer (node)
let errBuffer = common.ErrorInfo.encode(errinfo).finish()

//把Uint8Array (browser) 或者 Buffer (node) 解码成ErrorInfo对象
let message = common.ErrorInfo.decode(errBuffer)

//转化为一个对象
let obj = common.ErrorInfo.toObject(message, {
	enums: String, // enums as string names
	longs: String, // longs as strings (requires long.js)
	bytes: String,
})
console.log(obj);

输出的结果

{errorCode: 0, errorMessage: "success="}

结果打印出来发现多了一个= ,然后看官方文档

var object = AwesomeMessage.toObject(message, {
  enums: String,  // enums as string names
  longs: String,  // longs as strings (requires long.js)
  bytes: String,  // bytes as base64 encoded strings
  defaults: true, // includes default values
  arrays: true,   // populates empty arrays (repeated fields) even if defaults=false
  objects: true,  // populates empty objects (map fields) even if defaults=false
  oneofs: true    // includes virtual oneof fields set to the present field's name
});

//bytes as base64 encoded strings

字节作为base64编码的字符串, 那去掉bytes: String, 结果打印成了字节数组,

{errorCode: 0, errorMessage: Uint8Array(5)}

接下来试试用中文,结果直接报错了"Error: invalid encoding"

QQ截图20210522153630.png

最后修改的代码是这样

import {common} from '../../proto/bundle.js';

let errinfo = common.ErrorInfo.create({
	errorCode: 0,
	errorMessage:Buffer.from('成功')
})

//把ErrorInfo对象 编码成Uint8Array (browser) or Buffer (node)
let errBuffer = common.ErrorInfo.encode(errinfo).finish()

//把Uint8Array (browser) 或者 Buffer (node) 解码成ErrorInfo对象
let message = common.ErrorInfo.decode(errBuffer)

//转化为一个对象
let obj = common.ErrorInfo.toObject(message, {
	enums: String, // enums as string names
	longs: String, // longs as strings (requires long.js)
        defaults: true,//使用默认值,要不然需要判断undefined
	//bytes: String,
})
console.log(obj);
let buf = Buffer.from(obj.errorMessage)
console.log(`errorMessage = ${buf.toString()}`);

打印出来结果对了,然后在小程序模拟器上运行也是可以的

errorMessage = 成功

第二步:通过http把protobuf数据发给服务器,响应之后再解码protobuf得到结果

接下来是登录的proto文件

syntax = "proto3";

package user;

import "Common.proto";

message login_req
{
    bytes  phone            = 1;
    uint32 type             = 2;
    bytes  verify_info      = 3;
}

message login_rsp {
    ErrorInfo errInfo  = 1; // 错误码信息
    uint32   id       = 2;
    bytes    token    = 3; 
}

接下来是proto的编码,修改了或者新增了proto文件之后都要通过pbjs重新生成一下js文件

import {common,	user} from '../../proto/bundle.js';

const axios = require('axios');

let loginMessage = user.login_req.create({
	phone: Buffer.from('1234567890'),
	type: 1,
	verifyInfo: Buffer.from('123456a'),//这里要注意如果按照proto文件的来verify_info就错了
})
//proto对象转buffer
let buffer = user.login_req.encode(loginMessage).finish()
console.log(buffer);
let requestMessage = common.AwesomeMessage.create({
	version: 1,
	app: 1,
	server: 2,
	servant: 1005,
	data: buffer,
})
let requestBuffer = common.AwesomeMessage.encode(requestMessage).finish()
console.log(requestBuffer);

注意 开始是这样写的 js对象的verify_info属性对应于proto文件的verify_info,结果发现和java打印出来的字节数组对比,发现少了几个,后来打印loginMessage发现verify_info对应的值没了,然后去bundle.js发现了这个如下图

let loginMessage = user.login_req.create({
	phone: Buffer.from('1234567890'),
	type: 1,
	verify_info: Buffer.from('123456a'),//错误,应该是verifyInfo
})

QQ截图20210522160659.png

proto采用驼峰命名原则更好一些

接下来就是前端使用axios的http请求了

function transformRequest(data) {
    return common.MsgWebsocket.encode(requestMessage).finish()
}
axios.create({
    timeout: 15000,
    method: 'post',
    headers: {
        "X-Requested-With": "XMLHttpRequest",
        "Content-Type": "application/octet-stream",
    },
    responseType: 'arraybuffer',
}).post('http://********', requestMessage, {
        transformRequest: transformRequest
    })
    .then((response) => {
        console.log(response);
        if (response.status === 200) {
            try {
                let enc = new TextDecoder('utf-8')
                let res = JSON.parse(enc.decode(new Uint8Array(response.data))) //转化成json对象
                console.log(res);
            } catch (e) {
                //let resBuf = protobuf.util.newBuffer(response.data)
                let resBuf = Buffer.from(response.data)
                let resMessage = common.AwesomeMessage.decode(resBuf)
                let loginRspBuf = resMessage.data
                let loginRspMessage = user.login_rsp.decode(loginRspBuf)
                let obj = user.login_rsp.toObject(loginRspMessage, {
                    longs: String,
                    enums: String,
                })
                console.log(obj);
                console.log(`errorMessage = ${Buffer.from(loginRspMessage.errInfo.errorMessage).toString()}`);
            }
        }
    }, (err) => {
        console.log(err);
    });

大家可以看到为什么要转换为json对象,不是说的protobuf吗?这里好像是后端的一个bug

请求成功时,后端返回protobuf字节数组

请求失败时,后端返回json对象

此时请求成功和失败返回的http状态码都是200

ok,打印出服务器返回的protobuf结果了

QQ截图20210522163548.png

好,接下来就是在微信小程序中运行了,结果报错

QQ截图20210522163756.png

出错是在这一行,发现是axios这里报错了 QQ截图20210522164054.png

星星你个星星,微信小程序不支持axios!!!!!

第三步:在微信小程序中通过http把protobuf数据发给服务器,响应之后再解析protobuf得到结果

在这里uniapp已经封装了http的请求的api小程序的http请求的api基本上用法是一样的

重新写个方法wxhttpTest

uni.request({
    url: 'http://xxxxxxxxxxxxx',
    header: {
        "X-Requested-With": "XMLHttpRequest",
        "Content-Type": "application/octet-stream",
    },
    method: 'POST',
    timeout: 15000,
    dataType: 'protobuf',//随便写,只要不是json就行
    responseType: 'arraybuffer',
    data: requestBuffer 
}).then((res) => {
    console.log(res);
    //返回一个response的数组回来,第一个是null,第二个才是服务器返回的response
    //不知道是uniapp的问题还是微信小程序的问题
    for (let response of res) {
        if (response !== null && response !== undefined && response.statusCode === 200) {
            try {
                let jsonStr = Buffer.from(response.data).toString()
		let resJson = JSON.parse(jsonStr) //转化成json对象
                console.log(resJson);
            } catch (e) {
                //let resBuf = protobuf.util.newBuffer(response.data)
                let resBuf = Buffer.from(response.data)
                let resMessage = common.AwesomeMessage.decode(resBuf)
                let loginRspBuf = resMessage.data
                let loginRspMessage = user.login_rsp.decode(loginRspBuf)
                let obj = user.login_rsp.toObject(loginRspMessage, {
                    longs: String,
                    enums: String,
                    defaults: true,
                })
                console.log(obj);
                console.log(`errorMessage = ${Buffer.from(loginRspMessage.errInfo.errorMessage).toString()}`);
            }
        }
    }
}, (err) => {
    console.log(err);
});

结果返回的是json数据,数据输入错误

QQ截图20210522170152.png

也就是说后端收到的protobuf数据解析失败,然后返回json的错误给小程序了,到这里直接懵逼了,怎么办

继续搜索大法

在微信的开发社区搜索到这一篇

QQ截图20210522171104.png

然后我加上了这个

requestBuffer = new Uint8Array([...requestBuffer]).buffer

竟然好了,拿到了后端返回的protobuf数据,并且解析成功了

QQ截图20210522172040.png

最后 Android和ios真机调试也通过了。我是初学者,为什么加了new Uint8Array([...requestBuffer]).buffer这行代码就可以了,有大佬可以解释一下的吗?

最后感谢这些博文:

如何在前端中使用protobuf(vue篇)

axios使用protobuf进行通讯

axios请求设置responseType为'blob'或'arraybuffer'下载文件时,正确处理返回值为文件流或json对象的情况

developers.weixin.qq.com/community/d…