Merge pull request #13 from yoshuawuyts/rehydrate
toString: support rehydration
This commit is contained in:
@@ -21,6 +21,7 @@ productive package.
|
||||
- [keyboard](#keyboard)
|
||||
- [websockets](#websockets)
|
||||
- [Rendering in Node](#rendering-in-node)
|
||||
- [Rehydration](#rehydration)
|
||||
- [API](#api)
|
||||
- [FAQ](#faq)
|
||||
- [Installation](#installation)
|
||||
@@ -310,9 +311,10 @@ queueing data. You might want to use a package from `npm` or [write your
|
||||
own][ws-reconnect] if you're building something for production.
|
||||
|
||||
## 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][isomorphic]_.
|
||||
Sometimes it's necessary to render code inside of Node; for serving hyper fast
|
||||
first requests, testing or other purposes. Applications that are capable of
|
||||
being rendered in both Node and the browser are called
|
||||
_[isomorphic][isomorphic]_.
|
||||
|
||||
Rendering in Node is slightly different than in the browser. First off, to
|
||||
maintain performance all calls to `subscriptions`, `effects`, and `reducers`
|
||||
@@ -352,6 +354,45 @@ if (module.parent) module.exports = app
|
||||
else document.body.appendChild(app.start())
|
||||
```
|
||||
|
||||
### Rehydration
|
||||
Now that your application is succesfully rendering in Node, the next step would
|
||||
be to make it load a JavaScript bundle once has loaded the HTML. To do this we
|
||||
will use a technique called _rehydration_.
|
||||
|
||||
_Rehydration_ is when you take the static, server-rendered version of your
|
||||
application (static HTML, _dehydrated_ because it has no logic) and _rehydrate_
|
||||
it by booting up the JS and attaching event handlers on the DOM to make it
|
||||
dynamic again. It's like restoring flavor to cup noodles by adding hot water.
|
||||
|
||||
Because we're using something called `morphdom` under the hood, all we need is
|
||||
point at an `id` at the root of the application. The syntax for this is
|
||||
slightly different from what we've seen so far, because we're _updating_ a
|
||||
dehydrated DOM nodes to make them dynamic, rather than a new DOM tree and
|
||||
attaching it to the DOM.
|
||||
```js
|
||||
const choo = require('choo')
|
||||
const app = choo()
|
||||
|
||||
app.router((route) => [
|
||||
route('/', (params, state, send) => choo.view`
|
||||
<h1 id="app-root">${state.message}</h1>
|
||||
`)
|
||||
])
|
||||
|
||||
if (module.parent) module.exports = app
|
||||
else app.start('#app-root'))
|
||||
```
|
||||
|
||||
When the JS is booted on top of the dehydrated application, it will look for
|
||||
the `#app-root` id and load on top of it. You can choose any name you like for
|
||||
the id, but __make sure it's the same on every possible top level DOM node__,
|
||||
or else things might break. Furthermore to ensure things go smoothly, try and
|
||||
keep the initial state identical on both the server and the client.
|
||||
|
||||
And that's it! If you want to go down the route of mad performance, consider
|
||||
make all first request static and caching them using something like [bl][bl],
|
||||
[nginx][nginx], [varnish][varnish] or a global CDN.
|
||||
|
||||
## API
|
||||
### app = choo()
|
||||
Create a new `choo` app
|
||||
@@ -404,7 +445,7 @@ for the past year. I originally used `virtual-dom` with `virtual-app` and
|
||||
`wayfarer` where now it's `yo-yo` with `send-action` and `sheet-router`. The
|
||||
main benefit of using `choo` over these technologies separately is that it
|
||||
becomes easier for teams to pick up and gather around. The code base for `choo`
|
||||
itself is super petite (`~150` LOC) and mostly acts to enforce structure around
|
||||
itself is super petite (`~200` LOC) and mostly acts to enforce structure around
|
||||
some excellent npm packages. This is my take on modular frameworks; I hope
|
||||
you'll find it pleasant.
|
||||
|
||||
@@ -539,3 +580,6 @@ $ npm install choo
|
||||
[module-parent]: https://nodejs.org/dist/latest-v6.x/docs/api/modules.html#modules_module_parent
|
||||
[sse-reconnect]: http://stackoverflow.com/questions/24564030/is-an-eventsource-sse-supposed-to-try-to-reconnect-indefinitely
|
||||
[ws-reconnect]: http://stackoverflow.com/questions/13797262/how-to-reconnect-to-websocket-after-close-connection
|
||||
[bl]: https://github.com/rvagg/bl
|
||||
[varnish]: https://varnish-cache.org
|
||||
[nginx]: http://nginx.org/
|
||||
|
||||
@@ -4,6 +4,14 @@ const mainView = require('./views/main')
|
||||
|
||||
const app = choo()
|
||||
|
||||
app.model({
|
||||
namespace: 'message',
|
||||
state: {
|
||||
server: 'rehydration has kicked in, server data was tossed',
|
||||
client: 'hello client!'
|
||||
}
|
||||
})
|
||||
|
||||
app.router((route) => [
|
||||
route('/', mainView)
|
||||
])
|
||||
@@ -11,6 +19,5 @@ app.router((route) => [
|
||||
if (module.parent) {
|
||||
module.exports = app
|
||||
} else {
|
||||
const tree = app.start()
|
||||
document.body.appendChild(tree)
|
||||
app.start('#app-root')
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bankai": "^2.0.2",
|
||||
"bl": "^1.1.2",
|
||||
"browserify": "^13.0.1",
|
||||
"hyperstream": "^1.2.2",
|
||||
"server-router": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const serverRouter = require('server-router')
|
||||
const hyperstream = require('hyperstream')
|
||||
const browserify = require('browserify')
|
||||
const bankai = require('bankai')
|
||||
const http = require('http')
|
||||
|
||||
const PORT = 8080
|
||||
@@ -17,18 +20,26 @@ server.listen(PORT, () => process.stdout.write(`listening on port ${PORT}\n`))
|
||||
// null -> fn
|
||||
function createRouter () {
|
||||
const apiRouter = serverRouter('/404')
|
||||
apiRouter.on('/404', (req, res) => res.end('not found'))
|
||||
apiRouter.on('/', (req, res) => {
|
||||
res.end(JSON.stringify({ routes: [ '/', '/404' ] }))
|
||||
})
|
||||
|
||||
apiRouter.on('/404', (req, res) => res.end('404 not found'))
|
||||
apiRouter.on('/', (req, res) => res.end('nothing to be found here'))
|
||||
|
||||
const js = bankai.js(browserify, require.resolve('./client.js'))
|
||||
apiRouter.on('/bundle.js', (req, res) => js(req, res).pipe(res))
|
||||
|
||||
return apiRouter
|
||||
}
|
||||
|
||||
// render the client to string
|
||||
// based on the requested url
|
||||
// (obj, obj) -> null
|
||||
const createIndex = bankai.html({ favicon: false, css: false })
|
||||
function handleHtml (req, res) {
|
||||
const html = client.toString(req.url, { message: 'hello server!' })
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
res.end(html)
|
||||
|
||||
const state = { message: { server: 'hello server!' } }
|
||||
const inner = client.toString(req.url, state)
|
||||
const hs = hyperstream({ 'body': { _appendHtml: inner } })
|
||||
|
||||
createIndex(req, res).pipe(hs).pipe(res)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
const assert = require('assert')
|
||||
const choo = require('../../../')
|
||||
|
||||
module.exports = function (params, state, send) {
|
||||
const message = state.message
|
||||
const serverMessage = state.message.server
|
||||
const clientMessage = state.message.client
|
||||
|
||||
assert.equal(typeof serverMessage, 'string', 'server should be a string')
|
||||
assert.equal(typeof clientMessage, 'string', 'server should be a string')
|
||||
|
||||
return choo.view`
|
||||
<section>
|
||||
<h1>${message}</h1>
|
||||
<section id="app-root">
|
||||
<h1>server message: ${serverMessage}</h1>
|
||||
<h1>client message: ${clientMessage}</h1>
|
||||
<p>${`
|
||||
The first message is passed in by the server on compile time,
|
||||
the second message was set by the client.
|
||||
The more static the data you pass in, the more cachable your site
|
||||
beocmes (and thus performant). Try and keep the amount of properties
|
||||
you pass in on the server to a minimum for most applications - it'll
|
||||
make life a lot easier in the long run, hah.
|
||||
`}</p>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -25,14 +25,23 @@ function choo () {
|
||||
|
||||
// render the application to a string
|
||||
// (str, obj) -> str
|
||||
function toString (route, state) {
|
||||
function toString (route, serverState) {
|
||||
const initialState = {}
|
||||
const nsState = {}
|
||||
|
||||
_models.forEach(function (model) {
|
||||
if (model.state) apply(model.name, model.state, initialState)
|
||||
const ns = model.namespace
|
||||
if (ns) {
|
||||
if (!nsState[ns]) nsState[ns] = {}
|
||||
apply(ns, model.state, nsState)
|
||||
nsState[ns] = xtend(nsState[ns], serverState[ns])
|
||||
} else {
|
||||
apply(model.namespace, model.state, initialState)
|
||||
}
|
||||
})
|
||||
|
||||
const tree = _router(route, xtend(initialState, state), function () {
|
||||
const state = xtend(initialState, xtend(serverState, nsState))
|
||||
const tree = _router(route, state, function () {
|
||||
throw new Error('send() cannot be called on the server')
|
||||
})
|
||||
|
||||
@@ -40,8 +49,9 @@ function choo () {
|
||||
}
|
||||
|
||||
// start the application
|
||||
// obj -> DOMNode
|
||||
function start (opts) {
|
||||
// (str?, obj?) -> DOMNode
|
||||
function start (rootId, opts) {
|
||||
if (!opts) opts = rootId
|
||||
opts = opts || {}
|
||||
const name = opts.name || 'choo'
|
||||
const initialState = {}
|
||||
@@ -75,15 +85,32 @@ function choo () {
|
||||
}
|
||||
})
|
||||
|
||||
// the rootId is determined to find the application root
|
||||
// If an id is provided, the application will rehydrate
|
||||
// on the node. If no id is provided it will return
|
||||
// a tree that's ready to be appended to the DOM.
|
||||
//
|
||||
// The rootId is determined to find the application root
|
||||
// on update. Since the DOM nodes change between updates,
|
||||
// we must call document.querySelector() to find the root.
|
||||
// Use different names when loading multiple choo applications
|
||||
// on the same page
|
||||
const rootId = name + '-root'
|
||||
if (rootId) {
|
||||
document.addEventListener('DOMContentLoaded', function (event) {
|
||||
rootId = rootId.replace(/^#/, '')
|
||||
|
||||
const oldTree = document.querySelector('#' + rootId)
|
||||
assert.ok(oldTree, 'could not find node #' + rootId)
|
||||
|
||||
const newTree = _router(send.state().app.location, send.state(), send)
|
||||
|
||||
yo.update(oldTree, newTree)
|
||||
})
|
||||
} else {
|
||||
rootId = name + '-root'
|
||||
const tree = _router(send.state().app.location, send.state(), send)
|
||||
tree.setAttribute('id', rootId)
|
||||
return tree
|
||||
}
|
||||
|
||||
// handle an action by either reducers, effects
|
||||
// or both - return the new state when done
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
const test = require('tape')
|
||||
const choo = require('./')
|
||||
|
||||
test('should assert input types', function (t) {
|
||||
t.plan(1)
|
||||
t.throws(choo)
|
||||
})
|
||||
+28
-1
@@ -30,7 +30,7 @@ tape('should render on the server', function (t) {
|
||||
t.equal(html, expected, 'strings are equal')
|
||||
})
|
||||
|
||||
t.test('should extend existing models', function (t) {
|
||||
t.test('should extend flat existing models', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const app = choo()
|
||||
@@ -47,6 +47,33 @@ tape('should render on the server', function (t) {
|
||||
t.equal(html, expected, 'strings are equal')
|
||||
})
|
||||
|
||||
t.test('should extend namespaced existing models', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
const app = choo()
|
||||
app.model({
|
||||
namespace: 'hello',
|
||||
state: { bin: 'baz', beep: 'boop' }
|
||||
})
|
||||
app.router((route) => [
|
||||
route('/', function (params, state) {
|
||||
return choo.view`
|
||||
<h1>${state.hello.foo} ${state.hello.bin} ${state.hello.beep}</h1>
|
||||
`
|
||||
})
|
||||
])
|
||||
|
||||
const state = {
|
||||
hello: {
|
||||
foo: 'bar!',
|
||||
beep: 'beep'
|
||||
}
|
||||
}
|
||||
const html = app.toString('/', state)
|
||||
const expected = '<h1>bar! baz beep</h1>'
|
||||
t.equal(html, expected, 'strings are equal')
|
||||
})
|
||||
|
||||
t.test('should throw if called without route', function (t) {
|
||||
t.plan(1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user