koa-ssr
v0.3.0
Published
Koa server-side-rendering middleware using JSDOM
Downloads
8
Readme
koa-ssr
Use JSDOM to evaluate (and cache) your client-side app on server before serving response as HTML in Koa.
Perfect for serving static content generated by webpack production build.
Install
npm i koa-ssr
Usage
import Koa from 'koa';
import koaStatic from 'koa-static';
import koaSSR from 'koa-ssr';
const app = new Koa();
const root = __dirname + '/dist'
// serve static content as usual:
app.use(koaStatic(root, {
// DON'T let index.html be served statically
index: false // << important!
}))
app.use(koaSSR(root, {
// we'll (re-)generate it here
index: 'index.html'
}))
API
koaMiddleware = koaSSR(root, opts)
root
root directoryopts
options:
Options
index
[str]
(default:'index.html'
) Main index filehtml
[str]
Instead of index.html, provide an html stringtimeout
[num]
(default:5000
) After which if JSDOM hasn't finished loading (i.e.window[opts.modulesLoadedEventLabel]
hasn't been called (see below)) it throws an error (with{ koaSSR: {ctx, window} }
property attached).jsdom
[obj]
Config passed to JSDOM: jsdom.jsdom(opts.html, opts.jsdom).Eg. for shimming unimplemented APIs:
koaSSR(root, { jsdom: { created: (e, window) => { window.localStorage = new MockLocalStorage(); }, } })
console
[obj]
(default: modified debug (setDEBUG=koa-ssr:jsdom-client
))console
object for JSDOM'svirtualConsole
used as jsdom.createVirtualConsole().sendTo(console)Eg.
koaSSR(root, { console: console // native console object })
Note: You can also do this manually in
opts.jsdom.virtualConsole
, this is just a shorter version. It also tries to infer the type ofconsole
(checking for.log/err
etc methods) and adds the additional prefixes ('[JSDOM]'
) to messages.resourceLoader
[func]
(default:(res, cb, def) => def(res, cb)
) Wrapper around JSDOM'sresourceLoader
with an extra argumentdef
to load resources automatically fromroot
.Eg.
koaSSR(root, { resourceLoader: (res, cb, def) => { // either load the resource manually fs.readFile(res.url.pathname, 'utf8', cb) // or let koaSSR handle it def(res, cb); // or intercept def(res, (err, body) => { cb(null, body || 'something else') }) } })
Note: You can also provide this option as
opts.jsdom.resourceLoader
but it won't have the additional third argumentdef
.modulesLoadedEventLabel
[str]
(default:'onModulesLoaded'
) A special function is attached towindow[modulesLoadedEventLabel]
which **must be called** to indicate that your app has finished rendering. Failure would result in a timeout and an error thrown (with{ koaSSR: {ctx, window} }
property attached). See JSDOM: Dealing with asynchronous script loading as to why it needs you to do this instead of relying on defaultonload
or other such events. This can also be used as an indicator that your app is being rendered server-side so you may choose to deal with that aspect in your app as well.Eg.
client-app.js
import {h, render} from 'preact' if (window.onModulesLoaded) { // rendered on server const userData = window.userData // as attached in render function below } else { // not rendered on server const userData = localStorage.get('userdata') || await fetch('/api/user...') } render(h('div', {...data}, ['Hello world!']), document.body) if (window.onModulesLoaded) { window.onModulesLoaded(); }
cache
[bool|obj|function]
(default:true
) Whether (and where/how) to cache JSDOM responsesfalse
Doesn't uses a cache, JSDOM is run for every requesttrue|{}
Uses an object in memory (created or provided) to store JSDOM generated response as {url: body}function
Delegate caching and retrivingCalled with args:
ctx
Koa'sctx
[html]
(Pre-)final HTML string to be cached[window]
JSDOM's window object (JSDOM.jsdom(...).defaultView
)[serialize]
Alias forJSDOM.serializeDocument
The optional arguments (html, window, serialize) are passed only when the page was rendered with JSDOM. So when they're not passed, it expects you to return a pre-cached (if available) html string to use as a response instead. With this you can essentially control whether or not to actually invoke JSDOM for each request.
Eg. Caching to disk selectively (this functionality is available as a helper function
cacheToDisk
)const cacheIndex = {} koaSSR(root, { cache: (ctx, html, window, serialize) => { // parse URL and omit query strings const url = URL.parse(ctx.url).pathname; // ignore '?query=xyz' // choose a sanitized filename const filename = '.ssr-cache/' + (_.kebabCase(url) || 'index') + '.html'; // if html is provided, cache it: if (html) { fs.writeFile(filename, html); cacheIndex[filename] = true; return html; // and return it to be rendered } // if html isn't provided... // check if filename was cached if (cacheIndex[filename]) { ctx.type = 'html'; // (override stream's inferred type "application/octet-stream") return fs.createReadStream(filename); } // check if file exists anyways (from a previous run) if (await fs.exists(filename)) { cacheIndex[filename] = true; ctx.type = 'html'; return fs.createReadStream(filename); } // if nothing is returned, JSDOM will be invoked } })
render
[func]
(defaut:(ctx, html) => ctx.body = html
) Final function responsible for sending the finalhtml
as a response to the client by settingctx.body=
.Called with args:
ctx
Koa'sctx
html
Final rendered HTML[window]
JSDOM's window object (JSDOM.jsdom(...).defaultView
)[serialize]
Alias forJSDOM.serializeDocument
The optional arguments (window, serialize) are passed only when the page was rendered with JSDOM (either before caching for the first time, or when
cache
is set to false orcache
function decides not to cache).Use this to customize response (even the cached response) for different users. Eg.
koaSSR(root, { render: async (ctx, html) => { html = html.replace('</body>', ` <script> window.userData = ${await User.findOne(ctx.user)} window.queryData = ${await Search.findResult(ctx.query)} </script> </body>`) ctx.body = html } })
Note that in earlier eg. with
cache
we returned a stream in which case (use stream-replace because)html
here would also have been the same stream object (render
is called with the result ofcache
).
Helpers
Helper functions
cacheToDisk
Helper function to be used asopts.cache
for cahing to disk (as shown above).import koaSSR from 'koa-ssr' import {cacheToDisk} from 'koa-ssr/helpers' koaSSR(root, { cache: cacheToDisk(opts) })
Options:
parseUrl
[func]
(defaut:url => URL.parse(url).pathname
) Parse the urldir
[str]
(defaut:'.ssr-cache/'
) Directory to use for cache filesfilename
[func]
(defaut:url => Path.join(opts.dir, (_.kebabCase(url)||'index')+'.html')
) Generate filenameinvalidatePrevious
[bool]
(defaut:false
) Do not use cache created from a previous run