@creditkarma/hapi-galaxy
v2.0.0
Published
Front-end component rendering plugin for hapi.js.
Downloads
25
Readme
Hapi Galaxy
Front-end component rendering plugin for hapi.js.
Introduction
Use hapi-galaxy to transform any front-end component into server-rendered output.
Motivations
- Provide a bridge between Credit Karma's server side JavaScript rendering conventions and our Web App servers.
Front End code ships on-demand and often, so rendering servers need to be able to execute newly discovered modules safely and consistently.
- Isolated, multi-tennant rendering.
We need to render many versions of the same module at the same time, so every front end application gets rendered in a dedicated process using worker-farm.
- Programmatic integration with any framework that supports server side rendering.
Next.js, create-react-app, Vue.js, Ember, or even vanilla string templates will work. Not tied to any client side framework.
Assumptions
- Your front end code lives in some other module
- You have somehow packaged and exported that module so that is an entry point intended for server-side rendering
- e.g., a React app compiled with Babel and Webpack, without any references to browser specific globals in the runtime
- "Bring your own render method"
- Runtime errors are bad and make universal rendering unpredictable
- Requiring your entire application's dependency tree is onerous
- All renders are wrapped in a promise
- Dependencies for render belong to the front end application
- We use React today, but we may use something else tomorrow
Usage
Install hapi-galaxy and add it to your Hapi.js project's dependencies
npm install @creditkarma/hapi-galaxy --save
Then, register the plugin with your Hapi.js server:
server.register(require('hapi-galaxy'), pluginErr => {
if (pluginErr) throw pluginErr
server.route({
path: '/',
method: 'GET',
handler: {
galaxy: {
component: require.resolve('./frontend/component')
}
}
})
server.start((err) => {
console.log(`Server started at: ${server.info.uri}`);
})
})
Where require.resolve('./frontend/component')
is the absolute path to any module that uses the client interface, which means any async
function will work. This means that the minimum viable "front end component" would look like this:
module.exports = (props, path) => Promise.resolve('<h1>Hello World</h1>')
Options
The galaxy handler object has the following properties:
component
— a string with the name of the module to use whenrequire
'ing your FE code, or function with the signaturefunction(props, location)
which returns a promise that resolves with the rendered output of your component.- See the client interface guidelines for more details.
layout
– function with the signaturefunction(props, content)
which wraps your component output, presumably with in an HTML document with head and body tags- If no layout function is provided a generic HTML 5 layout will be used.
layout: false
will disable the default view.- Will be ignored if
view
option is provided.
path
— current url being requested. Useful for handling route logic within a bundled component.props
— object containing any additional properties to be passed to the view.view
– string corresponding to the vision view template to be used. The view template with be the output of the render and props used ascontent
andprops
, respectively.workerFarm
— object containing any configuration options for worker-farm
reply.galaxy(component, options, errorCallback)
component
— a string or function, following the same restrictions as thecomponent
option defined aboveoptions
— an object including the same keys and restrictions defined by the routegalaxy
handler options, excluding the componenterrorCallback
— a function with the signaturefunction(err)
which should handle the error returned
Client Interface
So, here's the thing:
Client code can throw errors when you try to render it on the server.
Obviously, in a perfect world this doesn't happen very often. But since it can, and since we don't want the output of random client errors manifesting themselves on a production server, your render method needs to be a Promise that resolves with the successful output of your render.
Here's what a simple Client would look like:
In frontend/component.js
'use strict'
import React from 'react'
import { renderToString } from 'react-dom/server'
import MyApp from './app'
export default props =>
new Promise((resolve, reject) => {
resolve(renderToString(<MyApp {...props} />))
})
In server.js
server.route({
path: '/',
method: 'GET',
handler: {
galaxy: {
component: require.resolve('./frontend/component')
}
}
})
This has several benefits over error-first callbacks for our use-case:
- It provides a consistent way of preventing client code from throwing errors in your server process. Consider the following:
- The server is a secondary runtime for the code being rendered and therefore it would be inappropriate to let the client code throw errors.
- We're delegating the render method to the client code, error-first callbacks would require a try/catch to do correctly.
- Error-swallowing becomes a feature as it provides de facto encapsulation.
- Promises are a requirement for using fetch and associated libraries).
- Async/await will be ready soon without compilation and it's interface is compatible with promises. Next.js uses this pattern to great effect and it contributes to a really nice developer experience.