2012年1月18日星期三

如何设计大规模 JavaScript 应用 (Part 1 - 概览)

背景

我一直很关注如何设计大规模的 JavaScript 应用,因为我一直在做的都是大规模的 JavaScript 应用。从百度 Hi 网页版到百度地图,从 Yahoo Search Direct 到豌豆荚客户端。好吧……Yahoo Search Direct 本身的规模不大,但作为一个网页插件它要能跑在任何宿主页 面上,其复杂度也不低。在大规模 JavaScript 应用开发和维护的过程中,有两个问题尤其值得关注:设计和性能。前者是必须在开发阶段之前做好的,开发开始后就来不及改了,只能事后重构;后者则更多发生在开发的中后期,等 profiling 结果出来了,再针对瓶颈做优化。在这个文章系列中,我们的关注点是设计。

尽管我一直在写跟设计相关的文章,不过现在看来我过去写下的文章都有点山寨了,也不够 update。在我看到 Large-scale JavaScript Application Architecture 这个幻灯片后,我决定重新写一个系列来说说我在大规模 JavaScript 应用方面的经验,也包括一些设想。

如果说大规模 JavaScript 应用设计方面有什么核心原则的话,我觉得核心原则就是这一条:永远不要尝试构建大规模应用。构建小应用,保证它们的可测性,然后把它们组装成大应用。因此,我觉得在讨论任何设计模式之前,我们先要讨论一下如何把大应用拆分成小应用。

“The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application”

- Justin Meyer

模块化

CommonJS Modules

先说最理想的模块化方式,那就是 CommonJS Modules。一个模块系统至少要能解决两个问题:依赖项的加载、私有作用域和公有导出成员的区分。CommonJS Modules 通过 requireexports 很好地实现了上述两个功能。下面是一个 CommonJS Modules 模块定义和使用的例子:

util.js

var util = {
    extend: function(target, source) {
        /* implementation */
    }
};
util.extend(exports, util);

feature.js

var util = require('util');
var features = {
    core: {
        start: function() {
            /* implementation */
        }
    }
};
util.extend(exports,features);

app.js

var features = require('features');
features.core.start();
AMD

考虑到浏览器里面没有 CommonJS 的 Modules 接口,也不可能完整实现这样的 Modules 接口,所以就有了 AMD (Asynchronous Module Definition) 这样的解决方案。AMD 使用 define 函数定义模块,要求模块提前声明依赖项,然后通过回调加载模块,解决了浏览器无法同步加载模块的问题。下面是一个 AMD 模块定义和使用的例子:

util.js

define('util', [], function() {
    return {
        extend: function(target, source) {
            /* implementation */
        }
    };
};

feature.js

define('features', ['util'], function(util) {
    return {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
});

app.js

define('app', ['feature'], function(feature) {
    features.core.start();
});
CommonJS Modules/Wrappings

考虑到 AMD 的写法跟 CommonJS Modules 的写法区别十分之大,要把已有的 CommonJS Module 写法模块改为兼容 AMD 不容易,所以又有人设计一些改动不那么大的写法,如 AMD 工厂函数的 function(require, exports, ...) {...} 签名,或 CommonJS Modules/Wrappings (意思是 Modules 的 Wrappings)。由于浏览器必须异步加载依赖项,所以这些写法只能通过对工厂函数源代码做静态分析提前找出依赖项,在异步加载好之后再执行工厂函数。这样做的坏处是工厂函数内部对 require 的调用缺乏灵活性。下面是一个 CommonJS Modules/Wrappings 模块定义和使用的例子:

util.js

define(function(require, exports, module) {
    var util = {
        extend: function(target, source) {
            /* implementation */
        }
    };
    util.extend(exports, util);
});

feature.js

define(function(require, exports, module) {
    var util = require('util');
    var features = {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
    util.extend(exports,features);
});

app.js

define(function(require, exports, module) {
    var features = require('features');
    features.core.start();
});
UMD

尽管 Node.js 写好的 CommonJS Modules 模块可以通过 CommonJS Modules/Wrapping 包装一下使得它能用在浏览器内,尽管包装过的模块通过 r.js 也能用于 Node.js 环境下,不过这不是完美的解决方案。因此,又有人提出 UMD (Universal Module Definition),希望提供跨平台的模块定义方案。

UMD 现在还没有定稿,不同的人提出了不同的解决方案。最全面的方案同时支持 AMD 和 Node.js,顺便还把传统的浏览器脚本顺序加载模式也兼容了。具体的做法就是判断环境中是否存在 AMD 所依赖的 define,如果存在的话就使用 AMD 加载,不存在的话就使用别的方式模拟 AMD 加载。

util.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define('util', [], factory);
    } else {
        root.util = factory();
    }
})(this, function() {
    return {
        extend: function(target, source) {
            /* implementation */
        }
    };
});

feature.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory(require('util'));
    } else if (typeof define === 'function' && define.amd) {
        define('feature', ['util'], factory);
    } else {
        root.feature = factory(root.util);
    }
})(this, function(util) {
    return features = {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
    util.extend(exports,features);
});

app.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory(require('feature'));
    } else if (typeof define === 'function' && define.amd) {
        define('app', ['feature'], factory);
    } else {
        root.app = factory(root.feature);
    }
})(this, function(feature) {
    var features = require('features');
    features.core.start();
});

小结

在这篇文章里面,我们了解了大规模 JavaScript 应用设计的核心原则:使用模块化的方式把大应用分解为小应用来编写和维护。同时我们也看了不同的模块定义方式,我们可以针对平台来选择一种合适的方式,也可以选择通用的方式但需要维护更多的代码。

在有了模块的概念之后,我们就可以讨论具体的设计模式了。这个系列接下来将会深入讨论每一个对大规模 JavaScript 应用设计有用的设计模式。如果你对这个系列的文章感兴趣,可以选择订阅本博客。

没有评论:

发表评论