From 4084775c8c9bb9600d24b5b6be537c3a5494468d Mon Sep 17 00:00:00 2001 From: Yoshua Wuyts Date: Sat, 21 May 2016 21:59:03 +0900 Subject: [PATCH] choo: add server rendering --- README.md | 75 +++++++++++++++++++++++++++++++++-- examples/server/client.js | 16 ++++++++ examples/server/package.json | 13 ++++++ examples/server/server.js | 22 ++++++++++ examples/server/views/main.js | 10 +++++ index.js | 19 ++++++++- 6 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 examples/server/client.js create mode 100644 examples/server/package.json create mode 100644 examples/server/server.js create mode 100644 examples/server/views/main.js diff --git a/README.md b/README.md index 59d745c..f6cd00e 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ productive package. - [server sent events](#server-sent-events-sse) - [keyboard](#keyboard) - [websockets](#websockets) +- [Rendering in Node](#rendering-in-node) - [API](#api) - [FAQ](#faq) - [Installation](#installation) @@ -27,7 +28,7 @@ productive package. ## Features - __minimal size:__ weighing under `8kb`, `choo` is a tiny little framework - __single state:__ immutable single state helps reason about changes -- __small api:__ with only 5 methods, there's not a lot to learn +- __small api:__ with only 6 methods, there's not a lot to learn - __minimal tooling:__ built for the cutting edge `browserify` compiler - __transparent side effects:__ using "effects" and "subscriptions" brings clarity to IO @@ -223,6 +224,52 @@ app.model({ }) ``` +## Rendering in Node +Sometimes it's necessary to render code inside of Node; for serving first +requests, testing or other purposes. Applications that are capable of being +rendered in both Node and the browser are called _[isomorphic +applications][isomorphic]_. + +Rendering in Node is slightly different than in the browser. First off, to +maintain performance all calls to `subscriptions`, `effects`, and `reducers` +are disabled. That means you need to know what the state of your application is +going to be _before_ you render it - no cheating! + +Secondly, the `send()` method inside `router` and `view` has been disabled. If +you call it your program will crash (give it a try, just to see!) + +Disabling all these things means that your program will render [`O(n)`][big-o] +(computer science speak for "constant time"), which is super neat. Off to +[10.000 QPS][qps] we go! + +To render in Node call the `.toString()` method instead of `.start()`. The +first argument is the path that should be rendered, the second is the state: +```js +const http = require('http') +const client = require('./client') // path to client entry point +http.createServer(function (req, res) { + const html = client.toString('/', { message: 'hello server!' }) + res.setHeader('Content-Type', 'text/html; charset=utf-8') + res.end(html) +}) +``` + +In order to make our `choo` app call `app.start()` in the browser and be +`require()`-able in Node, we check if [`module.parent`][module-parent] exists: +```js +const choo = require('choo') +const app = choo() + +app.router((route) => [ + route('/', (params, state, send) => choo.view` +

${state.message}

+ `) +]) + +if (module.parent) module.exports = app +else document.body.appendChild(app.start()) +``` + ## API ### app = choo() Create a new `choo` app @@ -252,6 +299,12 @@ Creates a new router. See documentation. Registered views have a signature of `(params, state, send)`, where `params` is URI partials. +### html = app.toString(route, state) +Render the application to a string. Useful for rendering on the server. First +argument is a path that's passed to the router. Second argument is the state +object. When calling `.toString()` instead of `.start()`, all calls to `send()` +are disabled, and `subscriptions`, `effects` and `reducers` aren't loaded. + ### tree = app.start() Start the application. Returns a DOM element that can be mounted using `document.body.appendChild()`. @@ -263,21 +316,28 @@ Start the application. Returns a DOM element that can be mounted using - __preact:__ [tbi] - __angular2:__ [tbi] -## Which packages was choo built on? +### Which packages was choo built on? - __views:__ [`yo-yo`](https://github.com/maxogden/yo-yo) - __models:__ [`send-action`](https://github.com/sethvincent/send-action), [`xtend`](https://github.com/raynos/xtend) - __routes:__ [`sheet-router`](https://github.com/yoshuawuyts/sheet-router) - __http:__ [`xhr`](https://github.com/Raynos/xhr) -## What packages do you recommend to pair with choo? +### Does choo use a virtual-dom? +`choo` uses [morphdom][morphdom], which diffs real DOM nodes instead of virtual +nodes. It turns out that [browsers are actually ridiculously good at dealing +with DOM nodes][morphdom-bench], and it has the added benefit of working with +_any_ library that produces valid DOM nodes. So to put a long answer short: +we're using something even better. + +### What packages do you recommend to pair with choo? - [tachyons](https://github.com/tachyons-css/tachyons) - functional CSS for humans - [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for browserify - [pull-stream](https://github.com/pull-stream/pull-stream) - minimal streams -## How can I optimize choo? +### How can I optimize choo? To bring down file size, consider running the following `browserify` transforms: - [unassertify](https://github.com/twada/unassertify) - remove `assert()` @@ -307,7 +367,14 @@ $ npm install choo [9]: https://npmjs.org/package/choo [10]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square [11]: https://github.com/feross/standard + [dom]: https://en.wikipedia.org/wiki/Document_Object_Model [keyboard-support]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Browser_compatibility [sse]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events [ws]: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API +[isomorphic]: https://en.wikipedia.org/wiki/Isomorphism +[big-o]: https://rob-bell.net/2009/06/a-beginners-guide-to-big-o-notation/ +[qps]: https://en.wikipedia.org/wiki/Queries_per_second +[morphdom]: https://github.com/patrick-steele-idem/morphdom +[morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks +[module-parent]: https://nodejs.org/dist/latest-v6.x/docs/api/modules.html#modules_module_parent diff --git a/examples/server/client.js b/examples/server/client.js new file mode 100644 index 0000000..16d09b9 --- /dev/null +++ b/examples/server/client.js @@ -0,0 +1,16 @@ +const choo = require('../../') + +const mainView = require('./views/main') + +const app = choo() + +app.router((route) => [ + route('/', mainView) +]) + +if (module.parent) { + module.exports = app +} else { + const tree = app.start() + document.body.appendChild(tree) +} diff --git a/examples/server/package.json b/examples/server/package.json new file mode 100644 index 0000000..687cd91 --- /dev/null +++ b/examples/server/package.json @@ -0,0 +1,13 @@ +{ + "name": "http", + "private": true, + "main": "client.js", + "scripts": { + "start": "NODE_ENV=development node server.js" + }, + "dependencies": { + "bankai": "^2.0.2", + "browserify": "^13.0.1", + "server-router": "^2.1.0" + } +} diff --git a/examples/server/server.js b/examples/server/server.js new file mode 100644 index 0000000..051d06c --- /dev/null +++ b/examples/server/server.js @@ -0,0 +1,22 @@ +const serverRouter = require('server-router') +const http = require('http') + +const PORT = 8080 + +const client = require('./client') + +const server = http.createServer(createRouter()) +server.listen(PORT, () => console.log(`listening on port ${PORT}`)) + +function createRouter () { + const router = serverRouter('/404') + + router.on('/404', (req, res) => res.end('not found')) + router.on('/', function (req, res, params) { + const html = client.toString('/', { message: 'hello server!' }) + res.setHeader('Content-Type', 'text/html; charset=utf-8') + res.end(html) + }) + + return router +} diff --git a/examples/server/views/main.js b/examples/server/views/main.js new file mode 100644 index 0000000..c0dd659 --- /dev/null +++ b/examples/server/views/main.js @@ -0,0 +1,10 @@ +const choo = require('../../../') + +module.exports = function (params, state, send) { + const message = state.message + return choo.view` +
+

${message}

+
+ ` +} diff --git a/index.js b/index.js index ed68c25..2a7d956 100644 --- a/index.js +++ b/index.js @@ -14,15 +14,31 @@ module.exports = choo function choo (opts) { opts = opts || {} const name = opts.name || 'choo' + const _models = [] var _router = null - var _models = [ appInit(opts) ] + start.toString = toString start.router = router start.model = model start.start = start return start + // render the application to a string + function toString (route, state) { + const initialState = {} + + _models.forEach(function (model) { + if (model.state) apply(model.name, model.state, initialState) + }) + + const tree = _router(route, xtend(initialState, state), function () { + throw new Error('send() cannot be called on the server') + }) + + return tree.toString() + } + // start the application // null -> DOMNode function start () { @@ -30,6 +46,7 @@ function choo (opts) { const reducers = {} const effects = {} + _models.push(appInit(opts)) _models.forEach(function (model) { if (model.state) apply(model.name, model.state, initialState) if (model.reducers) apply(model.name, model.reducers, reducers)