查看原文
其他

当creator遇上protobufjs—叛逆成长

ShawnZhang Creator星球游戏开发社区 2021-08-09

我们之前讲过要在Creator原生环境下使用protobufjs,使用伪装者的方式模拟nodejs的fs\path模块可以完美解决问题。但随着Creator1.7的到来,Shawn也尝了下鲜,但发现在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的异步加载方案。

  2. 分析原因

  3. 由于Creator的进化,经过调试分析,伪装者的策略存在了缺陷(就像人小的时候大人连蒙带骗,暂时把孩子给控制住了,但随着一孩子天天长大,他们的学习能力远超过大人的学习能力,原来的小把戏不适用了)。

  4. 应对办法

    已经实验过在js语言中,为已经存在的函数赋值,可以在运行时修改函数的表现,它是实现继承、多态或勾子常见的做法,这是一个实用的技术。我可以要在运行时修改protobufjs中的关键函数,将其中的具体实现自己重写一次不就行了吗?

    这样从物理表面上并没有修改源码,同时又可解决同步异步问题。

  5. 实施步骤

  6. 重写下面两个函数:

  • Util.fetch

  • Builder.prototype[‘import’]

      将其中调用nodejs模块代码摘掉,替换成Cocos jsb等价函数就可以解决问题。

三、逆境成长

经过上面对现状、问题、策略、步骤的自问自答,解决方法跃然纸上。看到这里有人可能会问,这不是四象限法法吗?

1. 四象限法

说实话最早我也不知道四象限法,它是这个周未我刚学到的新知识。当知道这种思考解决问题的方法时,我立刻就想起解决protobufjs在creator1.7模拟器上的问题,当时我不正是用的这种解决问题的吗?

打铁趁热,给大家介绍一下使用四象限法,把任何一个问题拆分成四个象限:

切开上下两部分,一个是现实,一个是理论;
切开左右两边,一边是过去,一边是未来;

从而构成思考问题的四个步骤,请看下图:


  1. 数据:问题是什么,描述过去的现实

  2. 分析:可能原因是什么,思考过去情况的理论原因

  3. 方向:应该采取的策略是什么,思考示未来情况的理论策略

  4. 下一步:具体的步骤是什么,思考未来情况的实现行动


这个思考过程有点像编写的一个数据转换函数的风格:

输入数据→解析数据→转换数据→生成结果

你还可以将生成的结果做为另一个函数的输入数据,构成一个可以循环使用的流程。

四象限法不仅是个思考工具,它还是一个行动实践指南,更多关于四象限法的知识可以参考《横向领导力》一书,它是我在得到App每天听本书栏目中无意见发现的,也推荐给你。

2. 引导

有了具体的实施步骤,不再废话了,直接上代码

1) 搞定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’)。

2) 拿下protobuf.Builder.prototype[‘import’]

有人可能会纳闷,为什么import函数要这样定义?

protobuf.Builder.prototype[‘import’] = function() { ... }

这是因为import是javascript中的关键字,不能定义一个名为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. 动态修改函数,它是随时间或环境的变化,做出最正确的引导

耐心引导是最好的选择。

四、小结

简单小结一下,上面两个函数的修改操作还是有点小小差别

  1. 静态函数与原型函数

    //修改静态函数 protobuf.Util.fetch = function myfetch(path, callbcak) {...} //修改原型函数 protobuf.Builder.prototype['import'] = function(json, filename) {...}

    需要注意protobuf.Util.fetch是静态函数,而import是Builder原型函数,相当于是修改的成员函数。

  2. 缓存函数指针

    //保存原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和大家一起叛逆成长。


欢迎关注「奎特尔星球」微信公众号,有代码、有教程、有视频、有故事,一起玩来玩吧!


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存