Skip to content

Instantly share code, notes, and snippets.

@mareksuscak
Last active August 29, 2015 14:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mareksuscak/49626aeee0363b5ab77d to your computer and use it in GitHub Desktop.
Save mareksuscak/49626aeee0363b5ab77d to your computer and use it in GitHub Desktop.
RFC: Este.js Part-of-Framework Deployment

Intro

As I mentioned in a comment which is part of the existing este.js deployment issue, we've implemented the application that couldn't be deployed as a node.js server-side app and este.js didn't provide the Part-of-Framework deployment option to deploy it as a static site sitting at the server that serves our backend written in a technology of one's choice (doesn't matter which one). Please note, we don't mind losing all the shiny isomorphic app advantages like the prerendered app returned as a part of the request's response at moment and we know este.js wasn't designed with that in mind. On the other hand este.js dev stack is really one of the best (if not the totally best) out there at the time of writing and that's why we'd like to stick with it.

So I have decided to make the neccessary changes to este.js core myself in our app directly and in a later stage as soon as everything is tested contribute it back to the community. Before diving deep into the details let me explain the deployment scenario first.

Scenario and Issues

We're deploying the este.js app for a preview to GitHub Pages (Project Pages) which means it is served from a subfolder (e.g. http://companyX.github.io/project-name/). In production environment it will probably (TBD yet) live in a subfolder too and if not, it will be served by something like nginx statically.

Now talk a bit about key issues that stopped us from the immediate deployment:

  1. Serving the assets properly
  2. Getting the correct app's index.html (incl. the initial state and the empty app container)
  3. Base URI acknowledgement due to deployment to a subfolder
  • a) <base href="/project-name/" />
  • b) <Route handler={App} path="/project-name/">

1. Serving the assets properly

We've resolved number 1 in a pretty straightforward and I would say elegant way (inspired by the great Ember.js btw) by using <base href="/" /> tag in src/server/frontend/html.react.js file (line 23 of the included file). You can read more about base tag here but to sum up it instructs browser to prepend all the relative links with the value given by its href attribute - and not only links in terms of a tags, but also img's src attributes and others so it basically defines an application base URI - great that's exactly what we needed. Then we had to modify all the links and strip away the initial / character (line 11 of the src/server/frontend/html.react.js and line 59 of the src/server/frontend/render.js. Neat. The assets are being server properly now. Let's go to the next point.

2. Getting the correct app's index.html

We haven't automated this one yet. We've manually downloaded the index.html from the running este's node.js app with env. var NODE_ENV=production and since then we're only updating the initial state as we are adding new state.

3. Base URI acknowledgement

We haven't automated this one either. Point 3a is resolved as a side effect of not automating number 2 yet. So our index.html is fixed on the server and we have the correct base URI in place. Point 3b must be done manually before each production build and that's really a pain to do.

Conclusion

What we aim for is to find a proper solution to points 2, 3a and 3b in a way so it can be merged back to the origin repo. In order to do that I consider these things to be essential to get there:

Solving point 2

We have to find a way of getting the complete bundle during the build incl. index.html containing empty app container. (it might as well be run by using optional build switch) - possibly as a webpack plugin that takes advantage of existing though slightly refactored version of src/server/frontend/render.js.

Solving points 3a and 3b

We have to inject an environment specific base URI to src/server/frontend/html.react.js line 23 as well as src/client/routes.js line 9 and src/server/main.js line 15 by getting its value from a config file.

// src/server/frontend/html.react.js
import Component from '../../client/components/component.react';
import React from 'react';
export default class Html extends Component {
render() {
// Only for production. For dev, it's handled by webpack with livereload.
const linkStyles = this.props.isProduction &&
<link
href={`build/app.css?v=${this.props.version}`}
rel="stylesheet"
/>;
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport" />
<title>{this.props.title}</title>
{linkStyles}
<link href="assets/img/favicon.ico" rel="shortcut icon"/>
<base href="/project-name/"/>
</head>
<body dangerouslySetInnerHTML={{__html: this.props.bodyHtml}} />
</html>
);
}
}
Html.propTypes = {
bodyHtml: React.PropTypes.string.isRequired,
isProduction: React.PropTypes.bool.isRequired,
title: React.PropTypes.string.isRequired,
version: React.PropTypes.string.isRequired
};
// src/server/main.js
import api from './api';
import config from './config';
import express from 'express';
import frontend from './frontend';
import {Server} from 'http';
const app = express();
const server = Server(app);
// Load API
app.use('/api/v1', api);
// Load react-js frontend.
app.use('/project-name', frontend);
// Add error handler. Four arguments need to be defined in order for the
// middleware to act as an error handler.
app.use((err, req, res, next) => {
const msg = err.stack || err;
console.log('Yay', msg);
res.status(500).send('500: ' + msg);
});
server.listen(config.port, () => {
console.log('Server started at port %s', config.port);
});
// src/server/frontend/render.js
import DocumentTitle from 'react-document-title';
import Html from './html.react';
import Immutable from 'immutable';
import Promise from 'bluebird';
import React from 'react';
import Router from 'react-router';
import config from '../config';
import initialState from '../initialstate';
import routes from '../../client/routes';
import {state} from '../../client/state';
import stateMerger from '../lib/merger';
export default function render(req, res, userState = {}) {
const appState = Immutable.fromJS(initialState).mergeWith(stateMerger, userState).toJS();
return renderPage(req, res, appState);
}
function renderPage(req, res, appState) {
return new Promise((resolve, reject) => {
const router = Router.create({
routes,
location: req.originalUrl,
onError: reject,
onAbort: (abortReason) => {
// Some requireAuth higher order component requested redirect.
if (abortReason.constructor.name === 'Redirect') {
const {to, params, query} = abortReason;
const path = router.makePath(to, params, query);
res.redirect(path);
resolve();
return;
}
reject(abortReason);
}
});
router.run((Handler, routerState) => {
const html = preloadAppStateThenRenderHtml(Handler, appState);
const notFound = routerState.routes.some(route => route.name === 'not-found');
const status = notFound ? 404 : 200;
res.status(status).send(html);
resolve();
});
});
}
function preloadAppStateThenRenderHtml(Handler, appState) {
// Load app state for server rendering.
state.load(appState);
return getPageHtml(Handler, appState);
}
function getPageHtml(Handler, appState) {
const appHtml = `<div id="app">${React.renderToString(<Handler />)}</div>`;
const appScriptSrc = config.isProduction
? 'build/app.js?v=' + config.version
: '//localhost:8888/build/app.js';
// Serialize app state for client.
let scriptHtml = `
<script>
(function() {
window._appState = ${JSON.stringify(appState)};
var app = document.createElement('script'); app.type = 'text/javascript'; app.async = true;
var src = '${appScriptSrc}';
app.src = src;
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(app, s);
})();
</script>`;
if (config.isProduction && config.googleAnalyticsId !== 'UA-XXXXXXX-X')
scriptHtml += `
<script>
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
e=o.createElement(i);r=o.getElementsByTagName(i)[0];
e.src='//www.google-analytics.com/analytics.js';
r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
ga('create','${config.googleAnalyticsId}');ga('send','pageview');
</script>`;
const title = DocumentTitle.rewind();
return '<!DOCTYPE html>' + React.renderToStaticMarkup(
<Html
bodyHtml={appHtml + scriptHtml}
isProduction={config.isProduction}
title={title}
version={config.version}
/>
);
}
// src/client/routes.js
import App from './app/app.react';
import Home from './pages/home.react';
import NotFound from './pages/notfound.react';
import React from 'react';
import {DefaultRoute, NotFoundRoute, Route} from 'react-router';
export default (
<Route handler={App} path="/project-name/">
<DefaultRoute handler={Home} name="home" />
<NotFoundRoute handler={NotFound} name="not-found" />
</Route>
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment