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

View File

@ -21,7 +21,9 @@ var Rx = require('rx'),
sourcemaps = require('gulp-sourcemaps'), sourcemaps = require('gulp-sourcemaps'),
// react app // react app
webpack = require('webpack-stream'), webpack = require('webpack'),
webpackStream = require('webpack-stream'),
WebpackDevServer = require('webpack-dev-server'),
webpackConfig = require('./webpack.config.js'), webpackConfig = require('./webpack.config.js'),
webpackConfigNode = require('./webpack.config.node.js'), webpackConfigNode = require('./webpack.config.node.js'),
@ -55,7 +57,6 @@ var paths = {
serverIgnore: [ serverIgnore: [
'gulpfile.js', 'gulpfile.js',
'public/', 'public/',
'!public/js/bundle*',
'node_modules/', 'node_modules/',
'client/', 'client/',
'seed', 'seed',
@ -225,7 +226,6 @@ var syncDepenedents = [
'js', 'js',
'less', 'less',
'dependents', 'dependents',
'pack-watch',
'build-manifest' 'build-manifest'
]; ];
@ -269,7 +269,7 @@ gulp.task('pack-client', function() {
return gulp.src(webpackConfig.entry) return gulp.src(webpackConfig.entry)
.pipe(plumber({ errorHandler: errorHandler })) .pipe(plumber({ errorHandler: errorHandler }))
.pipe(webpack(Object.assign( .pipe(webpackStream(Object.assign(
{}, {},
webpackConfig, webpackConfig,
webpackOptions webpackOptions
@ -289,51 +289,47 @@ gulp.task('pack-client', function() {
.pipe(gulp.dest(paths.manifest)); .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; var webpackCalled = false;
gulp.task('pack-watch', function(cb) { gulp.task('webpack-dev-server', function(cb) {
if (webpackCalled) { if (webpackCalled) {
console.log('webpack watching already runnning'); console.log('webpack dev server already runnning');
return cb(); return cb();
} }
gulp.src(webpackConfig.entry) var devServerOptions = {
.pipe(plumber({ errorHandler: errorHandler })) headers: {
.pipe(webpack(Object.assign( 'Access-Control-Allow-Credentials': 'true'
{}, },
webpackConfig, hot: true,
webpackOptions, noInfo: true,
{ watch: true } contentBase: false,
), null, function(notUsed, stats) { publicPath: '/js'
if (stats) { };
gutil.log(stats.toString(defaultStatsOptions)); 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) { if (!webpackCalled) {
debug('webpack init completed'); gutil.log('[webpack-dev-server]', 'webpack init completed');
webpackCalled = true; webpackCalled = true;
cb(); 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 manifestName = 'react-manifest.json';
var dest = webpackConfig.output.path; var dest = webpackConfig.output.path;
return gulp.src(dest + '/bundle.js') return gulp.src(dest + '/bundle.js')
@ -520,8 +516,6 @@ var watchDependents = [
'dependents', 'dependents',
'serve', 'serve',
'sync', 'sync',
'pack-watch',
'pack-watch-manifest',
'build-manifest' 'build-manifest'
]; ];
@ -540,14 +534,12 @@ gulp.task('watch', watchDependents, function() {
['dependents'] ['dependents']
); );
gulp.watch(paths.manifest + '/*.json', ['build-manifest-watch']); gulp.watch(paths.manifest + '/*.json', ['build-manifest-watch']);
gulp.watch(webpackConfig.output.path + '/bundle.js', ['pack-watch-manifest']);
}); });
gulp.task('default', [ gulp.task('default', [
'less', 'less',
'serve', 'serve',
'pack-watch', 'webpack-dev-server',
'pack-watch-manifest',
'build-manifest-watch', 'build-manifest-watch',
'watch', 'watch',
'sync' 'sync'

View File

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

View File

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

View File

@ -5,7 +5,11 @@ let trusted = [
]; ];
if (process.env.NODE_ENV !== 'production') { 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() { export default function csp() {

View File

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

View File

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

View File

@ -10,4 +10,4 @@ html(lang='en')
#fcc!= markup #fcc!= markup
script!= state script!= state
script(src=rev('/js', 'vendor-challenges.js')) 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 webpack = require('webpack');
var path = require('path'); var path = require('path');
var webpack = require('webpack');
var __DEV__ = process.env.NODE_ENV !== 'production'; var __DEV__ = process.env.NODE_ENV !== 'production';
module.exports = { module.exports = {
entry: './client', entry: './client',
devtool: 'inline-source-map',
output: { output: {
filename: 'bundle.js', filename: 'bundle.js',
path: path.join(__dirname, '/public/js'), path: path.join(__dirname, '/public/js'),
publicPath: 'public/' publicPath: __DEV__ ? 'http://localhost:2999/js' : '/js'
}, },
module: { module: {
loaders: [ loaders: [
@ -42,6 +42,8 @@ module.exports = {
'NODE_ENV': JSON.stringify(__DEV__ ? 'development' : 'production') 'NODE_ENV': JSON.stringify(__DEV__ ? 'development' : 'production')
}, },
'__DEVTOOLS__': !__DEV__ '__DEVTOOLS__': !__DEV__
}) }),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
] ]
}; };