在cocos creator中使用protobufjs(四)
2017-11-30 22:37
597 查看
我们之前讲过要在Creator原生环境下使用protobufjs,使用伪装者的方式模拟nodejs的fs\path模块可以完美解决问题。但随着Creator1.7的到来,Shawn也尝了下鲜,但发现在creator**模拟器环境**下,原来的伪装方案失效了。
上图是在Safari浏览器的调试界面,可以非常方便地在命令控制台上查看jsb上的对象、属性和方法,充分利用命令控制台的交互能力,它是学习js和cocos隐藏API的绝佳手段,特别是jsb函数。
进入require函数调试,发现nameMap是一个以文件名为Key,文件路径为Value的一个对象,里面没找到fs,看下图:
通过这个nameMap我明白了为什么在Creator中可以直接require(‘文件名’),而不需要完整路径,同时也明白了为什么js文件不能同名的原因。
继续追踪问题,从下图的代码m.deps[request]中查看到fs与path的值都是等于2。
一步步的逼近问题的真相了,scripts数组的2号元素,是一个对象,指向的文件名为preview-scripts/__node_modules/browser-resolve/empty.js,并不是我们伪装的fs模块,请看下图:
从调试的结果来看,Creator模拟器将fs\path模块认为是nodejs的模块,没有按普通模块进行加载,随后向Creator引擎组最为热心的Jare请教此问题时得到证实。
当时眼前一亮,猛拍一下自己的脑袋,我以前怎么没想到这个办法?不论是Web\iOS\Android所有平台的文件加载都可以用cc.loader.loadRes搞定,比protobufjs中实现的fetch都简单多了,cc.loader.loadRes为我提供了一盏明灯。
一束光在一片神经网络的触突上闪耀,电光石火的一瞬间,找到了一个方案
动态修改函数 + cc.loader.loadRes
请看下面代码,修改的Util.fetch方法
正在得意之时,脑子里翁的一声,有问题?如果这样去实现protobufjs的fetch函数,只能是异步加载,而我之前给pbkiller的范例都是同步加载!眼前一黑,回过神来,绝对不能用这种方法坑了我的插件用户。
保持同步与异步的加载接口
这两个方向如一座灯塔指引着我,我快速冷静下来,不要一牯脑地胡打乱撞。在安静片刻过后,我开始重新对问题进行分析:
1. 面临的问题是什么?
protobufjs库不能通过伪装的方式在creator1.7模拟器上工作,同时要考虑到pbkiller用户的同步加载习惯,不能单纯地使用cc.loader.loadRes的异步加载方案。
分析原因
由于Creator的进化,经过调试分析,伪装者的策略存在了缺陷(就像人小的时候大人连蒙带骗,暂时把孩子给控制住了,但随着一孩子天天长大,他们的学习能力远超过大人的学习能力,原来的小把戏不适用了)。
应对办法
已经实验过在js语言中,为已经存在的函数赋值,可以在运行时修改函数的表现,它是实现继承、多态或勾子常见的做法,这是一个实用的技术。我可以要在运行时修改protobufjs中的关键函数,将其中的具体实现自己重写一次不就行了吗?
这样从物理表面上并没有修改源码,同时又可解决同步异步问题。
4.实施步骤
重写下面两个函数:Util.fetch、Builder.prototype[‘import’]
想办法将其中调用nodejs模块代码摘掉,替换成Cocos jsb等价函数就可以解决问题。
打铁趁热,给大家介绍一下使用四象限法,把任何一个问题拆分成四个象限:
切开上下两部分,一个是现实,一个是理论;切开左右两边,一边是过去,一边是未来;
从而构成思考问题的四个步骤,请看下图:
1. 数据:问题是什么,描述过去的现实
2. 分析:可能原因是什么,思考过去情况的理论原因
3. 方向:应该采取的策略是什么,思考示未来情况的理论策略
4. 下一步:具体的步骤是什么,思考未来情况的实现行动
这个思考过程有点像编写的一个数据转换函数的风格:
输入数据→解析数据→转换数据→生成结果
你还可以将生成的结果做为另一个函数的输入数据,构成一个可以循环使用的流程。
四象限法不仅是个思考工具,它还是一个行动实践指南,更多关于四象限法的知识可以参考《横向领导力》一书,它是我在得到App每天听本书栏目中无意见发现的,也推荐给你。
通过上面的myfetch函数使用jsb.fileUtils.getStringFromFile轻松摘掉Util.fetch中的require(‘fs’)。
protobuf.Builder.prototype[‘import’] = function() { … }
这是因为import是javascript中的关键字,不能定义一个名为import的函数,但可以为一个对象上定义一个import属性,在这里这个属性是一个函数。
由于import函数代码太长,以下修改只给出了关键修改,主要是屏蔽代码。
import函数又长又难看,耐着性子满以为把问题解决了,可运行起来时会发现新的错误:propagateSyntax函数没有定义。更气人的是它是protobufjs中的一个内部函数,没有放在任何对象之上,引不出来,没办法只能将propagateSyntax函数在当前上下文中再写一遍。
还好,没再出现别的内部函数调用了,这下问题算是全部搞定了,终于我的程序可以运行起来了!
这段时间在学习如何带孩子,通过对protobufjs的几种解决方案对比看,我突然得出一些启发:
1. 修改源码好比是直接揍孩子,简单粗暴,但适应性差
2. 伪装是欺骗孩子,但随着孩子的成长,可能会失效
3. 动态修改函数,它是随时间或环境的变化,做出最正确的引导
耐心引导是最好的选择。
静态函数与原型函数
需要注意protobuf.Util.fetch是静态函数,而import是Builder原型函数,相当于是修改的成员函数。
缓存函数指针
有时候修改函数指针是为了做勾子监听或实现子类扩展,同时还要依赖原函数执行核心操作,这时就需要将原函数指针先保存起来。在适当的时机去调用,同时还要还原函数的this指针,所以要用函数的call方法,不能简单直接调用。
好了,以上就是今天的分享,希望能与Creator和大家一起叛逆成长。
欢迎关注「奎特尔星球」微信公众号,有代码、有教程、有视频、有故事,一起玩来玩吧!
一、疑犯追踪
1. 调试神器
追踪Bug这个问题,不得不大赞一下Creator1.7提供的新的底层JS引擎,它使得在原生jsb环境上的调试手段、效率、体验都有了质的飞跃。在iOS/Mac平台使用Safari浏览器,Android/Windows可使用Chrome及Chrome的衍生调试工具。上图是在Safari浏览器的调试界面,可以非常方便地在命令控制台上查看jsb上的对象、属性和方法,充分利用命令控制台的交互能力,它是学习js和cocos隐藏API的绝佳手段,特别是jsb函数。
2. 调试require函数
通过Safari的断点追踪,找到有一行protobufjs中的关键代码,require(‘fs’)的返回值为undefined,请看下面代码进入require函数调试,发现nameMap是一个以文件名为Key,文件路径为Value的一个对象,里面没找到fs,看下图:
通过这个nameMap我明白了为什么在Creator中可以直接require(‘文件名’),而不需要完整路径,同时也明白了为什么js文件不能同名的原因。
继续追踪问题,从下图的代码m.deps[request]中查看到fs与path的值都是等于2。
一步步的逼近问题的真相了,scripts数组的2号元素,是一个对象,指向的文件名为preview-scripts/__node_modules/browser-resolve/empty.js,并不是我们伪装的fs模块,请看下图:
从调试的结果来看,Creator模拟器将fs\path模块认为是nodejs的模块,没有按普通模块进行加载,随后向Creator引擎组最为热心的Jare请教此问题时得到证实。
二、一波三折
模拟的fs\path模块目前不能正常工作在Creator1.7模拟器,但在浏览器、自编译的MacApp、iOS、Android上都能正常运行。可是Creator模拟器是日常开发调试的利器,不能使用protobufjs库未免觉得遗憾。更要命的是,它会影响到我的pbkiller插件用户,面对这个问题绝对不可以马虎了事。1. 明灯
发现问题的第一时间,我火速向引擎组的大大汇报了此问题,热心的Jare建议使用cc.loader.loadRes函数抹平不同平台上文件的加载问题。当时眼前一亮,猛拍一下自己的脑袋,我以前怎么没想到这个办法?不论是Web\iOS\Android所有平台的文件加载都可以用cc.loader.loadRes搞定,比protobufjs中实现的fetch都简单多了,cc.loader.loadRes为我提供了一盏明灯。
2. 熄火
马上开始动手,但在准备动手前,我就想到绝对不能修改protobufjs的源码,因为我的pbkiller用户有些是用npm来管理的protobufjs,不可能让他们去修改node_moduls里的代码吧,这样太low了!一束光在一片神经网络的触突上闪耀,电光石火的一瞬间,找到了一个方案
动态修改函数 + cc.loader.loadRes
请看下面代码,修改的Util.fetch方法
let protobuf = require('protobufjs'); protobuf.Util.fetch = function myfetch(path, callback) { cc.loader.loadRes(path, (error, data) => { if (!error) { callback(data); } )} };
正在得意之时,脑子里翁的一声,有问题?如果这样去实现protobufjs的fetch函数,只能是异步加载,而我之前给pbkiller的范例都是同步加载!眼前一黑,回过神来,绝对不能用这种方法坑了我的插件用户。
3. 曙光
不能修改protobufjs源码保持同步与异步的加载接口
这两个方向如一座灯塔指引着我,我快速冷静下来,不要一牯脑地胡打乱撞。在安静片刻过后,我开始重新对问题进行分析:
1. 面临的问题是什么?
protobufjs库不能通过伪装的方式在creator1.7模拟器上工作,同时要考虑到pbkiller用户的同步加载习惯,不能单纯地使用cc.loader.loadRes的异步加载方案。
分析原因
由于Creator的进化,经过调试分析,伪装者的策略存在了缺陷(就像人小的时候大人连蒙带骗,暂时把孩子给控制住了,但随着一孩子天天长大,他们的学习能力远超过大人的学习能力,原来的小把戏不适用了)。
应对办法
已经实验过在js语言中,为已经存在的函数赋值,可以在运行时修改函数的表现,它是实现继承、多态或勾子常见的做法,这是一个实用的技术。我可以要在运行时修改protobufjs中的关键函数,将其中的具体实现自己重写一次不就行了吗?
这样从物理表面上并没有修改源码,同时又可解决同步异步问题。
4.实施步骤
重写下面两个函数:Util.fetch、Builder.prototype[‘import’]
想办法将其中调用nodejs模块代码摘掉,替换成Cocos jsb等价函数就可以解决问题。
三、逆境成长
经过上面对现状、问题、策略、步骤的自问自答,解决方法跃然纸上。看到这里有人可能会问,这不是四象限法法吗?1. 四象限法
说实话最早我也不知道四象限法,它是这个周未我刚学到的新知识。当知道这种思考解决问题的方法时,我立刻就想起解决protobufjs在creator1.7模拟器上的问题,当时我不正是用的这种解决问题的吗?打铁趁热,给大家介绍一下使用四象限法,把任何一个问题拆分成四个象限:
切开上下两部分,一个是现实,一个是理论;切开左右两边,一边是过去,一边是未来;
从而构成思考问题的四个步骤,请看下图:
1. 数据:问题是什么,描述过去的现实
2. 分析:可能原因是什么,思考过去情况的理论原因
3. 方向:应该采取的策略是什么,思考示未来情况的理论策略
4. 下一步:具体的步骤是什么,思考未来情况的实现行动
这个思考过程有点像编写的一个数据转换函数的风格:
输入数据→解析数据→转换数据→生成结果
你还可以将生成的结果做为另一个函数的输入数据,构成一个可以循环使用的流程。
四象限法不仅是个思考工具,它还是一个行动实践指南,更多关于四象限法的知识可以参考《横向领导力》一书,它是我在得到App每天听本书栏目中无意见发现的,也推荐给你。
2. 引导
有了具体的实施步骤,不再废话了,直接上代码搞定Util.fetch
//导入protobufjs let protobuf = require('protobufjs'); //保存原Util.fetch函数指针 let fetch = protobuf.Util.fetch; //编写了一个myfetch函数,覆盖protobuf.Util.fetch变量 protobuf.Util.fetch = function myfetch(path, callbcak) { //检查是否为原生环境 if (cc.sys.isNative) { //原生环境直接使用jsb提供的文件操作函数加载proto内容 let str = jsb.fileUtils.getStringFromFile(path); //如果是异步回调方式,使用callback参数返回数据 if (callbcak) { callbcak(str); return null; } //同步方式用返回值返回数据 return str; } //为web环境使用,protobufjs原来的处理函数 return fetch.call(this, path, callbcak); };
通过上面的myfetch函数使用jsb.fileUtils.getStringFromFile轻松摘掉Util.fetch中的require(‘fs’)。
拿下protobuf.Builder.prototype[‘import’]
有人可能会纳闷,为什么import函数要这样定义?protobuf.Builder.prototype[‘import’] = function() { … }
这是因为import是javascript中的关键字,不能定义一个名为import的函数,但可以为一个对象上定义一个import属性,在这里这个属性是一个函数。
由于import函数代码太长,以下修改只给出了关键修改,主要是屏蔽代码。
//由于import函数代码太长,以下修改只给出了关键修改,主要是屏蔽代码。 protobuf.Builder.prototype['import'] = function(json, filename) { var delim = '/'; // Make sure to skip duplicate imports if (typeof filename === 'string') { //--------------毙了----------- // if (ProtoBuf.Util.IS_NODE) // filename = require("path")['resolve'](filename); //----------------------------- if (this.files[filename] === true) return this.reset(); this.files[filename] = true; } else if (typeof filename === 'object') { // Object with root, file. var root = filename.root; //--------------毙了----------- // if (ProtoBuf.Util.IS_NODE) // root = require("path")['resolve'](root); //-------------------------------------------- if (root.indexOf("\\") >= 0 || filename.file.indexOf("\\") >= 0) delim = '\\'; //--------------毙了----------- //var fname; // if (ProtoBuf.Util.IS_NODE) // fname = require("path")['join'](root, filename.file); // else //---------------------------- var fname = root + delim + filename.file; if (this.files[fname] === true) return this.reset(); this.files[fname] = true; } // Import imports if (json['imports'] && json['imports'].length > 0) { ... for (var i=0; i<json['imports'].length; i++) { if (typeof json['imports'][i] === 'string') { // Import file if (!importRoot) throw Error("cannot determine import root"); var importFilename = json['imports'][i]; if (importFilename === "google/protobuf/descriptor.proto") continue; // Not needed and therefore not used //--------------毙了----------- // if (ProtoBuf.Util.IS_NODE) // importFilename = require("path")['join'](importRoot, importFilename); // else //----------------------------- importFilename = importRoot + delim + importFilename; if (this.files[importFilename] === true) continue; // Already imported ... } else // Import structure ... } if (resetRoot) // Reset import root override when all imports are done this.importRoot = null; } // Import structures if (json['package']) this.define(json['package']); if (json['syntax']) propagateSyntax(json); ... };
import函数又长又难看,耐着性子满以为把问题解决了,可运行起来时会发现新的错误:propagateSyntax函数没有定义。更气人的是它是protobufjs中的一个内部函数,没有放在任何对象之上,引不出来,没办法只能将propagateSyntax函数在当前上下文中再写一遍。
function propagateSyntax(parent) { if (parent['messages']) { parent['messages'].forEach(function(child) { child["syntax"] = parent["syntax"]; propagateSyntax(child); }); } if (parent['enums']) { parent['enums'].forEach(function(child) { child["syntax"] = parent["syntax"]; }); } }
还好,没再出现别的内部函数调用了,这下问题算是全部搞定了,终于我的程序可以运行起来了!
这段时间在学习如何带孩子,通过对protobufjs的几种解决方案对比看,我突然得出一些启发:
1. 修改源码好比是直接揍孩子,简单粗暴,但适应性差
2. 伪装是欺骗孩子,但随着孩子的成长,可能会失效
3. 动态修改函数,它是随时间或环境的变化,做出最正确的引导
耐心引导是最好的选择。
四、小结
简单小结一下,上面两个函数的修改操作还是有点小小差别静态函数与原型函数
//修改静态函数 protobuf.Util.fetch = function myfetch(path, callbcak) {...} //修改原型函数 protobuf.Builder.prototype['import'] = function(json, filename) {...}
需要注意protobuf.Util.fetch是静态函数,而import是Builder原型函数,相当于是修改的成员函数。
缓存函数指针
//保存原Util.fetch函数指针 let fetch = protobuf.Util.fetch; //编写了一个myfetch函数,覆盖protobuf.Util.fetch变量 protobuf.Util.fetch = function myfetch(path, callbcak) { ... //调用原始操作 fetch.call(this, path, callback); }
有时候修改函数指针是为了做勾子监听或实现子类扩展,同时还要依赖原函数执行核心操作,这时就需要将原函数指针先保存起来。在适当的时机去调用,同时还要还原函数的this指针,所以要用函数的call方法,不能简单直接调用。
好了,以上就是今天的分享,希望能与Creator和大家一起叛逆成长。
欢迎关注「奎特尔星球」微信公众号,有代码、有教程、有视频、有故事,一起玩来玩吧!
相关文章推荐
- cocos creator中使用protobuf(dcodeIO/protobuf.js 5.0)
- 在cocos creator中使用protobufjs(一)
- 在cocos creator中使用protobufjs(二)
- 在cocos creator中使用protobufjs(三)
- WebSocket中关于使用ProtoBuf传输数据介绍js部分
- ProtoBuf.js 使用技巧
- 白鹭egret配合protobufjs的使用
- 在node中使用protobuf.js
- 基于Netty5.0高级案例二之WebSocket中关于使用ProtoBuf传输数据介绍js部分
- WebSocket中关于使用ProtoBuf传输数据介绍js部分
- 在TS语言H5项目中使用Protobufjs(一) - Egret白鹭引擎篇
- ProtoBuf.js 使用技巧
- cocos creator: js中实现protobuf的打包和解析
- 在TS语言H5项目中使用Protobufjs(二) - Layaair腊鸭引擎篇
- ProtoBuf 的java使用
- Google Protobuf入门与使用
- Google Protobuf在Netty中的使用
- Unity使用protobuf-net实现的网络框架
- 大数据 --> ProtoBuf的使用和原理
- Android项目使用 protobuf和grpc简单例子