在 Cocos2d-js 中使用 Node.js 模块

Node.js

自 node.js 问世以来,javascript 在服务端的地位得到了许多人的认可。javascript 也不再是以往被认为的“前端的玩具语言”。在开发流程与规范上,node.js 提供了一套完整的方案,其中最基本的就是 CommonJS 规范。它将 javascript 划分为模块,模块之间通过 require 进行调用——这很像其它语言的 includeimport

此外,node.js 还提供了 npm 包管理工具(node package manager)。这使得全球的 node 开发者能够非常方便的共享和使用优秀的模块。可以说 node.js 的成长离不开这些优秀的开源社区。

模块化开发的好处非常多,最重要的就是使得测试驱动变得更加友好,不用再去处理复杂的依赖关系。mocha 是 node.js 环境下的一款非常好用的测试/行为驱动框架,能够结合各种第三方测试框架(只要它能抛出异常),我个人比较喜欢 should.js

于是我尝试使用 mocha + should.js 进行了一段时间的 BDD 开发,感觉非常好。很快就能把模块写好,并且得到一份不错的测试文档。

Cocos2d-js

我想如果能将这种开发方式运用在游戏的 model 层开发,肯定可以提高很多效率。但是现在使用的 Cocos2d-js 似乎有点水土不服。

Cocos2d-js 是 Cocos2d-html5 v3.0 版的新名字,它从 Coco2d-x 分支中独立出来,并且对 Cocos2d-x jsb 提供反向支持。为了更适应 javascript 开发者,Cocos2d-js 对原有的代码进行了大面积的修改,去掉了大部分 cpp 的风格代码,使它更接近 javascript 的开发方式。然后通过 javascript binding 的方式对 native 提供支持(原本由 Cocos2d-x jsb 提供对 web 的支持,现在反过来了)

Cocos2d-js 主要面向浏览器开发,也可通过 javascript binding 运行在手机平台。虽然都是使用 javascript 进行开发,但实际上这两个平台还是有很多区别的。

  • 在浏览器上,js 文件是异步加载的;
  • 在 native 开发中 js 是资源文件,是以同步方式加载的;

Cocos2d-js 的变通方式是在网页中追加 <script> 标签来实现异步加载&同步执行。然后在 native 的 spidermonkey 引擎中增加 require 方法,以同步的方式执行外部文件。不过 Cocos2d-js 提供的 require 并没有像 node.js 那样灵活,从源码中可以看到,只是一个很死板的 executeScript:

/* scriptCore.cpp */

// register some global functions
JS_DefineFunction(cx, global, "require", ScriptingCore::executeScript, 1, JSPROP_READONLY | JSPROP_PERMANENT);

这个 require 没办法像 CommonJS 规定的那样返回模块对象。并且这个 require function 是只读的,无法在 spidermonkey 里面用自己的 require 覆盖掉。

Solution

浏览器和 Cocos2d-x jsb 都没办法直接使用 require 来加载 node.js 的模块,难道到没有别的方法了吗?这时候有一个叫 browserify 的工具出现了,它可以将 node.js 的模块打包成一个 bundle.js 文件,然后直接在浏览器中加载这个 bundle.js 文件,就可以 require 包中的各种模块了。

但是 Cocos2d-x jsb 将 require 限制为只读,只好动个小手术,把 require 更名为 ccrequire 或者其它的了。

// register some global functions
JS_DefineFunction(cx, global, "ccrequire", ScriptingCore::executeScript, 1, JSPROP_READONLY | JSPROP_PERMANENT);

此外,还要同时修改 jsb_boot.js / jsb.js / jsb_debugger.js 里面 requireccrequire 即可。

这样,即不影响 Cocos2d-js 原有的 require 功能(其实只是简单的 excuteScript),又可以 require 由 browserify 打包的 node.js 模块了。而开发的过程中,则可以完全以 node.js 的方式对 model 层进行开发和测试。

标签: javascript, cocos2d-x, node.js, npm

已有 32 条评论

  1. Perfect...thanks!!!

  2. gagaga gagaga

    我觉得您说的有道理,已经反馈给cocos2d-js开发团队。

  3. pandamicro pandamicro

    我们应该会把require的READONLY属性取消,哈哈

    1. xiaodong xiaodong

      好像还是没有解除readonly 什么情况呢?

  4. Hi 木頭,

    您的这些建议非常好,我们也一直在思考如何解决这些问题。

    通过bundle.js 来加载浏览器上的模块,不知道您现在是怎么对引擎模块进行改造的?是否都采用闭包的方式?

    我们在异步加载引擎模块中也曾经投入过很多的精力,也有尝试过一些方案。但是,没有特别满意的。主要问题还是集中在性能降低、内存、和使用难度方面。导致最终没有发布到社区。不知道您现在的使用方式是如何的?希望有机会可以深入交流。谢谢

    非常感谢您的建议,下个版本中会将JSB的require去掉只读模式。

    祝好,
    林顺

    1. 在 -js 方面,我没有对引擎进行改造,还是以顺序加载的方式处理脚本。浏览器本身也没有require方面的冲突。所以我可以直接使用 http://browserify.org/ 这个工具打包需要用到的 node module 。将所有用到的模块打包成单一 bundle.js 文件,并在最开始加载。而之后的脚本就可以像 node.js 一样使用 require 从 bundle.js 里面取得需要用到的 module 。所以本质上还是同步的;

      在 jsb 方面,我只是对 require 做了点小改动,使得 bundle 的 require 可以正常工作。这使得在项目中使用一些开源的 node module 非常方便。对于自己编写的 module 则是按 CommonJS 的方式进行开发即可。对于性能方面,我还没遇到大问题,所以没有深入考虑。

      据我所知 node 用的是 v8 的引擎,并且进行一定的修订才实现 CommonJS 规范。如果要在 spidermonkey 上实现这一套异步方案,确实有很长的一条路要走。

  5. 非常感谢你的回复。
    确实,如果按照每次都先用browserify先将各个分离的文件,通过递归调用方式进行打包,按照整个文件加载,确实可以按照require的方式来引用。非常不多的idea。

    不知道每次debug都需要用工具打包为单个文件,用单个文件进行debug是否有不方面的地方?支持source map调试,效果如何?

    此外,不知你有没有试过用google closure compiler进行高级混淆,是否遇到问题。

    1. 可以借助 browerify 的 --debug 选项实现 source map,这样在 webstorm 调试的时候,就可以直接定位到单个文件中了。但是 webostorm 的 live editing(实时编辑)功能就没办法用了。因为定位到的代码并不是执行时的代码。

      混淆还没有尝试过,不过应该不会有大问题。等有空尝试后再告知。

      update: 2014/7/7
      测试了一下 google closure compiler,在 --compilation_level SIMPLE_OPTIMIZATIONS 下编译后运行是没有问题的,但使用 ADVANCED_OPTIMIZATIONS 会报错。一般情况下前者就够用了。

  6. Hi,木头,感谢你的回复。

    一般使用Advanced mode对代码的质量是有要求的,特别是不能出现定义和调用是字符串和函数混合使用方式,这样混淆的时候,函数会被混淆,字符串不会被混淆,就会断开耦合,导致出错。

    在游戏发布的时候,对高级混淆的要求是比较普遍的。

  7. 请问一下,那个头像是怎么设置的啊?

    1. 为啥我没有头像

    2. 头像服务是 gravatar.com 提供的,你可以在这里设置头像。

  8. @木頭 请问用require.js兼容browerify导出的bundle.js是否会有问题?

    1. 没有做过此类尝试呢。

  9. jiayifour jiayifour

    请问将原来的改造为ccrequire后,nodejs的require这个函数从哪里来呢 ?难道要包node.js的库吗?

    1. 使用 browserify.js 打包的时候会生成 require 接口。

      1. jiayifour jiayifour

        我也是这么认为的
        但是我在index.html里包了打包过的文件(underscore.js),为啥main.js里的require还是没有作用?
        感谢

        1. 可否提供个示例看看

          1. jiayifour jiayifour

            index.html中添加:
            ...

            ...

            main.js中添加:
            ...
            var _ = require( 'src/underscore_b.js' );
            cc.game.onStart = function(){
            cc.view.adjustViewPort(true);
            cc.view.setDesignResolutionSize(800, 450, cc.ResolutionPolicy.SHOW_ALL);
            cc.view.resizeWithBrowserSize(true);
            //load resources
            cc.LoaderScene.preload(g_resources, function () {
            cc.director.runScene(new MonstapScene());
            }, this);
            };
            cc.game.run();
            ...

            其中第一行的require报错未定义

          2. 确实没看到打包的 bundle.js 放在哪。应该在 之前插入 。
            另外贴代码请使用 gist.github.com 会方便一些。

          3. jiayifour jiayifour

            抱歉确实漏贴了,在index.html中:

            这里的script就是打包的文件

            然后在程序中无法使用require

            underscore_b.js如下:
            https://gist.github.com/jiayi411/1166da869290a4d526b2#file-gistfile1-js

          4. 好像评论自动把script过滤了..你打包用的命令是什么样的。貌似把 require 弄到闭包里了。

          5. jiayifour jiayifour

            哦?我是这样的:
            browserify underscore.js -o underscore_b.js

  10. jiayifour jiayifour

    原来不能贴"<script"的东西- -

    ""

    1. 其实 underscore.js 不需要打成 bundle 包就可以直接引入网页中使用了。

    2. 我看了一下你的 bundle.js ,你可以试试在最前面加上 require = (....) 然后就可以使用了。

  11. zxy zxy

    我现在使用cocos2d-x 中jsb , 然后用一个开源的使用node.js编写的javascrpit库, 这个库用到了node.js中的net模块, 请问我可以用类似的方式直接引用net模块不。 据我所知node.js中的net模块底层是靠c的网络库来支持的, 所以我思考的结果应该是就算可以引用 ,执行也会出错吧, 因为cocos2d用的不是v8,而是spindermonkey,

    1. 这确实不好办。

  12. [...]CocosLite采用browserify来解决这个问题,browserify可以把多个js文件打包成一个文件,并根据require顺序来合并文件,具体可参考[...]

  13. Zero Zero

    我和你使用了相同的方案browserify,我想问下jsb运行的时候,sourcemap你是怎么使用的?貌似直接打包的话没有效果

    1. 我没有在 jsb 里使用过 sourcemap 有机会的话研究研究。

  14. tiny tiny

    我是用browserify 打包,但是并没有把所有需要的代码打包到bundle.js文件里面,我看看了下代码是这样引用的,不知道问题出在哪里了,求指教var path = require("path"),
    ProtoBuf = require(path.join(__dirname, "dist", "ProtoBuf.js"));

    module.exports = ProtoBuf;

    }).call(this,"/../../../../../../../node_modules/protobufjs")
    },{"path":3}],3:[function(require,module,exports){
    (function (process){

评论已关闭