当creator遇上protobufjs—叛逆成长

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

  1. 应对办法 已经实验过在js语言中,为已经存在的函数赋值,可以在运行时修改函数的表现,它是实现继承、多态或勾子常见的做法,这是一个实用的技术。我可以要在运行时修改protobufjs中的关键函数,将其中的具体实现自己重写一次不就行了吗? 这样从物理表面上并没有修改源码,同时又可解决同步异步问题。
  2. 实施步骤

重写下面两个函数:

  • 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和大家一起叛逆成长。


本文分享自微信公众号 - Creator星球游戏开发社区(creator-star)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-10-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏杨建荣的学习笔记

Otter小试

之前因为机房多活的需要关注了Otter,搭建这个环境算是费劲了心思,在之前准备的过程中发现GitHub上面的说明文档和实际的操作还有一些差距,有些过程也是一...

14010
来自专栏leon的专栏

动态加载css方法实现和深入解析

此动态加载css方法 loadCss,剥离自Sea.js,并做了进一步的优化(优化代码后续会进行分析)。

8520
来自专栏leon的专栏

node命令行工具之实现项目工程自动初始化的标准流程

可以看出,传统的初始化步骤,花费的时间并不少。而且,人工操作的情况下,总有改漏的情况出现。这个缺点有时很致命。 甚至有马大哈,没有更新项目仓库地址,导致提交代码...

9320
来自专栏leon的专栏

React router动态加载组件-适配器模式的应用

以上是最常见的React router。在简单的单页应用中,这样写是ok的。因为打包后的单一js文件bundle.js也不过200k左右,gzip之后,对加载性...

11130
来自专栏程序员成长指北

干货 | 浅谈Node.js在携程的应用

潘斐斐,携程无线平台研发部高级研发工程师。2008年加入携程,目前负责携程Node.js技术栈的基础平台研发工作。

11640
来自专栏追逐时光

微信小程序之onLaunch与onload异步问题

   前端时间开发了一个微信小程序商城项目,因为这个项目我们的需求是进入小程序就通过wx.login({}) 这个api进行用户登录,获取系统后台的用户基本信息...

14020
来自专栏追逐时光

Linux系统彻底卸载MySQL数据库

输出结果表示,我安装的MySQL Server,Client都是5.6.44的,因为我系统支持的版本是要5.7+的版本,所以不得不卸载重装 

37510
来自专栏vue的实战

vue-axios调用接口

10720
来自专栏leon的专栏

webpack项目轻松混用css module

本文讲述css-loader开启css模块功能之后,如何与引用的npm包中样式文件不产生冲突。 比如antd-mobilenpm包的引入。在不做特殊处理的前提下...

10130
来自专栏vue的实战

2019-08-16 vue的axios的拦截器,响应器(api的集中处理。统一处理入参和出参)

36020

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励