2009年,美国程序员Ryan Dahl创造了node.js项目,将Javascript语言用于服务器端编程。
这标志"Javascript模块化编程"正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。
node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。
var math = require('math');
然后,就可以调用模块提供的方法:
var math = require('math');
math.add(2,3); // 5
是不是很简单?这个Require方法是一个函数,它用来识别外部模块(其实就是对象)的ID并返回其暴露的API。如果你要做的项目全部JS库都在本地,用node.js的模块系统就足够了。
当有了CommonJS(http://www.commonjs.org)规范的出现,其目标是为了构建Javascript在包括Web服务器,桌面,命令行工具,及浏览器方面的生态系统。
CommonJS制定了解决这些问题的一些规范,而Node.js就是这些规范的一种实现。Node.js自身实现了require方法作为其引入模块的方法,同时NPM也基于CommonJS定义的包规范,实现了依赖管理和模块自动安装等功能。这里我们将深入一下Node.js的require机制和NPM基于包规范的应用。
一,Node.JS模块包管理:
1.编写及加载使用模块
在Node.JS中编写及安装一个js的模块是很简单的。
首先写编写一个test.js,内容如下:
exports.myfunc= function () {
console.log('hello,world');
};
也可以写成(推荐这种写法):
var myfunc = function () {
console.log('hello,world');
};
exports.myfunc = myfunc;
保存这个文件就可以在你的别的js代码里调用了,在这里我写一个app.js内容如下:
var test = require('./test.js');
console.log( 'test my function:\n ' + test.myfunc());
在node.js的命令行中运行:
node app.js
你会发现打印出:
test my function:
hello world
分析一下上述的行为,在你用node app.js时,有如下后台动作:
首先node 用module对象来加载你的app.js.
然后会把你的js内容进行封装调用。原来app.js内从变成如下内容:
(function (exports, require, module, __filename, __dirname) {
var test = require('./test.js');
console.log( 'test my function:\n ' + test.myfunc());
})
再进行执行。在执行到require('./test.js')时,会按着路径进行搜索test.js
而其实这个require方法实际调用的就是module对象的load方法。load方法在载入、编译、缓存了module后,返回module的exports对象。这就是test.js文件中只有定义在exports对象上的方法才能被外部调用的原因。
其实require加载模块有一个复杂的逻辑。
这里简单说一下,
第一种情况。当require中的参数为没有路径,没有文件后缀的一个字符串时比如require('httpd');
Node.JS搜索顺序如下:
a.从缓存中加载。b.从原生模块加载.c.从文件加载。从文件加载也有是很一个顺序:
Node.JS运行一个js程序比如刚才通过node app.js时,module加载时会形成一个当前的有效路径,这是一个路径集合。
首先是当前路径,然后是当前路径的父目录,再是父目录的父目录,只到根目录。
搜索模块首先从当前路径中的node_modules子目录去寻找,找不到就找这个目录中的package.json文件中的main参数指定的目录,然后就是父目录的node_modules子目录,一直找到根目录,再没找到。就找NODE_PATH中设定的目录下去寻找。
第二种情况:require参数是相对或绝对路径下的模块名比如 require('./test.js') 或者require('/home/joezhang/nodejs/test')这个不用搜索直接加载,因为有绝对路径麻:)
有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。
但是,由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。还是上一节的代码,如果在浏览器中运行,会有一个很大的问题,你能看出来吗?
var math = require('math');
math.add(2, 3);
第二行math.add(2, 3),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。
这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function (math) {
math.add(2, 3);
});
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。
目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。最火的是Require.js。
Require.js中可以使用如下方式加载模块:
其中,require.js是需要加载的require.js库,main是你自己的main.js文件,都放在js路径下。data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。
main.js怎么写呢?常见的情况是,主模块依赖于其他模块,这时就要使用AMD规范定义的的require()函数。
// main.js
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
// some code here
});
require()函数接受两个参数。第一个参数是一个数组,表示所依赖的模块,上例就是['moduleA', 'moduleB', 'moduleC'],即主模块依赖这三个模块;第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。
require()异步加载moduleA,moduleB和moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
Sea.js 是一个适用于 Web 浏览器端的模块加载器。在 Sea.js 里,一切皆是模块,所有模块协同构建成模块系统。Sea.js 基于CMD规范。
CMD规范是折中了CommonJS和AMD而发展出来的,它规定了模块系统的最基本约定:模块必须是单独的文件;模块中不能引入新的自由变量;模块的执行必须是调用才执行(lazy)。
在CMD规范中模块的基本写法是define(factory),factory可以是一个函数或一个对象。如果是函数(工厂函数),则头三个参数必须是require,exports,module。因此,多数情况下定义一个模块要写成
define(function(require, exports, module) {
// The module code goes here
});
require接受模块的名称,返回模块暴露的API,有require.async模式可用于异步加载,接受一系列模块名称和一个可选的回调函数。export对象可用于在模块执行时动态加载API,module参数可有三项内容:module.uri接受模块的路径,module.dependencies列出模块的依赖,module.exports暴露模块的接口(同上面的exports功能)。
实例如下:
A typical sample
//math.js
define(function(require, exports, module) {
exports.add = function() {
var sum = 0, i = 0, args = arguments, l = args.length;
while (i sum += args[i++]; } return sum; }; }); //increment.js define(function(require, exports, module) { var add = require('math').add; exports.increment = function(val) { return add(val, 1); }; }); //program.js define(function(require, exports, module) { var inc = require('increment').increment; var a = 1; inc(a); // 2 module.id == "program"; }); Wrapped modules with non-function factory //object-data.js define({ foo: "bar" }); array-data.js define([ 'foo', 'bar' ]); string-data.js define('foo bar'); SeaJS 是一个国人开发的适用于浏览器端的 Javascript 模块加载器,是一个开源项目,目前由阿里、腾讯等公司共同维护。 相比与RequireJS还可以作为文件加载器。SeaJS 推荐用组合的思路解决问题:LABjs + SeaJS = Javascript 文件和模块加载器。 上面是 SeaJS 的狭义定位,SeaJS 还有一个广义定位:SeaJS 是浏览器端的 NodeJS. RequireJS和SeaJS两者的区别如下: 定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。SeaJS 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 服务器端。 遵循的规范不同。RequireJS 遵循的是 AMD(异步模块定义)规范,SeaJS 遵循的是 CMD (通用模块定义)规范。规范的不同,导致了两者 API 的不同。SeaJS 更简洁优雅,更贴近 CommonJS Modules/1.1 和 Node Modules 规范。 社区理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。SeaJS 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。 代码质量有差异。RequireJS 是没有明显的 bug,SeaJS 是明显没有 bug。 对调试等的支持有差异。SeaJS 通过插件,可以实现 Fiddler 中自动映射的功能,还可以实现自动 combo 等功能,非常方便。RequireJS 无这方面的支持。 插件机制不同。RequireJS 采取的是在源码中预留接口的形式,源码中留有为插件而写的代码。SeaJS 采取的插件机制则与 Javascript 语言以及Node 的方式一致:开放自身,让插件开发者可直接访问或修改,从而非常灵活,可以实现各种类型的插件。 还有不少细节差异就不多说了。总之,SeaJS 从 API 到实现,都比 RequireJS 更简洁优雅。因此推荐采用SeaJS作为前端的模块加载器。
为什么是SeaJS
首先在页面引入sea.js
然后写它的配置文件:
seajs.config({
base: "../module/",
alias: {
"jquery": "jquery/jquery/1.10.1/jquery.js"
}
});
这是为了配置基础路径,设置别名之类。
最后使用模块即可:
seajs.use("examples/hello/1.0.0/main");
官网seajs.org,里面说得非常详细,可下载示例研究。当然,所有模块的写法都要遵照上面CMD规范的要求。
这里提供一个不错的社区贡献的 API 文档:http://yslove.net/seajs/,列举了 Sea.js 的常用 API。只要掌握这些用法,就可以娴熟地进行模块化开发。
与Node.js的兼容
---------------
Sea.js 的初衷是为了让 CommonJS Modules/1.1 的模块能运行在浏览器端,但由于浏览器和服务器的实质差异,实际上这个梦无法完全达成,也没有必要去达成。更好的一种方式是,Sea.js 专注于 Web 浏览器端,CommonJS 则专注于服务器端,但两者有共通的部分。对于需要在两端都可以跑的模块,可以 有便捷的方案来快速迁移。
让 Sea.js 的模块跑在 Node 上
非常简单。首先需要安装 seajs 的 Node 模块:
$ npm install seajs -g
安装好后,在需要调用 Sea.js 模块的入口文件里,require 下 seajs :
a.js
define(function(require, exports) {
exports.name = 'A';
});
main.js
require('seajs');
var a = require('./a');
console.log(a.name);
这样就可以在 Node 环境中运行 Sea.js 的模块了: