13.18 Koa 框架
最后更新于:2022-04-01 05:10:08
Koa是一个类似于Express的Web开发框架,开发人员也是同一组人,但是使用了Generator函数,进行了架构的重新设计。也就是说,Koa的原理和内部结构很像Express,但是语法和内部结构进行了升级。
官方[faq](https://github.com/koajs/koa/blob/master/docs/faq.md#why-isnt-koa-just-express-40)有这样一个问题:”为什么koa不是Express 4.0?“,回答是这样的:”Koa与Express有很大差异,整个设计都是不同的,所以如果将Express 3.0按照这种写法升级到4.0,就意味着重写整个程序。所以,我们觉得创造一个新的库,是更合适的做法。“
[TOC]
## Koa应用
一个Koa应用就是一个对象,包含了一个middleware数组,这个数组由一组Generator函数组成。这些函数负责对HTTP请求进行各种加工,比如生成缓存、指定代理、请求重定向等等。
~~~
var koa = require('koa');
var app = koa();
app.use(function *(){
this.body = 'Hello World';
});
app.listen(3000);
~~~
上面代码中,变量app就是一个Koa应用。它监听3000端口,返回一个内容为Hello World的网页。
app.use方法用于向middleware数组添加Generator函数。
listen方法指定监听端口,并启动当前应用。它实际上等同于下面的代码。
~~~
var http = require('http');
var koa = require('koa');
var app = koa();
http.createServer(app.callback()).listen(3000);
~~~
## 中间件
Koa的中间件很像Express的中间件,也是对HTTP请求进行处理的函数,但是必须是一个Generator函数。而且,Koa的中间件是一个级联式(Cascading)的结构,也就是说,属于是层层调用,第一个中间件调用第二个中间件,第二个调用第三个,以此类推。上游的中间件必须等到下游的中间件返回结果,才会继续执行,这点很像递归。
中间件通过当前应用的use方法注册。
~~~
app.use(function* (next){
var start = new Date; // (1)
yield next; // (2)
var ms = new Date - start; // (3)
console.log('%s %s - %s', this.method, this.url, ms); // (4)
});
~~~
上面代码中,`app.use`方法的参数就是中间件,它是一个Generator函数,最大的特征就是function命令与参数之间,必须有一个星号。Generator函数的参数next,表示下一个中间件。
Generator函数内部使用yield命令,将程序的执行权转交给下一个中间件,即`yield next`,要等到下一个中间件返回结果,才会继续往下执行。上面代码中,Generator函数体内部,第一行赋值语句首先执行,开始计时,第二行yield语句将执行权交给下一个中间件,当前中间件就暂停执行。等到后面的中间件全部执行完成,执行权就回到原来暂停的地方,继续往下执行,这时才会执行第三行,计算这个过程一共花了多少时间,第四行将这个时间打印出来。
下面是一个两个中间件级联的例子。
~~~
app.use(function *() {
this.body = "header\n";
yield saveResults.call(this);
this.body += "footer\n";
});
function *saveResults() {
this.body += "Results Saved!\n";
}
~~~
上面代码中,第一个中间件调用第二个中间件saveResults,它们都向`this.body`写入内容。最后,`this.body`的输出如下。
~~~
header
Results Saved!
footer
~~~
只要有一个中间件缺少`yield next`语句,后面的中间件都不会执行,这一点要引起注意。
~~~
app.use(function *(next){
console.log('>> one');
yield next;
console.log('<< one');
});
app.use(function *(next){
console.log('>> two');
this.body = 'two';
console.log('<< two');
});
app.use(function *(next){
console.log('>> three');
yield next;
console.log('<< three');
});
~~~
上面代码中,因为第二个中间件少了`yield next`语句,第三个中间件并不会执行。
如果想跳过一个中间件,可以直接在该中间件的第一行语句写上`return yield next`。
~~~
app.use(function* (next) {
if (skip) return yield next;
})
~~~
由于Koa要求中间件唯一的参数就是next,导致如果要传入其他参数,必须另外写一个返回Generator函数的函数。
~~~
function logger(format) {
return function *(next){
var str = format
.replace(':method', this.method)
.replace(':url', this.url);
console.log(str);
yield next;
}
}
app.use(logger(':method :url'));
~~~
上面代码中,真正的中间件是logger函数的返回值,而logger函数是可以接受参数的。
### 多个中间件的合并
由于中间件的参数统一为next(意为下一个中间件),因此可以使用`.call(this, next)`,将多个中间件进行合并。
~~~
function *random(next) {
if ('/random' == this.path) {
this.body = Math.floor(Math.random()*10);
} else {
yield next;
}
};
function *backwards(next) {
if ('/backwards' == this.path) {
this.body = 'sdrawkcab';
} else {
yield next;
}
}
function *pi(next) {
if ('/pi' == this.path) {
this.body = String(Math.PI);
} else {
yield next;
}
}
function *all(next) {
yield random.call(this, backwards.call(this, pi.call(this, next)));
}
app.use(all);
~~~
上面代码中,中间件all内部,就是依次调用random、backwards、pi,后一个中间件就是前一个中间件的参数。
Koa内部使用koa-compose模块,进行同样的操作,下面是它的源码。
~~~
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
yield *next;
}
}
function *noop(){}
~~~
上面代码中,middleware是中间件数组。前一个中间件的参数是后一个中间件,依次类推。如果最后一个中间件没有next参数,则传入一个空函数。
## 路由
可以通过`this.path`属性,判断用户请求的路径,从而起到路由作用。
~~~
app.use(function* (next) {
if (this.path === '/') {
this.body = 'we are at home!';
}
})
// 等同于
app.use(function* (next) {
if (this.path !== '/') return yield next;
this.body = 'we are at home!';
})
~~~
下面是多路径的例子。
~~~
let koa = require('koa')
let app = koa()
// normal route
app.use(function* (next) {
if (this.path !== '/') {
return yield next
}
this.body = 'hello world'
});
// /404 route
app.use(function* (next) {
if (this.path !== '/404') {
return yield next;
}
this.body = 'page not found'
});
// /500 route
app.use(function* (next) {
if (this.path !== '/500') {
return yield next;
}
this.body = 'internal server error'
});
app.listen(8080)
~~~
上面代码中,每一个中间件负责一个路径,如果路径不符合,就传递给下一个中间件。
复杂的路由需要安装koa-router插件。
~~~
var app = require('koa')();
var Router = require('koa-router');
var myRouter = new Router();
myRouter.get('/', function *(next) {
this.response.body = 'Hello World!';
});
app.use(myRouter.routes());
app.listen(3000);
~~~
上面代码对根路径设置路由。
Koa-router实例提供一系列动词方法,即一种HTTP动词对应一种方法。典型的动词方法有以下五种。
* router.get()
* router.post()
* router.put()
* router.del()
* router.patch()
这些动词方法可以接受两个参数,第一个是路径模式,第二个是对应的控制器方法(中间件),定义用户请求该路径时服务器行为。
~~~
router.get('/', function *(next) {
this.body = 'Hello World!';
});
~~~
上面代码中,`router.get`方法的第一个参数是根路径,第二个参数是对应的函数方法。
注意,路径匹配的时候,不会把查询字符串考虑在内。比如,`/index?param=xyz`匹配路径`/index`。
有些路径模式比较复杂,Koa-router允许为路径模式起别名。起名时,别名要添加为动词方法的第一个参数,这时动词方法变成接受三个参数。
~~~
router.get('user', '/users/:id', function *(next) {
// ...
});
~~~
上面代码中,路径模式`\users\:id`的名字就是`user`。路径的名称,可以用来引用对应的具体路径,比如url方法可以根据路径名称,结合给定的参数,生成具体的路径。
~~~
router.url('user', 3);
// => "/users/3"
router.url('user', { id: 3 });
// => "/users/3"
~~~
上面代码中,user就是路径模式的名称,对应具体路径`/users/:id`。url方法的第二个参数3,表示给定id的值是3,因此最后生成的路径是`/users/3`。
Koa-router允许为路径统一添加前缀。
~~~
var router = new Router({
prefix: '/users'
});
router.get('/', ...); // 等同于"/users"
router.get('/:id', ...); // 等同于"/users/:id"
~~~
路径的参数通过`this.params`属性获取,该属性返回一个对象,所有路径参数都是该对象的成员。
~~~
// 访问 /programming/how-to-node
router.get('/:category/:title', function *(next) {
console.log(this.params);
// => { category: 'programming', title: 'how-to-node' }
});
~~~
param方法可以针对命名参数,设置验证条件。
~~~
router
.get('/users/:user', function *(next) {
this.body = this.user;
})
.param('user', function *(id, next) {
var users = [ '0号用户', '1号用户', '2号用户'];
this.user = users[id];
if (!this.user) return this.status = 404;
yield next;
})
~~~
上面代码中,如果`/users/:user`的参数user对应的不是有效用户(比如访问`/users/3`),param方法注册的中间件会查到,就会返回404错误。
redirect方法会将某个路径的请求,重定向到另一个路径,并返回301状态码。
~~~
router.redirect('/login', 'sign-in');
// 等同于
router.all('/login', function *() {
this.redirect('/sign-in');
this.status = 301;
});
~~~
redirect方法的第一个参数是请求来源,第二个参数是目的地,两者都可以用路径模式的别名代替。
## context对象
中间件当中的this表示上下文对象context,代表一次HTTP请求和回应,即一次访问/回应的所有信息,都可以从上下文对象获得。context对象封装了request和response对象,并且提供了一些辅助方法。每次HTTP请求,就会创建一个新的context对象。
~~~
app.use(function *(){
this; // is the Context
this.request; // is a koa Request
this.response; // is a koa Response
});
~~~
context对象的很多方法,其实是定义在ctx.request对象或ctx.response对象上面,比如,ctx.type和ctx.length对应于ctx.response.type和ctx.response.length,ctx.path和ctx.method对应于ctx.request.path和ctx.request.method。
context对象的全局属性。
* request:指向Request对象
* response:指向Response对象
* req:指向Node的request对象
* req:指向Node的response对象
* app:指向App对象
* state:用于在中间件传递信息。
~~~
this.state.user = yield User.find(id);
~~~
上面代码中,user属性存放在`this.state`对象上面,可以被另一个中间件读取。
context对象的全局方法。
* throw():抛出错误,直接决定了HTTP回应的状态码。
* assert():如果一个表达式为false,则抛出一个错误。
~~~
this.throw(403);
this.throw('name required', 400);
this.throw('something exploded');
this.throw(400, 'name required');
// 等同于
var err = new Error('name required');
err.status = 400;
throw err;
~~~
assert方法的例子。
~~~
// 格式
ctx.assert(value, [msg], [status], [properties])
// 例子
this.assert(this.user, 401, 'User not found. Please login!');
~~~
以下模块解析POST请求的数据。
* co-body
* [https://github.com/koajs/body-parser](https://github.com/koajs/body-parser)
* [https://github.com/koajs/body-parsers](https://github.com/koajs/body-parsers)
~~~
var parse = require('co-body');
// in Koa handler
var body = yield parse(this);
~~~
## 错误处理机制
Koa提供内置的错误处理机制,任何中间件抛出的错误都会被捕捉到,引发向客户端返回一个500错误,而不会导致进程停止,因此也就不需要forever这样的模块重启进程。
~~~
app.use(function *() {
throw new Error();
});
~~~
上面代码中,中间件内部抛出一个错误,并不会导致Koa应用挂掉。Koa内置的错误处理机制,会捕捉到这个错误。
当然,也可以额外部署自己的错误处理机制。
~~~
app.use(function *() {
try {
yield saveResults();
} catch (err) {
this.throw(400, '数据无效');
}
});
~~~
上面代码自行部署了try...catch代码块,一旦产生错误,就用`this.throw`方法抛出。该方法可以将指定的状态码和错误信息,返回给客户端。
对于未捕获错误,可以设置error事件的监听函数。
~~~
app.on('error', function(err){
log.error('server error', err);
});
~~~
error事件的监听函数还可以接受上下文对象,作为第二个参数。
~~~
app.on('error', function(err, ctx){
log.error('server error', err, ctx);
});
~~~
如果一个错误没有被捕获,koa会向客户端返回一个500错误“Internal Server Error”。
this.throw方法用于向客户端抛出一个错误。
~~~
this.throw(403);
this.throw('name required', 400);
this.throw(400, 'name required');
this.throw('something exploded');
this.throw('name required', 400)
// 等同于
var err = new Error('name required');
err.status = 400;
throw err;
~~~
`this.throw`方法的两个参数,一个是错误码,另一个是报错信息。如果省略状态码,默认是500错误。
`this.assert`方法用于在中间件之中断言,用法类似于Node的assert模块。
~~~
this.assert(this.user, 401, 'User not found. Please login!');
~~~
上面代码中,如果this.user属性不存在,会抛出一个401错误。
由于中间件是层级式调用,所以可以把`try { yield next }`当成第一个中间件。
~~~
app.use(function *(next) {
try {
yield next;
} catch (err) {
this.status = err.status || 500;
this.body = err.message;
this.app.emit('error', err, this);
}
});
app.use(function *(next) {
throw new Error('some error');
})
~~~
## cookie
cookie的读取和设置。
~~~
this.cookies.get('view');
this.cookies.set('view', n);
~~~
get和set方法都可以接受第三个参数,表示配置参数。其中的signed参数,用于指定cookie是否加密。如果指定加密的话,必须用`app.keys`指定加密短语。
~~~
app.keys = ['secret1', 'secret2'];
this.cookies.set('name', '张三', { signed: true });
~~~
this.cookie的配置对象的属性如下。
* signed:cookie是否加密。
* expires:cookie何时过期
* path:cookie的路径,默认是“/”。
* domain:cookie的域名。
* secure:cookie是否只有https请求下才发送。
* httpOnly:是否只有服务器可以取到cookie,默认为true。
## session
~~~
var session = require('koa-session');
var koa = require('koa');
var app = koa();
app.keys = ['some secret hurr'];
app.use(session(app));
app.use(function *(){
var n = this.session.views || 0;
this.session.views = ++n;
this.body = n + ' views';
})
app.listen(3000);
console.log('listening on port 3000');
~~~
## Request对象
Request对象表示HTTP请求。
(1)this.request.header
返回一个对象,包含所有HTTP请求的头信息。它也可以写成`this.request.headers`。
(2)this.request.method
返回HTTP请求的方法,该属性可读写。
(3)this.request.length
返回HTTP请求的Content-Length属性,取不到值,则返回undefined。
(4)this.request.path
返回HTTP请求的路径,该属性可读写。
(5)this.request.href
返回HTTP请求的完整路径,包括协议、端口和url。
~~~
this.request.href
// http://example.com/foo/bar?q=1
~~~
(6)this.request.querystring
返回HTTP请求的查询字符串,不含问号。该属性可读写。
(7)this.request.search
返回HTTP请求的查询字符串,含问号。该属性可读写。
(8)this.request.host
返回HTTP请求的主机(含端口号)。
(9)this.request.hostname
返回HTTP的主机名(不含端口号)。
(10)this.request.type
返回HTTP请求的Content-Type属性。
~~~
var ct = this.request.type;
// "image/png"
~~~
(11)this.request.charset
返回HTTP请求的字符集。
~~~
this.request.charset
// "utf-8"
~~~
(12)this.request.query
返回一个对象,包含了HTTP请求的查询字符串。如果没有查询字符串,则返回一个空对象。该属性可读写。
比如,查询字符串`color=blue&size=small`,会得到以下的对象。
~~~
{
color: 'blue',
size: 'small'
}
~~~
(13)this.request.fresh
返回一个布尔值,表示缓存是否代表了最新内容。通常与If-None-Match、ETag、If-Modified-Since、Last-Modified等缓存头,配合使用。
~~~
this.response.set('ETag', '123');
// 检查客户端请求的内容是否有变化
if (this.request.fresh) {
this.response.status = 304;
return;
}
// 否则就表示客户端的内容陈旧了,
// 需要取出新内容
this.response.body = yield db.find('something');
~~~
(14)this.request.stale
返回`this.request.fresh`的相反值。
(15)this.request.protocol
返回HTTP请求的协议,https或者http。
(16)this.request.secure
返回一个布尔值,表示当前协议是否为https。
(17)this.request.ip
返回发出HTTP请求的IP地址。
(18)this.request.subdomains
返回一个数组,表示HTTP请求的子域名。该属性必须与app.subdomainOffset属性搭配使用。app.subdomainOffset属性默认为2,则域名“tobi.ferrets.example.com”返回["ferrets", "tobi"],如果app.subdomainOffset设为3,则返回["tobi"]。
(19)this.request.is(types...)
返回指定的类型字符串,表示HTTP请求的Content-Type属性是否为指定类型。
~~~
// Content-Type为 text/html; charset=utf-8
this.request.is('html'); // 'html'
this.request.is('text/html'); // 'text/html'
this.request.is('text/*', 'text/html'); // 'text/html'
// Content-Type为s application/json
this.request.is('json', 'urlencoded'); // 'json'
this.request.is('application/json'); // 'application/json'
this.request.is('html', 'application/*'); // 'application/json'
~~~
如果不满足条件,返回false;如果HTTP请求不含数据,则返回undefined。
~~~
this.is('html'); // false
~~~
它可以用于过滤HTTP请求,比如只允许请求下载图片。
~~~
if (this.is('image/*')) {
// process
} else {
this.throw(415, 'images only!');
}
~~~
(20)this.request.accepts(types)
检查HTTP请求的Accept属性是否可接受,如果可接受,则返回指定的媒体类型,否则返回false。
~~~
// Accept: text/html
this.request.accepts('html');
// "html"
// Accept: text/*, application/json
this.request.accepts('html');
// "html"
this.request.accepts('text/html');
// "text/html"
this.request.accepts('json', 'text');
// => "json"
this.request.accepts('application/json');
// => "application/json"
// Accept: text/*, application/json
this.request.accepts('image/png');
this.request.accepts('png');
// false
// Accept: text/*;q=.5, application/json
this.request.accepts(['html', 'json']);
this.request.accepts('html', 'json');
// "json"
// No Accept header
this.request.accepts('html', 'json');
// "html"
this.request.accepts('json', 'html');
// => "json"
~~~
如果accepts方法没有参数,则返回所有支持的类型(text/html,application/xhtml+xml,image/webp,application/xml,_/_)。
如果accepts方法的参数有多个参数,则返回最佳匹配。如果都不匹配则返回false,并向客户端抛出一个406”Not Acceptable“错误。
如果HTTP请求没有Accept字段,那么accepts方法返回它的第一个参数。
accepts方法可以根据不同Accept字段,向客户端返回不同的字段。
~~~
switch (this.request.accepts('json', 'html', 'text')) {
case 'json': break;
case 'html': break;
case 'text': break;
default: this.throw(406, 'json, html, or text only');
}
~~~
(21)this.request.acceptsEncodings(encodings)
该方法根据HTTP请求的Accept-Encoding字段,返回最佳匹配,如果没有合适的匹配,则返回false。
~~~
// Accept-Encoding: gzip
this.request.acceptsEncodings('gzip', 'deflate', 'identity');
// "gzip"
this.request.acceptsEncodings(['gzip', 'deflate', 'identity']);
// "gzip"
~~~
注意,acceptEncodings方法的参数必须包括identity(意为不编码)。
如果HTTP请求没有Accept-Encoding字段,acceptEncodings方法返回所有可以提供的编码方法。
~~~
// Accept-Encoding: gzip, deflate
this.request.acceptsEncodings();
// ["gzip", "deflate", "identity"]
~~~
如果都不匹配,acceptsEncodings方法返回false,并向客户端抛出一个406“Not Acceptable”错误。
(22)this.request.acceptsCharsets(charsets)
该方法根据HTTP请求的Accept-Charset字段,返回最佳匹配,如果没有合适的匹配,则返回false。
~~~
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
this.request.acceptsCharsets('utf-8', 'utf-7');
// => "utf-8"
this.request.acceptsCharsets(['utf-7', 'utf-8']);
// => "utf-8"
~~~
如果acceptsCharsets方法没有参数,则返回所有可接受的匹配。
~~~
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
this.request.acceptsCharsets();
// ["utf-8", "utf-7", "iso-8859-1"]
~~~
如果都不匹配,acceptsCharsets方法返回false,并向客户端抛出一个406“Not Acceptable”错误。
(23)this.request.acceptsLanguages(langs)
该方法根据HTTP请求的Accept-Language字段,返回最佳匹配,如果没有合适的匹配,则返回false。
~~~
// Accept-Language: en;q=0.8, es, pt
this.request.acceptsLanguages('es', 'en');
// "es"
this.request.acceptsLanguages(['en', 'es']);
// "es"
~~~
如果acceptsCharsets方法没有参数,则返回所有可接受的匹配。
~~~
// Accept-Language: en;q=0.8, es, pt
this.request.acceptsLanguages();
// ["es", "pt", "en"]
~~~
如果都不匹配,acceptsLanguages方法返回false,并向客户端抛出一个406“Not Acceptable”错误。
(24)this.request.socket
返回HTTP请求的socket。
(25)this.request.get(field)
返回HTTP请求指定的字段。
## Response对象
Response对象表示HTTP回应。
(1)this.response.header
返回HTTP回应的头信息。
(2)this.response.socket
返回HTTP回应的socket。
(3)this.response.status
返回HTTP回应的状态码。默认情况下,该属性没有值。该属性可读写,设置时等于一个整数。
(4)this.response.message
返回HTTP回应的状态信息。该属性与`this.response.message`是配对的。该属性可读写。
(5)this.response.length
返回HTTP回应的Content-Length字段。该属性可读写,如果没有设置它的值,koa会自动从this.request.body推断。
(6)this.response.body
返回HTTP回应的信息体。该属性可读写,它的值可能有以下几种类型。
* 字符串:Content-Type字段默认为text/html或text/plain,字符集默认为utf-8,Content-Length字段同时设定。
* 二进制Buffer:Content-Type字段默认为application/octet-stream,Content-Length字段同时设定。
* Stream:Content-Type字段默认为application/octet-stream。
* JSON对象:Content-Type字段默认为application/json。
* null(表示没有信息体)
如果`this.response.status`没设置,Koa会自动将其设为200或204。
(7)this.response.get(field)
返回HTTP回应的指定字段。
~~~
var etag = this.get('ETag');
~~~
注意,get方法的参数是区分大小写的。
(8)this.response.set()
设置HTTP回应的指定字段。
~~~
this.set('Cache-Control', 'no-cache');
~~~
set方法也可以接受一个对象作为参数,同时为多个字段指定值。
~~~
this.set({
'Etag': '1234',
'Last-Modified': date
});
~~~
(9)this.response.remove(field)
移除HTTP回应的指定字段。
(10)this.response.type
返回HTTP回应的Content-Type字段,不包括“charset”参数的部分。
~~~
var ct = this.reponse.type;
// "image/png"
~~~
该属性是可写的。
~~~
this.reponse.type = 'text/plain; charset=utf-8';
this.reponse.type = 'image/png';
this.reponse.type = '.png';
this.reponse.type = 'png';
~~~
设置type属性的时候,如果没有提供charset参数,Koa会判断是否自动设置。如果`this.response.type`设为html,charset默认设为utf-8;但如果`this.response.type`设为text/html,就不会提供charset的默认值。
(10)this.response.is(types...)
该方法类似于`this.request.is()`,用于检查HTTP回应的类型是否为支持的类型。
它可以在中间件中起到处理不同格式内容的作用。
~~~
var minify = require('html-minifier');
app.use(function *minifyHTML(next){
yield next;
if (!this.response.is('html')) return;
var body = this.response.body;
if (!body || body.pipe) return;
if (Buffer.isBuffer(body)) body = body.toString();
this.response.body = minify(body);
});
~~~
上面代码是一个中间件,如果输出的内容类型为HTML,就会进行最小化处理。
(11)this.response.redirect(url, [alt])
该方法执行302跳转到指定网址。
~~~
this.redirect('back');
this.redirect('back', '/index.html');
this.redirect('/login');
this.redirect('http://google.com');
~~~
如果redirect方法的第一个参数是back,将重定向到HTTP请求的Referrer字段指定的网址,如果没有该字段,则重定向到第二个参数或“/”网址。
如果想修改302状态码,或者修改body文字,可以采用下面的写法。
~~~
this.status = 301;
this.redirect('/cart');
this.body = 'Redirecting to shopping cart';
~~~
(12)this.response.attachment([filename])
该方法将HTTP回应的Content-Disposition字段,设为“attachment”,提示浏览器下载指定文件。
(13)this.response.headerSent
该方法返回一个布尔值,检查是否HTTP回应已经发出。
(14)this.response.lastModified
该属性以Date对象的形式,返回HTTP回应的Last-Modified字段(如果该字段存在)。该属性可写。
~~~
this.response.lastModified = new Date();
~~~
(15)this.response.etag
该属性设置HTTP回应的ETag字段。
~~~
this.response.etag = crypto.createHash('md5').update(this.body).digest('hex');
~~~
注意,不能用该属性读取ETag字段。
(16)this.response.vary(field)
该方法将参数添加到HTTP回应的Vary字段。
## CSRF攻击
CSRF攻击是指用户的session被劫持,用来冒充用户的攻击。
koa-csrf插件用来防止CSRF攻击。原理是在session之中写入一个秘密的token,用户每次使用POST方法提交数据的时候,必须含有这个token,否则就会抛出错误。
~~~
var koa = require('koa');
var session = require('koa-session');
var csrf = require('koa-csrf');
var route = require('koa-route');
var app = module.exports = koa();
app.keys = ['session key', 'csrf example'];
app.use(session(app));
app.use(csrf());
app.use(route.get('/token', token));
app.use(route.post('/post', post));
function* token () {
this.body = this.csrf;
}
function* post() {
this.body = {ok: true};
}
app.listen(3000);
~~~
POST请求含有token,可以是以下几种方式之一,koa-csrf插件就能获得token。
* 表单的_csrf字段
* 查询字符串的_csrf字段
* HTTP请求头信息的x-csrf-token字段
* HTTP请求头信息的x-xsrf-token字段
## 数据压缩
koa-compress模块可以实现数据压缩。
~~~
app.use(require('koa-compress')())
app.use(function* () {
this.type = 'text/plain'
this.body = fs.createReadStream('filename.txt')
})
~~~
## 源码解读
每一个网站就是一个app,它由`lib/application`定义。
~~~
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2;
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
var app = Application.prototype;
exports = module.exports = Application;
~~~
`app.use()`用于注册中间件,即将Generator函数放入中间件数组。
~~~
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are allowed
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};
~~~
`app.listen()`就是`http.createServer(app.callback()).listen(...)`的缩写。
~~~
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
app.callback = function(){
var mw = [respond].concat(this.middleware);
var fn = this.experimental
? compose_es7(mw)
: co.wrap(compose(mw));
var self = this;
if (!this.listeners('error').length) this.on('error', this.onerror);
return function(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).catch(ctx.onerror);
}
};
~~~
上面代码中,`app.callback()`会返回一个函数,用来处理HTTP请求。它的第一行`mw = [respond].concat(this.middleware)`,表示将respond函数(这也是一个Generator函数)放入`this.middleware`,现在mw就变成了`[respond, S1, S2, S3]`。
`compose(mw)`将中间件数组转为一个层层调用的Generator函数。
~~~
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
yield *next;
}
}
function *noop(){}
~~~
上面代码中,下一个generator函数总是上一个Generator函数的参数,从而保证了层层调用。
`var fn = co.wrap(gen)`则是将Generator函数包装成一个自动执行的函数,并且返回一个Promise。
~~~
//co package
co.wrap = function (fn) {
return function () {
return co.call(this, fn.apply(this, arguments));
};
};
~~~
由于`co.wrap(compose(mw))`执行后,返回的是一个Promise,所以可以对其使用catch方法指定捕捉错误的回调函数`fn.call(ctx).catch(ctx.onerror)`。
将所有的上下文变量都放进context对象。
~~~
app.createContext = function(req, res){
var context = Object.create(this.context);
var request = context.request = Object.create(this.request);
var response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, this.keys);
context.accept = request.accept = accepts(req);
context.state = {};
return context;
};
~~~
真正处理HTTP请求的是下面这个Generator函数。
~~~
function *respond(next) {
yield *next;
// allow bypassing koa
if (false === this.respond) return;
var res = this.res;
if (res.headersSent || !this.writable) return;
var body = this.body;
var code = this.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
this.body = null;
return res.end();
}
if ('HEAD' == this.method) {
if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body));
return res.end();
}
// status body
if (null == body) {
this.type = 'text';
body = this.message || String(code);
this.length = Buffer.byteLength(body);
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
this.length = Buffer.byteLength(body);
res.end(body);
}
~~~
## 参考链接
* [Koa Guide](https://github.com/koajs/koa/blob/master/docs/guide.md)
* William XING, [Is Koa.js right for me?](http://william.xingyp.com/is-koa-js-right-for-me/)
13.17 Express框架
最后更新于:2022-04-01 05:10:06
[TOC]
## 概述
Express是目前最流行的基于Node.js的Web开发框架,提供各种模块,可以快速地搭建一个具有完整功能的网站。
Express的上手非常简单,首先新建一个项目目录,假定叫做hello-world。
~~~
$ mkdir hello-world
~~~
进入该目录,新建一个package.json文件,内容如下。
~~~
{
"name": "hello-world",
"description": "hello world test app",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "4.x"
}
}
~~~
上面代码定义了项目的名称、描述、版本等,并且指定需要4.0版本以上的Express。
然后,就可以安装了。
~~~
$ npm install
~~~
安装了Express及其依赖的模块以后,在项目根目录下,新建一个启动文件,假定叫做index.js。
~~~
var express = require('express');
var app = express();
app.use(express.static(__dirname + '/public'));
app.listen(8080);
~~~
上面代码运行之后,访问`http://localhost:8080`,就会在浏览器中打开当前目录的public子目录。如果public目录之中有一个图片文件my_image.png,那么可以用`http://localhost:8080/my_image.png`访问该文件。
你也可以在index.js之中,生成动态网页。
~~~
// index.js
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello world!');
});
app.listen(3000);
~~~
然后,在命令行下运行下面的命令,就可以在浏览器中访问项目网站了。
~~~
$ node index
~~~
默认情况下,网站运行在本机的3000端口,网页显示Hello World。
index.js中的`app.get`用于指定不同的访问路径所对应的回调函数,这叫做“路由”(routing)。上面代码只指定了根目录的回调函数,因此只有一个路由记录。实际应用中,可能有多个路由记录。
~~~
// index.js
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello world!');
});
app.get('/customer', function(req, res){
res.send('customer page');
});
app.get('/admin', function(req, res){
res.send('admin page');
});
app.listen(3000);
~~~
这时,最好就把路由放到一个单独的文件中,比如新建一个routes子目录。
~~~
// routes/index.js
module.exports = function (app) {
app.get('/', function (req, res) {
res.send('Hello world');
});
app.get('/customer', function(req, res){
res.send('customer page');
});
app.get('/admin', function(req, res){
res.send('admin page');
});
};
~~~
然后,原来的index.js就变成下面这样。
~~~
// index.js
var express = require('express');
var app = express();
var routes = require('./routes')(app);
app.listen(3000);
~~~
### 搭建HTTPs服务器
使用Express搭建HTTPs加密服务器,也很简单。
~~~
var fs = require('fs');
var options = {
key: fs.readFileSync('E:/ssl/myserver.key'),
cert: fs.readFileSync('E:/ssl/myserver.crt'),
passphrase: '1234'
};
var https = require('https');
var express = require('express');
var app = express();
app.get('/', function(req, res){
res.send('Hello World Expressjs');
});
var server = https.createServer(options, app);
server.listen(8084);
console.log('Server is running on port 8084');
~~~
## 运行原理
### 底层:http模块
Express框架建立在node.js内置的http模块上。 http模块生成服务器的原始代码如下。
~~~
var http = require("http");
var app = http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.end("Hello world!");
});
app.listen(3000, "localhost");
~~~
上面代码的关键是http模块的createServer方法,表示生成一个HTTP服务器实例。该方法接受一个回调函数,该回调函数的参数,分别为代表HTTP请求和HTTP回应的request对象和response对象。
### 对http模块的再包装
Express框架的核心是对http模块的再包装。上面的代码用Express改写如下。
~~~
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello world!');
});
app.listen(3000);
var express = require("express");
var http = require("http");
var app = express();
app.use(function(request, response) {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Hello world!\n");
});
http.createServer(app).listen(1337);
~~~
比较两段代码,可以看到它们非常接近,唯一的差别是createServer方法的参数,从一个回调函数变成了一个Epress对象的实例。而这个实例使用了use方法,加载了与上一段代码相同的回调函数。
Express框架等于在http模块之上,加了一个中间层,而use方法则相当于调用中间件。
### 什么是中间件
简单说,中间件(middleware)就是处理HTTP请求的函数,用来完成各种特定的任务,比如检查用户是否登录、分析数据、以及其他在需要最终将数据发送给用户之前完成的任务。它最大的特点就是,一个中间件处理完,再传递给下一个中间件。
node.js的内置模块http的createServer方法,可以生成一个服务器实例,该实例允许在运行过程中,调用一系列函数(也就是中间件)。当一个HTTP请求进入服务器,服务器实例会调用第一个中间件,完成后根据设置,决定是否再调用下一个中间件。中间件内部可以使用服务器实例的response对象(ServerResponse,即回调函数的第二个参数),以及一个next回调函数(即第三个参数)。每个中间件都可以对HTTP请求(request对象)做出回应,并且决定是否调用next方法,将request对象再传给下一个中间件。
一个不进行任何操作、只传递request对象的中间件,大概是下面这样:
~~~
function uselessMiddleware(req, res, next) {
next();
}
~~~
上面代码的next为中间件的回调函数。如果它带有参数,则代表抛出一个错误,参数为错误文本。
~~~
function uselessMiddleware(req, res, next) {
next('出错了!');
}
~~~
抛出错误以后,后面的中间件将不再执行,直到发现一个错误处理函数为止。
### use方法
use是express调用中间件的方法,它返回一个函数。下面是一个连续调用两个中间件的例子。
~~~
var express = require("express");
var http = require("http");
var app = express();
app.use(function(request, response, next) {
console.log("In comes a " + request.method + " to " + request.url);
next();
});
app.use(function(request, response) {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Hello world!\n");
});
http.createServer(app).listen(1337);
~~~
上面代码先调用第一个中间件,在控制台输出一行信息,然后通过next方法,调用第二个中间件,输出HTTP回应。由于第二个中间件没有调用next方法,所以不再request对象就不再向后传递了。
使用use方法,可以根据请求的网址,返回不同的网页内容。
~~~
var express = require("express");
var http = require("http");
var app = express();
app.use(function(request, response, next) {
if (request.url == "/") {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Welcome to the homepage!\n");
} else {
next();
}
});
app.use(function(request, response, next) {
if (request.url == "/about") {
response.writeHead(200, { "Content-Type": "text/plain" });
} else {
next();
}
});
app.use(function(request, response) {
response.writeHead(404, { "Content-Type": "text/plain" });
response.end("404 error!\n");
});
http.createServer(app).listen(1337);
~~~
上面代码通过request.url属性,判断请求的网址,从而返回不同的内容。
除了在回调函数内部,判断请求的网址,Express也允许将请求的网址写在use方法的第一个参数。
~~~
app.use('/', someMiddleware);
~~~
上面代码表示,只对根目录的请求,调用某个中间件。
因此,上面的代码可以写成下面的样子。
~~~
var express = require("express");
var http = require("http");
var app = express();
app.use("/", function(request, response, next) {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Welcome to the homepage!\n");
});
app.use("/about", function(request, response, next) {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Welcome to the about page!\n");
});
app.use(function(request, response) {
response.writeHead(404, { "Content-Type": "text/plain" });
response.end("404 error!\n");
});
http.createServer(app).listen(1337);
~~~
## Express的方法
### all方法和HTTP动词方法
针对不同的请求,Express提供了use方法的一些别名。比如,上面代码也可以用别名的形式来写。
~~~
var express = require("express");
var http = require("http");
var app = express();
app.all("*", function(request, response, next) {
response.writeHead(200, { "Content-Type": "text/plain" });
next();
});
app.get("/", function(request, response) {
response.end("Welcome to the homepage!");
});
app.get("/about", function(request, response) {
response.end("Welcome to the about page!");
});
app.get("*", function(request, response) {
response.end("404!");
});
http.createServer(app).listen(1337);
~~~
上面代码的all方法表示,所有请求都必须通过该中间件,参数中的“*”表示对所有路径有效。get方法则是只有GET动词的HTTP请求通过该中间件,它的第一个参数是请求的路径。由于get方法的回调函数没有调用next方法,所以只要有一个中间件被调用了,后面的中间件就不会再被调用了。
除了get方法以外,Express还提供post、put、delete方法,即HTTP动词都是Express的方法。
这些方法的第一个参数,都是请求的路径。除了绝对匹配以外,Express允许模式匹配。
~~~
app.get("/hello/:who", function(req, res) {
res.end("Hello, " + req.params.who + ".");
});
~~~
上面代码将匹配“/hello/alice”网址,网址中的alice将被捕获,作为req.params.who属性的值。需要注意的是,捕获后需要对网址进行检查,过滤不安全字符,上面的写法只是为了演示,生产中不应这样直接使用用户提供的值。
如果在模式参数后面加上问号,表示该参数可选。
~~~
app.get('/hello/:who?',function(req,res) {
if(req.params.id) {
res.end("Hello, " + req.params.who + ".");
}
else {
res.send("Hello, Guest.");
}
});
~~~
下面是一些更复杂的模式匹配的例子。
~~~
app.get('/forum/:fid/thread/:tid', middleware)
// 匹配/commits/71dbb9c
// 或/commits/71dbb9c..4c084f9这样的git格式的网址
app.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function(req, res){
var from = req.params[0];
var to = req.params[1] || 'HEAD';
res.send('commit range ' + from + '..' + to);
});
~~~
### set方法
set方法用于指定变量的值。
~~~
app.set("views", __dirname + "/views");
app.set("view engine", "jade");
~~~
上面代码使用set方法,为系统变量“views”和“view engine”指定值。
### response对象
(1)response.redirect方法
response.redirect方法允许网址的重定向。
~~~
response.redirect("/hello/anime");
response.redirect("http://www.example.com");
response.redirect(301, "http://www.example.com");
~~~
(2)response.sendFile方法
response.sendFile方法用于发送文件。
~~~
response.sendFile("/path/to/anime.mp4");
~~~
(3)response.render方法
response.render方法用于渲染网页模板。
~~~
app.get("/", function(request, response) {
response.render("index", { message: "Hello World" });
});
~~~
上面代码使用render方法,将message变量传入index模板,渲染成HTML网页。
### requst对象
(1)request.ip
request.ip属性用于获得HTTP请求的IP地址。
(2)request.files
request.files用于获取上传的文件。
## 项目开发实例
### 编写启动脚本
上一节使用express命令自动建立项目,也可以不使用这个命令,手动新建所有文件。
先建立一个项目目录(假定这个目录叫做demo)。进入该目录,新建一个package.json文件,写入项目的配置信息。
~~~
{
"name": "demo",
"description": "My First Express App",
"version": "0.0.1",
"dependencies": {
"express": "3.x"
}
}
~~~
在项目目录中,新建文件app.js。项目的代码就放在这个文件里面。
~~~
var express = require('express');
var app = express();
~~~
上面代码首先加载express模块,赋给变量express。然后,生成express实例,赋给变量app。
接着,设定express实例的参数。
~~~
// 设定port变量,意为访问端口
app.set('port', process.env.PORT || 3000);
// 设定views变量,意为视图存放的目录
app.set('views', path.join(__dirname, 'views'));
// 设定view engine变量,意为网页模板引擎
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
// 设定静态文件目录,比如本地文件
// 目录为demo/public/images,访问
// 网址则显示为http://localhost:3000/images
app.use(express.static(path.join(__dirname, 'public')));
~~~
上面代码中的set方法用于设定内部变量,use方法用于调用express的中间件。
最后,调用实例方法listen,让其监听事先设定的端口(3000)。
~~~
app.listen(app.get('port'));
~~~
这时,运行下面的命令,就可以在浏览器访问[http://127.0.0.1:3000。](http://127.0.0.1:3000%E3%80%82/)
~~~
node app.js
~~~
网页提示“Cannot GET /”,表示没有为网站的根路径指定可以显示的内容。所以,下一步就是配置路由。
### 配置路由
所谓“路由”,就是指为不同的访问路径,指定不同的处理方法。
(1)指定根路径
在app.js之中,先指定根路径的处理方法。
~~~
app.get('/', function(req, res) {
res.send('Hello World');
});
~~~
上面代码的get方法,表示处理客户端发出的GET请求。相应的,还有app.post、app.put、app.del(delete是JavaScript保留字,所以改叫del)方法。
get方法的第一个参数是访问路径,正斜杠(/)就代表根路径;第二个参数是回调函数,它的req参数表示客户端发来的HTTP请求,res参数代表发向客户端的HTTP回应,这两个参数都是对象。在回调函数内部,使用HTTP回应的send方法,表示向浏览器发送一个字符串。然后,运行下面的命令。
~~~
node app.js
~~~
此时,在浏览器中访问[http://127.0.0.1:3000,网页就会显示“Hello](http://127.0.0.1:3000%EF%BC%8C%E7%BD%91%E9%A1%B5%E5%B0%B1%E4%BC%9A%E6%98%BE%E7%A4%BA%E2%80%9CHello/) World”。
如果需要指定HTTP头信息,回调函数就必须换一种写法,要使用setHeader方法与end方法。
~~~
app.get('/', function(req, res){
var body = 'Hello World';
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', body.length);
res.end(body);
});
~~~
(2)指定特定路径
上面是处理根目录的情况,下面再举一个例子。假定用户访问/api路径,希望返回一个JSON字符串。这时,get可以这样写。
~~~
app.get('/api', function(request, response) {
response.send({name:"张三",age:40});
});
~~~
上面代码表示,除了发送字符串,send方法还可以直接发送对象。重新启动node以后,再访问路径/api,浏览器就会显示一个JSON对象。
~~~
{
"name": "张三",
"age": 40
}
~~~
我们也可以把app.get的回调函数,封装成模块。先在routes目录下面建立一个api.js文件。
~~~
// routes/api.js
exports.index = function (req, res){
res.json(200, {name:"张三",age:40});
}
~~~
然后,在app.js中加载这个模块。
~~~
// app.js
var api = require('./routes/api');
app.get('/api', api.index);
~~~
现在访问时,就会显示与上一次同样的结果。
如果只向浏览器发送简单的文本信息,上面的方法已经够用;但是如果要向浏览器发送复杂的内容,还是应该使用网页模板。
### 静态网页模板
在项目目录之中,建立一个子目录views,用于存放网页模板。
假定这个项目有三个路径:根路径(/)、自我介绍(/about)和文章(/article)。那么,app.js可以这样写:
~~~
var express = require('express');
var app = express();
app.get('/', function(req, res) {
res.sendfile('./views/index.html');
});
app.get('/about', function(req, res) {
res.sendfile('./views/about.html');
});
app.get('/article', function(req, res) {
res.sendfile('./views/article.html');
});
app.listen(3000);
~~~
上面代码表示,三个路径分别对应views目录中的三个模板:index.html、about.html和article.html。另外,向服务器发送信息的方法,从send变成了sendfile,后者专门用于发送文件。
假定index.html的内容如下:
~~~
<html>
<head>
<title>首页</title>
</head>
<body>
<h1>Express Demo</h1>
<footer>
<p>
<a href="/">首页</a> - <a href="/about">自我介绍</a> - <a href="/article">文章</a>
</p>
</footer>
</body>
</html>
~~~
上面代码是一个静态网页。如果想要展示动态内容,就必须使用动态网页模板。
## 动态网页模板
网站真正的魅力在于动态网页,下面我们来看看,如何制作一个动态网页的网站。
### 安装模板引擎
Express支持多种模板引擎,这里采用Handlebars模板引擎的服务器端版本[hbs](https://github.com/donpark/hbs)模板引擎。
先安装hbs。
~~~
npm install hbs --save-dev
~~~
上面代码将hbs模块,安装在项目目录的子目录node_modules之中。save-dev参数表示,将依赖关系写入package.json文件。安装以后的package.json文件变成下面这样:
~~~
// package.json文件
{
"name": "demo",
"description": "My First Express App",
"version": "0.0.1",
"dependencies": {
"express": "3.x"
},
"devDependencies": {
"hbs": "~2.3.1"
}
}
~~~
安装模板引擎之后,就要改写app.js。
~~~
// app.js文件
var express = require('express');
var app = express();
// 加载hbs模块
var hbs = require('hbs');
// 指定模板文件的后缀名为html
app.set('view engine', 'html');
// 运行hbs模块
app.engine('html', hbs.__express);
app.get('/', function (req, res){
res.render('index');
});
app.get('/about', function(req, res) {
res.render('about');
});
app.get('/article', function(req, res) {
res.render('article');
});
~~~
上面代码改用render方法,对网页模板进行渲染。render方法的参数就是模板的文件名,默认放在子目录views之中,后缀名已经在前面指定为html,这里可以省略。所以,res.render('index') 就是指,把子目录views下面的index.html文件,交给模板引擎hbs渲染。
### 新建数据脚本
渲染是指将数据代入模板的过程。实际运用中,数据都是保存在数据库之中的,这里为了简化问题,假定数据保存在一个脚本文件中。
在项目目录中,新建一个文件blog.js,用于存放数据。blog.js的写法符合CommonJS规范,使得它可以被require语句加载。
~~~
// blog.js文件
var entries = [
{"id":1, "title":"第一篇", "body":"正文", "published":"6/2/2013"},
{"id":2, "title":"第二篇", "body":"正文", "published":"6/3/2013"},
{"id":3, "title":"第三篇", "body":"正文", "published":"6/4/2013"},
{"id":4, "title":"第四篇", "body":"正文", "published":"6/5/2013"},
{"id":5, "title":"第五篇", "body":"正文", "published":"6/10/2013"},
{"id":6, "title":"第六篇", "body":"正文", "published":"6/12/2013"}
];
exports.getBlogEntries = function (){
return entries;
}
exports.getBlogEntry = function (id){
for(var i=0; i < entries.length; i++){
if(entries[i].id == id) return entries[i];
}
}
~~~
### 新建网页模板
接着,新建模板文件index.html。
~~~
<!-- views/index.html文件 -->
<h1>文章列表</h1>
{{#each entries}}
<p>
<a href="/article/{{id}}">{{title}}</a><br/>
Published: {{published}}
</p>
{{/each}}
~~~
模板文件about.html。
~~~
<!-- views/about.html文件 -->
<h1>自我介绍</h1>
<p>正文</p>
~~~
模板文件article.html。
~~~
<!-- views/article.html文件 -->
<h1>{{blog.title}}</h1>
Published: {{blog.published}}
<p/>
{{blog.body}}
~~~
可以看到,上面三个模板文件都只有网页主体。因为网页布局是共享的,所以布局的部分可以单独新建一个文件layout.html。
~~~
<!-- views/layout.html文件 -->
<html>
<head>
<title>{{title}}</title>
</head>
<body>
{{{body}}}
<footer>
<p>
<a href="/">首页</a> - <a href="/about">自我介绍</a>
</p>
</footer>
</body>
</html>
~~~
### 渲染模板
最后,改写app.js文件。
~~~
// app.js文件
var express = require('express');
var app = express();
var hbs = require('hbs');
// 加载数据模块
var blogEngine = require('./blog');
app.set('view engine', 'html');
app.engine('html', hbs.__express);
app.use(express.bodyParser());
app.get('/', function(req, res) {
res.render('index',{title:"最近文章", entries:blogEngine.getBlogEntries()});
});
app.get('/about', function(req, res) {
res.render('about', {title:"自我介绍"});
});
app.get('/article/:id', function(req, res) {
var entry = blogEngine.getBlogEntry(req.params.id);
res.render('article',{title:entry.title, blog:entry});
});
app.listen(3000);
~~~
上面代码中的render方法,现在加入了第二个参数,表示模板变量绑定的数据。
现在重启node服务器,然后访问[http://127.0.0.1:3000。](http://127.0.0.1:3000%E3%80%82/)
~~~
node app.js
~~~
可以看得,模板已经使用加载的数据渲染成功了。
### 指定静态文件目录
模板文件默认存放在views子目录。这时,如果要在网页中加载静态文件(比如样式表、图片等),就需要另外指定一个存放静态文件的目录。
~~~
app.use(express.static('public'));
~~~
上面代码在文件app.js之中,指定静态文件存放的目录是public。于是,当浏览器发出非HTML文件请求时,服务器端就到public目录寻找这个文件。比如,浏览器发出如下的样式表请求:
~~~
<link href="/bootstrap/css/bootstrap.css" rel="stylesheet">
~~~
服务器端就到public/bootstrap/css/目录中寻找bootstrap.css文件。
## ExpressJS 4.0的Router用法
Express 4.0的Router用法,做了大幅改变,增加了很多新的功能。Router成了一个单独的组件,好像小型的express应用程序一样,有自己的use、get、param和route方法。
### 基本用法
Express 4.0的router对象,需要单独新建。然后,使用该对象的HTTP动词方法,为不同的访问路径,指定回调函数;最后,挂载到某个路径
~~~
var router = express.Router();
router.get('/', function(req, res) {
res.send('首页');
});
router.get('/about', function(req, res) {
res.send('关于');
});
app.use('/', router);
~~~
上面代码先定义了两个访问路径,然后将它们挂载到根目录。如果最后一行改为app.use('/app', router),则相当于/app和/app/about这两个路径,指定了回调函数。
这种挂载路径和router对象分离的做法,为程序带来了更大的灵活性,既可以定义多个router对象,也可以为将同一个router对象挂载到多个路径。
### router.route方法
router实例对象的route方法,可以接受访问路径作为参数。
~~~
var router = express.Router();
router.route('/api')
.post(function(req, res) {
// ...
})
.get(function(req, res) {
Bear.find(function(err, bears) {
if (err) res.send(err);
res.json(bears);
});
});
app.use('/', router);
~~~
### router中间件
use方法为router对象指定中间件,即在数据正式发给用户之前,对数据进行处理。下面就是一个中间件的例子。
~~~
router.use(function(req, res, next) {
console.log(req.method, req.url);
next();
});
~~~
上面代码中,回调函数的next参数,表示接受其他中间件的调用。函数体中的next(),表示将数据传递给下一个中间件。
注意,中间件的放置顺序很重要,等同于执行顺序。而且,中间件必须放在HTTP动词方法之前,否则不会执行。
### 对路径参数的处理
router对象的param方法用于路径参数的处理,可以
~~~
router.param('name', function(req, res, next, name) {
// 对name进行验证或其他处理……
console.log(name);
req.name = name;
next();
});
router.get('/hello/:name', function(req, res) {
res.send('hello ' + req.name + '!');
});
~~~
上面代码中,get方法为访问路径指定了name参数,param方法则是对name参数进行处理。注意,param方法必须放在HTTP动词方法之前。
### app.route
假定app是Express的实例对象,Express 4.0为该对象提供了一个route属性。app.route实际上是express.Router()的缩写形式,除了直接挂载到根路径。因此,对同一个路径指定get和post方法的回调函数,可以写成链式形式。
~~~
app.route('/login')
.get(function(req, res) {
res.send('this is the login form');
})
.post(function(req, res) {
console.log('processing');
res.send('processing the login form!');
});
~~~
上面代码的这种写法,显然非常简洁清晰。
## 上传文件
首先,在网页插入上传文件的表单。
~~~
<form action="/pictures/upload" method="POST" enctype="multipart/form-data">
Select an image to upload:
<input type="file" name="image">
<input type="submit" value="Upload Image">
</form>
~~~
然后,服务器脚本建立指向`/upload`目录的路由。这时可以安装multer模块,它提供了上传文件的许多功能。
~~~
var express = require('express');
var router = express.Router();
var multer = require('multer');
var uploading = multer({
dest: __dirname + '../public/uploads/',
// 设定限制,每次最多上传1个文件,文件大小不超过1MB
limits: {fileSize: 1000000, files:1},
})
router.post('/upload', uploading, function(req, res) {
})
module.exports = router
~~~
上面代码是上传文件到本地目录。下面是上传到Amazon S3的例子。
首先,在S3上面新增CORS配置文件。
~~~
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
~~~
上面的配置允许任意电脑向你的bucket发送HTTP请求。
然后,安装aws-sdk。
~~~
$ npm install aws-sdk --save
~~~
下面是服务器脚本。
~~~
var express = require('express');
var router = express.Router();
var aws = require('aws-sdk');
router.get('/', function(req, res) {
res.render('index')
})
var AWS_ACCESS_KEY = 'your_AWS_access_key'
var AWS_SECRET_KEY = 'your_AWS_secret_key'
var S3_BUCKET = 'images_upload'
router.get('/sign', function(req, res) {
aws.config.update({accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY});
var s3 = new aws.S3()
var options = {
Bucket: S3_BUCKET,
Key: req.query.file_name,
Expires: 60,
ContentType: req.query.file_type,
ACL: 'public-read'
}
s3.getSignedUrl('putObject', options, function(err, data){
if(err) return res.send('Error with S3')
res.json({
signed_request: data,
url: 'https://s3.amazonaws.com/' + S3_BUCKET + '/' + req.query.file_name
})
})
})
module.exports = router
~~~
上面代码中,用户访问`/sign`路径,正确登录后,会收到一个JSON对象,里面是S3返回的数据和一个暂时用来接收上传文件的URL,有效期只有60秒。
浏览器代码如下。
~~~
// HTML代码为
// <br>Please select an image
// <input type="file" id="image">
// <br>
// <img id="preview">
document.getElementById("image").onchange = function() {
var file = document.getElementById("image").files[0]
if (!file) return
sign_request(file, function(response) {
upload(file, response.signed_request, response.url, function() {
document.getElementById("preview").src = response.url
})
})
}
function sign_request(file, done) {
var xhr = new XMLHttpRequest()
xhr.open("GET", "/sign?file_name=" + file.name + "&file_type=" + file.type)
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
var response = JSON.parse(xhr.responseText)
done(response)
}
}
xhr.send()
}
function upload(file, signed_request, url, done) {
var xhr = new XMLHttpRequest()
xhr.open("PUT", signed_request)
xhr.setRequestHeader('x-amz-acl', 'public-read')
xhr.onload = function() {
if (xhr.status === 200) {
done()
}
}
xhr.send(file)
}
~~~
上面代码首先监听file控件的change事件,一旦有变化,就先向服务器要求一个临时的上传URL,然后向该URL上传文件。
## 参考链接
* Raymond Camden, [Introduction to Express](http://net.tutsplus.com/tutorials/javascript-ajax/introduction-to-express/)
* Christopher Buecheler, [Getting Started With Node.js, Express, MongoDB](http://cwbuecheler.com/web/tutorials/2013/node-express-mongo/)
* Stephen Sugden, [A short guide to Connect Middleware](http://stephensugden.com/middleware_guide/)
* Evan Hahn, [Understanding Express.js](http://evanhahn.com/understanding-express/)
* Chris Sevilleja, [Learn to Use the New Router in ExpressJS 4.0](http://scotch.io/tutorials/javascript/learn-to-use-the-new-router-in-expressjs-4)
* Stefan Fidanov, [Limitless file uploading to Amazon S3 with Node & Express](http://www.terlici.com/2015/05/23/uploading-files-S3.html)
13.16 Net模块和DNS模块
最后更新于:2022-04-01 05:10:03
net模块用于底层的网络通信。
[TOC]
## 服务器端Socket接口
下面代码打开一个服务器端Socket接口,用来接受客户端的数据。
~~~
var serverPort = 9099;
var net = require('net');
var server = net.createServer(function(client) {
console.log('client connected');
console.log('client IP Address: ' + client.remoteAddress);
console.log('is IPv6: ' + net.isIPv6(client.remoteAddress));
console.log('total server connections: ' + server.connections);
// Waiting for data from the client.
client.on('data', function(data) {
console.log('received data: ' + data.toString());
// Write data to the client socket.
client.write('hello from server');
});
// Closed socket event from the client.
client.on('end', function() {
console.log('client disconnected');
});
});
server.on('error',function(err){
console.log(err);
server.close();
});
server.listen(serverPort, function() {
console.log('server started on port ' + serverPort);
});
~~~
上面代码中,createServer方法建立了一个服务端,一旦收到客户端发送的数据,就发出回应,同时还监听客户端是否中断通信。最后,listen方法打开服务端。
## 客户端Socket接口
客户端Socket接口用来向服务器发送数据。
~~~
var serverPort = 9099;
var server = 'localhost';
var net = require('net');
console.log('connecting to server...');
var client = net.connect({server:server,port:serverPort},function(){
console.log('client connected');
// send data
console.log('send data to server');
client.write('greeting from client socket');
});
client.on('data', function(data) {
console.log('received data: ' + data.toString());
client.end();
});
client.on('error',function(err){
console.log(err);
});
client.on('end', function() {
console.log('client disconnected');
});
~~~
上面代码连接服务器之后,就向服务器发送数据,然后监听服务器返回的数据。
## DNS模块
DNS模块用于解析域名。resolve4方法用于IPv4环境,resolve6方法用于IPv6环境,lookup方法在以上两种环境都可以使用,返回IP地址(address)和当前环境(IPv4或IPv6)。
~~~
var dns = require('dns');
dns.resolve4('www.pecollege.net', function (err, addresses) {
if (err)
console.log(err);
console.log('addresses: ' + JSON.stringify(addresses));
});
dns.lookup('www.pecollege.net', function (err, address, family) {
if (err)
console.log(err);
console.log('addresses: ' + JSON.stringify(address));
console.log('family: ' + JSON.stringify(family));
});
~~~
13.15 os模块
最后更新于:2022-04-01 05:10:01
os模块用于与硬件设备通信。
## Socket通信
下面例子列出当前系列的所有IP地址。
~~~
var os = require('os');
var interfaces = os.networkInterfaces();
for (item in interfaces) {
console.log('Network interface name: ' + item);
for (att in interfaces[item]) {
var address = interfaces[item][att];
console.log('Family: ' + address.family);
console.log('IP Address: ' + address.address);
console.log('Is Internal: ' + address.internal);
console.log('');
}
console.log('==================================');
}
~~~
13.14 Cluster模块
最后更新于:2022-04-01 05:09:58
[TOC]
## 概述
### 基本用法
Node.js默认单进程运行,对于多核CPU的计算机来说,这样做效率很低,因为只有一个核在运行,其他核都在闲置。cluster模块就是为了解决这个问题而提出的。
cluster模块允许设立一个主进程和若干个worker进程,由主进程监控和协调worker进程的运行。worker之间采用进程建通信交换消息,cluster模块内置一个负载均衡器,采用Round-robin算法协调各个worker进程之间的负载。运行时,所有新建立的链接都由主进程完成,然后主进程再把TCP连接分配给指定的worker进程。
~~~
var cluster = require('cluster');
var os = require('os');
if (cluster.isMaster){
for (var i = 0, n = os.cpus().length; i < n; i += 1){
cluster.fork();
}
} else {
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
}
~~~
上面代码先判断当前进程是否为主进程(cluster.isMaster),如果是的,就按照CPU的核数,新建若干个worker进程;如果不是,说明当前进程是worker进程,则在该进程启动一个服务器程序。
### cluster.worker对象
cluster.worker指向当前worker进程对象,主进程没有这个值。
它有如下属性。
(1)worker.id
work.id返回当前worker的独一无二的进程编号。这个编号也是cluster.workers中指向当前进程的索引值。
(2)worker.process
所有的worker进程都是用child_process.fork()生成的。child_process.fork()返回的对象,就被保存在worker.process之中。通过这个属性,可以获取worker所在的进程对象。
(3)worker.send()
该方法用于在主进程中,向子进程发送信息。
~~~
if (cluster.isMaster) {
var worker = cluster.fork();
worker.send('hi there');
} else if (cluster.isWorker) {
process.on('message', function(msg) {
process.send(msg);
});
}
~~~
上面代码的作用是,worker进程对主进程发出的每个消息,都做回声。
在worker进程中调用这个方法,等同于process.send(message)。
### cluster.workers对象
该对象只有主进程才有,包含了所有worker进程。每个成员的键值就是一个worker进程,键名就是该worker进程的worker.id属性。
~~~
function eachWorker(callback) {
for (var id in cluster.workers) {
callback(cluster.workers[id]);
}
}
eachWorker(function(worker) {
worker.send('big announcement to all workers');
});
~~~
上面代码用来遍历所有worker进程。
当前socket的data事件,也可以用id属性识别worker进程。
~~~
socket.on('data', function(id) {
var worker = cluster.workers[id];
});
~~~
## 属性与方法
### isMaster,isWorker
isMaster属性返回一个布尔值,表示当前进程是否为主进程。这个属性由process.env.NODE_UNIQUE_ID决定,如果process.env.NODE_UNIQUE_ID为未定义,就表示该进程是主进程。
isWorker属性返回一个布尔值,表示当前进程是否为work进程。它与isMaster属性的值正好相反。
### fork()
fork方法用于新建一个worker进程,上下文都复制主进程。只有主进程才能调用这个方法。
该方法返回一个worker对象。
### kill()
kill方法用于终止worker进程。它可以接受一个参数,表示系统信号。
如果当前是主进程,就会终止与worker.process的联络,然后将系统信号法发向worker进程。如果当前是worker进程,就会终止与主进程的通信,然后退出,返回0。
在以前的版本中,该方法也叫做 worker.destroy() 。
### listening事件
worker进程调用listen方面以后,“listening”就传向该进程的服务器,然后传向主进程。
该事件的回调函数接受两个参数,一个是当前worker对象,另一个是地址对象,包含网址、端口、地址类型(IPv4、IPv6、Unix socket、UDP)等信息。这对于那些服务多个网址的Node应用程序非常有用。
~~~
cluster.on('listening', function(worker, address) {
console.log("A worker is now connected to " + address.address + ":" + address.port);
});
~~~
## 实例:不中断地重启Node服务
重启服务需要关闭后再启动,利用cluster模块,可以做到先启动一个worker进程,再把原有的所有work进程关闭。这样就能实现不中断地重启Node服务。
下面是主进程的代码master.js。
~~~
var cluster = require('cluster');
console.log('started master with ' + process.pid);
// 新建一个worker进程
cluster.fork();
process.on('SIGHUP', function () {
console.log('Reloading...');
var new_worker = cluster.fork();
new_worker.once('listening', function () {
// 关闭所有其他worker进程
for(var id in cluster.workers) {
if (id === new_worker.id.toString()) continue;
cluster.workers[id].kill('SIGTERM');
}
});
});
~~~
上面代码中,主进程监听SIGHUP事件,如果发生该事件就关闭其他所有worker进程。之所以是SIGHUP事件,是因为nginx服务器监听到这个信号,会创造一个新的worker进程,重新加载配置文件。另外,关闭worker进程时,主进程发送SIGTERM信号,这是因为Node允许多个worker进程监听同一个端口。
下面是worker进程的代码server.js。
~~~
var cluster = require('cluster');
if (cluster.isMaster) {
require('./master');
return;
}
var express = require('express');
var http = require('http');
var app = express();
app.get('/', function (req, res) {
res.send('ha fsdgfds gfds gfd!');
});
http.createServer(app).listen(8080, function () {
console.log('http://localhost:8080');
});
~~~
使用时代码如下。
~~~
$ node server.js
started master with 10538
http://localhost:8080
~~~
然后,向主进程连续发出两次SIGHUP信号。
~~~
$ kill -SIGHUP 10538
$ kill -SIGHUP 10538
~~~
主进程会连续两次新建一个worker进程,然后关闭所有其他worker进程,显示如下。
~~~
Reloading...
http://localhost:8080
Reloading...
http://localhost:8080
~~~
最后,向主进程发出SIGTERM信号,关闭主进程。
~~~
$ kill 10538
~~~
## PM2模块
PM2模块是cluster模块的一个包装层。它的作用是尽量将cluster模块抽象掉,让用户像使用单进程一样,部署多进程Node应用。
~~~
// app.js
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world");
}).listen(8080);
~~~
上面代码是标准的Node架设Web服务器的方式,然后用PM2从命令行启动这段代码。
~~~
$ pm2 start app.js -i 4
~~~
上面代码的i参数告诉PM2,这段代码应该在cluster_mode启动,且新建worker进程的数量是4个。如果i参数的值是0,那么当前机器有几个CPU内核,PM2就会启动几个worker进程。
如果一个worker进程由于某种原因挂掉了,会立刻重启该worker进程。
~~~
# 重启所有worker进程
$ pm2 reload all
~~~
每个worker进程都有一个id,可以用下面的命令查看单个worker进程的详情。
~~~
$ pm2 show <worker id>
~~~
正确情况下,PM2采用fork模式新建worker进程,即主进程fork自身,产生一个worker进程。`pm2 reload`命令则会用spawn方式启动,即一个接一个启动worker进程,一个新的worker启动成功,再杀死一个旧的worker进程。采用这种方式,重新部署新版本时,服务器就不会中断服务。
~~~
$ pm2 reload <脚本文件名>
~~~
关闭worker进程的时候,可以部署下面的代码,让worker进程监听shutdown消息。一旦收到这个消息,进行完毕收尾清理工作再关闭。
~~~
process.on('message', function(msg) {
if (msg === 'shutdown') {
close_all_connections();
delete_logs();
server.close();
process.exit(0);
}
});
~~~
## 参考链接
* José F. Romaniello, [Reloading node with no downtime](http://joseoncode.com/2015/01/18/reloading-node-with-no-downtime/)
* Joni Shkurti, [Node.js clustering made easy with PM2](https://keymetrics.io/2015/03/26/pm2-clustering-made-easy/)
13.13 assert 模块
最后更新于:2022-04-01 05:09:56
assert模块是Node的内置模块,主要用于断言。如果表达式不符合预期,就抛出一个错误。该模块提供11个方法,但只有少数几个是常用的。
[TOC]
## assert()
assert方法接受两个参数,当第一个参数对应的布尔值为true时,不会有任何提示,返回undefined。当第一个参数对应的布尔值为false时,会抛出一个错误,该错误的提示信息就是第二个参数设定的字符串。
~~~
// 格式
assert(value, message)
// 例子
var assert = require('assert');
function add (a, b) {
return a + b;
}
var expected = add(1,2);
assert( expected === 3, '预期1加2等于3');
~~~
上面代码不会有任何输出,因为assert方法的第一个参数是true。
~~~
assert( expected === 4, '预期1加2等于3')
// AssertionError: 预期1加2等于3
~~~
上面代码会抛出一个错误,因为assert方法的第一个参数是false。
## assert.ok()
ok是assert方法的另一个名字,与assert方法完全一样。
## assert.equal()
equal方法接受三个参数,第一个参数是实际值,第二个是预期值,第三个是错误的提示信息。
~~~
// 格式
assert.equal(actual, expected, [message])
assert.equal(true, value, message);
// 等同于
assert(value, message);
// 例子
var assert = require('assert');
function add (a, b) {
return a + b;
}
var expected = add(1,2);
// 以下三句效果相同
assert(expected == 3, '预期1+2等于3');
assert.ok(expected == 3, '预期1+2等于3');
assert.equal(expected, 3, '预期1+2等于3');
~~~
equal方法内部使用的是相等运算符(==),而不是严格运算符(===),进行比较运算。
## assert.notEqual()
notEqual方法的用法与equal方法类似,但只有在实际值等于预期值时,才会抛出错误。
~~~
// 格式
assert.notEqual(actual, expected, [message])
// 用法
var assert = require('assert');
function add (a, b) {
return a + b;
}
var expected = add(1,2);
// 以下三种写法效果相同
assert(expected != 4, '预期不等于4');
assert.ok(expected != 4, '预期不等于4');
assert.notEqual(expected, 4, '预期不等于4');
~~~
notEqual方法内部使用不相等运算符(!=),而不是严格不相等运算符(!==),进行比较运算。
## assert.deepEqual()
deepEqual方法用来比较两个对象。只要它们的属性一一对应,且值都相等,就认为两个对象相等,否则抛出一个错误。
~~~
// 格式
assert.deepEqual(actual, expected, [message])
// 例子
var assert = require('assert');
var list1 = [1, 2, 3, 4, 5];
var list2 = [1, 2, 3, 4, 5];
assert.deepEqual(list1, list2, '预期两个数组应该有相同的属性');
var person1 = { "name":"john", "age":"21" };
var person2 = { "name":"john", "age":"21" };
assert.deepEqual(person1, person2, '预期两个对象应该有相同的属性');
~~~
## assert.notDeepEqual()
notDeepEqual方法与deepEqual方法正好相反,用来断言两个对象是否不相等。
~~~
// 格式
assert.notDeepEqual(actual, expected, [message])
// 例子
var assert = require('assert');
var list1 = [1, 2, ,3, 4, 5];
var list2 = [1, 2, 3, 4, 5];
assert.notDeepEqual(list1, list2, '预期两个对象不相等');
var person1 = { "name":"john", "age":"21" };
var person2 = { "name":"jane", "age":"19" };
// deepEqual checks the elements in the objects are identical
assert.notDeepEqual(person1, person2, '预期两个对象不相等');
~~~
## assert.strictEqual()
strictEqual方法使用严格相等运算符(===),比较两个表达式。
~~~
// 格式
assert.strictEqual(actual, expected, [message])
// 例子
var assert = require('assert');
assert.strictEqual(1, '1', '预期严格相等');
// AssertionError: 预期严格相等
~~~
## assert.notStrictEqual()
assert.notStrictEqual方法使用严格不相等运算符(!==),比较两个表达式。
~~~
// 格式
assert.notStrictEqual(actual, expected, [message])
// 例子
var assert = require('assert');
assert.notStrictEqual(1, true, '预期严格不相等');
~~~
## assert.throws()
throws方法预期某个代码块会抛出一个错误,且抛出的错误符合指定的条件。
~~~
// 格式
assert.throws(block, [error], [message])
// 例一,抛出的错误符合某个构造函数
assert.throws(
function() {
throw new Error("Wrong value");
},
Error,
'不符合预期的错误类型'
);
// 例二、抛出错误的提示信息符合正则表达式
assert.throws(
function() {
throw new Error("Wrong value");
},
/value/,
'不符合预期的错误类型'
);
// 例三、抛出的错误符合自定义函数的校验
assert.throws(
function() {
throw new Error("Wrong value");
},
function(err) {
if ( (err instanceof Error) && /value/.test(err) ) {
return true;
}
},
'不符合预期的错误类型'
);
~~~
## assert.doesNotThrow()
doesNotThrow方法与throws方法正好相反,预期某个代码块不抛出错误。
~~~
// 格式
assert.doesNotThrow(block, [message])
// 用法
assert.doesNotThrow(
function() {
console.log("Nothing to see here");
},
'预期不抛出错误'
);
~~~
## assert.ifError()
ifError方法断言某个表达式是否false,如果该表达式对应的布尔值等于true,就抛出一个错误。它对于验证回调函数的第一个参数十分有用,如果该参数为true,就表示有错误。
~~~
// 格式
assert.ifError(value)
// 用法
function sayHello(name, callback) {
var error = false;
var str = "Hello "+name;
callback(error, str);
}
// use the function
sayHello('World', function(err, value){
assert.ifError(err);
// ...
})
~~~
## assert.fail()
fail方法用于抛出一个错误。
~~~
// 格式
assert.fail(actual, expected, message, operator)
// 例子
var assert = require('assert');
assert.fail(21, 42, 'Test Failed', '###')
// AssertionError: Test Failed
assert.fail(21, 21, 'Test Failed', '###')
// AssertionError: Test Failed
assert.fail(21, 42, undefined, '###')
// AssertionError: 21 ### 42
~~~
该方法共有四个参数,但是不管参数是什么值,它总是抛出一个错误。如果message参数对应的布尔值不为false,抛出的错误信息就是message,否则错误信息就是“实际值 + 分隔符 + 预期值”。
13.12 Http模块
最后更新于:2022-04-01 05:09:54
[TOC]
## 基本用法
### 处理GET请求
Http模块主要用于搭建HTTP服务。使用Node.js搭建HTTP服务器非常简单。
~~~
var http = require('http');
http.createServer(function (request, response){
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello World\n');
}).listen(8080, "127.0.0.1");
console.log('Server running on port 8080.');
~~~
上面代码第一行`var http = require("http")`,表示加载http模块。然后,调用http模块的createServer方法,创造一个服务器实例,将它赋给变量http。
ceateServer方法接受一个函数作为参数,该函数的request参数是一个对象,表示客户端的HTTP请求;response参数也是一个对象,表示服务器端的HTTP回应。response.writeHead方法表示,服务器端回应一个HTTP头信息;response.end方法表示,服务器端回应的具体内容,以及回应完成后关闭本次对话。最后的listen(8080)表示启动服务器实例,监听本机的8080端口。
将上面这几行代码保存成文件app.js,然后用node调用这个文件,服务器就开始运行了。
~~~
$ node app.js
~~~
这时命令行窗口将显示一行提示“Server running at port 8080.”。打开浏览器,访问[http://localhost:8080,网页显示“Hello](http://localhost:8080%EF%BC%8C%E7%BD%91%E9%A1%B5%E6%98%BE%E7%A4%BA%E2%80%9CHello/) world!”。
上面的例子是当场生成网页,也可以事前写好网页,存在文件中,然后利用fs模块读取网页文件,将其返回。
~~~
var http = require('http');
var fs = require('fs');
http.createServer(function (request, response){
fs.readFile('data.txt', function readData(err, data) {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end(data);
});
}).listen(8080, "127.0.0.1");
console.log('Server running on port 8080.');
~~~
下面的修改则是根据不同网址的请求,显示不同的内容,已经相当于做出一个网站的雏形了。
~~~
var http = require("http");
http.createServer(function(req, res) {
// 主页
if (req.url == "/") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end("Welcome to the homepage!");
}
// About页面
else if (req.url == "/about") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end("Welcome to the about page!");
}
// 404错误
else {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 error! File not found.");
}
}).listen(8080, "localhost");
~~~
回调函数的req(request)对象,拥有以下属性。
* url:发出请求的网址。
* method:HTTP请求的方法。
* headers:HTTP请求的所有HTTP头信息。
### 处理POST请求
当客户端采用POST方法发送数据时,服务器端可以对data和end两个事件,设立监听函数。
~~~
var http = require('http');
http.createServer(function (req, res) {
var content = "";
req.on('data', function (chunk) {
content += chunk;
});
req.on('end', function () {
res.writeHead(200, {"Content-Type": "text/plain"});
res.write("You've sent: " + content);
res.end();
});
}).listen(8080);
~~~
data事件会在数据接收过程中,每收到一段数据就触发一次,接收到的数据被传入回调函数。end事件则是在所有数据接收完成后触发。
对上面代码稍加修改,就可以做出文件上传的功能。
~~~
"use strict";
var http = require('http');
var fs = require('fs');
var destinationFile, fileSize, uploadedBytes;
http.createServer(function (request, response) {
response.writeHead(200);
destinationFile = fs.createWriteStream("destination.md");
request.pipe(destinationFile);
fileSize = request.headers['content-length'];
uploadedBytes = 0;
request.on('data', function (d) {
uploadedBytes += d.length;
var p = (uploadedBytes / fileSize) * 100;
response.write("Uploading " + parseInt(p, 0) + " %\n");
});
request.on('end', function () {
response.end("File Upload Complete");
});
}).listen(3030, function () {
console.log("server started");
});
~~~
## 发出请求
### get()
get方法用于发出get请求。
~~~
function getTestPersonaLoginCredentials(callback) {
return http.get({
host: 'personatestuser.org',
path: '/email'
}, function(response) {
var body = '';
response.on('data', function(d) {
body += d;
});
response.on('end', function() {
var parsed = JSON.parse(body);
callback({
email: parsed.email,
password: parsed.pass
});
});
});
},
~~~
### request()
request方法用于发出HTTP请求,它的使用格式如下。
~~~
http.request(options[, callback])
~~~
request方法的options参数,可以是一个对象,也可以是一个字符串。如果是字符串,就表示这是一个URL,Node内部就会自动调用`url.parse()`,处理这个参数。
options对象可以设置如下属性。
* host:HTTP请求所发往的域名或者IP地址,默认是localhost。
* hostname:该属性会被`url.parse()`解析,优先级高于host。
* port:远程服务器的端口,默认是80。
* localAddress:本地网络接口。
* socketPath:Unix网络套接字,格式为host:port或者socketPath。
* method:指定HTTP请求的方法,格式为字符串,默认为GET。
* path:指定HTTP请求的路径,默认为根路径(/)。可以在这个属性里面,指定查询字符串,比如`/index.html?page=12`。如果这个属性里面包含非法字符(比如空格),就会抛出一个错误。
* headers:一个对象,包含了HTTP请求的头信息。
* auth:一个代表HTTP基本认证的字符串`user:password`。
* agent:控制缓存行为,如果HTTP请求使用了agent,则HTTP请求默认为`Connection: keep-alive`,它的可能值如下:
* undefined(默认):对当前host和port,使用全局Agent。
* Agent:一个对象,会传入agent属性。
* false:不缓存连接,默认HTTP请求为`Connection: close`。
* keepAlive:一个布尔值,表示是否保留socket供未来其他请求使用,默认等于false。
* keepAliveMsecs:一个整数,当使用KeepAlive的时候,设置多久发送一个TCP KeepAlive包,使得连接不要被关闭。默认等于1000,只有keepAlive设为true的时候,该设置才有意义。
request方法的callback参数是可选的,在response事件发生时触发,而且只触发一次。
`http.request()`返回一个`http.ClientRequest`类的实例。它是一个可写数据流,如果你想通过POST方法发送一个文件,可以将文件写入这个ClientRequest对象。
下面是发送POST请求的一个例子。
~~~
var postData = querystring.stringify({
'msg' : 'Hello World!'
});
var options = {
hostname: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length
}
};
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(postData);
req.end();
~~~
注意,上面代码中,`req.end()`必须被调用,即使没有在请求体内写入任何数据,也必须调用。因为这表示已经完成HTTP请求。
发送过程的任何错误(DNS错误、TCP错误、HTTP解析错误),都会在request对象上触发error事件。
## 搭建HTTPs服务器
搭建HTTPs服务器需要有SSL证书。对于向公众提供服务的网站,SSL证书需要向证书颁发机构购买;对于自用的网站,可以自制。
自制SSL证书需要OpenSSL,具体命令如下。
~~~
openssl genrsa -out key.pem
openssl req -new -key key.pem -out csr.pem
openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
rm csr.pem
~~~
上面的命令生成两个文件:ert.pem(证书文件)和 key.pem(私钥文件)。有了这两个文件,就可以运行HTTPs服务器了。
Node.js提供一个https模块,专门用于处理加密访问。
~~~
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};
var a = https.createServer(options, function (req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
~~~
上面代码显示,HTTPs服务器与HTTP服务器的最大区别,就是createServer方法多了一个options参数。运行以后,就可以测试是否能够正常访问。
~~~
curl -k https://localhost:8000
~~~
## 模块属性
(1)HTTP请求的属性
* headers:HTTP请求的头信息。
* url:请求的路径。
## 模块方法
(1)http模块的方法
* createServer(callback):创造服务器实例。
(2)服务器实例的方法
* listen(port):启动服务器监听指定端口。
(3)HTTP回应的方法
* setHeader(key, value):指定HTTP头信息。
* write(str):指定HTTP回应的内容。
* end():发送HTTP回应。
13.11 Child Process模块
最后更新于:2022-04-01 05:09:52
child_process模块用于新建子进程。子进程的运行结果储存在系统缓存之中(最大200KB),等到子进程运行结束以后,主进程再用回调函数读取子进程的运行结果。
[TOC]
## exec()
exec方法用于执行bash命令。
~~~
var exec = require('child_process').exec;
var ls = exec('ls -l', function (error, stdout, stderr) {
if (error) {
console.log(error.stack);
console.log('Error code: '+error.code);
}
console.log('Child Process STDOUT: '+stdout);
});
~~~
上面代码的exec方法用于新建一个子进程,然后缓存它的运行结果,运行结束后调用回调函数。
exec方法的第一个参数是所要执行的shell命令,第二个参数是回调函数,该函数接受三个参数,分别是发生的错误、标准输出的显示结果、标准错误的显示结果。
由于标准输出和标准错误都是流对象(stream),可以监听data事件,因此上面的代码也可以写成下面这样。
~~~
var exec = require('child_process').exec;
var child = exec('ls -l');
child.stdout.on('data', function(data) {
console.log('stdout: ' + data);
});
child.stderr.on('data', function(data) {
console.log('stdout: ' + data);
});
child.on('close', function(code) {
console.log('closing code: ' + code);
});
~~~
上面的代码还表明,子进程本身有close事件,可以设置回调函数。
上面的代码还有一个好处。监听data事件以后,可以实时输出结果,否则只有等到子进程结束,才会输出结果。所以,如果子进程运行时间较长,或者是持续运行,第二种写法更好。
下面是另一个例子,假定有一个child.js文件。
~~~
// child.js
var exec = require('child_process').exec;
exec('node -v', function(error, stdout, stderr) {
console.log('stdout: ' + stdout);
console.log('stderr: ' + stderr);
if (error !== null) {
console.log('exec error: ' + error);
}
});
~~~
运行后,该文件的输出结果如下。
~~~
$ node child.js
stdout: v0.11.14
stderr:
~~~
exec方法会直接调用bash(`/bin/sh`程序)来解释命令,所以如果有用户输入的参数,exec方法是不安全的。
~~~
var path = ";user input";
child_process.exec('ls -l ' + path, function (err, data) {
console.log(data);
});
~~~
上面代码表示,在bash环境下,`ls -l; user input`会直接运行。如果用户输入恶意代码,将会带来安全风险。因此,在有用户输入的情况下,最好不使用exec方法,而是使用execFile方法。
## execFile()
execFile方法直接执行特定的程序,参数作为数组传入,不会被bash解释,因此具有较高的安全性。
~~~
var child_process = require('child_process');
var path = ".";
child_process.execFile('/bin/ls', ['-l', path], function (err, result) {
console.log(result)
});
~~~
上面代码中,假定path来自用户输入,如果其中包含了分号或反引号,ls程序不理解它们的含义,因此也就得不到运行结果,安全性就得到了提高。
## spawn()
spawn方法创建一个子进程来执行特定命令,用法与execFile方法类似,但是没有回调函数,只能通过监听事件,来获取运行结果。它属于异步执行,适用于子进程长时间运行的情况。
~~~
var child_process = require('child_process');
var path = '.';
var ls = child_process.spawn('/bin/ls', ['-l', path]);
ls.stdout.on('data', function (data) {
console.log('stdout: ' + data);
});
ls.stderr.on('data', function (data) {
console.log('stderr: ' + data);
});
ls.on('close', function (code) {
console.log('child process exited with code ' + code);
});
~~~
spawn方法接受两个参数,第一个是可执行文件,第二个是参数数组。
spawn对象返回一个对象,代表子进程。该对象部署了EventEmitter接口,它的data事件可以监听,从而得到子进程的输出结果。
spawn方法与exec方法非常类似,只是使用格式略有区别。
~~~
child_process.exec(command, [options], callback)
child_process.spawn(command, [args], [options])
~~~
## fork()
fork方法直接创建一个子进程,执行Node脚本,`fork('./child.js')` 相当于 `spawn('node', ['./child.js'])` 。与spawn方法不同的是,fork会在父进程与子进程之间,建立一个通信管道,用于进程之间的通信。
~~~
var n = child_process.fork('./child.js');
n.on('message', function(m) {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });
~~~
上面代码中,fork方法返回一个代表进程间通信管道的对象,对该对象可以监听message事件,用来获取子进程返回的信息,也可以向子进程发送信息。
child.js脚本的内容如下。
~~~
process.on('message', function(m) {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });
~~~
上面代码中,子进程监听message事件,并向父进程发送信息。
## send()
使用 child_process.fork() 生成新进程之后,就可以用 child.send(message, [sendHandle]) 向新进程发送消息。新进程中通过监听message事件,来获取消息。
下面的例子是主进程的代码。
~~~
var cp = require('child_process');
var n = cp.fork(__dirname + '/sub.js');
n.on('message', function(m) {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });
~~~
下面是子进程sub.js代码。
~~~
process.on('message', function(m) {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });
~~~
## 参考链接
* Lift Security Team, [Avoiding Command Injection in Node.js](https://blog.liftsecurity.io/2014/08/19/Avoid-Command-Injection-Node.js): 为什么execFile()的安全性高于exec()
* Krasimir Tsonev, [Node.js: managing child processes](http://tech.pro/tutorial/2074/nodejs-managing-child-processes)
* byvoid, [Node.js中的child_process及进程通信](https://www.byvoid.com/zhs/blog/node-child-process-ipc): exec()、execFile()、fork()、spawn()四种方法的简介
13.10 stream接口
最后更新于:2022-04-01 05:09:49
[TOC]
## Stream是什么?
Unix操作系统从很早以前,就有Stream(流)这个概念,它是不同进程之间传递数据的一种方式。管道命令Pipe就起到在不同命令之间,连接Stream的作用。
Stream把较大的数据,拆成很小的部分。只要命令部署了Stream接口,就可以把一个流的输出接到另一个流的输入。Node引入了这个概念,通过Stream为异步读写数据提供的统一接口。无论是硬盘数据、网络数据,还是内存数据,都可以采用这个接口读写。
读写数据有两种方式。一种方式是同步处理,即先将数据全部读入内存,然后处理。它的优点是符合直觉,流程非常自然,缺点是如果遇到大文件,要花很长时间,可能要过很久才能进入数据处理的步骤。另一种方式就是Stream方式,它是系统读取外部数据实际上的方式,即每次只读入数据的一小块,像“流水”一样。所以,Stream方式就是每当系统读入了一小块数据,就会触发一个事件,发出“新数据块”的信号,只要监听这个事件,就能掌握进展,做出相应处理,这样就提高了程序的性能。
Stream接口最大特点就是通过事件通信,具有readable、writable、drain、data、end、close等事件,既可以读取数据,也可以写入数据。读写数据时,每读入(或写入)一段数据,就会触发一次data事件,全部读取(或写入)完毕,触发end事件。如果发生错误,则触发error事件。
一个对象只要部署了Stream接口,就可以从读取数据,或者写入数据。Node内部很多涉及IO处理的对象,都部署了Stream接口,比如HTTP连接、文件读写、标准输入输出等。
## 基本用法
Node的I/O操作都是异步的,所以与磁盘和网络的交互,都要通过回调函数。一个典型的写文件操作,可能像下面这样。
~~~
var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
fs.readFile(__dirname + '/data.txt', function (err, data) {
res.end(data);
});
});
server.listen(8000);
~~~
上面的代码有一个问题,那就是它必须将整个data.txt文件读入内存,然后再输入。如果data.txt非常大,就会占用大量的内容。一旦有多个并发请求,操作就会变得非常缓慢,用户不得不等很久,才能得到结果。
由于参数req和res都部署了Stream接口,可以使用`fs.createReadStream()`替代`fs.readFile()`,就能解决这个问题。
~~~
var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
var stream = fs.createReadStream(__dirname + '/data.txt');
stream.pipe(res);
});
server.listen(8000);
~~~
Stream接口的最大特点,就是数据会发出node和data事件,内置的pipe方法会处理这两个事件。
数据流通过pipe方法,可以方便地导向其他具有Stream接口的对象。
~~~
var fs = require('fs');
var zlib = require('zlib');
fs.createReadStream('wow.txt')
.pipe(zlib.createGzip())
.pipe(process.stdout);
~~~
上面代码先打开文本文件wow.txt,然后压缩,再导向标准输出。
~~~
fs.createReadStream('wow.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('wow.gz'));
~~~
上面代码压缩文件wow.txt以后,又将其写回压缩文件。
下面代码新建一个Stream实例,然后指定写入事件和终止事件的回调函数,再将其接到标准输入之上。
~~~
var stream = require('stream');
var Stream = stream.Stream;
var ws = new Stream;
ws.writable = true;
ws.write = function(data) {
console.log("input=" + data);
}
ws.end = function(data) {
console.log("bye");
}
process.stdin.pipe(ws);
~~~
调用上面的脚本,会产生以下结果。
~~~
$ node pipe_out.js
hello
input=hello
^d
bye
~~~
上面代码调用脚本下,键入hello,会输出`input=hello`。然后按下ctrl-d,会输出bye。使用管道命令,可以看得更清楚。
~~~
$ echo hello | node pipe_out.js
input=hello
bye
~~~
Stream接口分成三类。
* 可读数据流接口,用于读取数据。
* 可写数据流接口,用于写入数据。
* 双向数据流接口,用于读取和写入数据,比如Node的tcp sockets、zlib、crypto都部署了这个接口。
## 可读数据流
“可读数据流”表示数据的来源,只要一个对象提供“可读数据流”,就表示你可以从其中读取数据。
“可读数据流”有两种状态:流动态和暂停态。处于流动态时,数据会尽快地从数据源导向用户的程序;处于暂停态时,必须显式调用`stream.read()`等指令,“可读数据流”才会释放数据。刚刚新建的时候,“可读数据流”处于暂停态。
三种方法可以让暂停态转为流动态。
* 添加data事件的监听函数
* 调用resume方法
* 调用pipe方法将数据送往一个可写数据流
如果转为流动态时,没有data事件的监听函数,也没有pipe方法的目的地,那么数据将遗失。
以下两种方法可以让流动态转为暂停态。
* 不存在pipe方法的目的地时,调用pause方法
* 存在pipe方法的目的地时,移除所有data事件的监听函数,并且调用unpipe方法,移除所有pipe方法的目的地
注意,只移除data事件的监听函数,并不会自动引发数据流进入“暂停态”。另外,存在pipe方法的目的地时,调用pause方法,并不能保证数据流总是处于暂停态,一旦那些目的地发出数据请求,数据流有可能会继续提供数据。
每当系统有新的数据,该接口可以监听到data事件,从而回调函数。
~~~
var fs = require('fs');
var readableStream = fs.createReadStream('file.txt');
var data = '';
readableStream.setEncoding('utf8');
readableStream.on('data', function(chunk) {
data+=chunk;
});
readableStream.on('end', function() {
console.log(data);
});
~~~
上面代码中,fs模块的createReadStream方法,是部署了Stream接口的文件读取方法。该方法对指定的文件,返回一个对象。该对象只要监听data事件,回调函数就能读到数据。
除了data事件,监听readable事件,也可以读到数据。
~~~
var fs = require('fs');
var readableStream = fs.createReadStream('file.txt');
var data = '';
var chunk;
readableStream.setEncoding('utf8');
readableStream.on('readable', function() {
while ((chunk=readableStream.read()) !== null) {
data += chunk;
}
});
readableStream.on('end', function() {
console.log(data)
});
~~~
readable事件表示系统缓冲之中有可读的数据,使用read方法去读出数据。如果没有数据可读,read方法会返回null。
“可读数据流”除了read方法,还有以下方法。
* Readable.pause() :暂停数据流。已经存在的数据,也不再触发data事件,数据将保留在缓存之中,此时的数据流称为静态数据流。如果对静态数据流再次调用pause方法,数据流将重新开始流动,但是缓存中现有的数据,不会再触发data事件。
* Readable.resume():恢复暂停的数据流。
* readable.unpipe():从管道中移除目的地数据流。如果该方法使用时带有参数,会阻止“可读数据流”进入某个特定的目的地数据流。如果使用时不带有参数,则会移除所有的目的地数据流。
### read()
read方法从系统缓存读取并返回数据。如果读不到数据,则返回null。
该方法可以接受一个整数作为参数,表示所要读取数据的数量,然后会返回该数量的数据。如果读不到足够数量的数据,返回null。如果不提供这个参数,默认返回系统缓存之中的所有数据。
只在“暂停态”时,该方法才有必要手动调用。“流动态”时,该方法是自动调用的,直到系统缓存之中的数据被读光。
~~~
var readable = getReadableStreamSomehow();
readable.on('readable', function() {
var chunk;
while (null !== (chunk = readable.read())) {
console.log('got %d bytes of data', chunk.length);
}
});
~~~
如果该方法返回一个数据块,那么它就触发了data事件。
### setEncoding()
调用该方法,会使得数据流返回指定编码的字符串,而不是缓存之中的二进制对象。比如,调用`setEncoding('utf8')`,数据流会返回UTF-8字符串,调用`setEncoding('hex')`,数据流会返回16进制的字符串。
该方法会正确处理多字节的字符,而缓存的方法`buf.toString(encoding)`不会。所以如果想要从数据流读取字符串,应该总是使用该方法。
~~~
var readable = getReadableStreamSomehow();
readable.setEncoding('utf8');
readable.on('data', function(chunk) {
assert.equal(typeof chunk, 'string');
console.log('got %d characters of string data', chunk.length);
});
~~~
### resume()
resume方法会使得“可读数据流”继续释放data事件,即转为流动态。
~~~
var readable = getReadableStreamSomehow();
readable.resume();
readable.on('end', function(chunk) {
console.log('数据流到达尾部,未读取任务数据');
});
~~~
上面代码中,调用resume方法使得数据流进入流动态,只定义end事件的监听函数,不定义data事件的监听函数,表示不从数据流读取任何数据,只监听数据流到达尾部。
### pause()
pause方法使得流动态的数据流,停止释放data事件,转而进入暂停态。任何此时已经可以读到的数据,都将停留在系统缓存。
~~~
var readable = getReadableStreamSomehow();
readable.on('data', function(chunk) {
console.log('读取%d字节的数据', chunk.length);
readable.pause();
console.log('接下来的1秒内不读取数据');
setTimeout(function() {
console.log('数据恢复读取');
readable.resume();
}, 1000);
});
~~~
### isPaused()
该方法返回一个布尔值,表示“可读数据流”被客户端手动暂停(即调用了pause方法),目前还没有调用resume方法。
~~~
var readable = new stream.Readable
readable.isPaused() // === false
readable.pause()
readable.isPaused() // === true
readable.resume()
readable.isPaused() // === false
~~~
### pipe()
pipe方法是自动传送数据的机制,就像管道一样。它从“可读数据流”读出所有数据,将其写出指定的目的地。整个过程是自动的。
~~~
var fs = require('fs');
var readableStream = fs.createReadStream('file1.txt');
var writableStream = fs.createWriteStream('file2.txt');
readableStream.pipe(writableStream);
~~~
上面代码使用pipe方法,将file1的内容写入file2。整个过程由pipe方法管理,不用手动干预,所以可以将传送数据写得很简洁。
pipe方法返回目的地的数据流,因此可以使用链式写法,将多个数据流操作连在一起。
~~~
var fs = require('fs');
var zlib = require('zlib');
fs.createReadStream('input.txt.gz')
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('output.txt'));
~~~
上面代码采用链式写法,先读取文件,然后进行压缩,最后输出。
下面的写法模拟了Unix系统的cat命令,将标准输出写入标准输入。
~~~
process.stdin.pipe(process.stdout);
~~~
当来源地的数据流读取完成,默认会调用目的地的end方法,就不再能够写入。对pipe方法传入第二个参数`{ end: false }`,可以让目的地的数据流保持打开。
~~~
reader.pipe(writer, { end: false });
reader.on('end', function() {
writer.end('Goodbye\n');
});
~~~
上面代码中,目的地数据流默认不会调用end方法,只能手动调用,因此“Goodbye”会被写入。
### unpipe()
该方法移除pipe方法指定的数据流目的地。如果没有参数,则移除所有的pipe方法目的地。如果有参数,则移除该参数指定的目的地。如果没有匹配参数的目的地,则不会产生任何效果。
~~~
var readable = getReadableStreamSomehow();
var writable = fs.createWriteStream('file.txt');
readable.pipe(writable);
setTimeout(function() {
console.log('停止写入file.txt');
readable.unpipe(writable);
console.log('手动关闭file.txt的写入数据流');
writable.end();
}, 1000);
~~~
上面代码写入file.txt的时间,只有1秒钟,然后就停止写入。
### 事件
(1)readable
readable事件在数据流能够向外提供数据时触发。
~~~
var readable = getReadableStreamSomehow();
readable.on('readable', function() {
// there is some data to read now
});
~~~
(2)data
对于那些没有显式暂停的数据流,添加data事件监听函数,会将数据流切换到流动态,尽快向外提供数据。
~~~
var readable = getReadableStreamSomehow();
readable.on('data', function(chunk) {
console.log('got %d bytes of data', chunk.length);
});
~~~
(3)end
无法再读取到数据时,会触发end事件。也就是说,只有当前数据被完全读取完,才会触发end事件,比如不停地调用read方法。
~~~
var readable = getReadableStreamSomehow();
readable.on('data', function(chunk) {
console.log('got %d bytes of data', chunk.length);
});
readable.on('end', function() {
console.log('there will be no more data.');
});
~~~
(4)close
数据源关闭时,close事件被触发。并不是所有的数据流都支持这个事件。
(5)error
当读取数据发生错误时,error事件被触发。
## 可写数据流
“可写数据流”允许你将数据写入某个目的地。它是数据写入的一种抽象,不同的数据目的地部署了这个接口以后,就可以用统一的方法写入。
以下是部署了可写数据流的一些场合。
* 客户端的http requests
* 服务器的http responses
* fs write streams
* zlib streams
* crypto streams
* tcp sockets
* child process stdin
* process.stdout, process.stderr
下面是fs模块的可写数据流的例子。
~~~
var fs = require('fs');
var readableStream = fs.createReadStream('file1.txt');
var writableStream = fs.createWriteStream('file2.txt');
readableStream.setEncoding('utf8');
readableStream.on('data', function(chunk) {
writableStream.write(chunk);
});
~~~
上面代码中,fs模块的createWriteStream方法针对特定文件,创建了一个“可写数据流”,本质上就是对写入操作部署了Stream接口。然后,“可写数据流”的write方法,可以将数据写入文件。
### write()
write方法用于向“可写数据流”写入数据。它接受两个参数,一个是写入的内容,可以是字符串,也可以是一个stream对象(比如可读数据流),另一个是写入完成后的回调函数。
它返回一个布尔值,表示本次数据是否处理完成。如果返回true,就表示可以写入新的数据了。如果等待写入的数据被缓存了,就返回false。不过,在返回false的情况下,也可以继续传入新的数据等待写入。只是这时,新的数据不会真的写入,只会缓存在内存中。为了避免内存消耗,比较好的做法还是等待该方法返回true,然后再写入。
### cork(),uncork()
cork方法可以强制等待写入的数据进入缓存。当调用uncork方法或end方法时,缓存的数据就会吐出。
### setDefaultEncoding()
setDefaultEncoding方法用于将写入的数据编码成新的格式。它返回一个布尔值,表示编码是否成功,如果返回false就表示编码失败。
### end()
end方法用于终止“可写数据流”。该方法可以接受三个参数,全部都是可选参数。第一个参数是最后所要写入的数据,可以是字符串,也可以是stream对象;第二个参数是写入编码;第三个参数是一个回调函数,finish事件触发时,会调用这个回调函数。
~~~
var file = fs.createWriteStream('example.txt');
file.write('hello, ');
file.end('world!');
~~~
上面代码会在数据写入结束时,在尾部写入“world!”。
调用end方法之后,再写入数据会报错。
~~~
var file = fs.createWriteStream('example.txt');
file.end('world!');
file.write('hello, '); // 报错
~~~
### 事件
(1)drain事件
`writable.write(chunk)`返回false以后,当缓存数据全部写入完成,可以继续写入时,会触发drain事件。
~~~
function writeOneMillionTimes(writer, data, encoding, callback) {
var i = 1000000;
write();
function write() {
var ok = true;
do {
i -= 1;
if (i === 0) {
writer.write(data, encoding, callback);
} else {
ok = writer.write(data, encoding);
}
} while (i > 0 && ok);
if (i > 0) {
writer.once('drain', write);
}
}
}
~~~
上面代码是一个写入100万次的例子,通过drain事件得到可以继续写入的通知。
(2)finish事件
调用end方法时,所有缓存的数据释放,触发finish事件。该事件的回调函数没有参数。
~~~
var writer = getWritableStreamSomehow();
for (var i = 0; i < 100; i ++) {
writer.write('hello, #' + i + '!\n');
}
writer.end('this is the end\n');
writer.on('finish', function() {
console.error('all writes are now complete.');
});
~~~
(3)pipe事件
“可写数据流”调用pipe方法,将数据流导向写入目的地时,触发该事件。
该事件的回调函数,接受发出该事件的“可读数据流”对象作为参数。
~~~
var writer = getWritableStreamSomehow();
var reader = getReadableStreamSomehow();
writer.on('pipe', function(src) {
console.error('something is piping into the writer');
assert.equal(src, reader);
});
reader.pipe(writer);
~~~
(4)unpipe事件
“可读数据流”调用unpipe方法,将可写数据流移出写入目的地时,触发该事件。
该事件的回调函数,接受发出该事件的“可读数据流”对象作为参数。
~~~
var writer = getWritableStreamSomehow();
var reader = getReadableStreamSomehow();
writer.on('unpipe', function(src) {
console.error('something has stopped piping into the writer');
assert.equal(src, reader);
});
reader.pipe(writer);
reader.unpipe(writer);
~~~
(5)error事件
如果写入数据或pipe数据时发生错误,就会触发该事件。
该事件的回调函数,接受一个Error对象作为参数。
## HTTP请求
HTTP对象使用Stream接口,实现网络数据的读写。
~~~
var http = require('http');
var server = http.createServer(function (req, res) {
// req is an http.IncomingMessage, which is a Readable Stream
// res is an http.ServerResponse, which is a Writable Stream
var body = '';
// we want to get the data as utf8 strings
// If you don't set an encoding, then you'll get Buffer objects
req.setEncoding('utf8');
// Readable streams emit 'data' events once a listener is added
req.on('data', function (chunk) {
body += chunk;
});
// the end event tells you that you have entire body
req.on('end', function () {
try {
var data = JSON.parse(body);
} catch (er) {
// uh oh! bad json!
res.statusCode = 400;
return res.end('error: ' + er.message);
}
// write back something interesting to the user:
res.write(typeof data);
res.end();
});
});
server.listen(1337);
// $ curl localhost:1337 -d '{}'
// object
// $ curl localhost:1337 -d '"foo"'
// string
// $ curl localhost:1337 -d 'not json'
// error: Unexpected token o
~~~
data事件表示读取或写入了一块数据。
~~~
req.on('data', function(buf){
// Do something with the Buffer
});
~~~
使用req.setEncoding方法,可以设定字符串编码。
~~~
req.setEncoding('utf8');
req.on('data', function(str){
// Do something with the String
});
~~~
end事件,表示读取或写入数据完毕。
~~~
var http = require('http');
http.createServer(function(req, res){
res.writeHead(200);
req.on('data', function(data){
res.write(data);
});
req.on('end', function(){
res.end();
});
}).listen(3000);
~~~
上面代码相当于建立了“回声”服务,将HTTP请求的数据体,用HTTP回应原样发送回去。
system模块提供了pump方法,有点像Linux系统的管道功能,可以将一个数据流,原封不动得转给另一个数据流。所以,上面的例子也可以用pump方法实现。
~~~
var http = require('http'),
sys = require('sys');
http.createServer(function(req, res){
res.writeHead(200);
sys.pump(req, res);
}).listen(3000);
~~~
## fs模块
fs模块的createReadStream方法用于新建读取数据流,createWriteStream方法用于新建写入数据流。使用这两个方法,可以做出一个用于文件复制的脚本copy.js。
~~~
// copy.js
var fs = require('fs');
console.log(process.argv[2], '->', process.argv[3]);
var readStream = fs.createReadStream(process.argv[2]);
var writeStream = fs.createWriteStream(process.argv[3]);
readStream.on('data', function (chunk) {
writeStream.write(chunk);
});
readStream.on('end', function () {
writeStream.end();
});
readStream.on('error', function (err) {
console.log("ERROR", err);
});
writeStream.on('error', function (err) {
console.log("ERROR", err);
});d all your errors, you wouldn't need to use domains.
~~~
上面代码非常容易理解,使用的时候直接提供源文件路径和目标文件路径,就可以了。
~~~
node cp.js src.txt dest.txt
~~~
Streams对象都具有pipe方法,起到管道作用,将一个数据流输入另一个数据流。所以,上面代码可以重写成下面这样:
~~~
var fs = require('fs');
console.log(process.argv[2], '->', process.argv[3]);
var readStream = fs.createReadStream(process.argv[2]);
var writeStream = fs.createWriteStream(process.argv[3]);
readStream.on('open', function () {
readStream.pipe(writeStream);
});
readStream.on('end', function () {
writeStream.end();
});
~~~
## 错误处理
下面是压缩后发送文件的代码。
~~~
http.createServer(function (req, res) {
// set the content headers
fs.createReadStream('filename.txt')
.pipe(zlib.createGzip())
.pipe(res)
})
~~~
上面的代码没有部署错误处理机制,一旦发生错误,就无法处理。所以,需要加上error事件的监听函数。
~~~
http.createServer(function (req, res) {
// set the content headers
fs.createReadStream('filename.txt')
.on('error', onerror)
.pipe(zlib.createGzip())
.on('error', onerror)
.pipe(res)
function onerror(err) {
console.error(err.stack)
}
})
~~~
上面的代码还是存在问题,如果客户端中断下载,写入的数据流就会收不到close事件,一直处于等待状态,从而造成内存泄漏。因此,需要使用[on-finished模块](https://github.com/jshttp/on-finished)用来处理这种情况。
~~~
http.createServer(function (req, res) {
var stream = fs.createReadStream('filename.txt')
// set the content headers
stream
.on('error', onerror)
.pipe(zlib.createGzip())
.on('error', onerror)
.pipe(res)
onFinished(res, function () {
// make sure the stream is always destroyed
stream.destroy()
})
})
~~~
## 参考链接
* James Halliday, [cs294-101-streams-lecture](https://github.com/substack/cs294-101-streams-lecture)
13.9 Events模块
最后更新于:2022-04-01 05:09:47
[TOC]
## 概述
### 基本用法
Events模块是node.js对“发布/订阅”模式(publish/subscribe)的部署。一个对象通过这个模块,向另一个对象传递消息。该模块通过EventEmitter属性,提供了一个构造函数。该构造函数的实例具有on方法,可以用来监听指定事件,并触发回调函数。任意对象都可以发布指定事件,被EventEmitter实例的on方法监听到。
下面是一个实例,先建立一个消息中心,然后通过on方法,为各种事件指定回调函数,从而将程序转为事件驱动型,各个模块之间通过事件联系。
~~~
var EventEmitter = require("events").EventEmitter;
var ee = new EventEmitter();
ee.on("someEvent", function () {
console.log("event has occured");
});
ee.emit("someEvent");
~~~
上面代码在加载events模块后,通过EventEmitter属性建立了一个EventEmitter对象实例,这个实例就是消息中心。然后,通过on方法为someEvent事件指定回调函数。最后,通过emit方法触发someEvent事件。
### on方法
默认情况下,Node.js允许同一个事件最多可以指定10个回调函数。
~~~
ee.on("someEvent", function () { console.log("event 1"); });
ee.on("someEvent", function () { console.log("event 2"); });
ee.on("someEvent", function () { console.log("event 3"); });
~~~
超过10个回调函数,会发出一个警告。这个门槛值可以通过setMaxListeners方法改变。
~~~
ee.setMaxListeners(20);
~~~
### emit方法
EventEmitter实例的emit方法,用来触发事件。它的第一个参数是事件名称,其余参数都会依次传入回调函数。
~~~
var EventEmitter = require('events').EventEmitter;
var myEmitter = new EventEmitter;
var connection = function(id){
console.log('client id: ' + id);
};
myEmitter.on('connection', connection);
myEmitter.emit('connection', 6);
~~~
## EventEmitter接口的部署
Events模块的作用,还在于其他模块可以部署EventEmitter接口,从而也能够订阅和发布消息。
~~~
var EventEmitter = require('events').EventEmitter;
function Dog(name) {
this.name = name;
}
Dog.prototype.__proto__ = EventEmitter.prototype;
// 另一种写法
// Dog.prototype = Object.create(EventEmitter.prototype);
var simon = new Dog('simon');
simon.on('bark', function(){
console.log(this.name + ' barked');
});
setInterval(function(){
simon.emit('bark');
}, 500);
~~~
上面代码新建了一个构造函数Dog,然后让其继承EventEmitter,因此Dog就拥有了EventEmitter的接口。最后,为Dog的实例指定bark事件的监听函数,再使用EventEmitter的emit方法,触发bark事件。
Node内置模块util的inherits方法,提供了另一种继承EventEmitter的写法。
~~~
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var Radio = function(station) {
var self = this;
setTimeout(function() {
self.emit('open', station);
}, 0);
setTimeout(function() {
self.emit('close', station);
}, 5000);
this.on('newListener', function(listener) {
console.log('Event Listener: ' + listener);
});
};
util.inherits(Radio, EventEmitter);
module.exports = Radio;
~~~
上面代码中,Radio是一个构造函数,它的实例继承了EventEmitter接口。下面是使用这个模块的例子。
~~~
var Radio = require('./radio.js');
var station = {
freq: '80.16',
name: 'Rock N Roll Radio',
};
var radio = new Radio(station);
radio.on('open', function(station) {
console.log('"%s" FM %s 打开', station.name, station.freq);
console.log('♬ ♫♬');
});
radio.on('close', function(station) {
console.log('"%s" FM %s 关闭', station.name, station.freq);
});
~~~
## 事件类型
Events模块默认支持两个事件。
* newListener事件:添加新的回调函数时触发。
* removeListener事件:移除回调时触发。
~~~
ee.on("newListener", function (evtName){
console.log("New Listener: " + evtName);
});
ee.on("removeListener", function (evtName){
console.log("Removed Listener: " + evtName);
});
function foo (){}
ee.on("save-user", foo);
ee.removeListener("save-user", foo);
// New Listener: removeListener
// New Listener: save-user
// Removed Listener: save-user
~~~
上面代码会触发两次newListener事件,以及一次removeListener事件。
## EventEmitter实例的方法
### once方法
该方法类似于on方法,但是回调函数只触发一次。
~~~
var EventEmitter = require('events').EventEmitter;
var myEmitter = new EventEmitter;
myEmitter.once('message', function(msg){
console.log('message: ' + msg);
});
myEmitter.emit('message', 'this is the first message');
myEmitter.emit('message', 'this is the second message');
myEmitter.emit('message', 'welcome to nodejs');
~~~
上面代码触发了三次message事件,但是回调函数只会在第一次调用时运行。
下面代码指定,一旦服务器连通,只调用一次的回调函数。
~~~
server.once('connection', function (stream) {
console.log('Ah, we have our first user!');
});
~~~
该方法返回一个EventEmitter对象,因此可以链式加载监听函数。
### removeListener方法
该方法用于移除回调函数。它接受两个参数,第一个是事件名称,第二个是回调函数名称。这就是说,不能用于移除匿名函数。
~~~
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter;
emitter.on('message', console.log);
setInterval(function(){
emitter.emit('message', 'foo bar');
}, 300);
setTimeout(function(){
emitter.removeListener('message', console.log);
}, 1000);
~~~
上面代码每300毫秒触发一次message事件,直到1000毫秒后取消监听。
另一个例子是使用removeListener方法模拟once方法。
~~~
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter;
function onlyOnce () {
console.log("You'll never see this again");
emitter.removeListener("firstConnection", onlyOnce);
}
emitter.on("firstConnection", onlyOnce);
~~~
(3)removeAllListeners方法
该方法用于移除某个事件的所有回调函数。
~~~
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter;
// some code here
emitter.removeAllListeners("firstConnection");
~~~
如果不带参数,则表示移除所有事件的所有回调函数。
~~~
emitter.removeAllListeners();
~~~
(4)listener方法
该方法接受一个事件名称作为参数,返回该事件所有回调函数组成的数组。
~~~
var EventEmitter = require('events').EventEmitter;
var ee = new EventEmitter;
function onlyOnce () {
console.log(ee.listeners("firstConnection"));
ee.removeListener("firstConnection", onlyOnce);
console.log(ee.listeners("firstConnection"));
}
ee.on("firstConnection", onlyOnce)
ee.emit("firstConnection");
ee.emit("firstConnection");
// [ [Function: onlyOnce] ]
// []
~~~
上面代码显示两次回调函数组成的数组,第一次只有一个回调函数onlyOnce,第二次是一个空数组,因为removeListener方法取消了回调函数。
## 参考链接
* Hage Yaapa, [Node.js EventEmitter Tutorial](http://www.hacksparrow.com/node-js-eventemitter-tutorial.html)
13.8 Buffer对象
最后更新于:2022-04-01 05:09:45
[TOC]
## 概述
Buffer对象是Node.js用来处理二进制数据的一个接口。JavaScript比较擅长处理Unicode数据,对于处理二进制格式的数据(比如TCP数据流),就不太擅长。Buffer对象就是为了解决这个问题而提供的。该对象也是一个构造函数,它的实例代表了V8引擎分配的一段内存,基本上是一个数组,成员都为整数值。
Buffer是Node原生提供的全局对象,可以直接使用,不需要`require('buffer')`。
Buffer对象与字符串的互相转换,需要指定编码格式。目前,Buffer对象支持以下编码格式。
* ascii
* utf8
* utf16le:UTF-16的小头编码,支持大于U+10000的四字节字符。
* ucs2:utf16le的别名。
* base64
* hex:将每个字节转为两个十六进制字符。
V8引擎将Buffer对象占用的内存,解释为一个整数数组,而不是二进制数组。所以,`new Uint32Array(new Buffer([1,2,3,4]))`,生成的Uint32Array数组是一个4个成员的Uint32Array数组,而不是只有单个成员([0x1020304]或者[0x4030201])的。
注意,这时类型化数组所对应的内存是从Buffer对象拷贝的,而不是共享的。类型化数组的buffer属性,保留指向原Buffer对象的指针。
类型化数组的操作,与Buffer对象的操作基本上是兼容的,只有轻微的差异。比如,类型化数组的slice方法返回原内存的拷贝,而Buffer对象的slice方法创造原内存的一个视图(view)。
## Buffer构造函数
Buffer作为构造函数,可以用new命令生成一个实例,它可以接受多种形式的参数。
~~~
// 参数是整数,指定分配多少个字节内存
var hello = new Buffer(5);
// 参数是数组,数组成员必须是整数值
var hello = new Buffer([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
hello.toString() // 'Hello'
// 参数是字符串(默认为utf8编码)
var hello = new Buffer('Hello');
// 参数是字符串(不省略编码)
var hello = new Buffer('Hello', 'utf8');
// 参数是另一个Buffer实例,等同于拷贝后者
var hello1 = new Buffer('Hello');
var hello2 = new Buffer(hello1);
~~~
## 类的方法
### Buffer.isEncoding()
Buffer.isEncoding方法返回一个布尔值,表示Buffer实例是否为指定编码。
~~~
Buffer.isEncoding('utf8')
// true
~~~
### Buffer.isBuffer()
Buffer.isBuffer方法接受一个对象作为参数,返回一个布尔值,表示该对象是否为Buffer实例。
~~~
Buffer.isBuffer(Date) // false
~~~
### Buffer.byteLength()
Buffer.byteLength方法返回字符串实际占据的字节长度,默认编码方式为utf8。
~~~
Buffer.byteLength('Hello', 'utf8') // 5
~~~
### Buffer.concat()
Buffer.concat方法将一组Buffer对象合并为一个Buffer对象。
~~~
var i1 = new Buffer('Hello');
var i2 = new Buffer(' ');
var i3 = new Buffer('World');
Buffer.concat([i1, i2, i3]).toString()
// 'Hello World'
~~~
需要注意的是,如果Buffer.concat的参数数组只有一个成员,就直接返回该成员。如果有多个成员,就返回一个多个成员合并的新Buffer对象。
Buffer.concat方法还可以接受第二个参数,指定合并后Buffer对象的总长度。
~~~
var i1 = new Buffer('Hello');
var i2 = new Buffer(' ');
var i3 = new Buffer('World');
Buffer.concat([i1, i2, i3], 10).toString()
// 'Hello Worl'
~~~
省略第二个参数时,Node内部会计算出这个值,然后再据此进行合并运算。因此,显式提供这个参数,能提供运行速度。
## 实例属性
### length
length属性返回Buffer对象所占据的内存长度。注意,这个值与Buffer对象的内容无关。
~~~
buf = new Buffer(1234);
buf.length // 1234
buf.write("some string", 0, "ascii");
buf.length // 1234
~~~
上面代码中,不管写入什么内容,length属性总是返回Buffer对象的空间长度。如果想知道一个字符串所占据的字节长度,可以将其传入Buffer.byteLength方法。
length属性是可写的,但是这会导致未定义的行为,不建议使用。如果想修改Buffer对象的长度,建议使用slice方法返回一个新的Buffer对象。
## 实例方法
### write()
write方法可以向指定的Buffer对象写入数据。它的第一个参数是所写入的内容,第二个参数(可省略)是所写入的起始位置(从0开始),第三个参数(可省略)是编码方式,默认为utf8。
~~~
var buf = new Buffer(5);
buf.write('He');
buf.write('l', 2);
buf.write('lo', 3);
console.log(buf.toString());
// "Hello"
~~~
### slice()
slice方法返回一个按照指定位置、从原对象切割出来的Buffer实例。它的两个参数分别为切割的起始位置和终止位置。
~~~
var buf = new Buffer('just some data');
var chunk = buf.slice(4, 9);
chunk.toString()
// "some"
~~~
### toString()
toString方法将Buffer对象,按照指定编码(默认为utf8)转为字符串。
~~~
var hello = new Buffer('Hello');
hello // <Buffer 48 65 6c 6c 6f>
hello.toString() // "Hello"
~~~
toString方法可以只返回指定位置内存的内容,它的第二个参数表示起始位置,第三个参数表示终止位置,两者都是从0开始计算。
~~~
var buf = new Buffer('just some data');
console.log(buf.toString('ascii', 4, 9));
// "some"
~~~
### toJSON()
toJSON方法将Buffer实例转为JSON对象。如果JSON.stringify方法调用Buffer实例,默认会先调用toJSON方法。
~~~
var buf = new Buffer('test');
var json = JSON.stringify(buf);
json // '[116,101,115,116]'
var copy = new Buffer(JSON.parse(json));
copy // <Buffer 74 65 73 74>
~~~
13.7 process对象
最后更新于:2022-04-01 05:09:42
process对象是Node的一个全局对象,提供当前node进程的信息。它可以在脚本的任意位置使用,不必通过require命令加载。该对象部署了EventEmitter接口。
[TOC]
## 进程信息
通过process对象,可以获知当前进程的很多信息。
### 退出码
进程退出时,会返回一个整数值,表示退出时的状态。这个整数值就叫做退出码。下面是常见的Node进程退出码。
* 0,正常退出
* 1,发生未捕获错误
* 5,V8执行错误
* 8,不正确的参数
* 128 + 信号值,如果Node接受到退出信号(比如SIGKILL或SIGHUP),它的退出码就是128加上信号值。由于128的二进制形式是10000000, 所以退出码的后七位就是信号值。
## 属性
process对象提供一系列属性,用于返回系统信息。
* process.argv:返回当前进程的命令行参数数组。
* process.env:返回一个对象,成员为当前shell的环境变量,比如process.env.HOME。
* process.installPrefix:node的安装路径的前缀,比如`/usr/local`,则node的执行文件目录为`/usr/local/bin/node`。
* process.pid:当前进程的进程号。
* process.platform:当前系统平台,比如Linux。
* process.title:默认值为“node”,可以自定义该值。
* process.version:Node的版本,比如v0.10.18。
下面是主要属性的介绍。
### stdout,stdin,stderr
以下属性指向系统IO。
(1)stdout
stdout属性指向标准输出(文件描述符1)。它的write方法等同于console.log,可用在标准输出向用户显示内容。
~~~
console.log = function(d) {
process.stdout.write(d + '\n');
};
~~~
下面代码表示将一个文件导向标准输出。
~~~
var fs = require('fs');
fs.createReadStream('wow.txt')
.pipe(process.stdout);
~~~
上面代码中,由于process.stdout和process.stdin与其他进程的通信,都是流(stream)形式,所以必须通过pipe管道命令中介。
~~~
var fs = require('fs');
var zlib = require('zlib');
fs.createReadStream('wow.txt')
.pipe(zlib.createGzip())
.pipe(process.stdout);
~~~
上面代码通过pipe方法,先将文件数据压缩,然后再导向标准输出。
(2)stdin
stdin代表标准输入(文件描述符0)。
~~~
process.stdin.pipe(process.stdout)
~~~
上面代码表示将标准输入导向标准输出。
由于stdin和stdout都部署了stream接口,所以可以使用stream接口的方法。
~~~
process.stdin.setEncoding('utf8');
process.stdin.on('readable', function() {
var chunk = process.stdin.read();
if (chunk !== null) {
process.stdout.write('data: ' + chunk);
}
});
process.stdin.on('end', function() {
process.stdout.write('end');
});
~~~
(3)stderr
stderr属性指向标准错误(文件描述符2)。
### argv,execPath,execArgv
argv属性返回一个数组,由命令行执行脚本时的各个参数组成。它的第一个成员总是node,第二个成员是脚本文件名,其余成员是脚本文件的参数。
请看下面的例子,新建一个脚本文件argv.js。
~~~
// argv.js
console.log("argv: ",process.argv);
~~~
在命令行下调用这个脚本,会得到以下结果。
~~~
$ node argv.js a b c
[ 'node', '/path/to/argv.js', 'a', 'b', 'c' ]
~~~
上面代码表示,argv返回数组的成员依次是命令行的各个部分,真正的参数实际上是从`process.argv[2]`开始。要得到真正的参数部分,可以把argv.js改写成下面这样。
~~~
// argv.js
var myArgs = process.argv.slice(2);
console.log(myArgs);
~~~
execPath属性返回执行当前脚本的Node二进制文件的绝对路径。
~~~
> process.execPath
'/usr/local/bin/node'
>
~~~
execArgv属性返回一个数组,成员是命令行下执行脚本时,在Node可执行文件与脚本文件之间的命令行参数。
~~~
# script.js的代码为
# console.log(process.execArgv);
$ node --harmony script.js --version
~~~
## 方法
process对象提供以下方法:
* process.chdir():切换工作目录到指定目录。
* process.cwd():返回运行当前脚本的工作目录的路径。
* process.exit():退出当前进程。
* process.getgid():返回当前进程的组ID(数值)。
* process.getuid():返回当前进程的用户ID(数值)。
* process.nextTick():指定回调函数在当前执行栈的尾部、下一次Event Loop之前执行。
* process.on():监听事件。
* process.setgid():指定当前进程的组,可以使用数字ID,也可以使用字符串ID。
* process.setuid():指定当前进程的用户,可以使用数字ID,也可以使用字符串ID。
### process.cwd(),process.chdir()
cwd方法返回进程的当前目录,chdir方法用来切换目录。
~~~
> process.cwd()
'/home/aaa'
> process.chdir('/home/bbb')
> process.cwd()
'/home/bbb'
~~~
## process.nextTick()
process.nextTick()将任务放到当前执行栈的尾部。
~~~
process.nextTick(function () {
console.log('下一次Event Loop即将开始!');
});
~~~
上面代码可以用`setTimeout(f,0)`改写,效果接近,但是原理不同。`setTimeout(f,0)`是将任务放到当前任务队列的尾部,在下一次Event Loop时执行。另外,nextTick的效率更高,因为不用检查是否到了指定时间。
~~~
setTimeout(function () {
console.log('已经到了下一轮Event Loop!');
}, 0)
~~~
### process.exit()
process.exit方法用来退出当前进程,它可以接受一个数值参数。如果参数大于0,表示执行失败;如果等于0表示执行成功。
~~~
if (err) {
process.exit(1);
} else {
process.exit(0);
}
~~~
process.exit()执行时,会触发exit事件。
### process.on()
process.on方法用来监听各种事件,并指定回调函数。
~~~
process.on('uncaughtException', function(err){
console.log('got an error: %s', err.message);
process.exit(1);
});
setTimeout(function(){
throw new Error('fail');
}, 100);
~~~
上面代码是process监听Node的一个全局性事件uncaughtException,只要有错误没有捕获,就会触发这个事件。
process支持的事件有以下一些。
* data事件:数据输出输入时触发
* SIGINT事件:接收到系统信号时触发
~~~
process.on('SIGINT', function () {
console.log('Got a SIGINT. Goodbye cruel world');
process.exit(0);
});
~~~
使用时,向该进程发出系统信号,就会导致进程退出。
~~~
$ kill -s SIGINT [process_id]
~~~
SIGTERM信号表示内核要求当前进程停止,进程可以自行停止,也可以忽略这个信号。
~~~
var http = require('http');
var server = http.createServer(function (req, res) {
});
process.on('SIGTERM', function () {
server.close(function () {
process.exit(0);
});
});
~~~
上面代码表示,进程接到SIGTERM信号之后,关闭服务器,然后退出进程。需要注意的是,这时进程不会马上退出,而是要回应完最后一个请求,处理完所有回调函数,然后再退出。
### process.kill()
process.kill方法用来对指定ID的线程发送信号,默认为SIGINT信号。
~~~
process.on('SIGTERM', function(){
console.log('terminating');
process.exit(1);
});
setTimeout(function(){
console.log('sending SIGTERM to process %d', process.pid);
process.kill(process.pid, 'SIGTERM');
}, 500);
setTimeout(function(){
console.log('never called');
}, 1000);
~~~
上面代码中,500毫秒后向当前进程发送SIGTERM信号(终结进程),因此1000毫秒后的指定事件不会被触发。
## 事件
### exit事件
当前进程退出时,会触发exit事件,可以对该事件指定回调函数。
~~~
process.on('exit', function () {
fs.writeFileSync('/tmp/myfile', '需要保存到硬盘的信息');
});
~~~
注意,此时回调函数只能执行同步操作,不能包含异步操作,因为执行完回调函数,进程就会退出,无法监听到回调函数的操作结果。
~~~
process.on('exit', function(code) {
// 不会执行
setTimeout(function() {
console.log('This will not run');
}, 0);
});
~~~
上面代码在exit事件的回调函数里面,指定了一个下一轮事件循环,所要执行的操作。这是无效的,不会得到执行。
### beforeExit事件
beforeExit事件在Node清空了Event Loop以后,再没有任何待处理的任务时触发。正常情况下,如果没有任何待处理的任务,Node进程会自动退出,设置beforeExit事件的监听函数以后,就可以提供一个机会,再部署一些任务,使得Node进程不退出。
beforeExit事件与exit事件的主要区别是,beforeExit的监听函数可以部署异步任务,而exit不行。
此外,如果是显式终止程序(比如调用process.exit()),或者因为发生未捕获的错误,而导致进程退出,这些场合不会触发beforeExit事件。因此,不能使用该事件替代exit事件。
### uncaughtException事件
当前进程抛出一个没有被捕捉的错误时,会触发uncaughtException事件。
~~~
process.on('uncaughtException', function (err) {
console.error('An uncaught error occurred!');
console.error(err.stack);
});
~~~
部署uncaughtException事件的监听函数,是免于node进程终止的最后措施,否则node就要执行`process.exit()`。出于除错的目的,并不建议发生错误,还保持进程运行。
抛出错误之前部署的异步操作,还是会继续执行。只有完成以后,Node进程才会退出。
~~~
process.on('uncaughtException', function(err) {
console.log('Caught exception: ' + err);
});
setTimeout(function() {
console.log('本行依然执行');
}, 500);
// 下面的表达式抛出错误
nonexistentFunc();
~~~
上面代码中,抛出错误之后,此前setTimeout指定的回调函数亦然会执行。
### 信号事件
操作系统内核向Node进程发出信号,会触发信号事件。实际开发中,主要对SIGTERM和SIGINT信号部署监听函数,这两个信号在非Windows平台会导致进程退出,但是只要部署了监听函数,Node进程收到信号后就不会退出。
~~~
// 读取标准输入,这主要是为了不让当前进程退出
process.stdin.resume();
process.on('SIGINT', function() {
console.log('SIGINT信号,按Control-D退出');
});
~~~
上面代码部署了SIGINT信号的监听函数,当用户按下Ctrl-C后,会显示提示文字。
## 参考链接
* José F. Romaniello, [Graceful shutdown in node.js](http://joseoncode.com/2014/07/21/graceful-shutdown-in-node-dot-js/)
13.6 Path模块
最后更新于:2022-04-01 05:09:40
[TOC]
## path.join()
`path.join`方法用于连接路径。该方法的主要用途在于,会正确使用当前系统的路径分隔符,Unix系统是”/“,Windows系统是”\“。
~~~
var path = require('path');
path.join(mydir, "foo");
~~~
上面代码在Unix系统下,会返回路径`mydir/foo`。
## path.resolve()
`path.resolve`方法用于将相对路径转为绝对路径。
它可以接受多个参数,依次表示所要进入的路径,直到将最后一个参数转为绝对路径。如果根据参数无法得到绝对路径,就以当前所在路径作为基准。除了根目录,该方法的返回值都不带尾部的斜杠。
~~~
// 格式
path.resolve([from ...], to)
// 实例
path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')
~~~
上面代码的实例,执行效果类似下面的命令。
~~~
$ cd foo/bar
$ cd /tmp/file/
$ cd ..
$ cd a/../subfile
$ pwd
~~~
更多例子。
~~~
path.resolve('/foo/bar', './baz')
// '/foo/bar/baz'
path.resolve('/foo/bar', '/tmp/file/')
// '/tmp/file'
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif')
// 如果当前目录是/home/myself/node,返回
// /home/myself/node/wwwroot/static_files/gif/image.gif
~~~
该方法忽略非字符串的参数。
13.5 fs 模块
最后更新于:2022-04-01 05:09:38
[TOC]
fs是filesystem的缩写,该模块提供本地文件的读写能力,基本上是POSIX文件操作命令的简单包装。但是,这个模块几乎对所有操作提供异步和同步两种操作方式,供开发者选择。
## readFileSync()
readFileSync方法用于同步读取文件,返回一个字符串。
~~~
var text = fs.readFileSync(fileName, "utf8");
// 将文件按行拆成数组
text.split(/\r?\n/).forEach(function (line) {
// ...
});
~~~
该方法的第一个参数是文件路径,第二个参数是文本文件编码,默认为utf8。
不同系统的行结尾字符不同,可以用下面的方法判断。
~~~
// 方法一,查询现有的行结尾字符
var EOL = fileContents.indexOf("\r\n") >= 0 ? "\r\n" : "\n";
// 方法二,根据当前系统处理
var EOL = (process.platform === 'win32' ? '\r\n' : '\n')
~~~
## writeFileSync()
writeFileSync方法用于同步写入文件。
~~~
fs.writeFileSync(fileName, str, 'utf8');
~~~
它的第一个参数是文件路径,第二个参数是写入文件的字符串,第三个参数是文件编码,默认为utf8。
## exists(path, callback)
exists方法用来判断给定路径是否存在,然后不管结果如何,都会调用回调函数。
~~~
fs.exists('/path/to/file', function (exists) {
util.debug(exists ? "it's there" : "no file!");
});
~~~
上面代码表明,回调函数的参数是一个表示文件是否存在的布尔值。
需要注意的是,不要在open方法之前调用exists方法,open方法本身就能检查文件是否存在。
下面的例子是如果给定目录存在,就删除它。
~~~
if(fs.exists(outputFolder)) {
console.log("Removing "+outputFolder);
fs.rmdir(outputFolder);
}
~~~
## mkdir(),writeFile(),readfile()
mkdir方法用于新建目录。
~~~
var fs = require('fs');
fs.mkdir('./helloDir',0777, function (err) {
if (err) throw err;
});
~~~
mkdir接受三个参数,第一个是目录名,第二个是权限值,第三个是回调函数。
writeFile方法用于写入文件。
~~~
var fs = require('fs');
fs.writeFile('./helloDir/message.txt', 'Hello Node', function (err) {
if (err) throw err;
console.log('文件写入成功');
});
~~~
readfile方法用于读取文件内容。
~~~
var fs = require('fs');
fs.readFile('./helloDir/message.txt','UTF-8' ,function (err, data) {
if (err) throw err;
console.log(data);
});
~~~
上面代码使用readFile方法读取文件。readFile方法的第一个参数是文件名,第二个参数是文件编码,第三个参数是回调函数。可用的文件编码包括“ascii”、“utf8”和“base64”。如果没有指定文件编码,返回的是原始的缓存二进制数据,这时需要调用buffer对象的toString方法,将其转为字符串。
~~~
var fs = require('fs');
fs.readFile('example_log.txt', function (err, logData) {
if (err) throw err;
var text = logData.toString();
});
~~~
readFile方法是异步操作,所以必须小心,不要同时发起多个readFile请求。
~~~
for(var i = 1; i <= 1000; i++) {
fs.readFile('./'+i+'.txt', function() {
// do something with the file
});
}
~~~
上面代码会同时发起1000个readFile异步请求,很快就会耗尽系统资源。
## mkdirSync(),writeFileSync(),readFileSync()
这三个方法是建立目录、写入文件、读取文件的同步版本。
~~~
fs.mkdirSync('./helloDirSync',0777);
fs.writeFileSync('./helloDirSync/message.txt', 'Hello Node');
var data = fs.readFileSync('./helloDirSync/message.txt','UTF-8');
console.log('file created with contents:');
console.log(data);
~~~
对于流量较大的服务器,最好还是采用异步操作,因为同步操作时,只有前一个操作结束,才会开始后一个操作,如果某个操作特别耗时(常常发生在读写数据时),会导致整个程序停顿。
## readdir()
readdir方法用于读取目录,返回一个所包含的文件和子目录的数组。
~~~
fs.readdir(process.cwd(), function (err, files) {
if (err) {
console.log(err);
return;
}
var count = files.length;
var results = {};
files.forEach(function (filename) {
fs.readFile(filename, function (data) {
results[filename] = data;
count--;
if (count <= 0) {
// 对所有文件进行处理
}
});
});
});
~~~
## stat()
stat方法的参数是一个文件或目录,它产生一个对象,该对象包含了该文件或目录的具体信息。我们往往通过该方法,判断正在处理的到底是一个文件,还是一个目录。
~~~
var fs = require('fs');
fs.readdir('/etc/', function (err, files) {
if (err) throw err;
files.forEach( function (file) {
fs.stat('/etc/' + file, function (err, stats) {
if (err) throw err;
if (stats.isFile()) {
console.log("%s is file", file);
}
else if (stats.isDirectory ()) {
console.log("%s is a directory", file);
}
console.log('stats: %s',JSON.stringify(stats));
});
});
});
~~~
## watchfile(),unwatchfile()
watchfile方法监听一个文件,如果该文件发生变化,就会自动触发回调函数。
~~~
var fs = require('fs');
fs.watchFile('./testFile.txt', function (curr, prev) {
console.log('the current mtime is: ' + curr.mtime);
console.log('the previous mtime was: ' + prev.mtime);
});
fs.writeFile('./testFile.txt', "changed", function (err) {
if (err) throw err;
console.log("file write complete");
});
~~~
unwatchfile方法用于解除对文件的监听。
## createReadStream()
createReadStream方法往往用于打开大型的文本文件,创建一个读取操作的数据流。所谓大型文本文件,指的是文本文件的体积很大,读取操作的缓存装不下,只能分成几次发送,每次发送会触发一个data事件,发送结束会触发end事件。
~~~
var fs = require('fs');
function readLines(input, func) {
var remaining = '';
input.on('data', function(data) {
remaining += data;
var index = remaining.indexOf('\n');
var last = 0;
while (index > -1) {
var line = remaining.substring(last, index);
last = index + 1;
func(line);
index = remaining.indexOf('\n', last);
}
remaining = remaining.substring(last);
});
input.on('end', function() {
if (remaining.length > 0) {
func(remaining);
}
});
}
function func(data) {
console.log('Line: ' + data);
}
var input = fs.createReadStream('lines.txt');
readLines(input, func);
~~~
## createWriteStream()
createWriteStream方法创建一个写入数据流对象,该对象的write方法用于写入数据,end方法用于结束写入操作。
~~~
var out = fs.createWriteStream(fileName, { encoding: "utf8" });
out.write(str);
out.end();
~~~
13.4 npm模块管理器
最后更新于:2022-04-01 05:09:35
[TOC]
## 简介
npm有两层含义。一层含义是Node.js的开放式模块登记和管理系统,网址为[http://npmjs.org](http://npmjs.org/)。另一层含义是Node.js默认的模块管理器,是一个命令行下的软件,用来安装和管理node模块。
npm不需要单独安装。在安装node的时候,会连带一起安装npm。但是,node附带的npm可能不是最新版本,最好用下面的命令,更新到最新版本。
~~~
$ npm install npm@latest -g
~~~
上面的命令之所以最后一个参数是npm,是因为npm本身也是Node.js的一个模块。
node安装完成后,可以用下面的命令,查看一下npm的帮助文件。
~~~
# npm命令列表
$ npm help
# 各个命令的简单用法
$ npm -l
~~~
下面的命令分别查看npm的版本和配置。
~~~
$ npm -v
$ npm config list -l
~~~
## npm info
`npm info`命令可以查看每个模块的具体信息。比如,查看underscore模块信息的命令是:
~~~
$ npm info underscore
{ name: 'underscore',
description: 'JavaScript\'s functional programming helper library.',
'dist-tags': { latest: '1.5.2', stable: '1.5.2' },
repository:
{ type: 'git',
url: 'git://github.com/jashkenas/underscore.git' },
homepage: 'http://underscorejs.org',
main: 'underscore.js',
version: '1.5.2',
devDependencies: { phantomjs: '1.9.0-1' },
licenses:
{ type: 'MIT',
url: 'https://raw.github.com/jashkenas/underscore/master/LICENSE' },
files:
[ 'underscore.js',
'underscore-min.js',
'LICENSE' ],
readmeFilename: 'README.md'}
~~~
上面命令返回一个JavaScript对象,包含了underscore模块的详细信息。这个对象的每个成员,都可以直接从info命令查询。
~~~
$ npm info underscore description
JavaScript's functional programming helper library.
$ npm info underscore homepage
http://underscorejs.org
$ npm info underscore version
1.5.2
~~~
## npm search
向npm仓库搜索某个模块,使用search命令(可使用正则搜索)。
~~~
$ npm search <搜索词>
~~~
如果不加搜索词,`npm search`默认返回npm仓库的所有模块。
## npm list
`npm list`命令列出当前目录安装的所有模块。如果使用global参数,就是列出全局安装的模块。
~~~
$ npm list
$ npm -global list
~~~
## npm install
Node模块采用`npm install`命令安装。每个模块可以“全局安装”,也可以“本地安装”。两者的差异是模块的安装位置,以及调用方法。
“全局安装”指的是将一个模块直接下载到Node的安装目录中,各个项目都可以调用。“本地安装”指的是将一个模块下载到当前目录的node_modules子目录,然后只有在当前目录和它的子目录之中,才能调用这个模块。一般来说,全局安装只适用于工具模块,比如npm和grunt。
默认情况下,`npm install`命令是“本地安装”某个模块。
~~~
$ npm install <package name>
~~~
npm也支持直接输入github地址。
~~~
$ npm install git://github.com/package/path.git
$ npm install git://github.com/package/path.git#0.1.0
~~~
运行上面命令后,模块文件将下载到当前目录的`node_modules`子目录。
使用global参数,可以“全局安装”某个模块。global参数可以被简化成g参数。
~~~
$ sudo npm install -global [package name]
$ sudo npm install -g [package name]
~~~
install命令总是安装模块的最新版本,如果要安装模块的特定版本,可以在模块名后面加上@和版本号。
~~~
$ npm install sax@latest
$ npm install sax@0.1.1
$ npm install sax@">=0.1.0 <0.2.0"
~~~
install命令可以使用不同参数,指定所安装的模块属于哪一种性质的依赖关系,即出现在packages.json文件的哪一项中。
* --save:模块名将被添加到dependencies,可以简化为参数`-S`。
* --save-dev: 模块名将被添加到devDependencies,可以简化为参数`-D`。
~~~
$ npm install sax --save
$ npm install node-tap --save-dev
# 或者
$ npm install sax -S
$ npm install node-tap -D
~~~
`npm install`默认会安装dependencies字段和devDependencies字段中的所有模块,如果使用production参数,可以只安装dependencies字段的模块。
~~~
$ npm install --production
# 或者
$ NODE_ENV=production npm install
~~~
一旦安装了某个模块,就可以在代码中用require命令调用这个模块。
~~~
var backbone = require('backbone')
console.log(backbone.VERSION)
~~~
## 语义版本(SemVer)
npm采用”语义版本“管理软件包。所谓语义版本,就是指版本号为`a.b.c`的形式,其中a是大版本号,b是小版本号,c是补丁号。
一个软件发布的时候,默认就是 1.0.0 版。如果以后发布补丁,就增加最后一位数字,比如1.0.1;如果增加新功能,且不影响原有的功能,就增加中间的数字(即小版本号),比如1.1.0;如果引入的变化,破坏了向后兼容性,就增加第一位数字(即大版本号),比如2.0.0。
npm允许使用特殊符号,指定所要使用的版本范围,假定当前版本是1.0.4。
* 只接受补丁包:1.0 或者 1.0.x 或者 ~1.0.4
* 只接受小版本和补丁包:1 或者 1.x 或者 ^1.0.4
* 接受所有更新:* or x
对于~和^,要注意区分。前者表示接受当前小版本(如果省略小版本号,则是当前大版本)的最新补丁包,后者表示接受当前大版本的最新小版本和最新补丁包。
~~~
~2.2.1 // 接受2.2.1,不接受2.3.0
^2.2.1 // 接受2.2.1和2.3.0
~2.2 // 接受2.2.0和2.2.1,不接受2.3.0
^2.2 // 接受2.2.0、2.2.1和2.3.0
~2 // 接受2.0.0、2.1.0、2.2.0、2.2.1和2.3.0
^2 // 接受2.0.0、2.1.0、2.2.0、2.2.1和2.3.0
~~~
还可以使用数学运算符(比如>, = or <=等),指定版本范围。
~~~
>2.1
1.0.0 - 1.2.0
>1.0.0-alpha
>=1.0.0-rc.0 <1.0.1
^2 <2.2 || > 2.3
~~~
注意,如果使用连字号,它的两端必须有空格。如果不带空格,会被npm理解成预发布的tag,比如1.0.0-rc.1。
## npm update,npm uninstall
npm update 命令可以升级本地安装的模块。
~~~
npm update [package name]
~~~
加上global参数,可以升级全局安装的模块。
~~~
npm update -global [package name]
~~~
npm uninstall 命令,删除本地安装的模块。
~~~
npm uninstall [package name]
~~~
加上global参数,可以删除全局安装的模块。
~~~
sudo npm uninstall [package name] -global
~~~
## npm shrinkwrap
对于一个项目来说,通常不会写死依赖的npm模块的版本。比如,开发时使用某个模块的版本是1.0,那么等到用户安装时,如果该模块升级到1.1,往往就会安装1.1。
但是,对于开发者来说,有时最好锁定所有依赖的版本,防止模块升级带来意想不到的问题。但是,由于模块自己还依赖别的模块,这一点往往很难做到。举例来说,项目依赖A模块,A模块依赖B模块。即使写死A模块的版本,但是B模块升级依然可能导致不兼容。
`npm shrinkwrap`命令就是用来彻底锁定所有模块的版本。
~~~
$ npm shrinkwrap
~~~
运行上面这个命令以后,会在项目目录下生成一个npm-shrinkwrap.json文件,里面包含当前项目用到的所有依赖(包括依赖的依赖,以此类推),以及它们的准确版本,也就是当前正在使用的版本。
只要存在`npm-shrinkwrap.json`文件,下一次用户使用`npm install`命令安装依赖的时候,就会安装所有版本完全相同的模块。
如果执行`npm shrinkwrap`的时候,加上参数dev,还可以记录devDependencies字段中模块的准确版本。
~~~
$ npm shrinkwrap --dev
~~~
## npm prune
`npm prune`命令与`npm shrinkwrap`配套使用。
使用`npm shrinkwrap`的时候,有时可能存在某个已经安装的模块不在dependencies字段内的情况,这时`npm shrinkwrap` 就会报错。
`npm prune`命令可以移除所有不在dependencies字段内的模块。所有指定模块名,则移除单个模块。
~~~
$ npm prune
$ npm package <package name>
~~~
## npm run
package.json文件有一项scripts,用于指定脚本命令,供npm直接调用。
~~~
{
"name": "myproject",
"devDependencies": {
"jshint": "latest",
"browserify": "latest",
"mocha": "latest"
},
"scripts": {
"lint": "jshint **.js",
"test": "mocha test/"
}
}
~~~
上面代码中,scripts指定了两项命令lint和test。命令行输入`npm run lint`,就会执行`jshint **.js`,输入`npm run test`,就会执行`mocha test/`。npm内置了两个命令简写,`npm test`等同于执行`npm run lint`,`npm start`等同于执行`npm run start`。
`npm run`会创建一个shell,执行指定的命令,并将`node_modules/.bin`加入PATH变量,这意味着本地模块可以直接运行。也就是说,`npm run lint`直接运行`jshint **.js`即可,而不用`./node_modules/.bin/jshint **.js`。
如果直接运行`npm run`不给出任何参数,就会列出scripts属性下所有命令。
~~~
Available scripts in the user-service package:
lint
jshint **.js
test
mocha test/
~~~
下面是另一个package.json文件的例子。
~~~
"scripts": {
"watch": "watchify client/main.js -o public/app.js -v",
"build": "browserify client/main.js -o public/app.js",
"start": "npm run watch & nodemon server.js",
"test": "node test/all.js"
},
~~~
上面代码在scripts项,定义了四个别名,每个别名都有对应的脚本命令。
~~~
npm run watch
npm run build
npm run start
npm run test
~~~
其中,start和test属于特殊命令,可以省略run。
~~~
npm start
npm test
~~~
如果希望一个操作的输出,是另一个操作的输入,可以借用Linux系统的管道命令,将两个操作连在一起。
~~~
"build-js": "browserify browser/main.js | uglifyjs -mc > static/bundle.js"
~~~
但是,更方便的写法是引用其他`npm run`命令。
~~~
"build": "npm run build-js && npm run build-css"
~~~
上面的写法是先运行`npm run build-js`,然后再运行`npm run build-css`,两个命令中间用`&&`连接。如果希望两个命令同时平行执行,它们中间可以用`&`连接。
下面是一个流操作的例子。
~~~
"devDependencies": {
"autoprefixer": "latest",
"cssmin": "latest"
},
"scripts": {
"build:css": "autoprefixer -b 'last 2 versions' < assets/styles/main.css | cssmin > dist/main.css"
}
~~~
写在scripts属性中的命令,也可以在`node_modules/.bin`目录中直接写成bash脚本。
~~~
#!/bin/bash
cd site/main;
browserify browser/main.js | uglifyjs -mc > static/bundle.js
~~~
假定上面的脚本文件名为build.sh,并且权限为可执行,就可以在scripts属性中引用该文件。
~~~
"build-js": "bin/build.sh"
~~~
`npm run`为每条命令提供了pre和post两个钩子(hook)。以`npm run lint`为例,执行这条命令之前,npm会先查看有没有定义prelint和postlint两个钩子,如果有的话,就会先执行`npm run prelint`,然后执行`npm run lint`,最后执行`npm run postlint`。所有命令都是这样,包括`npm test`(即实际存在`npm run pretest`、`npm run test`、`npm run posttest`三条命令)。如果执行过程出错,就不会执行排在后面的命令,即如果pretest命令执行出错,就不会接着执行 test和posttest命令。不能在pre命令之前再加pre,即prepretest命令不起作用。另外,还可以为一些内部命令指定pre和post的钩子:install、uninstall、publish、update。
~~~
"scripts": {
"lint": "jshint **.js",
"build": "browserify index.js > myproject.min.js",
"test": "mocha test/",
"prepublish": "npm run build # also runs npm run prebuild",
"prebuild": "npm run test # also runs npm run pretest",
"pretest": "npm run lint"
}
~~~
`npm run`命令还可以添加参数。
~~~
"scripts": {
"test": "mocha test/"
}
~~~
上面代码指定`npm test`,实际运行`mocha test/`。可以在`npm test`命令后面加上参数,比如`npm run test -- anothertest.js`,实际运行的是`mocha test/ anothertest.js`。
## npm link
一般来说,每个项目都会在项目目录内,安装所需的模块文件。也就是说,各个模块是局部安装。但是有时候,我们希望模块是一个符号链接,连到外部文件,这时候就需要用到npm link命令。
为了理解npm link,请设想这样一个场景。你开发了一个模块myModule,目录为src/myModule,你自己的项目myProject要用到这个模块,项目目录为src/myProject。每一次,你更新myModule,就要用`npm publish`命令发布,然后切换到项目目录,使用`npm update`更新模块。这样显然很不方便,如果我们可以从项目目录建立一个符号链接,直接连到模块目录,就省去了中间步骤,项目可以直接使用最新版的模块。
先在模块目录(src/myModule)下运行npm link命令。
~~~
src/myModule$ npm link
~~~
上面的命令会在npm的全局模块目录内(比如/usr/local/lib/node_modules/),生成一个符号链接文件,该文件的名字就是package.json文件中指定的文件名。
~~~
/usr/local/lib/node_modules/myModule -> src/myModule
~~~
然后,切换到你需要放置该模块的项目目录,再次运行npm link命令,并指定模块名。
~~~
src/myProject$ npm link myModule
~~~
上面命令等同于生成了本地模块的符号链接。
~~~
src/myProject/node_modules/myModule -> /usr/local/lib/node_modules/myModule
~~~
然后,就可以在你的项目中,加载该模块了。
~~~
var myModule = require('myModule');
~~~
这样一来,myModule的任何变化,都可以直接在myProject中调用。但是,同时也出现了风险,任何在myProject目录中对myModule的修改,都会反映到模块的源码中。
npm link命令有一个简写形式,显示连接模块的本地目录。
~~~
$ src/myProject$ npm link ../myModule
~~~
上面的命令等同于下面几条命令。
~~~
$ src/myProject$ cd ../myModule
$ src/myModule$ npm link
$ src/myModule$ cd ../myProject
$ src/myProject$ npm link myModule
~~~
如果你的项目不再需要该模块,可以在项目目录内使用npm unlink命令,删除符号链接。
~~~
src/myProject$ npm unlink myModule
~~~
一般来说,npm公共模块都安装在系统目录(比如/usr/local/lib/),普通用户没有写入权限,需要用到sudo命令。这不是很方便,我们可以在没有root的情况下,用好npm。
首先在主目录下新建配置文件.npmrc,然后在该文件中将prefix变量定义到主目录下面。
~~~
prefix = /home/yourUsername/npm
~~~
然后在主目录下新建npm子目录。
~~~
$ mkdir ~/npm
~~~
此后,全局安装的模块都会安装在这个子目录中,npm也会到`~/npm/bin`目录去寻找命令。因此,`npm link`就不再需要 root权限了。
最后,将这个路径在.bash_profile文件(或.bashrc文件)中加入PATH变量。
~~~
export PATH=~/npm/bin:$PATH
~~~
## npm publish
在发布你的模块之前,需要先设定个人信息。
~~~
$ npm set init.author.name “张三”
$ npm set init.author.email “zhangsan@email.com”
$ npm set init.author.url “http://your.url.com"
~~~
这些信息会存放在用户主目录的`~/.npmrc`文件,使得用户不用每个项目都输入。如果遇到某个模块的作者信息,与全局设置不同,可以在项目目录中,单独设置一次上面这些命令。
然后,向npm系统申请用户名。
~~~
$ npm adduser
~~~
运行上面的命令之后,屏幕上会提示输入用户名,然后是输入Email地址和密码。
如果已经注册过,就使用下面的命令登录。
~~~
$ npm login
~~~
最后,使用npm publish命令发布。
~~~
$ npm publish
~~~
## npm version
`npm version`命令用来修改项目的版本号。当你完成代码修改,要发布新版本的时候,就用这个命令更新一下软件的版本。
~~~
$ npm version <update_type> -m "<message>"
~~~
`npm version`命令的update_type参数有三个选项:patch、minor、major。
* `npm version patch`增加一位补丁号(比如 1.1.1 -> 1.1.2)
* `npm version minor`增加一位小版本号(比如 1.1.1 -> 1.2.0)
* `npm version major`增加一位大版本号(比如 1.1.1 -> 2.0.0)。
下面是一个例子。
~~~
$ npm version patch -m "Version %s - xxxxxx"
~~~
上面命令的%s表示新的版本号。
除了增加版本号,`npm version`命令还会为这次修改,新增一个git commit记录,以及一个新的git tag。
由于更新npm网站的唯一方法,就是发布一个新的版本。因此,除了第一次发布,这个命令与`npm publish`几乎是配套的,先使用它,再使用`npm publish`。
## 参考链接
* James Halliday, [task automation with npm run](http://substack.net/task_automation_with_npm_run): npm run命令(package.json文件的script属性)的用法
* Keith Cirkel, [How to Use npm as a Build Tool](http://blog.keithcirkel.co.uk/how-to-use-npm-as-a-build-tool/)
* justjs, [npm link: developing your own npm modules without tears](http://justjs.com/posts/npm-link-developing-your-own-npm-modules-without-tears)
13.3 package.json文件
最后更新于:2022-04-01 05:09:33
[TOC]
## 概述
每个项目的根目录下面,一般都有一个package.json文件,定义了这个项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)。npm install 命令根据这个配置文件,自动下载所需的模块,也就是配置项目所需的运行和开发环境。
下面是一个最简单的package.json文件,只定义两项元数据:项目名称和项目版本。
~~~
{
"name" : "xxx",
"version" : "0.0.0",
}
~~~
上面代码说明,package.json文件内部就是一个json对象,该对象的每一个成员就是当前项目的一项设置。比如name就是项目名称,version是版本(遵守“大版本.次要版本.小版本”的格式)。
下面是一个更完整的package.json文件。
~~~
{
"name": "Hello World",
"version": "0.0.1",
"author": "张三",
"description": "第一个node.js程序",
"keywords":["node.js","javascript"],
"repository": {
"type": "git",
"url": "https://path/to/url"
},
"license":"MIT",
"engines": {"node": "0.10.x"},
"bugs":{"url":"http://path/to/bug","email":"bug@example.com"},
"contributors":[{"name":"李四","email":"lisi@example.com"}],
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "latest",
"mongoose": "~3.8.3",
"handlebars-runtime": "~1.0.12",
"express3-handlebars": "~0.5.0",
"MD5": "~1.2.0"
},
"devDependencies": {
"bower": "~1.2.8",
"grunt": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-jshint": "~0.7.2",
"grunt-contrib-uglify": "~0.2.7",
"grunt-contrib-clean": "~0.5.0",
"browserify": "2.36.1",
"grunt-browserify": "~1.3.0",
}
}
~~~
下面详细解释package.json文件的成员。
## scripts
scripts指定了运行脚本命令的npm命令行缩写,比如start指定了运行npm run start时,所要执行的命令。
下面的设置指定了npm run preinstall、npm run postinstall、npm run start、npm run test时,所要执行的命令。
~~~
"scripts": {
"preinstall": "echo here it comes!",
"postinstall": "echo there it goes!",
"start": "node index.js",
"test": "tap test/*.js"
}
~~~
## dependencies,devDependencies
dependencies和devDependencies两项,分别指定了项目运行所依赖的模块、项目开发所需要的模块。
dependencies和devDependencies这两项,都指向一个对象。该对象的各个成员,分别由模块名和对应的版本要求组成。对应的版本可以加上各种限定,主要有以下几种:
* 指定版本:比如1.2.2,遵循“大版本.次要版本.小版本”的格式规定,安装时只安装指定版本。
* 波浪号(tilde)+指定版本:比如~1.2.2,表示安装1.2.x的最新版本(不低于1.2.2),但是不安装1.3.x,也就是说安装时不改变大版本号和次要版本号。
* 插入号(caret)+指定版本:比如ˆ1.2.2,表示安装1.x.x的最新版本(不低于1.2.2),但是不安装2.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
* latest:安装最新版本。
package.json文件可以手工编写,也可以使用npm init命令自动生成。
~~~
npm init
~~~
这个命令采用互动方式,要求用户回答一些问题,然后在当前目录生成一个基本的package.json文件。所有问题之中,只有项目名称(name)和项目版本(version)是必填的,其他都是选填的。
有了package.json文件,直接使用npm install命令,就会在当前目录中安装所需要的模块。
~~~
npm install
~~~
如果一个模块不在package.json文件之中,可以单独安装这个模块,并使用相应的参数,将其写入package.json文件之中。
~~~
npm install express --save
npm install express --save-dev
~~~
上面代码表示单独安装express模块,--save参数表示将该模块写入dependencies属性,--save-dev表示将该模块写入devDependencies属性。
## bin
bin项用来指定各个内部命令对应的可执行文件的位置。
~~~
"bin": {
"someTool": "./bin/someTool.js"
}
~~~
上面代码指定,someTool 命令对应的可执行文件为 bin 子目录下的 someTool.js。Npm会寻找这个文件,在`node_modules/.bin/`目录下建立符号链接。在上面的例子中,someTool.js会建立符号链接`npm_modules/.bin/someTool`。由于`node_modules/.bin/`目录会在运行时加入系统的PATH变量,因此在运行npm时,就可以不带路径,直接通过命令来调用这些脚本。
因此,像下面这样的写法可以采用简写。
~~~
scripts: {
start: './node_modules/someTool/someTool.js build'
}
// 简写为
scripts: {
start: 'someTool build'
}
~~~
所有`node_modules/.bin/`目录下的命令,都可以用`npm run [命令]`的格式运行。在命令行下,键入`npm run`,然后按tab键,就会显示所有可以使用的命令。
## 其他
### browser
browser指定该模板供浏览器使用的版本。Browserify这样的浏览器打包工具,通过它就知道该打包那个文件。
~~~
"browser": {
"tipso": "./node_modules/tipso/src/tipso.js"
},
~~~
### engines
engines指明了该项目所需要的node.js版本。
### man
man用来指定当前模块的man文档的位置。
~~~
"man" :[ "./doc/calc.1" ]
~~~
### preferGlobal
preferGlobal的值是布尔值,表示当用户不将该模块安装为全局模块时(即不用--global参数),要不要显示警告,表示该模块的本意就是安装为全局模块。
### style
style指定供浏览器使用时,样式文件所在的位置。样式文件打包工具parcelify,通过它知道样式文件的打包位置。
~~~
"style": [
"./node_modules/tipso/src/tipso.css"
]
~~~
13.2 CommonJS规范
最后更新于:2022-04-01 05:09:31
[TOC]
## 概述
CommonJS是服务器模块的规范,Node.js采用了这个规范。
根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在一个文件定义的变量(还包括函数和类),都是私有的,对其他文件是不可见的。
~~~
var x = 5;
var addX = function(value) {
return value + x;
};
~~~
上面代码中,变量x和函数addX,是当前文件私有的,其他文件不可见。
如果想在多个文件分享变量,必须定义为global对象的属性。
~~~
global.warning = true;
~~~
上面代码的waining变量,可以被所有文件读取。当然,这样写法是不推荐的。
CommonJS规定,每个文件的对外接口是module.exports对象。这个对象的所有属性和方法,都可以被其他文件导入。
~~~
var x = 5;
var addX = function(value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
~~~
上面代码通过module.exports对象,定义对外接口,输出变量x和函数addX。module.exports对象是可以被其他文件导入的,它其实就是文件内部与外部通信的桥梁。
require方法用于在其他文件加载这个接口,具体用法参见《Require命令》的部分。
~~~
var example = require('./example.js');
console.log(example.x); // 5
console.log(addX(1)); // 6
~~~
## module对象
每个模块都有一个module变量,该变量指向当前模块。module不是全局变量,而是每个模块都有的本地变量。
* module.id 模块的识别符,通常是带有绝对路径的模块文件名。
* module.filename 模块的文件名。
* module.loaded 返回一个布尔值,表示模块是否已经完成加载。
* module.parent 返回一个对象,表示调用该模块的模块。
* module.children 返回一个数组,表示该模块要用到的其他模块。
下面是一个示例文件,最后一行输出module变量。
~~~
// example.js
var jquery = require('jquery');
exports.$ = jquery;
console.log(module);
~~~
执行这个文件,命令行会输出如下信息。
~~~
{ id: '.',
exports: { '/div>: [Function] },
parent: null,
filename: '/path/to/example.js',
loaded: false,
children:
[ { id: '/path/to/node_modules/jquery/dist/jquery.js',
exports: [Function],
parent: [Circular],
filename: '/path/to/node_modules/jquery/dist/jquery.js',
loaded: true,
children: [],
paths: [Object] } ],
paths:
[ '/home/user/deleted/node_modules',
'/home/user/node_modules',
'/home/node_modules',
'/node_modules' ]
}
~~~
### module.exports属性
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
~~~
var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
setTimeout(function() {
module.exports.emit('ready');
}, 1000);
~~~
上面模块会在加载后1秒后,发出ready事件。其他文件监听该事件,可以写成下面这样。
~~~
var a = require('./a');
a.on('ready', function() {
console.log('module a is ready');
});
~~~
### exports变量
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
~~~
var exports = module.exports;
~~~
造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。
~~~
exports.area = function (r) {
return Math.PI * r * r;
};
exports.circumference = function (r) {
return 2 * Math.PI * r;
};
~~~
注意,不能直接将exports变量指向一个函数。因为这样等于切断了exports与module.exports的联系。
~~~
exports = function (x){ console.log(x);};
~~~
上面这样的写法是无效的,因为它切断了exports与module.exports之间的链接。
下面的写法也是无效的。
~~~
exports.hello = function() {
return 'hello';
};
module.exports = 'Hello world';
~~~
上面代码中,hello函数是无法对外输出的,因为`module.exports`被重新赋值了。
如果一个模块的对外接口,就是一个函数或对象时,不能使用exports输出,只能使用module.exports输出。
~~~
module.exports = function (x){ console.log(x);};
~~~
如果你觉得,exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。
## AMD规范与CommonJS规范的兼容性
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
AMD规范使用define方法定义模块,下面就是一个例子:
~~~
define(['package/lib'], function(lib){
function foo(){
lib.log('hello world!');
}
return {
foo: foo
};
});
~~~
AMD规范允许输出的模块兼容CommonJS规范,这时define方法需要写成下面这样:
~~~
define(function (require, exports, module){
var someModule = require("someModule");
var anotherModule = require("anotherModule");
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
exports.asplode = function (){
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};
});
~~~
## require命令
### 基本用法
Node.js使用CommonJS模块规范,内置的require命令用于加载模块文件。
require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
~~~
// example.js
var invisible = function () {
console.log("invisible");
}
exports.message = "hi";
exports.say = function () {
console.log(message);
}
~~~
运行下面的命令,可以输出exports对象。
~~~
var example = require('./example.js');
example
// {
// message: "hi",
// say: [Function]
// }
~~~
如果模块输出的是一个函数,那就不能定义在exports对象上面,而要定义在`module.exports`变量上面。
~~~
module.exports = function () {
console.log("hello world")
}
require('./example2.js')()
~~~
上面代码中,require命令调用自身,等于是执行`module.exports`,因此会输出 hello world。
### 加载规则
require命令接受模块名作为参数。
(1)如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,`require('/home/marco/foo.js')`将加载/home/marco/foo.js。
(2)如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,`require('./circle')`将加载当前脚本同一目录的circle.js。
(3)如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
举例来说,脚本`/home/user/projects/foo.js`执行了`require('bar.js')`命令,Node会依次搜索以下文件。
* /home/user/projects/node_modules/bar.js
* /home/user/node_modules/bar.js
* /home/node_modules/bar.js
* /node_modules/bar.js
这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。
(4)如果传入require方法的是一个目录,那么require会先查看该目录的package.json文件,然后加载main字段指定的脚本文件。否则取不到main字段,则会加载`index.js`文件或`index.node`文件。
举例来说,下面是一行普通的require命令语句。
~~~
var utils = require( "utils" );
~~~
Node寻找utils脚本的顺序是,首先寻找核心模块,然后是全局安装模块,接着是项目安装的模块。
~~~
[
'/usr/local/lib/node',
'~/.node_modules',
'./node_modules/utils.js',
'./node_modules/utils/package.json',
'./node_modules/utils/index.js'
]
~~~
(5)如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js文件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会议编译后二进制文件解析。
(6)如果想得到require命令加载的确切文件名,使用require.resolve()方法。
### 模块的缓存
第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的exports属性。
~~~
require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// "hello"
~~~
上面代码中,连续三次使用require命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个message属性。但是第三次加载的时候,这个message属性依然存在,这就证明require命令并没有重新加载模块文件,而是输出了缓存。
如果想要多次执行某个模块,可以输出一个函数,然后多次调用这个函数。
缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require命令还是会重新加载该模块。
### 模块的循环加载
如果发生模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本。
~~~
// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';
// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
~~~
上面代码是三个JavaScript文件。其中,a.js加载了b.js,而b.js又加载a.js。这时,Node返回a.js的不完整版本,所以执行结果如下。
~~~
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
~~~
修改main.js,再次加载a.js和b.js。
~~~
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
~~~
执行上面代码,结果如下。
~~~
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
main.js a2
main.js b2
~~~
上面代码中,第二次加载a.js和b.js时,会直接从缓存读取exports属性,所以a.js和b.js内部的console.log语句都不会执行了。
### require.main
正常的脚本调用时,require.main属性指向模块本身。
~~~
require.main === module
// true
~~~
如果是在REPL环境使用require命令,则上面的表达式返回false。
通过require.main属性,可以获取模块的信息。比如,module对象有一个filename属性(正常情况下等于 __filename),可以通过require.main.filename属性,得知当前模块的入口文件。
## 参考链接
* Addy Osmani, [Writing Modular JavaScript With AMD, CommonJS & ES Harmony](http://addyosmani.com/writing-modular-js/)
* Pony Foo, [A Gentle Browserify Walkthrough](http://blog.ponyfoo.com/2014/08/25/a-gentle-browserify-walkthrough)
* Nico Reed, [What is require?]([https://docs.nodejitsu.com/articles/getting-started/what-is-require)](https://docs.nodejitsu.com/articles/getting-started/what-is-require%EF%BC%89)
13.1 Node.js 概述
最后更新于:2022-04-01 05:09:28
[TOC]
## 简介
Node是JavaScript语言的服务器运行环境。所谓“运行环境”有两层意思:首先,JavaScript语言通过Node在服务器运行,在这个意义上,Node有点像JavaScript虚拟机;其次,Node提供大量工具库,使得JavaScript语言与操作系统互动(比如读写文件、新建子进程),在这个意义上,Node又是JavaScript的工具库。
Node内部采用Google公司的V8引擎,作为JavaScript语言解释器;通过自行开发的libuv库,调用操作系统资源。
### 安装与更新
访问官方网站[nodejs.org](http://nodejs.org/)了解安装细节。
安装完成以后,运行下面的命令,查看是否能正常运行。
~~~
$ node --version
# 或者
$ node -v
~~~
更新node.js版本,可以通过node.js的n模块完成。
~~~
$ sudo npm install n -g
$ sudo n stable
~~~
上面代码通过n模块,将node.js更新为最新发布的稳定版。
n模块也可以指定安装特定版本的node。
~~~
$ sudo n 0.10.21
~~~
### 版本管理工具nvm
如果想在同一台机器,同时安装多个版本的node.js,就需要用到版本管理工具nvm。
~~~
$ git clone https://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh
~~~
安装以后,nvm的执行脚本,每次使用前都要激活,建议将其加入~/.bashrc文件(假定使用Bash)。激活后,就可以安装指定版本的Node。
~~~
# 安装最新版本
$ nvm install node
# 安装指定版本
$ nvm install 0.12.1
# 使用已安装的最新版本
$ nvm use node
# 使用指定版本的node
$ nvm use 0.12
~~~
nvm也允许进入指定版本的REPL环境。
~~~
$ nvm run 0.12
~~~
如果在项目根目录下新建一个.nvmrc文件,将版本号写入其中,就只输入`nvm use`命令即可,不再需要附加版本号。
下面是其他经常用到的命令。
~~~
# 查看本地安装的所有版本
$ nvm ls
# 查看服务器上所有可供安装的版本。
$ nvm ls-remote
# 退出已经激活的nvm,使用deactivate命令。
$ nvm deactivate
~~~
### 基本用法
安装完成后,运行node.js程序,就是使用node命令读取JavaScript脚本。
假定当前目录有一个demo.js的脚本文件,运行时这样写。
~~~
node demo
// 或者
node demo.js
~~~
### REPL环境
在命令行键入node命令,后面没有文件名,就进入一个Node.js的REPL环境(Read–eval–print loop,"读取-求值-输出"循环),可以直接运行各种JavaScript命令。
~~~
$ node
> 1+1
2
>
~~~
如果使用参数 --use_strict,则REPL将在严格模式下运行。
~~~
$ node --use_strict
~~~
REPL是Node.js与用户互动的shell,各种基本的shell功能都可以在里面使用,比如使用上下方向键遍历曾经使用过的命令。
特殊变量下划线(_)表示上一个命令的返回结果。
~~~
> 1+1
2
> _+1
3
~~~
在REPL中,如果运行一个表达式,会直接在命令行返回结果。如果运行一条语句,就不会有任何输出,因为语句没有返回值。
~~~
> x = 1
1
> var x = 1
~~~
上面代码的第二条命令,没有显示任何结果。因为这是一条语句,不是表达式,所以没有返回值。
### 异步操作
Node采用V8引擎处理JavaScript脚本,最大特点就是单线程运行,一次只能运行一个任务。这导致Node大量采用异步操作(asynchronous opertion),即任务不是马上执行,而是插在任务队列的尾部,等到前面的任务运行完后再执行。
由于这种特性,某一个任务的后续操作,往往采用回调函数(callback)的形式进行定义。
~~~
var isTrue = function(value, callback) {
if (value === true) {
callback(null, "Value was true.");
}
else {
callback(new Error("Value is not true!"));
}
}
~~~
上面代码就把进一步的处理,交给回调函数callback。
Node约定,如果某个函数需要回调函数作为参数,则回调函数是最后一个参数。另外,回调函数本身的第一个参数,约定为上一步传入的错误对象。
~~~
var callback = function (error, value) {
if (error) {
return console.log(error);
}
console.log(value);
}
~~~
上面代码中,callback的第一个参数是Error对象,第二个参数才是真正的数据参数。这是因为回调函数主要用于异步操作,当回调函数运行时,前期的操作早结束了,错误的执行栈早就不存在了,传统的错误捕捉机制try...catch对于异步操作行不通,所以只能把错误交给回调函数处理。
~~~
try {
db.User.get(userId, function(err, user) {
if(err) {
throw err
}
// ...
})
} catch(e) {
console.log(‘Oh no!’);
}
~~~
上面代码中,db.User.get方法是一个异步操作,等到抛出错误时,可能它所在的try...catch代码块早就运行结束了,这会导致错误无法被捕捉。所以,Node统一规定,一旦异步操作发生错误,就把错误对象传递到回调函数。
如果没有发生错误,回调函数的第一个参数就传入null。这种写法有一个很大的好处,就是说只要判断回调函数的第一个参数,就知道有没有出错,如果不是null,就肯定出错了。另外,这样还可以层层传递错误。
~~~
if(err) {
// 除了放过No Permission错误意外,其他错误传给下一个回调函数
if(!err.noPermission) {
return next(err);
}
}
~~~
### 全局对象和全局变量
Node提供以下几个全局对象,它们是所有模块都可以调用的。
* global:表示Node所在的全局环境,类似于浏览器的window对象。需要注意的是,如果在浏览器中声明一个全局变量,实际上是声明了一个全局对象的属性,比如`var x = 1`等同于设置`window.x = 1`,但是Node不是这样,至少在模块中不是这样(REPL环境的行为与浏览器一致)。在模块文件中,声明`var x = 1`,该变量不是`global`对象的属性,`global.x`等于undefined。这是因为模块的全局变量都是该模块私有的,其他模块无法取到。
* process:该对象表示Node所处的当前进程,允许开发者与该进程互动。
* console:指向Node内置的console模块,提供命令行环境中的标准输入、标准输出功能。
Node还提供一些全局函数。
* setTimeout():用于在指定毫秒之后,运行回调函数。实际的调用间隔,还取决于系统因素。间隔的毫秒数在1毫秒到2,147,483,647毫秒(约24.8天)之间。如果超过这个范围,会被自动改为1毫秒。该方法返回一个整数,代表这个新建定时器的编号。
* clearTimeout():用于终止一个setTimeout方法新建的定时器。
* setInterval():用于每隔一定毫秒调用回调函数。由于系统因素,可能无法保证每次调用之间正好间隔指定的毫秒数,但只会多于这个间隔,而不会少于它。指定的毫秒数必须是1到2,147,483,647(大约24.8天)之间的整数,如果超过这个范围,会被自动改为1毫秒。该方法返回一个整数,代表这个新建定时器的编号。
* clearInterval():终止一个用setInterval方法新建的定时器。
* require():用于加载模块。
* Buffer():用于操作二进制数据。
Node提供两个全局变量,都以两个下划线开头。
* _filename:指向当前运行的脚本文件名。
* _dirname:指向当前运行的脚本所在的目录。
除此之外,还有一些对象实际上是模块内部的局部变量,指向的对象根据模块不同而不同,但是所有模块都适用,可以看作是伪全局变量,主要为module, module.exports, exports等。
## 模块化结构
### 概述
Node.js采用模块化结构,按照[CommonJS规范](http://wiki.commonjs.org/wiki/CommonJS)定义和使用模块。模块与文件是一一对应关系,即加载一个模块,实际上就是加载对应的一个模块文件。
require命令用于指定加载模块,加载时可以省略脚本文件的后缀名。
~~~
var circle = require('./circle.js');
// 或者
var circle = require('./circle');
~~~
require方法的参数是模块文件的名字。它分成两种情况,第一种情况是参数中含有文件路径(比如上例),这时路径是相对于当前脚本所在的目录,第二种情况是参数中不含有文件路径,这时Node到模块的安装目录,去寻找已安装的模块(比如下例)。
~~~
var bar = require('bar');
~~~
有时候,一个模块本身就是一个目录,目录中包含多个文件。这时候,Node在package.json文件中,寻找main属性所指明的模块入口文件。
~~~
{
"name" : "bar",
"main" : "./lib/bar.js"
}
~~~
上面代码中,模块的启动文件为lib子目录下的bar.js。当使用`require('bar')`命令加载该模块时,实际上加载的是`./node_modules/bar/lib/bar.js`文件。下面写法会起到同样效果。
~~~
var bar = require('bar/lib/bar.js')
~~~
如果模块目录中没有package.json文件,node.js会尝试在模块目录中寻找index.js或index.node文件进行加载。
模块一旦被加载以后,就会被系统缓存。如果第二次还加载该模块,则会返回缓存中的版本,这意味着模块实际上只会执行一次。如果希望模块执行多次,则可以让模块返回一个函数,然后多次调用该函数。
### 核心模块
如果只是在服务器运行JavaScript代码,用处并不大,因为服务器脚本语言已经有很多种了。Node.js的用处在于,它本身还提供了一系列功能模块,与操作系统互动。这些核心的功能模块,不用安装就可以使用,下面是它们的清单。
* http:提供HTTP服务器功能。
* url:解析URL。
* fs:与文件系统交互。
* querystring:解析URL的查询字符串。
* child_process:新建子进程。
* util:提供一系列实用小工具。
* path:处理文件路径。
* crypto:提供加密和解密功能,基本上是对OpenSSL的包装。
上面这些核心模块,源码都在Node的lib子目录中。为了提高运行速度,它们安装时都会被编译成二进制文件。
核心模块总是最优先加载的。如果你自己写了一个HTTP模块,`require('http')`加载的还是核心模块。
### 自定义模块
Node模块采用CommonJS规范。只要符合这个规范,就可以自定义模块。
下面是一个最简单的模块,假定新建一个foo.js文件,写入以下内容。
~~~
// foo.js
module.exports = function(x) {
console.log(x);
};
~~~
上面代码就是一个模块,它通过module.exports变量,对外输出一个方法。
这个模块的使用方法如下。
~~~
// index.js
var m = require('./foo');
m("这是自定义模块");
~~~
上面代码通过require命令加载模块文件foo.js(后缀名省略),将模块的对外接口输出到变量m,然后调用m。这时,在命令行下运行index.js,屏幕上就会输出“这是自定义模块”。
~~~
$ node index
这是自定义模块
~~~
module变量是整个模块文件的顶层变量,它的exports属性就是模块向外输出的接口。如果直接输出一个函数(就像上面的foo.js),那么调用模块就是调用一个函数。但是,模块也可以输出一个对象。下面对foo.js进行改写。
~~~
// foo.js
var out = new Object();
function p(string) {
console.log(string);
}
out.print = p;
module.exports = out;
~~~
上面的代码表示模块输出out对象,该对象有一个print属性,指向一个函数。下面是这个模块的使用方法。
~~~
// index.js
var m = require('./foo');
m.print("这是自定义模块");
~~~
上面代码表示,由于具体的方法定义在模块的print属性上,所以必须显式调用print属性。
## 异常处理
Node是单线程运行环境,一旦抛出的异常没有被捕获,就会引起整个进程的崩溃。所以,Node的异常处理对于保证系统的稳定运行非常重要。
一般来说,Node有三种方法,传播一个错误。
* 使用throw语句抛出一个错误对象,即抛出异常。
* 将错误对象传递给回调函数,由回调函数负责发出错误。
* 通过EventEmitter接口,发出一个error事件。
### try...catch结构
最常用的捕获异常的方式,就是使用try...catch结构。但是,这个结构无法捕获异步运行的代码抛出的异常。
~~~
try {
process.nextTick(function () {
throw new Error("error");
});
} catch (err) {
//can not catch it
console.log(err);
}
try {
setTimeout(function(){
throw new Error("error");
},1)
} catch (err) {
//can not catch it
console.log(err);
}
~~~
上面代码分别用process.nextTick和setTimeout方法,在下一轮事件循环抛出两个异常,代表异步操作抛出的错误。它们都无法被catch代码块捕获,因此catch代码块所在的那部分已经运行结束了。
一种解决方法是将错误捕获代码,也放到异步执行。
~~~
function async(cb, err) {
setTimeout(function() {
try {
if (true)
throw new Error("woops!");
else
cb("done");
} catch(e) {
err(e);
}
}, 2000)
}
async(function(res) {
console.log("received:", res);
}, function(err) {
console.log("Error: async threw an exception:", err);
});
// Error: async threw an exception: Error: woops!
~~~
上面代码中,async函数异步抛出的错误,可以同样部署在异步的catch代码块捕获。
这两种处理方法都不太理想。一般来说,Node只在很少场合才用try/catch语句,比如使用`JSON.parse`解析JSON文本。
### 回调函数
Node采用的方法,是将错误对象作为第一个参数,传入回调函数。这样就避免了捕获代码与发生错误的代码不在同一个时间段的问题。
~~~
fs.readFile('/foo.txt', function(err, data) {
if (err !== null) throw err;
console.log(data);
});
~~~
上面代码表示,读取文件`foo.txt`是一个异步操作,它的回调函数有两个参数,第一个是错误对象,第二个是读取到的文件数据。如果第一个参数不是null,就意味着发生错误,后面代码也就不再执行了。
下面是一个完整的例子。
~~~
function async2(continuation) {
setTimeout(function() {
try {
var res = 42;
if (true)
throw new Error("woops!");
else
continuation(null, res); // pass 'null' for error
} catch(e) {
continuation(e, null);
}
}, 2000);
}
async2(function(err, res) {
if (err)
console.log("Error: (cps) failed:", err);
else
console.log("(cps) received:", res);
});
// Error: (cps) failed: woops!
~~~
上面代码中,async2函数的回调函数的第一个参数就是一个错误对象,这是为了处理异步操作抛出的错误。
### EventEmitter接口的error事件
发生错误的时候,也可以用EventEmitter接口抛出error事件。
~~~
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();
emitter.emit('error', new Error('something bad happened'));
~~~
使用上面的代码必须小心,因为如果没有对error事件部署监听函数,会导致整个应用程序崩溃。所以,一般总是必须同时部署下面的代码。
~~~
emitter.on('error', function(err) {
console.error('出错:' + err.message);
});
~~~
### uncaughtException事件
当一个异常未被捕获,就会触发uncaughtException事件,可以对这个事件注册回调函数,从而捕获异常。
~~~
process.on('uncaughtException', function(err) {
console.error('Error caught in uncaughtException event:', err);
});
try {
setTimeout(function(){
throw new Error("error");
},1)
} catch (err) {
//can not catch it
console.log(err);
}
~~~
只要给uncaughtException配置了回调,Node进程不会异常退出,但异常发生的上下文已经丢失,无法给出异常发生的详细信息。而且,异常可能导致Node不能正常进行内存回收,出现内存泄露。所以,当uncaughtException触发后,最好记录错误日志,然后结束Node进程。
~~~
process.on('uncaughtException', function(err) {
logger(err);
process.exit(1);
});
~~~
### unhandledRejection事件
iojs有一个unhandledRejection事件,用来监听没有捕获的Promise对象的rejected状态。
~~~
var promise = new Promise(function(resolve, reject) {
reject(new Error("Broken."));
});
promise.then(function(result) {
console.log(result);
})
~~~
上面代码中,promise的状态变为rejected,并且抛出一个错误。但是,不会有任何反应,因为没有设置任何处理函数。
只要监听unhandledRejection事件,就能解决这个问题。
~~~
process.on('unhandledRejection', function (err, p) {
console.error(err.stack);
})
~~~
需要注意的是,unhandledRejection事件的监听函数有两个参数,第一个是错误对象,第二个是产生错误的promise对象。这可以提供很多有用的信息。
~~~
var http = require('http');
http.createServer(function (req, res) {
var promise = new Promise(function(resolve, reject) {
reject(new Error("Broken."))
})
p.info = {url: req.url}
}).listen(8080)
process.on('unhandledRejection', function (err, p) {
if (p.info && p.info.url) {
console.log('Error in URL', p.info.url)
}
console.error(err.stack)
})
~~~
上面代码会在出错时,输出用户请求的网址。
~~~
Error in URL /testurl
Error: Broken.
at /Users/mikeal/tmp/test.js:9:14
at Server.<anonymous> (/Users/mikeal/tmp/test.js:4:17)
at emitTwo (events.js:87:13)
at Server.emit (events.js:169:7)
at HTTPParser.parserOnIncoming [as onIncoming] (_http_server.js:471:12)
at HTTPParser.parserOnHeadersComplete (_http_common.js:88:23)
at Socket.socketOnData (_http_server.js:322:22)
at emitOne (events.js:77:13)
at Socket.emit (events.js:166:7)
at readableAddChunk (_stream_readable.js:145:16)
~~~
## 命令行脚本
node脚本可以作为命令行脚本使用。
~~~
$ node foo.js
~~~
上面代码执行了foo.js脚本文件。
foo.js文件的第一行,如果加入了解释器的位置,就可以将其作为命令行工具直接调用。
~~~
#!/usr/bin/env node
~~~
调用前,需更改文件的执行权限。
~~~
$ chmod u+x myscript.js
$ ./foo.js arg1 arg2 ...
~~~
作为命令行脚本时,`console.log`用于输出内容到标准输出,`process.stdin`用于读取标准输入,`child_process.exec()`用于执行一个shell命令。
## 参考链接
* Cody Lindley, [Package Managers: An Introductory Guide For The Uninitiated Front-End Developer](http://tech.pro/tutorial/1190/package-managers-an-introductory-guide-for-the-uninitiated-front-end-developer)
* Stack Overflow, [What is Node.js?](http://stackoverflow.com/questions/1884724/what-is-node-js)
* Andrew Burgess, [Using Node's Event Module](http://dev.tutsplus.com/tutorials/using-nodes-event-module--net-35941)
* James Halliday, [task automation with npm run](http://substack.net/task_automation_with_npm_run)- Romain Prieto, [Working on related Node.js modules locally](http://www.asyncdev.net/2013/12/working-on-related-node-modules-locally/)
* Alon Salant, [Export This: Interface Design Patterns for Node.js Modules](http://bites.goodeggs.com/posts/export-this/)
* Node.js Manual & Documentation, [Modules](http://nodejs.org/api/modules.html)
* Brent Ertz, [Creating and publishing a node.js module](http://quickleft.com/blog/creating-and-publishing-a-node-js-module)
* Fred K Schott, ["npm install --save" No Longer Using Tildes](http://fredkschott.com/post/2014/02/npm-no-longer-defaults-to-tildes/)
* Satans17, [Node稳定性的研究心得](http://satans17.github.io/2014/05/04/node%E7%A8%B3%E5%AE%9A%E6%80%A7%E7%9A%84%E7%A0%94%E7%A9%B6%E5%BF%83%E5%BE%97/)
* Axel Rauschmayer, [Write your shell scripts in JavaScript, via Node.js](http://www.2ality.com/2011/12/nodejs-shell-scripting.html)
草稿三:Node.js
最后更新于:2022-04-01 05:09:26
12.5 如何做到 jQuery-free?
最后更新于:2022-04-01 05:09:24
[TOC]
## 概述
jQuery是最流行的JavaScript工具库。据[统计](http://w3techs.com/technologies/details/js-jquery/all/all),目前全世界57.3%的网站使用它。也就是说,10个网站里面,有6个使用jQuery。如果只考察使用工具库的网站,这个比例就会上升到惊人的91.7%。
jQuery如此受欢迎,以至于有被滥用的趋势。许多开发者不管什么样的项目,都一股脑使用jQuery。但是,jQuery本质只是一个中间层,提供一套统一易用的DOM操作接口,消除浏览器之间的差异。多了这一层中间层,操作的性能和效率多多少少会打一些折扣。
2006年,jQuery诞生的时候,主要是为了解决IE6与标准的不兼容问题。如今的[情况](http://en.wikipedia.org/wiki/Usage_share_of_web_browsers)已经发生了很大的变化。IE的市场份额不断下降,以ECMAScript为基础的JavaScript标准语法,正得到越来越广泛的支持,不同浏览器对标准的支持越来越好、越来越趋同。开发者直接使用JavaScript标准语法,就能同时在各大浏览器运行,不再需要通过jQuery获取兼容性。
另一方面,jQuery臃肿的[体积](http://mathiasbynens.be/demo/jquery-size)也让人头痛不已。jQuery 2.0的原始大小为235KB,优化后为81KB;如果是支持IE6、7、8的jQuery 1.8.3,原始大小为261KB,优化后为91KB。即使有CDN,浏览器加载这样大小的脚本,也会产生不小的开销。
所以,对于一些不需要支持老式浏览器的小型项目来说,不使用jQuery,直接使用DOM原生接口,可能是更好的选择。开发者有必要了解,jQuery的一些常用操作所对应的DOM写法。而且,理解jQuery背后的原理,会帮助你更好地使用jQuery。要知道有一种极端的说法是,如果你不理解一样东西,就不要使用它。
下面就探讨如何用JavaScript标准语法,取代jQuery的一些主要功能,做到jQuery-free。
## 选取DOM元素
jQuery的核心是通过各种选择器,选中DOM元素,可以用querySelectorAll方法模拟这个功能。
~~~
var $ = document.querySelectorAll.bind(document);
~~~
这里需要注意的是,querySelectorAll方法返回的是NodeList对象,它很像数组(有数字索引和length属性),但不是数组,不能使用pop、push等数组特有方法。如果有需要,可以考虑将Nodelist对象转为数组。
~~~
myList = Array.prototype.slice.call(myNodeList);
~~~
## DOM操作
DOM本身就具有很丰富的操作方法,可以取代jQuery提供的操作方法。
获取父元素。
~~~
// jQuery写法
$("#elementID").parent()
// DOM写法
document.getElementById("elementID").parentNode
~~~
获取下一个同级元素。
~~~
// jQuery写法
$("#elementID").next()
// DOM写法
document.getElementById("elementID").nextSibling
~~~
尾部追加DOM元素。
~~~
// jQuery写法
$(parent).append($(child));
// DOM写法
parent.appendChild(child)
~~~
头部插入DOM元素。
~~~
// jQuery写法
$(parent).prepend($(child));
// DOM写法
parent.insertBefore(child, parent.childNodes[0])
~~~
生成DOM元素。
~~~
// jQuery写法
$("<p>")
// DOM写法
document.createElement("p")
~~~
删除DOM元素。
~~~
// jQuery写法
$(child).remove()
// DOM写法
child.parentNode.removeChild(child)
~~~
清空子元素。
~~~
// jQuery写法
$("#elementID").empty()
// DOM写法
var element = document.getElementById("elementID");
while(element.firstChild) element.removeChild(element.firstChild);
~~~
检查是否有子元素。
~~~
// jQuery写法
if (!$("#elementID").is(":empty")){}
// DOM写法
if (document.getElementById("elementID").hasChildNodes()){}
~~~
克隆元素。
~~~
// jQuery写法
$("#elementID").clone()
// DOM写法
document.getElementById("elementID").cloned(true)
~~~
## 事件的监听
jQuery使用on方法,监听事件和绑定回调函数。
~~~
$('button').on('click', function(){
ajax( ... );
});
~~~
完全可以自己定义on方法,将它指向addEventListener方法。
~~~
Element.prototype.on = Element.prototype.addEventListener;
~~~
为了使用方便,可以在NodeList对象上也部署这个方法。
~~~
NodeList.prototype.on = function (event, fn) {
[]['forEach'].call(this, function (el) {
el.on(event, fn);
});
return this;
};
~~~
取消事件绑定的off方法,也可以自己定义。
~~~
Element.prototype.off = Element.prototype.removeEventListener;
~~~
## 事件的触发
jQuery的trigger方法则需要单独部署,相对复杂一些。
~~~
Element.prototype.trigger = function (type, data) {
var event = document.createEvent('HTMLEvents');
event.initEvent(type, true, true);
event.data = data || {};
event.eventName = type;
event.target = this;
this.dispatchEvent(event);
return this;
};
~~~
在NodeList对象上也部署这个方法。
~~~
NodeList.prototype.trigger = function (event) {
[]['forEach'].call(this, function (el) {
el['trigger'](event);
});
return this;
};
~~~
## $(document).ready
DOM加载完成,会触发DOMContentLoaded事件,等同于jQuery的$(document).ready方法。
~~~
document.addEventListener("DOMContentLoaded", function() {
// ...
});
~~~
不过,目前的最佳实践,是将JavaScript脚本文件都放在页面底部加载。这样的话,其实$(document).ready方法(可以简写为$(function))已经不必要了,因为等到运行的时候,DOM对象已经生成了。
## attr方法
jQuery使用attr方法,读写网页元素的属性。
~~~
$("#picture").attr("src", "http://url/to/image")
~~~
DOM提供getAttribute和setAttribute方法读写元素属性。
~~~
imgElement.setAttribute("src", "http://url/to/image")
~~~
DOM还允许直接读取属性值,写法要简洁许多。
~~~
imgElement.src = "http://url/to/image";
~~~
> 需要注意的是,文本框元素(input)的this.value返回的是输入框中的值,链接元素(a标签)的this.href返回的是绝对URL。如果需要用到这两个网页元素的属性准确值,可以用this.getAttribute('value')和this.getAttibute('href')。
## addClass方法
jQuery的addClass方法,用于为DOM元素添加一个class。
~~~
$('body').addClass('hasJS');
~~~
DOM元素本身有一个可读写的className属性,可以用来操作class。
~~~
document.body.className = 'hasJS';
// or
document.body.className += ' hasJS';
~~~
HTML 5还提供一个classList对象,功能更强大(IE 9不支持)。
~~~
document.body.classList.add('hasJS');
document.body.classList.remove('hasJS');
document.body.classList.toggle('hasJS');
document.body.classList.contains('hasJS');
~~~
## CSS
jQuery的css方法,用来设置网页元素的样式。
~~~
$(node).css( "color", "red" );
~~~
DOM元素有一个style属性,可以直接操作。
~~~
element.style.color = "red”;;
// or
element.style.cssText += 'color:red';
~~~
## 数据储存
jQuery对象可以储存数据。
~~~
$("body").data("foo", 52);
~~~
HTML 5有一个dataset对象,也有类似的功能(IE 10不支持),不过只能保存字符串。
~~~
element.dataset.user = JSON.stringify(user);
element.dataset.score = score;
~~~
## Ajax
jQuery的ajax方法,用于异步操作。
~~~
$.ajax({
type: "POST",
url: "some.php",
data: { name: "John", location: "Boston" }
}).done(function( msg ) {
alert( "Data Saved: " + msg );
});
~~~
我们自定义一个ajax函数,简单模拟jQuery的ajax方法。
~~~
function ajax(url, opts){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
var completed = 4;
if(xhr.readyState === completed){
if(xhr.status === 200){
opts.success(xhr.responseText, xhr);
}else{
opts.error(xhr.responseText, xhr);
}
}
};
xhr.open(opts.method, url, true);
xhr.send(opts.data);
}
~~~
使用的时候,除了网址,还需要传入一个自己构造的option对象。
~~~
ajax('/foo', {
method: 'GET',
success: function(response){
console.log(response);
},
error: function(response){
console.log(response);
}
});
~~~
## 动画
jQuery的animate方法,用于生成动画效果。
~~~
$foo.animate('slow', { x: '+=10px' })
~~~
jQuery的动画效果,很大部分基于DOM。但是目前,CSS 3的动画远比DOM强大,所以可以把动画效果写进CSS,然后通过操作DOM元素的class,来展示动画。
~~~
foo.classList.add('animate')
~~~
如果需要对动画使用回调函数,CSS 3也定义了相应的事件。
~~~
el.addEventListener("webkitTransitionEnd", transitionEnded);
el.addEventListener("transitionend", transitionEnded);
~~~
## 替代方案
由于jQuery体积过大,替代方案层出不穷。
其中,最有名的是[zepto.js](http://zeptojs.com/)。它的设计目标是以最小的体积,做到最大兼容jQuery的API。它的1.0版的原始大小是55KB,优化后是29KB,gzip压缩后为10KB。
如果不求最大兼容,只希望模拟jQuery的基本功能。那么,[min.js](https://github.com/remy/min.js)优化后只有200字节,而[dolla](https://github.com/lelandrichardson/dolla)优化后是1.7KB。
此外,jQuery本身也采用模块设计,可以只选择使用自己需要的模块。具体做法参见jQuery的[github网站](https://github.com/jquery/jquery),或者使用专用的[Web界面](http://projects.jga.me/jquery-builder/)。
## 参考链接
* Remy Sharp,[I know jQuery. Now what?](http://remysharp.com/2013/04/19/i-know-jquery-now-what/)
* Hemanth.HM,[Power of Vanilla JS](http://h3manth.com/new/blog/2013/power-of-vanilla-js/)
* Burke Holland, [5 Things You Should Stop Doing With jQuery](http://flippinawesome.org/2013/05/06/5-things-you-should-stop-doing-with-jquery/)
* Burke Holland, [Out-Growing jQuery](http://tech.pro/tutorial/1385/out-growing-jquery)
* Nicolas Bevacqua, [Uncovering the Native DOM API](http://flippinawesome.org/2013/06/17/uncovering-the-native-dom-api/)
* Pony Foo, [Getting Over jQuery](http://blog.ponyfoo.com/2013/07/09/getting-over-jquery)
* Hemanth.HM, [JavaScript vs Jquery+CoffeeScript](http://h3manth.com/notes/jq-cs.html)