做前端开发的都知道,JavaScript语言本身没有模块化机制(至少在ES6之前是这样)。早期的项目,所有JS代码都写在一个文件里,函数和变量都是全局的。项目小的时候还好,代码一多就乱了——函数名冲突、变量覆盖、依赖关系混乱,改一个地方可能影响十个地方。

后来大家开始用各种方式模拟模块化。最早是命名空间,把所有函数挂在一个全局对象下,比如App.Utils.doSomething(),减少全局变量的数量。然后是IIFE(立即执行函数表达式),用闭包封装私有变量,只暴露需要的接口。但这些都是"民间方案",没有统一标准,不同项目的写法千差万别。

2015年的今天,前端模块化已经有了比较成熟的方案,主要是两大阵营:AMD(异步模块定义)和CommonJS。AMD的代表是RequireJS,CommonJS的代表是Browserify。今天就来聊聊这两种方案的使用经验和对比。

RequireJS:AMD的代表

RequireJS是最早流行的JavaScript模块加载器,实现了AMD规范。它的核心思想是异步加载模块,模块的依赖在定义时声明,加载器会自动解析依赖关系,按正确的顺序加载模块。

使用RequireJS,首先要在页面引入require.js,然后配置模块路径:

require.config({
    baseUrl: 'js',
    paths: {
        jquery: 'lib/jquery.min',
        underscore: 'lib/underscore.min',
        utils: 'app/utils'
    }
});

定义模块用define函数,声明依赖和返回值:

define(['jquery', 'utils'], function($, utils) {
    function UserList() {
        $('#list').html(utils.render());
    }
    return UserList;
});

加载模块用require函数:

require(['user-list'], function(UserList) {
    new UserList();
});

RequireJS的优点是异步加载,适合浏览器环境——模块按需加载,不需要一次性加载所有代码。而且它是纯前端方案,不需要构建工具,直接在浏览器里运行。

但缺点也很明显。第一,AMD的写法比较啰嗦,每个模块都要写define和依赖数组,代码量大。第二,模块路径配置复杂,项目大了之后paths配置会很长。第三,调试不太方便,模块是异步加载的,断点调试需要等加载完成。

我之前的一个项目用了RequireJS,一开始觉得挺优雅,但项目越来越大之后,配置越来越复杂,加载速度也变慢了——因为模块拆得太细,一个页面要加载几十个JS文件,HTTP请求开销很大。后来用r.js做了合并优化,才解决了这个问题。

Browserify:CommonJS的前端实现

Browserify是另一种思路,它把Node.js的CommonJS模块规范带到了浏览器端。CommonJS是Node.js使用的模块规范,用require引入模块,用module.exports导出接口,写法简洁直观。

用Browserify,模块的写法和Node.js一模一样:

// utils.js
function formatDate(date) {
    return date.toISOString().split('T')[0];
}
module.exports = { formatDate: formatDate };

// main.js
var $ = require('jquery');
var utils = require('./utils');
$('#date').text(utils.formatDate(new Date()));

然后用Browserify命令把所有模块打包成一个浏览器可执行的JS文件:

browserify main.js -o bundle.js

页面里只需要引入bundle.js就行了。

Browserify的优点很明显。第一,写法简洁,和Node.js一致,学习成本低。第二,可以直接使用npm上的大量模块,前端和后端的模块可以复用。第三,打包成一个文件,减少HTTP请求,加载速度快。

缺点是需要构建步骤,不能像RequireJS那样直接在浏览器里运行。每次修改代码都要重新打包,虽然可以用watch模式自动打包,但还是多了一步。另外,Browserify打包后的文件体积可能比较大,需要配合uglify做压缩。

我最近的新项目用了Browserify,体验比RequireJS好很多。代码写法更自然,npm生态丰富,构建流程也不复杂。配合npm scripts和watchify,开发体验很流畅。

未来是ES6模块

不管是AMD还是CommonJS,都是JavaScript没有原生模块化之前的过渡方案。好消息是,ES6(ECMAScript 2015)已经在制定中,预计今年6月正式发布,其中就包含了原生的模块系统。

ES6模块用import引入,用export导出,语法简洁,同时支持静态分析(编译时就能确定依赖关系,不需要运行时加载器):

// utils.js
export function formatDate(date) {
    return date.toISOString().split('T')[0];
}

// main.js
import $ from 'jquery';
import { formatDate } from './utils';

ES6模块是未来的标准,但目前浏览器支持还不好,需要用Babel转译。不过已经有很多项目开始用了,相信一两年内会成为主流。

总结

JavaScript模块化从全局函数到IIFE,从AMD到CommonJS,再到未来的ES6模块,一直在演进。目前这个时间点(2015年初),如果是新项目,我推荐用Browserify+CommonJS,写法简洁、生态丰富、构建不复杂。如果是老项目维护,RequireJS也还能用,但要注意做好模块合并优化。

模块化不只是技术方案的选择,更是一种思维方式——把复杂的系统拆分成独立的、可复用的模块,每个模块只做一件事,模块之间通过清晰的接口通信。这种思维方式,不管用什么语言、什么框架,都是通用的。