diff --git a/README.md b/README.md index e173b0a..7077f8c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Response streaming middleware for Express 4. +**IMPMORTANT:** If you want your streamed responses to have GZIP enabled, please use the excellent [express `compression` middleware](https://www.npmjs.com/package/compression). If you use `compression` as an app-wide middleware, `express-stream` will automitically take advantage of it. + Check out the [demo app](https://express-stream-demo.herokuapp.com/). # What is This? @@ -61,7 +63,8 @@ stream.streamAfter('post-body-view'); //Add the middleware to the desired routes app.get('/', stream.stream(), function (req, res) { - res.render('landing'); //This route will now stream + res.stream('landing'); //This route will now stream + res.close(); }); ``` @@ -81,10 +84,9 @@ Because express-stream's two middleware functions patch express's res object dif | Function | Scope | Description | Arguments | |---|---|---|---|---| | stream.pipe() | middleware | This middleware function is written for client-side rendering. It can be used as a loose BigPipe implementation. | N/A | -|res.stream(view, options, callback) | res | When you use `.pipe()`, this funciton is added to the `res` object. It is the same as `res.render()` except that it does not close the HTTP connection. | Same as express | -| res.pipe(output, encoding) | res | Send a string of JavaScrpit to the client | `Output`: JavaScript output as a string `Encoding`: defaults to 'UTF-8' | -| res.close(output, encoding) | res | Same as `res.pipe()`, but closes the connection when finished. | Same as `res.pipe()` | -| stream.wrapJavascript(val) | stream | Whether to wrap all `res.pipe()` and `res.close()` output with '' | `val`: boolean stating whether to use this feature, defaults to false | +|res.stream(view, options) | res | When you use `.pipe()`, this funciton is added to the `res` object. It is the same as `res.render()` except that it does not close the HTTP connection and does not accept a callback | Same as express | +| res.streamText(output) | res | Send a string of text to the client | `Output`: Text output as a string | +| res.close() | res | Closes the connection when finished. | | # stream.stream() @@ -98,7 +100,7 @@ Set an app-wide options object to be merged with the `options` param passed to a * options: type: object, default: {} -## .streamBefore(view, options, callback) +## .streamBefore(view, options) > **_App-wide API Call_** @@ -110,7 +112,6 @@ If `view` is an array, all other passed params will be ignored. * view: type: string || array of strings || array of objects * options: same as express's `options` param -* callback: same as express's `callback` param #### Examples @@ -133,7 +134,7 @@ var globalHeadList = [ stream.streamBefore(globalHeadList); ``` -## .streamAfter(view, options, callback) +## .streamAfter(view, options) > **_App-wide API Call_** @@ -145,7 +146,6 @@ If `view` is an array, all other passed params will be ignored. * view: type: string || array of strings || array of objects * options: same as express's `options` param -* callback: same as express's `callback` param #### Examples @@ -168,41 +168,38 @@ var globalHeadList = [ stream.streamAfter(globalHeadList); ``` -## .openHtmlOpenHead(view, options, callback) +## .openHtmlOpenHead(view, options) > **_App-wide API Call_** -If `view` is `true`, this will simply stream a `` string to the client. If `view` is a string, this will stream the associated view with optional `options` and `callback`. +If `view` is `true`, this will simply stream a `` string to the client. If `view` is a string, this will stream the associated view with optional `options`. #### Arguments * view: boolean or string * options: same as express's `options` param -* callback: same as express's `callback` param -## .closeHeadOpenBody(view, options, callback) +## .closeHeadOpenBody(view, options) > **_App-wide API Call_** -If `view` is `true`, this will simply stream a `` string to the client. If `view` is a string, this will stream the associated view with optional `options` and `callback`. +If `view` is `true`, this will simply stream a `` string to the client. If `view` is a string, this will stream the associated view with optional `options`. #### Arguments * view: boolean or string * options: same as express's `options` param -* callback: same as express's `callback` param -## .closeBodyCloseHtml(view, options, callback) +## .closeBodyCloseHtml(view, options) > **_App-wide API Call_** -If `view` is `true`, this will simply stream a `` string to the client. If `view` is a string, this will stream the associated view with optional `options` and `callback`. +If `view` is `true`, this will simply stream a `` string to the client. If `view` is a string, this will stream the associated view with optional `options`. #### Arguments * view: boolean or string * options: same as express's `options` param -* callback: same as express's `callback` param ## .useAllAutoTags(val) @@ -214,7 +211,7 @@ A convenience method to set the same boolean value for `openHtmlOpenHead`, `clos * val: boolean or string -## .stream(headView, headOptions, headCallback) +## .stream(headView, headOptions) > **_Middleware-only API Call_** @@ -226,7 +223,6 @@ If `headView` is an array, all other passed params will be ignored. * headView: type: string || array of strings || array of objects * headOptions: same as express's `options` param -* headCallback: same as express's `callback` param #### Examples @@ -234,7 +230,8 @@ With `headView` as a string ```javascript app.get('/stream-route', stream.stream('render-blocking-assets', {custom: data}), function (req, res){ - res.render('stream-body'); + res.stream('stream-body'); + res.close(); }); ``` @@ -242,7 +239,8 @@ With `headView` as an array of strings ```javascript app.get('/stream-route', stream.stream(['blocking-one', 'blocking-two']), function (req, res){ - res.render('stream-body'); + res.stream('stream-body'); + res.close(); }); ``` @@ -254,30 +252,39 @@ var blockingList = [ {view: 'blocking-two'} ] app.get('/stream-route', stream.stream(blockingList), function (req, res){ - res.render('stream-body'); + res.stream('stream-body'); + res.close(); }); ``` -## res.render(view, options, callback) +## res.stream(view, options) > **_Route-specific API Call_** -Compiles and streams a view, then compiles and streams the views set by `.streamAfter()`, then closes the connection. +Compiles and streams a view just like `res.render()`, but does not trigger the `.streamAfter()` array and does not close the connection. #### Arguments -* All arguments are identical to `express`'s `res.render()` call +* All arguments are identical to `express`'s `res.render()` call expect that `callback` is missing. -## res.stream(view, options, callback) +## res.streamText(text) > **_Route-specific API Call_** -Compiles and streams a view just like `res.render()`, but does not trigger the `.streamAfter()` array and does not close the connection. +Streams the provided text to the client. #### Arguments -* All arguments are identical to `express`'s `res.render()` call +* All arguments are identical to `express`'s `res.render()` call excpet that `callback` is missing. # More! Usage examples are coming. In the mean time, see this [demo app](https://express-stream-demo.herokuapp.com/). + +# Breaking Change History + +#### 1.0.0 + +* `stream.pipe()` now only exposes `res.stream(view, data)`, `res.streamText(text)`, and `res.close()`. +* `stream.stream()`'s `res.render(view, data)` function is now `res.stream(view, data)`. +* Custom `res.*` functions no longer accept the `callback` param. diff --git a/expressStream.js b/expressStream.js index 9b28341..6e5ba84 100644 --- a/expressStream.js +++ b/expressStream.js @@ -1,3 +1,49 @@ +/* + * Output html to the client + */ +function stream(res, html){ + res.write(html); + if(res.flush) res.flush(); +} + +/* + * BigPipe (client-side render) implementation + */ +exports.pipe = function(){ + return function (req, res, next){ + // var onloadSent = false; + + // function sendOnloadEvent(){ + // if(onloadSent) return; + // var html = ''; + // stream(res, html); + // onloadSent = true; + // } + + res.stream = function(view, data){ + res.render(view, data, function (err, html){ + stream(res, html); + // sendOnloadEvent(); + }); + } + + res.streamText = function(text){ + stream(res, text); + } + + next(); + } +} + +/* + * Server-side render implementation + */ var globalOptions = {}; var streamBefore = []; var streamAfter = []; @@ -5,9 +51,9 @@ var openHtmlOpenHead = false; var closeHeadOpenBody = false; var closeBodyCloseHtml = false; -function getStreamableValue(view, options, callback){ +function getStreamableValue(view, options){ if(typeof view === 'string'){ - return [{view: view, options: options, callback: callback}]; + return [{view: view, options: options}]; } else if(view instanceof Array){ return streamBefore = view; @@ -15,15 +61,14 @@ function getStreamableValue(view, options, callback){ return []; } -function getAutoTagValue(view, options, callback){ +function getAutoTagValue(view, options){ if(typeof view === 'boolean'){ return view; } else if(typeof view === 'string'){ return { view: view, - options: options, - callback: callback + options: options } } return false; @@ -33,12 +78,12 @@ exports.globalOptions = function(opts){ globalOptions = (typeof opts === 'object') ? opts : {}; } -exports.streamBefore = function(view, options, callback){ - streamBefore = getStreamableValue(view, options, callback); +exports.streamBefore = function(view, options){ + streamBefore = getStreamableValue(view, options); } -exports.streamAfter = function(view, options, callback){ - streamAfter = getStreamableValue(view, options, callback); +exports.streamAfter = function(view, options){ + streamAfter = getStreamableValue(view, options); } exports.useAllAutoTags = function(val){ @@ -49,30 +94,30 @@ exports.useAllAutoTags = function(val){ } } -exports.openHtmlOpenHead = function(view, options, callback){ - openHtmlOpenHead = getAutoTagValue(view, options, callback); +exports.openHtmlOpenHead = function(view, options){ + openHtmlOpenHead = getAutoTagValue(view, options); } -exports.closeHeadOpenBody = function(view, options, callback){ - closeHeadOpenBody = getAutoTagValue(view, options, callback); +exports.closeHeadOpenBody = function(view, options){ + closeHeadOpenBody = getAutoTagValue(view, options); } -exports.closeBodyCloseHtml = function(view, options, callback){ - closeBodyCloseHtml = getAutoTagValue(view, options, callback); +exports.closeBodyCloseHtml = function(view, options){ + closeBodyCloseHtml = getAutoTagValue(view, options); } -exports.stream = function(headView, headOptions, headCallback, configView){ +exports.stream = function(headView, headOptions, configView){ return function (req, res, next){ - var headViews = getStreamableValue(headView, headOptions, headCallback); + var headViews = getStreamableValue(headView, headOptions); function streamAutoTags(input, html){ if(input){ if(typeof input === 'boolean'){ - res.write(html); + stream(res, html); } else if(typeof input === 'object' && input.view){ - res.stream(input.view, input.options, input.callback); + res.stream(input.view, input.options); } } } @@ -88,7 +133,7 @@ exports.stream = function(headView, headOptions, headCallback, configView){ res.stream(input[i]); } else if(typeof input[i] === 'object'){ - res.stream(input[i].view, input[i].options, input[i].callback); + res.stream(input[i].view, input[i].options); } } } @@ -107,31 +152,21 @@ exports.stream = function(headView, headOptions, headCallback, configView){ return globalOptions; } - res.set = function(){} - - res._render = res.render; - res.render = function (view, options, callback) { - this.isFinalChunk = true; - this._render(view, mergeOptions(options), callback); - return this; + res.stream = function (view, options) { + this.render(view, mergeOptions(options), function (err, html){ + stream(res, html); + }); } - res.stream = function (view, options, callback) { - this.isFinalChunk = false; - this._render(view, mergeOptions(options), callback); - return this; + res.streamText = function (text) { + stream(res, text); } res._end = res.end; - res.end = function (chunk, encoding) { - if(chunk){ - this.write(chunk, encoding); - } - if(this.isFinalChunk){ - streamArrayOrString(streamAfter); - streamAutoTags(closeBodyCloseHtml, ''); - res._end(); - } + res.end = function () { + streamArrayOrString(streamAfter); + streamAutoTags(closeBodyCloseHtml, ''); + res._end(); } streamAutoTags(openHtmlOpenHead, ''); @@ -145,70 +180,3 @@ exports.stream = function(headView, headOptions, headCallback, configView){ next(); } } - -var wrapJavascript = false; - -exports.wrapJavascript = function(val){ - wrapJavascript = (typeof val === 'boolean') ? val : false; -} - -exports.pipe = function(){ - return function (req, res, next){ - - var onloadSent = false; - - function sendOnloadEvent(){ - var chunk = ''; - res.write(chunk); - } - - function wrapChunk(chunk){ - if(chunk && wrapJavascript){ - chunk = ''; - } - return chunk; - } - - res.set = function(){} - - res._render = res.render; - res.stream = function (view, options, callback) { - this.isFinalChunk = false; - this._render(view, options, callback); - if(!onloadSent){ - sendOnloadEvent(); - onloadSent = true; - } - } - - res.pipe = function (chunk, encoding) { - this.isFinalChunk = false; - chunk = wrapChunk(chunk); - this.end(chunk, encoding); - } - - res.close = function (chunk, encoding) { - this.isFinalChunk = true; - chunk = wrapChunk(chunk); - this.end(chunk, encoding); - } - - res._end = res.end; - res.end = function (chunk, encoding) { - if(chunk){ - this.write(chunk, encoding); - } - if(this.isFinalChunk){ - res._end(); - } - } - - next(); - } -} diff --git a/package.json b/package.json index d8d4528..46dcae7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-stream", - "version": "0.8.0", + "version": "1.0.0-rc.1", "description": "Response streaming middleware for Express 4.", "main": "./expressStream.js", "scripts": {