一、环境
- protobufjs:6.x.x、7.x.x版本皆可
- cocos creator:需使用“导入映射”,建议v3.3及以上版本
- node参考版本:v16.15.0
- npm参考版本:8.5.5,安装protobufjs或执行脚本时报错,升级至指定及以上版本再试
二、基本目标
- 使用静态加载,单文件(最小化网络请求)
- es6模块规范(效率更好)
- 导入方式支持导入所有和导入指定模块
- 自动导入使用pb及代码提示
- 若干工具函数(通用消息编解码、获得pb对象名、获得pb类型、pb克隆)
- 支持64位数据
- 适配微信小游戏平台
三、安装protobufjs
package.json中dependencies有指定版本则直接使用npm install,否则使用npm install --save protobufjs
需要注意的是protobufjs7需要单独安装protobufcli(npm install --save protobufjs-cli),protobufjs6则在安装protobufjs时默认集成。
四、构建pb流程
1、提供构建protobuf协议指令
package.json
"scripts": {
"protocli": "node ./tools/build_proto.js",
"buildproto": "npm run protocli -- ./assets/Proto ./assets/Scripts/Protobuf ./assets/Scripts/Protobuf pb"
}
2、缩减生成单文件大小
--no-verify --no-convert --no-delimited --no-beautify --no-service
移除不需要的内容,需要通过生成带注释的js文件来生成ts,后可再生成一份不带注释的js文件替换
3、修正模块
解决es6规范default无定义的问题;微信小游戏平台生成代码被混淆后可以根据pb对象获得pb名;64位数据支持
function esModuleCorrect(path) {
let file = readFileSync(path, { encoding: 'utf8' });
// let result = file.replace("import * as $protobuf", "import { default as $protobuf }");
// es导入最终存储在commonjs的内容是另一份对象
let result = file.replace(`const $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {});`, "const $root = {};");
result = result.replace(/\$protobuf\./g, "$protobuf.default.");
let extraContent = [`import Long from 'long';`, `$protobuf.default.util.Long = Long;`, `$protobuf.default.configure();`];
let dataArray = result.split(/\r\n|\n|\r/gm);
dataArray.splice(2, 0, ...extraContent);
writeFileSync(path, dataArray.join('\r\n'));
}
4、输出生成pb文件的package.json
function genEsModuleConfig(path) {
let config = {"type": "module"};
writeFileSync(path, JSON.stringify(config, null, 4));
}
五、导入映射
添加文件import-map.json
{
"imports": {
"pb": "./assets/Scripts/Protobuf/pb.js"
}
}
项目设置填上对应import map路径
tsconfig.json修改为
"compilerOptions": {
"strict": false,
"baseUrl": "./",
"paths": {
"pb": ["./assets/Scripts/Protobuf/pb"]
},
"allowSyntheticDefaultImports": true
}
如果遇到找不到模块 "pb"报错:一般为配置未能刷新,重启cocos creator即可
六、使用
安装依赖:npm install
构建协议:npm run buildproto
使用示例(无需主动import,代码提示自动导入即可,对应pb对象也有代码提示):
import pb from 'pb';//导入全部pb
import { PlayerInfo } from 'pb';//导入指定pb
import Long from 'long';
let message: PlayerInfo = PlayerInfo.create();
message.id = 1;
message.name = "cocos";
message.money = Long.fromString("18446744073709551615");
let buffer = PlayerInfo.encode(message).finish();
let decoded = PlayerInfo.decode(buffer);
七、pb工具函数
encode
function PbEncode(message: any): Uint8Array {
let writer = message.constructor.encode(message) as Writer;
return writer?.finish();
}
decode
function PbDecode<T extends Message>(name: string, arr: Uint8Array): T {
try {
let pbType = GetPbTypeByName(name);
return pbType.decode(arr);
} catch (e) {
console.error(`pb decode error, name: ${name}, error: ${e}`);
}
}
克隆pb(类似其他语言的CopyFrom)
function ClonePb<T>(obj: T | T[]): T | T[] {
if (typeof obj !== "object" || !obj) return obj;
let cpy: T;
if (Array.isArray(obj) || ArrayBuffer.isView(obj)) {
let len = (obj as T[]).length;
cpy = new obj.constructor(len);
for (var i = 0; i < len; ++i) {
cpy[i] = ClonePb(obj[i]);
}
} else {
cpy = Object.create(obj.constructor.prototype);
cpy.constructor = obj.constructor;
for (var i = 0, keys = Object.keys(obj), len = keys.length; i < len; ++i) {
cpy[keys[i]] = ClonePb(obj[keys[i]]);
}
}
return cpy;
}
根据pb对象获得pb名
function GetPbNameByPb(pb: Function | Object): string {
let messageName: string = pbToName.get(typeof pb == "object" ? pb.constructor : pb);
if (messageName == undefined) {
return "";
}
return messageName;
}
根据pb名获得pb类型
function GetPbTypeByName(name: string): any {
let paths = name.split('.');
let current = pb;
for (let i = 0; i < paths.length; ++i) {
if (current[paths[i]] == undefined) {
return undefined;
} else {
current = current[paths[i]];
}
}
return current;
}