0.3.0 • Published 7 years ago

koa-ssr v0.3.0

Weekly downloads
13
License
-
Repository
github
Last release
7 years ago

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.

Eg. project

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 directory
  • opts options:

Options

  • index [str] (default: 'index.html') Main index file
  • html [str] Instead of index.html, provide an html string
  • timeout [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 (set DEBUG=koa-ssr:jsdom-client)) console object for JSDOM's virtualConsole 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 of console (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's resourceLoader with an extra argument def to load resources automatically from root.

    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 argument def.

  • modulesLoadedEventLabel [str] (default: 'onModulesLoaded') A special function is attached to window[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 default onload 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 responses

    • false Doesn't uses a cache, JSDOM is run for every request
    • true|{} Uses an object in memory (created or provided) to store JSDOM generated response as {url: body}
    • function Delegate caching and retriving

      Called with args:

      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 final html as a response to the client by setting ctx.body=.

    Called with args:

    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 or cache 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 of cache).

Helpers

Helper functions

  • cacheToDisk Helper function to be used as opts.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 url
    • dir [str] (defaut: '.ssr-cache/') Directory to use for cache files
    • filename [func] (defaut: url => Path.join(opts.dir, (_.kebabCase(url)||'index')+'.html')) Generate filename
    • invalidatePrevious [bool] (defaut: false) Do not use cache created from a previous run