CoffeeScript编译器(二):是如何打包到客户端的?

2013-01-14 05:09

CoffeeScript编译器(二):是如何打包到客户端的?

by island205

at 2013-01-13 21:09:36

original http://island205.com/2013/01/13/how-to-package-to-browser/

CoffeeScript编译器中有很多细节可以学习,今天就来看看CoffeeScript是如何把Node模块打包到浏览器端的?

CNode上有篇帖子:发现coffee-script在浏览器端的是自己实现的require,有谁研究过原理和实现么?

这是一个什么问题?

目前CoffeeScript的源码使用CoffeeScript写的,全都放在src下面,而且遵循的是Node的包规范,即使用require、exports、module这三个对象来组织代码。这些文件会被编译到lib/coffee-script下面,当然也都是一个个Node包文件。这些文件在Node环境中当然可以运行,但在浏览器环境中就不一样了,那如何能让这些代码在浏览器中运行呢?

说白了,就是如何能让require、exports、module在客户端也能转起来。

可能的解决方案

SeaJS提供了一种可能,我们可以使用CMD标准,把源码包起来,然后使用SeaJS加载到浏览器中运行:

define(function (require, exports, module) {

    // coffee-script module code

})

作为一个语言编译器为什么要依赖别的类库呢?因此CoffeeScript不是这样做的。

CoffeeScript的做法

你可以到这里一探究竟。这是CoffeeScript项目的make文件,这个task就是打包成可运行在浏览器端的CoffeeScript。这个命令会把['helpers', 'rewriter', 'lexer', 'parser', 'scope', 'nodes', 'coffee-script', 'browser']里面的文件统统打包到一个文件中,打成如下的形式:

(function (root) {
    var CoffeeScript = function () {
        function require(path) {
            return require[path];
        }

        require['./helper'] = new function () {
            var exports = this;
            // helper code
        };

        require['./rewriter'] = new function () {
            var exports = this;
            // rewriter code
        };

        // and so on

        require['./coffee-script'] = new function () {
            var exports = this;

            // code

            Lexer = require('./lexer').Lexer;

            parser = require('./parser').parser;
            // code
        };

        return require['./coffee-script'];
    }();

    if (typeof define === 'function' && define.amd) {
        define(function () {
            return CoffeeScript;
        });
    } else {
        root.CoffeeScript = CoffeeScript;
    }
}(this));

这样看就明白了。

  1. 声明一个大家都能用的require函数对象,把模块作为这个对象的属性挂在下面,通过require(‘module name’)获取依赖模块,这解决了require的问题;
  2. 使用new function () { var exports = this },实例化一个匿名函数,将每个包中的对外接口通过exports,即实例化的对象也就是this,暴露出来;这解决了exports的问题;
  3. module的问题——对于CoffeeScript来说,并没有module的问题,因为在lib下面,没有一个文件使用了module来组织代码。

noloader

在此基础上,我在github上放了个项目,noloader,希望能解决几个问题,欢迎围观,^_^。

CoffeeScript打包的方式是手动指明依赖的,如何能够支持更一般的情况呢,noloader使用uglify-js进行语法分析,找出每个包文件的依赖,自动的获取依赖树,然后根据依赖树打包。

CoffeeScript打包的方式并不支持module关键字,无法使用到其他项目中;noloader使用下面的代码模式来打包,能够支持module关键字:

function require(id) {
    return require[id]
}

// util module
void(function () {
    var module = {
        exports: {}
    },
    exports = module.exports

    (function (require, exports, module) {
        require('./lang/lang')
        // code
        exports.util = util

    })(require, exports, module)
    require['./util'] = module.exports
})()

当然,noloader目前还不是很完美,没有处理循环依赖的情况,require方面沿用CoffeeScript的方式,很粗糙,还有本质上的bug。不过noloader的期望是,能够提供一个自动化的express中间件,完全可以使用Node包模式开发客户端程序,可复用服务端的模块,根据浏览器中的引用请求,自动的按需打包。