I learnt quite a lot in the past week. Firstly there's no substitute to good sleep. Secondly, you can actually eat a salmon steak raw! Thirdly, it's really really hard to hot reload a server bundle if you are writing a universal app.
On the client side, webpack-dev-middleware and react-hot-loader have been around for a while now and have become indispensable to developers. However, what about the server side?
One solution is to use nodemon to restart the server on file changes. This works if you don't bundle your server side code. If you do, you'll have to use a task runner like gulp to first compile your code, then use nodemon to restart the server.
I don't particularly like this solution because I need to introduce gulp and nodemon into my project. I believe webpack can solve anything and everything. So the challenge was set: use only webpack to implement server side bundling and hot reload.
The final product is universal-hot-reload if you want to skip straight to dessert.
We want to use webpack to watch our server files for changes, rebundle on change and restart the express server for changes to take effect.
Watching files for changes is already supported by webpack out of the box so that's easy. For example we can do this:
function watchServerChanges() {
const webpack = require('webpack');
const serverCompiler = webpack(require('path/to/webpack/server/config');
const options = {
aggregateTimeout: 300, // wait so long for more changes
poll: true // use polling instead of native watchers
};
serverCompiler.watch(options, function onServerChange(err, stats) {
if (err) {
console.log('Server bundling error:' + JSON.stringify(err));
return;
}
// TODO: restart express here
});
}
We can call watchServerChanges on our server bootstrap and walah problem solved. Well not quite. How do we tackle restarting express on server file changes?
The http.Server object provides a close method which sounds like it might do the job. For example we can theoretically do this:
function onServerChange(err, stats) {
if (err) {
console.log('Server bundling error:' + JSON.stringify(err));
return;
}
// TODO: somehow get a reference to the running http.Server object;
//const httpServer = ???
httpServer.close(function () {
console.log('Server restarted ' + new Date());
});
}
Two things we need to solve here:
Let's take a look at some code.
require('babel-polyfill');
// require the server entry file where the express server is initialised
// expect the server entry file to export a http.Server object
const httpServer = require('./server');
//... then we can call httpServer.close() at some point later
import express from 'express';
const PORT = 3000;
const app = express();
//... your other express middlewares
// the listen method returns the currently running instance of
// http.Server object
const httpServer = app.listen(PORT, () => {
console.log(`Listening at ${PORT}`);
});
// export httpServer object so we can access it in bootstrap
module.exports = httpServer;
What's happening here:
We are not done yet. Merely setting module.exports will not expose the exported object to the consumer of our bundled js. Essentially we need to create a library out of our app. Meaning the main export of our app needs to be exposed to the consumer. To do this, in your webpack server config file, set output.libraryTarget = 'commonjs2', like so:
const webpack = require('webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
devtool: 'cheap-module-inline-source-map',
// set this to your server entry point. This should be where you start
// your express server with .listen()
entry: './src/server/server.js',
// tell webpack this bundle will be used in nodejs environment.
target: 'node',
// Omit node_modules code from the bundle. You don't want and don't
// need them in the bundle.
externals: [nodeExternals()],
output: {
path: path.resolve('dist'),
filename: 'serverBundle.js',
// IMPORTANT! Add module.exports to the beginning of the bundle // so your main module is exposed to the consumer of the bundle. libraryTarget: 'commonjs2' },
// The rest of the config is pretty standard and can contain
// other webpack stuff you need.
module: {
loaders: [
{
test: /\.json$/,
loader: 'json-loader'
},
{
test: /\.jsx?$/,
loader: 'babel',
include: path.resolve('src')
}]
}
};
Now we have http.Server, we can call close() to stop the server. However as mentioned earlier, close() shuts down the server ONLY when all existing connections have closed. It does not terminate stale or idle keep-alive connections. As a result after close() is invoked, your server will continue to run. It will not accept new connections and it will wait for existing connections to timeout, which is in minutes.
Obviously you can't develop rapidly like this. You can't call this hot-reload, it's more like shit-reload. A little googling reveals a promising solution from stackoverflow by Golo Roden.
The takeaway from that post is that each time a connection is made to our server, we have to keep a reference to the socket object that represents the connection. I store these in a Map for better performance rather than a normal object as suggested in the post. We can then call socket.destroy() in our watch handler for all sockets, which terminates all connections to our http.Server. The close() method can then proceed to shut the server. What does it look like in code?
require('babel-polyfill');
watchServerChanges();
// Starts the express server. Caches sockets and delete those caches
// on socket close. Returns the running http.Server object and the
// socket Map cache.
function initHttpServer() {
// start the server, getting back a reference to http.Server
const httpServer = require('/path/to/server/bundle');
const sockets = new Map(); // cache all sockets in a Map
let nextSocketId = 0;
httpServer.on('connection', function (socket) {
var socketId = nextSocketId++;
sockets.set(socketId, socket);
// remove socket from Map on close.
// this is not where we terminate the socket! That happens
// in webpack.watch done handler below
socket.on('close', function () {
sockets.delete(socketId);
});
});
return {httpServer, sockets};
}
function watchServerChanges() {
const webpack = require('webpack');
// contains the running http.Server and socket cache
let httpServerInitObject;
let initialLoad = true;
const compiler = webpack(require('path/to/webpack/server/config');
const compilerOptions = {
aggregateTimeout: 300, // wait so long for more changes
poll: true // use polling instead of native watchers
};
// watch file changes
compiler.watch(compilerOptions, function onServerChange(err, stats){
if (err) {
console.log('Server bundling error:' + JSON.stringify(err));
return;
}
if (initialLoad) {
// first time run, just start the server, no need to restart
initialLoad = false;
httpServerInitObject = initHttpServer();
console.log('Server bundling done');
} else {
// subsequent runs need to close the server and restart
// call close() method, but this won't complete until all
// sockets are destroyed below.
httpServerInitObject.httpServer.close(function () {
// if we reach this step, that means we have succeeded
// in shutting down the server!!! Omg fuck yea!
httpServerInitObject = initHttpServer(); // re-start
console.log('Server restarted ' + new Date());
});
// This is where the magic happens: destroy all open sockets
for (var socket of httpServerInitObject.sockets.values()) {
socket.destroy();
}
}
});
}
If you try the code above, you realise that it does not freakin work. Why? We are forgetting one thing: node caches all modules when required. How is this a problem? At initial load, initHttpServer() will require your server.bundle.js and node will cache that. Subsequent calls to initHttpServer() will re-use that same server.bundle.js which is not we want. We want to require the newly bundled server.bundle.js which webpack produces. To do this we need to clear the require cache:
function clearCache() {
const cacheIds = Object.keys(require.cache);
for(let id of cacheIds) {
if (id === 'path/to/server/bundle') {
delete require.cache[id];
return;
}
}
}
You call clearCache() in onServerChange callback prior to starting the express server like so:
//... same code as above
// watch file changes
compiler.watch(compilerOptions, function onServerChange(err, stats){
if (err) {
console.log('Server bundling error:' + JSON.stringify(err));
return;
}
// This solves all first world problems clearCache();
//... same as above
});
// ...
You can combine this with existing client hot reload techniques using webpack-dev-server and react-hot-loader to achieve the ultimate universal hot reload experience. I have done this in universal-hot-reload so you can check the source code for that if you are interested. But for now, I'm out of time .. I need to take a shower.
So webpack does solve everything and anything like I said in the beginning.. can it do my laundry and feed my dog too?
The final product is universal-hot-reload. Check it out! Let me know if there's any issues.