Bartekus/R3Fuse

View on GitHub
client/kit/entry/server.js

Summary

Maintainability
A
2 hrs
Test Coverage
/* eslint-disable no-param-reassign, no-console */

// Server entry point, for Webpack.  This will spawn a Koa web server
// and listen for HTTP requests.  Clients will get a return render of React
// or the file they have requested
//
// Note:  No HTTP optimisation is performed here (gzip, http/2, etc).  Node.js
// will nearly always be slower than Nginx or an equivalent, dedicated proxy,
// so it's usually better to leave that stuff to a faster upstream provider

// ----------------------
// IMPORTS

/* Node */
import path from 'path';

/* NPM */

// Patch global.`fetch` so that Apollo calls to GraphQL work
import 'isomorphic-fetch';

// Needed to read manifest files
import { readFileSync } from 'fs';

// React UI
import React from 'react';

// React utility to transform JSX to HTML (to send back to the client)
import ReactDOMServer from 'react-dom/server';

// Koa 2 web server.  Handles incoming HTTP requests, and will serve back
// the React render, or any of the static assets being compiled
import Koa from 'koa';

// Apollo tools to connect to a GraphQL server.  We'll grab the
// `ApolloProvider` HOC component, which will inject any 'listening' React
// components with GraphQL data props.  We'll also use `getDataFromTree`
// to await data being ready before rendering back HTML to the client
import { ApolloProvider, getDataFromTree } from 'react-apollo';

// HTTP header hardening
import koaHelmet from 'koa-helmet';

// Koa Router, for handling URL requests
import KoaRouter from 'koa-router';

// Static file handler
import koaStatic from 'koa-static';

// High-precision timing, so we can debug response time to serve a request
import ms from 'microseconds';

// React Router HOC for figuring out the exact React hierarchy to display
// based on the URL
import { StaticRouter } from 'react-router';

// <Helmet> component for retrieving <head> section, so we can set page
// title, meta info, etc along with the initial HTML
import Helmet from 'react-helmet';

/* Local */

// Grab the shared Apollo Client
import { serverClient } from 'kit/lib/apollo';

// Custom redux store creator.  This will allow us to create a store 'outside'
// of Apollo, so we can apply our own reducers and make use of the Redux dev
// tools in the browser
import createNewStore from 'kit/lib/redux';

// Initial view to send back HTML render
import Html from 'kit/views/ssr';

// App entry point
import App from 'src/app';

// Import paths.  We'll use this to figure out where our public folder is
// so we can serve static files
import PATHS from 'config/paths';

// ----------------------

// Read in manifest files
const [manifest, chunkManifest] = ['manifest', 'chunk-manifest'].map(
  name => JSON.parse(
    readFileSync(path.resolve(PATHS.dist, `${name}.json`), 'utf8'),
  ),
);

const scripts = [
  'manifest.js',
  'vendor.js',
  'browser.js'].map(key => manifest[key]);

// Port to bind to.  Takes this from the `PORT` environment var, or assigns
// to 4000 by default
const PORT = process.env.PORT || 4000;

// Run the server
(async function server() {
  // Set up routes
  const router = (new KoaRouter())
    // Set-up a general purpose /ping route to check the server is alive
    .get('/ping', async ctx => {
      ctx.body = 'pong';
    })

    // Favicon.ico.  By default, we'll serve this as a 204 No Content.
    // If /favicon.ico is available as a static file, it'll try that first
    .get('/favicon.ico', async ctx => {
      ctx.res.statusCode = 204;
    })

    // Everything else is React
    .get('/*', async ctx => {
      const route = {};

      // Create a new server Apollo client for this request
      const client = serverClient();

      // Create a new Redux store for this request
      const store = createNewStore(client);

      // Generate the HTML from our React tree.  We're wrapping the result
      // in `react-router`'s <StaticRouter> which will pull out URL info and
      // store it in our empty `route` object
      const components = (
        <StaticRouter location={ctx.request.url} context={route}>
          <ApolloProvider store={store} client={client}>
            <App />
          </ApolloProvider>
        </StaticRouter>
      );

      // Wait for GraphQL data to be available in our initial render,
      // before dumping HTML back to the client
      await getDataFromTree(components);

      // Full React HTML render
      const html = ReactDOMServer.renderToString(components);

      // Render the view with our injected React data.  We'll pass in the
      // Helmet component to generate the <head> tag, as well as our Redux
      // store state so that the browser can continue from the server
      ctx.body = `<!DOCTYPE html>\n${ReactDOMServer.renderToStaticMarkup(
        <Html
          html={html}
          head={Helmet.rewind()}
          window={{
            webpackManifest: chunkManifest,
            __STATE__: store.getState(),
          }}
          scripts={scripts}
          css={manifest['browser.css']} />,
      )}`;
    });

  // Start Koa
  (new Koa())

    // Preliminary security for HTTP headers
    .use(koaHelmet())

    // Error wrapper.  If an error manages to slip through the middleware
    // chain, it will be caught and logged back here
    .use(async (ctx, next) => {
      try {
        await next();
      } catch (e) {
        // TODO we've used rudimentary console logging here.  In your own
        // app, I'd recommend you implement third-party logging so you can
        // capture errors properly
        console.log('Error', e.message);
        ctx.body = 'There was an error. Please try again later.';
      }
    })

    // It's useful to see how long a request takes to respond.  Add the
    // timing to a HTTP Response header
    .use(async (ctx, next) => {
      const start = ms.now();
      await next();
      const end = ms.parse(ms.since(start));
      const total = end.microseconds + (end.milliseconds * 1e3) + (end.seconds * 1e6);
      ctx.set('Response-Time', `${total / 1e3}ms`);
    })

    // Serve static files from our dist/public directory, which is where
    // the compiled JS, images, etc will wind up.  Note this is being checked
    // FIRST before any routes -- static files always take priority
    .use(koaStatic(PATHS.public, {
      // All asset names contain the hashes of their contents so we can
      // assume they are immutable for caching
      maxage: 31536000000,
      // Don't defer to middleware.  If we have a file, serve it immediately
      defer: false,
    }))

    // If the requests makes it here, we'll assume they need to be handled
    // by the router
    .use(router.routes())
    .use(router.allowedMethods())

    // Bind to the specified port
    .listen(PORT);
}());