javascript没有模块系统,没有原生的支持密闭作用域或依赖管理。
javascript没有标准库,除了核心库,没有文件系统的API,没有IO流API等。
javascript没有标准接口,没有和Web Server或者数据库的统一接口。
javascript没有包管理系统,不能自动加载和安装依赖。
commonjs的出现就是为了构建javascript在包括web server,桌面,命令行工具以及浏览器方面的生态系统。Commonjs制定了解决问题的规范,Node.js则实现了这些规范。
Node.js实现了require方法来引入模块,NPM基于Commonjs的包规范实现了依赖管理和模块自动安装等功能。
例如定义一个”node-circle-module.js”模块,其内容为:
var PI = Math.PI;
'exports'.area = function(r) {
return PI * r * r;
};
'exports'.circumference = function(r) {
return 2 * PI * r;
};
再定义一个app主程序,其内容为:
var circle = **require**("./node-circle-modle.js");
console.log(' The area of a circle of radius 4 is ' + circle.area(4));
console.log(' The circle of a circle of radius 4 is ' + circle.circumference(4));
请注意关键字exports和require。
Node.js的模块分为两类,一类为原生(核心)模块,一类为文件模块。原生模块在Node.js源码编译时编译进了二进制执行文件,加载速度最快。而文件模块是动态加载的,加载速度慢于原生模块。但是Node.js对原生模块和文件模块都进行了缓存,因此不会在require时产生额外的重复开销。原生模块定义在lib目录下。
文件模块的加载主要是由原生模块module来实现和完成。Node.js在启动时会默认加载module模块,进程直接调用到runMain静态方法。
// bootstrap main module.
Module.runMain = function () {
// Load the main module -- the command line argument.
Module._load(process.argv[1], null, true);
};
(function (exports, require, module, __filename, __dirname) {
var circl = require('./node-circle-module.js');
console.log('The area of a circle of radius 4 is circle.area(4));
}
NPM包基于commonJS包规范,你可以通过如下命令来发布符合规范的node包
npm publish folder
前端代码通过script标签载入,Node.js在载入后进行了包装,保证不会污染全局变量。类库开发者需要将类库代码包装在一个闭包内。例如underscore库的定义方式。
(function() {
// Establish the root object, 'window' in the browser, or 'global' on the server.
var root = this;
var _ = function (obj) {
return new wrapper(obj);
};
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = moudle.exports = _;
}
exports._ = _;
} else if ( typeof define === 'function' && define.amd) {
// Register as a named module with AMD.
define('underscore', function() {
return _;
});
} else {
root._ = _;
}
}).call(this);
统一了前后端JavaScript的编程模型;
利用事件机制充分利用一步IO突破单线程编程模型的性能瓶颈。
对Node.js作者的采访
var options = {
host: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST'
};
var req = http.request(options, function (res) {
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS:' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log('Body:' + chunk);
});
});
req.on('error', function (e) {
console.log('Problem with request:' + e.message);
});
// Write data to request body
req.write('data\n');
req.write('data\n');
req.end();
其中on(‘error’)就是事件侦听器。但是,如果Node.js中的代码对一个事件添加了超过10个侦听器,就会收到一条警告。因为Node.js是单线程运行,如果侦听器太多,可能导致内存泄露。可以通过以下代码来修改限制:
emitter.setMaxListener(0);
function Stream() {
events.EventEmitter.call(this);
}
// util是Node.js中封装的工具模块。
util.inherits(Stream, events.EventEmitter);
例如在渲染一张页面时从多个数据源拉取数据,并最终渲染至客户端。
api.getUser('username', function (profile) {
// Got the profile
});
api.getTimeline('username', function (timeline) {
// Got the timeline
});
api.getSkin('username', function (skin) {
// Got the skin
});
如果写成低效的串行(深度嵌套)方式则如下所示:
api.getUser('username', function (profile) {
// Got the profile
api.getTimeline('username', function (timeline) {
// Got the timeline
api.getSkin('username', function (skin) {
// Got the skin
});
});
});
EventProxy实现了多事件协作。如上代码采用eventproxy的写法如下所示:
var proxy = new EventProxy();
proxy.all('profile', 'timeline', 'skin', function (profile, timeline, skin) {
// TODO
});
api.getUser("username", function (profile) {
proxy.emit('profile', profile);
});
api.getUser("username", function (profile) {
proxy.emit('timeline', timeline);
});
api.getUser("username", function (profile) {
proxy.emit('skin', skin);
});
var select = function (callback) {
db.select('SQL', function (results) {
callback(results);
});
};
第一种改进,加入事件锁:
var status = 'ready';
var select = function (callback) {
if (status === 'ready') {
status = 'pending';
db.select('SQL', function (results) {
callback(results);
status = 'ready';
});
}
};
但是如果连续多次调用select,将只有第一次调用生效,后续select没有数据服务。因此,需要引入事件队列。
var proxy = new EventProxy();
var status = 'ready';
var select = function (callback) {
proxy.once('selected', callback);
if (status === 'ready') {
status = 'pending';
db.select('SQL', function (results) {
proxy.emit('selected', results);
status = 'ready';
});
}
};
fs.open = function (path, flags, mode, callback) {
callback = arguments[arguments.length - 1];
if (typeof(callback) !== 'function') {
callback = noop;
}
mode = modeNum(mode, 438 /* =066 */);
binding.open(pathModule._makeLong(path),
stringToFlags(flags),
mode,
callback);
};
connect模块是Node.js的中间件框架,也是Node.js下最为流行的web框架,它具有如下几个特点:
- 模型简单
- 中间件易于组合和插拔
- 中间件易于定制和优化
- 丰富的中间件
Node.js中一个最简单的web服务是是这样的:
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello world\n');
}).listen(1337, '127.0.0.1');
http模块基于事件处理网络访问主要有两个因素,请求和响应。connet的中间件主要也是处理请求,让后响应客户端或是下一个中间件继续处理。例如一个最简单的中间件原型如下所示:
function (req, res, next) { /* 中间件 */ }
三个参数分别对应请求对象、响应对象和下一个中间件。
// middleware
app.use(connect.staticCache());
app.use(connect.static(__dirname + '/public'));
app.use(connect.cookieParser());
app.use(connect.session());
app.use(connect.query());
app.use(connect.bodyParser());
app.use(connect.csrf());
app.use(function (req, res, next) { /* 中间件的代码 */ });
app.listen(3001);
connect提供use方法用于注册中间件到一个connect对象的队列中,我们称该队列为中间件队列。connect的部分核心代码如下:
app.stack = [];
app.use = function (route, fn) {
// ...
// add the middleware
debug('use %s %s', route || '/', fn.name || 'anonymous');
this.stack.push({route: route, handle: fn});
return this;
};
流式处理的好处在于:每个中间件的职责都是单一的。开发者可以这样来将复杂的业务逻辑进行分解。
每一个完整的中间件,都包含路由信息和中间件函数。路由信息的作用是过滤不匹配的URL。请求在路由信息不匹配时,将直接传递给下一个中间件处理。
静态文件中间件
connect的static中间件提供了MIME,缓存控制,传输压缩,安全,欢迎页,断点续传等所有功能。代码如下所示:
var connect = require('connect');
var app = connect();
app.use(connect.static(__dirname + '/public'));
动静分离。静态文件中间件可以在动静混杂的场景下调用fs.stat来监测文件系统是否存在静态文件,这回造成不必要的系统调用和性能降低。解决影响性能的方法就是动静分离,利用路由监测,避免不必要的系统调用,可以有效降低对动态请求的性能影响。
app.use(‘/public’, connect.static(__dirname + ‘/public’));
大型cdn会直接在域名上动静请求分开,小型应用中,适当地进行动静分离可避免不必要的性能损耗。
缓存策略包括客户端和服务端两部分:
客户端缓存主要利用HTTP响应头cache-control和expires制定相应的过期策略。默认情况下的静态中间件最大缓存设置为0,即浏览器关闭后缓存会被清除。生产环境可以设置缓存有效节省网络带宽。
app.use(‘/public’, connect.static(__dirname + ‘/public’), {maxAge: 86500000});
maxAge选项的单位为毫秒
为浏览器请求url添加自动变化的MD5值可以有效清除缓存。
staticCache可以解决静态服务器重复读取磁盘造成的压力。
app.use(connect.staticCache());
app.use('/public', connect.static(__dirname + '/public'), {maxAge: 86400000});
这是一个提供上层缓存功能的中间件,能够将磁盘中的文件缓存到内存中,以提高响应速度和提高性能。