streaming view rendering

Allows for a view engine to return a stream upon being asked to render a
path. This allows for the engine to simply stream out data as it is
loaded from disk (or other IO location) without having to collect it all
first and then send it.

Fallback mode for all existing engines that call `fn` upon completion
takes over if no stream is returned. This works for both app.render and
res.render
Esse commit está contido em:
Roman Shtylman
2013-11-09 21:27:01 -05:00
commit b0da4b1a94
4 arquivos alterados com 104 adições e 6 exclusões
+20 -1
Ver Arquivo
@@ -501,7 +501,26 @@ app.render = function(name, options, fn){
// render
try {
view.render(opts, fn);
var view_stream = view.render(opts, fn);
// if the engine returned a stream AND user specified a function
// then we will capture data for user
if (view_stream && fn) {
var body = '';
view_stream.on('data', function(chunk) {
body += chunk;
});
view_stream.once('error', function(err) {
fn(err);
});
view_stream.once('end', function() {
fn(null, body);
});
}
return view_stream;
} catch (err) {
fn(err);
}
+41 -4
Ver Arquivo
@@ -792,12 +792,49 @@ res.render = function(view, options, fn){
// merge res.locals
options._locals = self.locals;
// default callback to respond
fn = fn || function(err, str){
// support for non streaming view rendering
// if renderer uses streams, this will not be called
// see below for how streams are handled
var respond = fn || function(err, str){
if (view_stream) return;
if (err) return req.next(err);
self.send(str);
};
// render
app.render(view, options, fn);
var view_stream = app.render(view, options, respond);
// old style, will have already called respond for us
if (!view_stream) {
return;
}
// user wants to handle sending themselves
if (fn) {
var body = '';
view_stream.on('data', function(chunk) {
body += chunk;
});
view_stream.once('error', function(err) {
fn(err);
});
view_stream.once('end', function() {
fn(null, body);
});
return;
}
// set response headers
self.statusCode = 200;
if (!self.get('Content-Type')) {
self.charset = self.charset || 'utf-8';
self.type('html');
}
self.setHeader('Transfer-Encoding', 'chunked');
// start streaming the response
view_stream.pipe(self);
};
+1 -1
Ver Arquivo
@@ -73,5 +73,5 @@ View.prototype.lookup = function(path){
*/
View.prototype.render = function(options, fn){
this.engine(this.path, options, fn);
return this.engine(this.path, options, fn);
};
+42
Ver Arquivo
@@ -1,5 +1,6 @@
var express = require('../')
, request = require('./support/http')
, fs = require('fs');
function render(path, options, fn) {
@@ -10,6 +11,10 @@ function render(path, options, fn) {
});
}
function streaming_render(path, options, fn) {
return fs.createReadStream(path, { encoding: 'utf-8' });
}
describe('app', function(){
describe('.engine(ext, fn)', function(){
it('should map a template engine', function(done){
@@ -76,5 +81,42 @@ describe('app', function(){
done();
})
})
it('should support streaming engines', function(done) {
var app = express();
app.set('views', __dirname + '/fixtures');
app.engine('.html', streaming_render);
app.set('view engine', '.html');
app.locals.user = { name: 'tobi' };
// using function should just call the function when done
app.render('user', function(err, str){
if (err) return done(err);
str.should.equal('<p>{{user.name}}</p>');
done();
})
});
it('should render a response using the streaming engine', function(done) {
var app = express();
app.set('views', __dirname + '/fixtures');
app.engine('.html', streaming_render);
app.set('view engine', '.html');
app.locals.user = { name: 'tobi' };
app.get('/', function(req, res) {
res.render('user');
});
request(app)
.get('/')
.expect(200)
.end(function(err, res) {
if (err) return done(err);
res.header['transfer-encoding'].should.equal('chunked');
res.text.should.equal('<p>{{user.name}}</p>');
done();
});
});
})
})