Add webpack cold reloading

On changes to the react bundle
webpack will store the current redux state
in localStorage, waits (to allow the server to restart)
then refreshes the page. On page load, it checks if it
has state stored and loads it into the app.
This commit is contained in:
Berkeley Martinez
2016-03-14 17:22:56 -07:00
parent 6898d961bf
commit 4e12c45057
10 changed files with 114 additions and 83 deletions

16
client/cold-reload.js Normal file
View File

@ -0,0 +1,16 @@
import store from 'store';
const key = '__cold-storage__';
export function isColdStored() {
return store.has(key);
}
export function getColdStorage() {
const coldReloadData = store.get(key);
store.remove(key);
return coldReloadData;
}
export function saveToColdStorage(data) {
store.set(key, data);
}

View File

@ -6,7 +6,7 @@ import { Router } from 'react-router';
import { routeReducer as routing, syncHistory } from 'react-router-redux';
import { createHistory } from 'history';
import app$ from '../common/app';
import createApp from '../common/app';
import provideStore from '../common/app/provide-store';
// client specific sagas
@ -14,20 +14,26 @@ import sagas from './sagas';
// render to observable
import render from '../common/app/utils/render';
import {
isColdStored,
getColdStorage,
saveToColdStorage
} from './cold-reload';
Rx.config.longStackSupport = !!debug.enabled;
const log = debug('fcc:client');
const DOMContainer = document.getElementById('fcc');
const initialState = window.__fcc__.data;
const hotReloadTimeout = 5000;
const csrfToken = window.__fcc__.csrf.token;
const initialState = isColdStored() ?
getColdStorage() :
window.__fcc__.data;
initialState.app.csrfToken = csrfToken;
const serviceOptions = { xhrPath: '/services', context: { _csrf: csrfToken } };
Rx.config.longStackSupport = !!debug.enabled;
const history = createHistory();
const appLocation = history.createLocation(
location.pathname + location.search
);
const routingMiddleware = syncHistory(history);
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
@ -35,9 +41,8 @@ const shouldRouterListenForReplays = !!window.devToolsExtension;
const clientSagaOptions = { doc: document };
// returns an observable
app$({
location: appLocation,
createApp({
history,
serviceOptions,
initialState,
@ -48,24 +53,23 @@ app$({
reducers: { routing },
enhancers: [ devTools ]
})
.flatMap(({ props, store }) => {
// because of weirdness in react-routers match function
// we replace the wrapped returned in props with the first one
// we passed in. This might be fixed in react-router 2.0
props.history = history;
.doOnNext(({ store }) => {
if (shouldRouterListenForReplays && store) {
log('routing middleware listening for replays');
routingMiddleware.listenForReplays(store);
}
log('rendering');
return render(
if (module.hot && typeof module.hot.accept === 'function') {
module.hot.accept('../common/app', function() {
saveToColdStorage(store.getState());
setTimeout(() => window.location.reload(), hotReloadTimeout);
});
}
})
.doOnNext(() => log('rendering'))
.flatMap(({ props, store }) => render(
provideStore(React.createElement(Router, props), store),
DOMContainer
);
})
))
.subscribe(
() => debug('react rendered'),
err => { throw err; },

View File

@ -21,7 +21,9 @@ var Rx = require('rx'),
sourcemaps = require('gulp-sourcemaps'),
// react app
webpack = require('webpack-stream'),
webpack = require('webpack'),
webpackStream = require('webpack-stream'),
WebpackDevServer = require('webpack-dev-server'),
webpackConfig = require('./webpack.config.js'),
webpackConfigNode = require('./webpack.config.node.js'),
@ -55,7 +57,6 @@ var paths = {
serverIgnore: [
'gulpfile.js',
'public/',
'!public/js/bundle*',
'node_modules/',
'client/',
'seed',
@ -225,7 +226,6 @@ var syncDepenedents = [
'js',
'less',
'dependents',
'pack-watch',
'build-manifest'
];
@ -269,7 +269,7 @@ gulp.task('pack-client', function() {
return gulp.src(webpackConfig.entry)
.pipe(plumber({ errorHandler: errorHandler }))
.pipe(webpack(Object.assign(
.pipe(webpackStream(Object.assign(
{},
webpackConfig,
webpackOptions
@ -289,51 +289,47 @@ gulp.task('pack-client', function() {
.pipe(gulp.dest(paths.manifest));
});
var defaultStatsOptions = {
colors: gutil.colors.supportsColor,
hash: false,
timings: false,
chunks: false,
chunkModules: false,
modules: false,
children: true,
version: true,
cached: false,
cachedAssets: false,
reasons: false,
source: false,
errorDetails: false
};
var webpackCalled = false;
gulp.task('pack-watch', function(cb) {
gulp.task('webpack-dev-server', function(cb) {
if (webpackCalled) {
console.log('webpack watching already runnning');
console.log('webpack dev server already runnning');
return cb();
}
gulp.src(webpackConfig.entry)
.pipe(plumber({ errorHandler: errorHandler }))
.pipe(webpack(Object.assign(
{},
webpackConfig,
webpackOptions,
{ watch: true }
), null, function(notUsed, stats) {
if (stats) {
gutil.log(stats.toString(defaultStatsOptions));
var devServerOptions = {
headers: {
'Access-Control-Allow-Credentials': 'true'
},
hot: true,
noInfo: true,
contentBase: false,
publicPath: '/js'
};
webpackConfig.entry = [
'webpack-dev-server/client?http://localhost:2999/',
'webpack/hot/dev-server'
].concat(webpackConfig.entry);
var compiler = webpack(webpackConfig);
var devServer = new WebpackDevServer(compiler, devServerOptions);
devServer.use(function(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
next();
});
return devServer.listen('2999', 'localhost', function(err) {
if (err) {
throw new gutil.PluginError('webpack-dev-server', err);
}
if (!webpackCalled) {
debug('webpack init completed');
gutil.log('[webpack-dev-server]', 'webpack init completed');
webpackCalled = true;
cb();
}
}))
.pipe(gulp.dest(webpackConfig.output.path));
});
});
gulp.task('pack-watch-manifest', ['pack-watch'], function() {
gulp.task('pack-watch-manifest', function() {
var manifestName = 'react-manifest.json';
var dest = webpackConfig.output.path;
return gulp.src(dest + '/bundle.js')
@ -520,8 +516,6 @@ var watchDependents = [
'dependents',
'serve',
'sync',
'pack-watch',
'pack-watch-manifest',
'build-manifest'
];
@ -540,14 +534,12 @@ gulp.task('watch', watchDependents, function() {
['dependents']
);
gulp.watch(paths.manifest + '/*.json', ['build-manifest-watch']);
gulp.watch(webpackConfig.output.path + '/bundle.js', ['pack-watch-manifest']);
});
gulp.task('default', [
'less',
'serve',
'pack-watch',
'pack-watch-manifest',
'webpack-dev-server',
'build-manifest-watch',
'watch',
'sync'

View File

@ -135,6 +135,7 @@
"url-regex": "^3.0.0",
"validator": "^5.0.0",
"webpack": "^1.9.12",
"webpack-dev-server": "^1.14.1",
"webpack-stream": "^3.1.0",
"xss-filters": "^1.2.6",
"yargs": "^4.1.0",

View File

@ -5,7 +5,7 @@ import debug from 'debug';
import renderToString from '../../common/app/utils/render-to-string';
import provideStore from '../../common/app/provide-store';
import app$ from '../../common/app';
import createApp from '../../common/app';
const log = debug('fcc:react-server');
@ -14,7 +14,8 @@ const log = debug('fcc:react-server');
const routes = [
'/videos',
'/videos/*',
'/challenges'
'/challenges',
'/map'
];
const devRoutes = [];
@ -37,9 +38,9 @@ export default function reactSubRouter(app) {
function serveReactApp(req, res, next) {
const serviceOptions = { req };
app$({
location: req.path,
serviceOptions
createApp({
serviceOptions,
location: req.path
})
// if react-router does not find a route send down the chain
.filter(({ redirect, props }) => {
@ -47,7 +48,7 @@ export default function reactSubRouter(app) {
res.redirect(redirect.pathname + redirect.search);
}
if (!props) {
log(`react tried to find ${location.pathname} but got 404`);
log(`react tried to find ${req.path} but got 404`);
return next();
}
return !!props;

View File

@ -5,7 +5,11 @@ let trusted = [
];
if (process.env.NODE_ENV !== 'production') {
trusted.push('ws://localhost:3001');
trusted = trusted.concat([
'ws://localhost:3001',
'http://localhost:2999',
'ws://localhost:2999'
]);
}
export default function csp() {

View File

@ -1,4 +1,7 @@
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint):\s/i;
import config from '../../webpack.config';
const __DEV__ = process.env.NODE_ENV !== 'production';
export default function jadeHelpers() {
return function jadeHelpersMiddleware(req, res, next) {
@ -6,6 +9,12 @@ export default function jadeHelpers() {
return str.replace(challengesRegex, '');
};
res.locals.getBundleLocation = function getBundleLocation() {
return __DEV__ ?
config.output.publicPath + '/bundle.js' :
'js/bundle.js';
};
next();
};
}

View File

@ -3,12 +3,14 @@ var pmx = require('pmx');
pmx.init();
var _ = require('lodash'),
Rx = require('rx'),
loopback = require('loopback'),
boot = require('loopback-boot'),
expressState = require('express-state'),
path = require('path'),
setupPassport = require('./component-passport');
Rx.config.longStackSupport = process.env.NODE_DEBUG !== 'production';
var app = loopback();
var isBeta = !!process.env.BETA;

View File

@ -10,4 +10,4 @@ html(lang='en')
#fcc!= markup
script!= state
script(src=rev('/js', 'vendor-challenges.js'))
script(src=rev('/js', 'bundle.js'))
script(src=getBundleLocation())

View File

@ -1,15 +1,15 @@
var webpack = require('webpack');
var path = require('path');
var webpack = require('webpack');
var __DEV__ = process.env.NODE_ENV !== 'production';
module.exports = {
entry: './client',
devtool: 'inline-source-map',
output: {
filename: 'bundle.js',
path: path.join(__dirname, '/public/js'),
publicPath: 'public/'
publicPath: __DEV__ ? 'http://localhost:2999/js' : '/js'
},
module: {
loaders: [
@ -42,6 +42,8 @@ module.exports = {
'NODE_ENV': JSON.stringify(__DEV__ ? 'development' : 'production')
},
'__DEVTOOLS__': !__DEV__
})
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
]
};