Set Your Phasers to the Cloud (Part 2)

By Pete Vanderhoef | Sr. Software Engineer

For this next part we are going setup a basic node/React site (the ultimate goal being a running a Phaser app built on node/React running in AWS). First we’ll dig into the dependencies we need and then we’ll move on to the various bits of code necessary to get up and running.

Development Dependencies

There are many ways one could solve this problem, my approach will rely on the following dependencies:

  1. Node and NPM (Server Framework)
  2. React (Library for building user interfaces)
    npm install --save react react-dom react-router
    
  3. Babel (Transpiler for JavaScript)
    npm install --save babel-cli babel-core babel-preset-es2015 babel-preset-react ejs
    
  4. Express (Web Application Framework)
    npm install --save express
    
  5. Webpack (Bundler for JavaScript)
    npm install --save-dev webpack babel-loader http-server
    

Installing these in the project directory is fairly painless and should only take a few minutes, then we can actually get to the code!

Breaking down the code

Infrastructure

First up, lets lay down some infrastructure for our app…

package.json - This file (at the root of the project) is used to set our project dependancies. For the most part this file is auto-generated.

{
  "name": "phasers-to-the-cloud",
  "version": "1.0.0",
  "description": "Demo - Phaser running in AWS",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server --hot"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "babel-cli": "^6.11.4",
    "babel-core": "^6.13.2",
    "babel-preset-es2015": "^6.24.1"
  },
  "devDependencies": {
    "babel-loader": "^6.2.10",
    "babel-preset-env": "^1.6.0",
    "http-server": "^0.9.0",
    "webpack": "^1.13.3"
  }
}

webpack.config.js - The main function of this file is for defining how to minify our code. If you look below, you’ll see we have an output file (bundle.js) defined for our minified source.

const webpack = require('webpack');
const path = require('path');

module.exports = {
  entry: path.join(__dirname, 'src', 'app-client.js'),
  output: {
    path: path.join(__dirname, 'src', 'static', 'js'),
    filename: 'bundle.js'
  },
  module: {
    loaders: [{
      test: path.join(__dirname, 'src'),
      loader: ['babel-loader'],
      query: {
        cacheDirectory: 'babel_cache',
        presets: ['react', 'es2015']
      }
    }]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    }),
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.optimize.UglifyJsPlugin({
      compress: { warnings: false },
      mangle: true,
      sourcemap: false,
      beautify: false,
      dead_code: true
    })
  ]
};

app-client.js - This file is basically taking our AppRoutes React component (below) and telling the browser to render it inside the element with an Id of ‘main’.

import React from 'react';
import ReactDOM from 'react-dom';
import AppRoutes from './components/AppRoutes';

window.onload = () => {
  ReactDOM.render(<AppRoutes/>, document.getElementById('main'));
};

routes.js - Here we are setting up the routes (urls) for our site (similar to AreaRegestration files from .Net). We are nesting all of our subroutes inside our root route.

import React from 'react'
import { Route, IndexRoute } from 'react-router'
import Layout from './components/Layout';
import IndexPage from './components/IndexPage';
import NotFoundPage from './components/NotFoundPage';

const routes = (
  <Route path="/" component={Layout}>
    <IndexRoute component={IndexPage}/>
    <Route path="*" component={NotFoundPage}/>
  </Route>
);

export default routes;

server.js - Now we are utlizing the Express framework to setup server side routing for the app. We configure the server to handle ejs style views (think scriptlets in webforms or razor views) and to handle a few HTTP status codes.

import path from 'path';
import { Server } from 'http';
import Express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { match, RouterContext } from 'react-router';
import routes from './routes';
import NotFoundPage from './components/NotFoundPage';

// initialize the server and configure support for ejs templates
const app = new Express();
const server = new Server(app);
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// define the folder that will be used for static assets
app.use(Express.static(path.join(__dirname, 'static')));

// universal routing and rendering
app.get('*', (req, res) => {
  match(
    { routes, location: req.url },
    (err, redirectLocation, renderProps) => {

      // in case of error display the error message
      if (err) {
        return res.status(500).send(err.message);
      }

      // in case of redirect propagate the redirect to the browser
      if (redirectLocation) {
        return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
      }

      // generate the React markup for the current route
      let markup;
      if (renderProps) {
        // if the current route matched we have renderProps
        markup = renderToString(<RouterContext {...renderProps}/>);
      } else {
        // otherwise we can render a 404 page
        markup = renderToString(<NotFoundPage/>);
        res.status(404);
      }

      // render the index template with the embedded React markup
      return res.render('index', { markup });
    }
  );
});

// start the server
const port = process.env.PORT || 3000;
const env = process.env.NODE_ENV || 'production';
server.listen(port, err => {
  if (err) {
    return console.error(err);
  }
  console.info(`Server running on http://localhost:${port} [${env}]`);
});

Page Content & React Components

The first thing you’ll notice is that the React components look like they embed HTML inside a JavaScript function. These are actually using a JSX (Javascript XML) extension to give a little more readability to the markup (the HTML esentially compiles down to JavaScript…which then renders back to HTML…).

Layout.js (JSX) - This is our base template for the app. It is configured on the root node (in the routes.js above) of the app, having all other routes/pages/components as children. This allows it to act as a layout file (such as those in .Net MVC apps) to lay down the base markup for the app. All other ‘pages’ will then be injected into this via {this.props.children}.

import React from 'react';
import { Link } from 'react-router';

export default class Layout extends React.Component {
  render() {
    return (
      <div className="app-container">
        <header>
          <Link to="/">
            <img className="logo" src="/img/game_logo.png"/>
          </Link>
        </header>
        <div className="app-content">{this.props.children}</div>
        <footer>
          <p>
            This is a demo app to showcase universal rendering and routing with <strong>React</strong> and <strong>Express</strong>.
          </p>
        </footer>
      </div>
    );
  }
}

IndexPage.js (JSX) - This is a component for the index page, it will get injected into the layout (similar to the behavior of MVC razor views).

import React from 'react';

export default class IndexPage extends React.Component {
  render() {
    return (
      <div>
        Hello World!!!!!!
      </div>
    );
  }
}

NotFoundPage.js (JSX) - Another component but for the 404 error page.

import React from 'react';
import { Link } from 'react-router';

export default class NotFoundPage extends React.Component {
  render() {
    return (
      <div className="not-found">
        <h1>404</h1>
        <h2>Page not found!</h2>
        <p>
          <Link to="/">Go back to the main page</Link>
        </p>
      </div>
    );
  }
}

AppRoutes.js (JSX) - This component is related more to infrastructure of the app. We defined our routes in the routes.js file above, but they aren’t wired up to the app yet. Here we are taking in those routes, definining how to handle browser history and telling the browser to scroll the window back to the top after each route is handled.

import React from 'react';
import { Router, browserHistory } from 'react-router';
import routes from '../routes';

export default class AppRoutes extends React.Component {
  render() {
    return (
      <Router history={browserHistory} routes={routes} onUpdate={() => window.scrollTo(0, 0)}/>
    );
  }
}

index.ejs - Finally, we have the entry point of the app. As opposed to a static .html file, we have this .ejs file that functions similarly to razor views. It provides all the standard markup that a static .html file would, but also gives us access to using server side scriptlets, such as <%- markup -%>.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Set your Phasers to the Cloud - Demo</title>
    <link rel="stylesheet" href="/css/style.css">
  </head>
  <body>
    <div id="main"><%- markup -%></div>
    <script src="/js/bundle.js"></script>
  </body>
</html>

Now that we have all this, we can bundle our source using the following PowerShell command:

set NODE_ENV=production | node_modules/.bin/webpack -p

Finally we can test this out by running the following PowerShell command to start the server (and browse to http://localhost:3000):

set NODE_ENV=production node_modules/.bin/babel-node --presets "react,es2015" src/server.js

…So now we have all of our pieces down for creating a simple single page app running on node and React. In the final post we’ll get into coding with Phaser and getting it all hosted with AWS in Set Your Phasers to…the Cloud!!! (Part 3 - The Game)!

Written on September 22, 2017