美文网首页Cocos Creator程序员
当creator遇上protobufjs—叛逆成长

当creator遇上protobufjs—叛逆成长

作者: 张晓衡 | 来源:发表于2017-10-19 21:31 被阅读391次

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

  3. 应对办法
    已经实验过在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和大家一起叛逆成长。


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


相关文章

网友评论

    本文标题:当creator遇上protobufjs—叛逆成长

    本文链接:https://www.haomeiwen.com/subject/jkhhuxtx.html