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)
';