Mercurial > hg > nodescore
comparison node_modules/express/lib/response.js @ 73:0c3a2942ddee
now using express to server static content
author | Rob Canning <rc@kiben.net> |
---|---|
date | Sun, 29 Jun 2014 12:11:51 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
72:9af4250ff7d5 | 73:0c3a2942ddee |
---|---|
1 /** | |
2 * Module dependencies. | |
3 */ | |
4 | |
5 var escapeHtml = require('escape-html'); | |
6 var http = require('http'); | |
7 var path = require('path'); | |
8 var mixin = require('utils-merge'); | |
9 var sign = require('cookie-signature').sign; | |
10 var normalizeType = require('./utils').normalizeType; | |
11 var normalizeTypes = require('./utils').normalizeTypes; | |
12 var setCharset = require('./utils').setCharset; | |
13 var contentDisposition = require('./utils').contentDisposition; | |
14 var deprecate = require('./utils').deprecate; | |
15 var statusCodes = http.STATUS_CODES; | |
16 var cookie = require('cookie'); | |
17 var send = require('send'); | |
18 var basename = path.basename; | |
19 var extname = path.extname; | |
20 var mime = send.mime; | |
21 var vary = require('vary'); | |
22 | |
23 /** | |
24 * Response prototype. | |
25 */ | |
26 | |
27 var res = module.exports = { | |
28 __proto__: http.ServerResponse.prototype | |
29 }; | |
30 | |
31 /** | |
32 * Set status `code`. | |
33 * | |
34 * @param {Number} code | |
35 * @return {ServerResponse} | |
36 * @api public | |
37 */ | |
38 | |
39 res.status = function(code){ | |
40 this.statusCode = code; | |
41 return this; | |
42 }; | |
43 | |
44 /** | |
45 * Set Link header field with the given `links`. | |
46 * | |
47 * Examples: | |
48 * | |
49 * res.links({ | |
50 * next: 'http://api.example.com/users?page=2', | |
51 * last: 'http://api.example.com/users?page=5' | |
52 * }); | |
53 * | |
54 * @param {Object} links | |
55 * @return {ServerResponse} | |
56 * @api public | |
57 */ | |
58 | |
59 res.links = function(links){ | |
60 var link = this.get('Link') || ''; | |
61 if (link) link += ', '; | |
62 return this.set('Link', link + Object.keys(links).map(function(rel){ | |
63 return '<' + links[rel] + '>; rel="' + rel + '"'; | |
64 }).join(', ')); | |
65 }; | |
66 | |
67 /** | |
68 * Send a response. | |
69 * | |
70 * Examples: | |
71 * | |
72 * res.send(new Buffer('wahoo')); | |
73 * res.send({ some: 'json' }); | |
74 * res.send('<p>some html</p>'); | |
75 * res.send(404, 'Sorry, cant find that'); | |
76 * res.send(404); | |
77 * | |
78 * @api public | |
79 */ | |
80 | |
81 res.send = function(body){ | |
82 var req = this.req; | |
83 var head = 'HEAD' == req.method; | |
84 var type; | |
85 var encoding; | |
86 var len; | |
87 | |
88 // settings | |
89 var app = this.app; | |
90 | |
91 // allow status / body | |
92 if (2 == arguments.length) { | |
93 // res.send(body, status) backwards compat | |
94 if ('number' != typeof body && 'number' == typeof arguments[1]) { | |
95 this.statusCode = arguments[1]; | |
96 } else { | |
97 this.statusCode = body; | |
98 body = arguments[1]; | |
99 } | |
100 } | |
101 | |
102 switch (typeof body) { | |
103 // response status | |
104 case 'number': | |
105 this.get('Content-Type') || this.type('txt'); | |
106 this.statusCode = body; | |
107 body = http.STATUS_CODES[body]; | |
108 break; | |
109 // string defaulting to html | |
110 case 'string': | |
111 if (!this.get('Content-Type')) this.type('html'); | |
112 break; | |
113 case 'boolean': | |
114 case 'object': | |
115 if (null == body) { | |
116 body = ''; | |
117 } else if (Buffer.isBuffer(body)) { | |
118 this.get('Content-Type') || this.type('bin'); | |
119 } else { | |
120 return this.json(body); | |
121 } | |
122 break; | |
123 } | |
124 | |
125 // write strings in utf-8 | |
126 if ('string' === typeof body) { | |
127 encoding = 'utf8'; | |
128 type = this.get('Content-Type'); | |
129 | |
130 // reflect this in content-type | |
131 if ('string' === typeof type) { | |
132 this.set('Content-Type', setCharset(type, 'utf-8')); | |
133 } | |
134 } | |
135 | |
136 // populate Content-Length | |
137 if (undefined !== body && !this.get('Content-Length')) { | |
138 len = Buffer.isBuffer(body) | |
139 ? body.length | |
140 : Buffer.byteLength(body, encoding); | |
141 this.set('Content-Length', len); | |
142 } | |
143 | |
144 // ETag support | |
145 var etag = len !== undefined && app.get('etag fn'); | |
146 if (etag && ('GET' === req.method || 'HEAD' === req.method)) { | |
147 if (!this.get('ETag')) { | |
148 etag = etag(body, encoding); | |
149 etag && this.set('ETag', etag); | |
150 } | |
151 } | |
152 | |
153 // freshness | |
154 if (req.fresh) this.statusCode = 304; | |
155 | |
156 // strip irrelevant headers | |
157 if (204 == this.statusCode || 304 == this.statusCode) { | |
158 this.removeHeader('Content-Type'); | |
159 this.removeHeader('Content-Length'); | |
160 this.removeHeader('Transfer-Encoding'); | |
161 body = ''; | |
162 } | |
163 | |
164 // respond | |
165 this.end((head ? null : body), encoding); | |
166 | |
167 return this; | |
168 }; | |
169 | |
170 /** | |
171 * Send JSON response. | |
172 * | |
173 * Examples: | |
174 * | |
175 * res.json(null); | |
176 * res.json({ user: 'tj' }); | |
177 * res.json(500, 'oh noes!'); | |
178 * res.json(404, 'I dont have that'); | |
179 * | |
180 * @api public | |
181 */ | |
182 | |
183 res.json = function(obj){ | |
184 // allow status / body | |
185 if (2 == arguments.length) { | |
186 // res.json(body, status) backwards compat | |
187 if ('number' == typeof arguments[1]) { | |
188 this.statusCode = arguments[1]; | |
189 return 'number' === typeof obj | |
190 ? jsonNumDeprecated.call(this, obj) | |
191 : jsonDeprecated.call(this, obj); | |
192 } else { | |
193 this.statusCode = obj; | |
194 obj = arguments[1]; | |
195 } | |
196 } | |
197 | |
198 // settings | |
199 var app = this.app; | |
200 var replacer = app.get('json replacer'); | |
201 var spaces = app.get('json spaces'); | |
202 var body = JSON.stringify(obj, replacer, spaces); | |
203 | |
204 // content-type | |
205 this.get('Content-Type') || this.set('Content-Type', 'application/json'); | |
206 | |
207 return this.send(body); | |
208 }; | |
209 | |
210 var jsonDeprecated = deprecate(res.json, | |
211 'res.json(obj, status): Use res.json(status, obj) instead'); | |
212 | |
213 var jsonNumDeprecated = deprecate(res.json, | |
214 'res.json(num, status): Use res.status(status).json(num) instead'); | |
215 | |
216 /** | |
217 * Send JSON response with JSONP callback support. | |
218 * | |
219 * Examples: | |
220 * | |
221 * res.jsonp(null); | |
222 * res.jsonp({ user: 'tj' }); | |
223 * res.jsonp(500, 'oh noes!'); | |
224 * res.jsonp(404, 'I dont have that'); | |
225 * | |
226 * @api public | |
227 */ | |
228 | |
229 res.jsonp = function(obj){ | |
230 // allow status / body | |
231 if (2 == arguments.length) { | |
232 // res.json(body, status) backwards compat | |
233 if ('number' == typeof arguments[1]) { | |
234 this.statusCode = arguments[1]; | |
235 return 'number' === typeof obj | |
236 ? jsonpNumDeprecated.call(this, obj) | |
237 : jsonpDeprecated.call(this, obj); | |
238 } else { | |
239 this.statusCode = obj; | |
240 obj = arguments[1]; | |
241 } | |
242 } | |
243 | |
244 // settings | |
245 var app = this.app; | |
246 var replacer = app.get('json replacer'); | |
247 var spaces = app.get('json spaces'); | |
248 var body = JSON.stringify(obj, replacer, spaces) | |
249 .replace(/\u2028/g, '\\u2028') | |
250 .replace(/\u2029/g, '\\u2029'); | |
251 var callback = this.req.query[app.get('jsonp callback name')]; | |
252 | |
253 // content-type | |
254 this.get('Content-Type') || this.set('Content-Type', 'application/json'); | |
255 | |
256 // fixup callback | |
257 if (Array.isArray(callback)) { | |
258 callback = callback[0]; | |
259 } | |
260 | |
261 // jsonp | |
262 if (callback && 'string' === typeof callback) { | |
263 this.set('Content-Type', 'text/javascript'); | |
264 var cb = callback.replace(/[^\[\]\w$.]/g, ''); | |
265 body = 'typeof ' + cb + ' === \'function\' && ' + cb + '(' + body + ');'; | |
266 } | |
267 | |
268 return this.send(body); | |
269 }; | |
270 | |
271 var jsonpDeprecated = deprecate(res.json, | |
272 'res.jsonp(obj, status): Use res.jsonp(status, obj) instead'); | |
273 | |
274 var jsonpNumDeprecated = deprecate(res.json, | |
275 'res.jsonp(num, status): Use res.status(status).jsonp(num) instead'); | |
276 | |
277 /** | |
278 * Transfer the file at the given `path`. | |
279 * | |
280 * Automatically sets the _Content-Type_ response header field. | |
281 * The callback `fn(err)` is invoked when the transfer is complete | |
282 * or when an error occurs. Be sure to check `res.sentHeader` | |
283 * if you wish to attempt responding, as the header and some data | |
284 * may have already been transferred. | |
285 * | |
286 * Options: | |
287 * | |
288 * - `maxAge` defaulting to 0 | |
289 * - `root` root directory for relative filenames | |
290 * - `hidden` serve hidden files, defaulting to false | |
291 * | |
292 * Other options are passed along to `send`. | |
293 * | |
294 * Examples: | |
295 * | |
296 * The following example illustrates how `res.sendfile()` may | |
297 * be used as an alternative for the `static()` middleware for | |
298 * dynamic situations. The code backing `res.sendfile()` is actually | |
299 * the same code, so HTTP cache support etc is identical. | |
300 * | |
301 * app.get('/user/:uid/photos/:file', function(req, res){ | |
302 * var uid = req.params.uid | |
303 * , file = req.params.file; | |
304 * | |
305 * req.user.mayViewFilesFrom(uid, function(yes){ | |
306 * if (yes) { | |
307 * res.sendfile('/uploads/' + uid + '/' + file); | |
308 * } else { | |
309 * res.send(403, 'Sorry! you cant see that.'); | |
310 * } | |
311 * }); | |
312 * }); | |
313 * | |
314 * @api public | |
315 */ | |
316 | |
317 res.sendfile = function(path, options, fn){ | |
318 options = options || {}; | |
319 var self = this; | |
320 var req = self.req; | |
321 var next = this.req.next; | |
322 var done; | |
323 | |
324 | |
325 // support function as second arg | |
326 if ('function' == typeof options) { | |
327 fn = options; | |
328 options = {}; | |
329 } | |
330 | |
331 // socket errors | |
332 req.socket.on('error', error); | |
333 | |
334 // errors | |
335 function error(err) { | |
336 if (done) return; | |
337 done = true; | |
338 | |
339 // clean up | |
340 cleanup(); | |
341 if (!self.headersSent) self.removeHeader('Content-Disposition'); | |
342 | |
343 // callback available | |
344 if (fn) return fn(err); | |
345 | |
346 // list in limbo if there's no callback | |
347 if (self.headersSent) return; | |
348 | |
349 // delegate | |
350 next(err); | |
351 } | |
352 | |
353 // streaming | |
354 function stream(stream) { | |
355 if (done) return; | |
356 cleanup(); | |
357 if (fn) stream.on('end', fn); | |
358 } | |
359 | |
360 // cleanup | |
361 function cleanup() { | |
362 req.socket.removeListener('error', error); | |
363 } | |
364 | |
365 // Back-compat | |
366 options.maxage = options.maxage || options.maxAge || 0; | |
367 | |
368 // transfer | |
369 var file = send(req, path, options); | |
370 file.on('error', error); | |
371 file.on('directory', next); | |
372 file.on('stream', stream); | |
373 file.pipe(this); | |
374 this.on('finish', cleanup); | |
375 }; | |
376 | |
377 /** | |
378 * Transfer the file at the given `path` as an attachment. | |
379 * | |
380 * Optionally providing an alternate attachment `filename`, | |
381 * and optional callback `fn(err)`. The callback is invoked | |
382 * when the data transfer is complete, or when an error has | |
383 * ocurred. Be sure to check `res.headersSent` if you plan to respond. | |
384 * | |
385 * This method uses `res.sendfile()`. | |
386 * | |
387 * @api public | |
388 */ | |
389 | |
390 res.download = function(path, filename, fn){ | |
391 // support function as second arg | |
392 if ('function' == typeof filename) { | |
393 fn = filename; | |
394 filename = null; | |
395 } | |
396 | |
397 filename = filename || path; | |
398 this.set('Content-Disposition', contentDisposition(filename)); | |
399 return this.sendfile(path, fn); | |
400 }; | |
401 | |
402 /** | |
403 * Set _Content-Type_ response header with `type` through `mime.lookup()` | |
404 * when it does not contain "/", or set the Content-Type to `type` otherwise. | |
405 * | |
406 * Examples: | |
407 * | |
408 * res.type('.html'); | |
409 * res.type('html'); | |
410 * res.type('json'); | |
411 * res.type('application/json'); | |
412 * res.type('png'); | |
413 * | |
414 * @param {String} type | |
415 * @return {ServerResponse} for chaining | |
416 * @api public | |
417 */ | |
418 | |
419 res.contentType = | |
420 res.type = function(type){ | |
421 return this.set('Content-Type', ~type.indexOf('/') | |
422 ? type | |
423 : mime.lookup(type)); | |
424 }; | |
425 | |
426 /** | |
427 * Respond to the Acceptable formats using an `obj` | |
428 * of mime-type callbacks. | |
429 * | |
430 * This method uses `req.accepted`, an array of | |
431 * acceptable types ordered by their quality values. | |
432 * When "Accept" is not present the _first_ callback | |
433 * is invoked, otherwise the first match is used. When | |
434 * no match is performed the server responds with | |
435 * 406 "Not Acceptable". | |
436 * | |
437 * Content-Type is set for you, however if you choose | |
438 * you may alter this within the callback using `res.type()` | |
439 * or `res.set('Content-Type', ...)`. | |
440 * | |
441 * res.format({ | |
442 * 'text/plain': function(){ | |
443 * res.send('hey'); | |
444 * }, | |
445 * | |
446 * 'text/html': function(){ | |
447 * res.send('<p>hey</p>'); | |
448 * }, | |
449 * | |
450 * 'appliation/json': function(){ | |
451 * res.send({ message: 'hey' }); | |
452 * } | |
453 * }); | |
454 * | |
455 * In addition to canonicalized MIME types you may | |
456 * also use extnames mapped to these types: | |
457 * | |
458 * res.format({ | |
459 * text: function(){ | |
460 * res.send('hey'); | |
461 * }, | |
462 * | |
463 * html: function(){ | |
464 * res.send('<p>hey</p>'); | |
465 * }, | |
466 * | |
467 * json: function(){ | |
468 * res.send({ message: 'hey' }); | |
469 * } | |
470 * }); | |
471 * | |
472 * By default Express passes an `Error` | |
473 * with a `.status` of 406 to `next(err)` | |
474 * if a match is not made. If you provide | |
475 * a `.default` callback it will be invoked | |
476 * instead. | |
477 * | |
478 * @param {Object} obj | |
479 * @return {ServerResponse} for chaining | |
480 * @api public | |
481 */ | |
482 | |
483 res.format = function(obj){ | |
484 var req = this.req; | |
485 var next = req.next; | |
486 | |
487 var fn = obj.default; | |
488 if (fn) delete obj.default; | |
489 var keys = Object.keys(obj); | |
490 | |
491 var key = req.accepts(keys); | |
492 | |
493 this.vary("Accept"); | |
494 | |
495 if (key) { | |
496 this.set('Content-Type', normalizeType(key).value); | |
497 obj[key](req, this, next); | |
498 } else if (fn) { | |
499 fn(); | |
500 } else { | |
501 var err = new Error('Not Acceptable'); | |
502 err.status = 406; | |
503 err.types = normalizeTypes(keys).map(function(o){ return o.value }); | |
504 next(err); | |
505 } | |
506 | |
507 return this; | |
508 }; | |
509 | |
510 /** | |
511 * Set _Content-Disposition_ header to _attachment_ with optional `filename`. | |
512 * | |
513 * @param {String} filename | |
514 * @return {ServerResponse} | |
515 * @api public | |
516 */ | |
517 | |
518 res.attachment = function(filename){ | |
519 if (filename) this.type(extname(filename)); | |
520 this.set('Content-Disposition', contentDisposition(filename)); | |
521 return this; | |
522 }; | |
523 | |
524 /** | |
525 * Set header `field` to `val`, or pass | |
526 * an object of header fields. | |
527 * | |
528 * Examples: | |
529 * | |
530 * res.set('Foo', ['bar', 'baz']); | |
531 * res.set('Accept', 'application/json'); | |
532 * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); | |
533 * | |
534 * Aliased as `res.header()`. | |
535 * | |
536 * @param {String|Object|Array} field | |
537 * @param {String} val | |
538 * @return {ServerResponse} for chaining | |
539 * @api public | |
540 */ | |
541 | |
542 res.set = | |
543 res.header = function(field, val){ | |
544 if (2 == arguments.length) { | |
545 if (Array.isArray(val)) val = val.map(String); | |
546 else val = String(val); | |
547 if ('content-type' == field.toLowerCase() && !/;\s*charset\s*=/.test(val)) { | |
548 var charset = mime.charsets.lookup(val.split(';')[0]); | |
549 if (charset) val += '; charset=' + charset.toLowerCase(); | |
550 } | |
551 this.setHeader(field, val); | |
552 } else { | |
553 for (var key in field) { | |
554 this.set(key, field[key]); | |
555 } | |
556 } | |
557 return this; | |
558 }; | |
559 | |
560 /** | |
561 * Get value for header `field`. | |
562 * | |
563 * @param {String} field | |
564 * @return {String} | |
565 * @api public | |
566 */ | |
567 | |
568 res.get = function(field){ | |
569 return this.getHeader(field); | |
570 }; | |
571 | |
572 /** | |
573 * Clear cookie `name`. | |
574 * | |
575 * @param {String} name | |
576 * @param {Object} options | |
577 * @param {ServerResponse} for chaining | |
578 * @api public | |
579 */ | |
580 | |
581 res.clearCookie = function(name, options){ | |
582 var opts = { expires: new Date(1), path: '/' }; | |
583 return this.cookie(name, '', options | |
584 ? mixin(opts, options) | |
585 : opts); | |
586 }; | |
587 | |
588 /** | |
589 * Set cookie `name` to `val`, with the given `options`. | |
590 * | |
591 * Options: | |
592 * | |
593 * - `maxAge` max-age in milliseconds, converted to `expires` | |
594 * - `signed` sign the cookie | |
595 * - `path` defaults to "/" | |
596 * | |
597 * Examples: | |
598 * | |
599 * // "Remember Me" for 15 minutes | |
600 * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); | |
601 * | |
602 * // save as above | |
603 * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) | |
604 * | |
605 * @param {String} name | |
606 * @param {String|Object} val | |
607 * @param {Options} options | |
608 * @api public | |
609 */ | |
610 | |
611 res.cookie = function(name, val, options){ | |
612 options = mixin({}, options); | |
613 var secret = this.req.secret; | |
614 var signed = options.signed; | |
615 if (signed && !secret) throw new Error('cookieParser("secret") required for signed cookies'); | |
616 if ('number' == typeof val) val = val.toString(); | |
617 if ('object' == typeof val) val = 'j:' + JSON.stringify(val); | |
618 if (signed) val = 's:' + sign(val, secret); | |
619 if ('maxAge' in options) { | |
620 options.expires = new Date(Date.now() + options.maxAge); | |
621 options.maxAge /= 1000; | |
622 } | |
623 if (null == options.path) options.path = '/'; | |
624 var headerVal = cookie.serialize(name, String(val), options); | |
625 | |
626 // supports multiple 'res.cookie' calls by getting previous value | |
627 var prev = this.get('Set-Cookie'); | |
628 if (prev) { | |
629 if (Array.isArray(prev)) { | |
630 headerVal = prev.concat(headerVal); | |
631 } else { | |
632 headerVal = [prev, headerVal]; | |
633 } | |
634 } | |
635 this.set('Set-Cookie', headerVal); | |
636 return this; | |
637 }; | |
638 | |
639 | |
640 /** | |
641 * Set the location header to `url`. | |
642 * | |
643 * The given `url` can also be "back", which redirects | |
644 * to the _Referrer_ or _Referer_ headers or "/". | |
645 * | |
646 * Examples: | |
647 * | |
648 * res.location('/foo/bar').; | |
649 * res.location('http://example.com'); | |
650 * res.location('../login'); | |
651 * | |
652 * @param {String} url | |
653 * @api public | |
654 */ | |
655 | |
656 res.location = function(url){ | |
657 var req = this.req; | |
658 | |
659 // "back" is an alias for the referrer | |
660 if ('back' == url) url = req.get('Referrer') || '/'; | |
661 | |
662 // Respond | |
663 this.set('Location', url); | |
664 return this; | |
665 }; | |
666 | |
667 /** | |
668 * Redirect to the given `url` with optional response `status` | |
669 * defaulting to 302. | |
670 * | |
671 * The resulting `url` is determined by `res.location()`, so | |
672 * it will play nicely with mounted apps, relative paths, | |
673 * `"back"` etc. | |
674 * | |
675 * Examples: | |
676 * | |
677 * res.redirect('/foo/bar'); | |
678 * res.redirect('http://example.com'); | |
679 * res.redirect(301, 'http://example.com'); | |
680 * res.redirect('http://example.com', 301); | |
681 * res.redirect('../login'); // /blog/post/1 -> /blog/login | |
682 * | |
683 * @param {String} url | |
684 * @param {Number} code | |
685 * @api public | |
686 */ | |
687 | |
688 res.redirect = function(url){ | |
689 var head = 'HEAD' == this.req.method; | |
690 var status = 302; | |
691 var body; | |
692 | |
693 // allow status / url | |
694 if (2 == arguments.length) { | |
695 if ('number' == typeof url) { | |
696 status = url; | |
697 url = arguments[1]; | |
698 } else { | |
699 status = arguments[1]; | |
700 } | |
701 } | |
702 | |
703 // Set location header | |
704 this.location(url); | |
705 url = this.get('Location'); | |
706 | |
707 // Support text/{plain,html} by default | |
708 this.format({ | |
709 text: function(){ | |
710 body = statusCodes[status] + '. Redirecting to ' + encodeURI(url); | |
711 }, | |
712 | |
713 html: function(){ | |
714 var u = escapeHtml(url); | |
715 body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>'; | |
716 }, | |
717 | |
718 default: function(){ | |
719 body = ''; | |
720 } | |
721 }); | |
722 | |
723 // Respond | |
724 this.statusCode = status; | |
725 this.set('Content-Length', Buffer.byteLength(body)); | |
726 this.end(head ? null : body); | |
727 }; | |
728 | |
729 /** | |
730 * Add `field` to Vary. If already present in the Vary set, then | |
731 * this call is simply ignored. | |
732 * | |
733 * @param {Array|String} field | |
734 * @param {ServerResponse} for chaining | |
735 * @api public | |
736 */ | |
737 | |
738 res.vary = function(field){ | |
739 // checks for back-compat | |
740 if (!field) return this; | |
741 if (Array.isArray(field) && !field.length) return this; | |
742 | |
743 vary(this, field); | |
744 | |
745 return this; | |
746 }; | |
747 | |
748 /** | |
749 * Render `view` with the given `options` and optional callback `fn`. | |
750 * When a callback function is given a response will _not_ be made | |
751 * automatically, otherwise a response of _200_ and _text/html_ is given. | |
752 * | |
753 * Options: | |
754 * | |
755 * - `cache` boolean hinting to the engine it should cache | |
756 * - `filename` filename of the view being rendered | |
757 * | |
758 * @api public | |
759 */ | |
760 | |
761 res.render = function(view, options, fn){ | |
762 options = options || {}; | |
763 var self = this; | |
764 var req = this.req; | |
765 var app = req.app; | |
766 | |
767 // support callback function as second arg | |
768 if ('function' == typeof options) { | |
769 fn = options, options = {}; | |
770 } | |
771 | |
772 // merge res.locals | |
773 options._locals = self.locals; | |
774 | |
775 // default callback to respond | |
776 fn = fn || function(err, str){ | |
777 if (err) return req.next(err); | |
778 self.send(str); | |
779 }; | |
780 | |
781 // render | |
782 app.render(view, options, fn); | |
783 }; |