Last active
February 7, 2019 03:31
-
-
Save peggyrayzis/6f11b39949242a6f74ab356a2760e820 to your computer and use it in GitHub Desktop.
Webpack 2 + PWA support (Tree Shaking, Code Splitting w/ React Router v4, Service Worker)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Show hidden characters
{ | |
"presets": [ | |
"react", | |
"stage-2", | |
[ | |
"env", | |
{ | |
"targets": { | |
"browsers": [ | |
"last 2 versions", | |
"> 1%" | |
] | |
}, | |
// this is necessary for tree shaking | |
"modules": false | |
} | |
] | |
], | |
"plugins": [ | |
"transform-runtime", | |
"transform-flow-strip-types" | |
] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @flow | |
import debug from 'debug' | |
import React, { Component } from 'react' | |
import { Provider } from 'react-redux' | |
import store from './store' | |
import Router from './router' | |
type Logger = (s: string, ...any) => void | |
const logger: Logger = debug('client:app') | |
class App extends Component { | |
componentDidMount (): void { | |
// bootstrap service worker if supported | |
if ('serviceWorker' in navigator && | |
window.location.protocol === 'https:' && | |
!process.env.DISABLE_SERVICE_WORKER) { | |
const registration = window.runtime.register() | |
window.registerEvents(registration, { | |
onInstalled (): void { | |
logger('[ServiceWorker] service worker was installed') | |
}, | |
onUpdateReady (): void { | |
logger('[ServiceWorker] service worker update is ready') | |
}, | |
onUpdating (): void { | |
logger('[ServiceWorker] service worker is updating') | |
}, | |
onUpdateFailed (): void { | |
logger('[ServiceWorker] service worker update failed') | |
}, | |
onUpdated (): void { | |
logger('[ServiceWorker] service worker was updated') | |
} | |
}) | |
} | |
} | |
render (): React.Element<*> { | |
return ( | |
<div> | |
<Provider store={store}> | |
<Router /> | |
</Provider> | |
</div> | |
) | |
} | |
} | |
export default App |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @flow | |
import React, { Component } from 'react' | |
import { | |
BrowserRouter as Router, | |
Route, | |
Switch | |
} from 'react-router-dom' | |
type GetComponent = () => Promise<?ReactClass<*>> | |
type State = { Component: ?ReactClass<*> } | |
type AppRouterComponent = () => React.Element<*> | |
function asyncComponent (getComponent: GetComponent) { | |
let ImportedComponent | |
return class AsyncComponent extends Component { | |
state: State | |
constructor (): void { | |
super(...arguments) | |
this.state = { Component: ImportedComponent } | |
} | |
componentWillMount (): void { | |
if (!this.state.Component) { | |
getComponent() | |
.then(Component => { | |
ImportedComponent = Component | |
this.setState({ Component }) | |
}) | |
} | |
} | |
render (): ?React.Element<*> { | |
const { Component } = this.state | |
return Component ? <Component {...this.props} /> : null | |
} | |
} | |
} | |
const MatchDetail = asyncComponent(() => | |
// flow will throw an error here. see https://github.com/facebook/flow/issues/2968 | |
import('./providers/match-detail') | |
.then(m => m.default) | |
.catch(e => console.error(e))) | |
const MatchList = asyncComponent(() => | |
// flow will throw an error here. see https://github.com/facebook/flow/issues/2968 | |
import('./providers/match-list') | |
.then(m => m.default) | |
.catch(e => console.error(e))) | |
const AppRouter: AppRouterComponent = () => ( | |
<Router> | |
<Switch> | |
<Route exact path="/list" component={MatchList} /> | |
<Route exact path="/detail" component={MatchDetail} /> | |
</Switch> | |
</Router> | |
) | |
export default AppRouter |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// service worker | |
/* global self */ | |
/** | |
* When the user navigates to your site, | |
* the browser tries to redownload the script file that defined the service worker in the background. | |
* If there is even a byte's difference in the service worker file compared to what it currently has, | |
* it considers it 'new'. | |
*/ | |
const { assets } = global.serviceWorkerOption | |
const CACHE_NAME = 'v0.0.1' | |
let assetsToCache = [ | |
...assets, | |
'./' | |
] | |
assetsToCache = assetsToCache.map(path => new self.URL(path, global.location).toString()) | |
// when the service worker is first added to a computer | |
self.addEventListener('install', event => { | |
// add core files to cache during serviceworker installation | |
event.waitUntil( | |
global.caches | |
.open(CACHE_NAME) | |
.then(cache => cache.addAll(assetsToCache)) | |
.catch(error => { | |
console.error(error) | |
throw error | |
}) | |
) | |
}) | |
// after the install event | |
self.addEventListener('activate', (event) => { | |
// clean the caches | |
event.waitUntil( | |
global.caches | |
.keys() | |
.then(cacheNames => | |
Promise.all( | |
cacheNames.map(cacheName => { | |
// delete the caches that are not the current one | |
if (cacheName.indexOf(CACHE_NAME) === 0) { | |
return null | |
} else { | |
return global.caches.delete(cacheName) | |
} | |
}) | |
) | |
) | |
) | |
}) | |
self.addEventListener('message', event => { | |
switch (event.data.action) { | |
case 'skipWaiting': | |
if (self.skipWaiting) { | |
self.skipWaiting() | |
} | |
break | |
} | |
}) | |
self.addEventListener('fetch', event => { | |
const requestURL = new self.URL(event.request.url) | |
event.respondWith( | |
global.caches.match(event.request) | |
.then(response => { | |
// we have a copy of the response in our cache, so return it | |
if (response) return response | |
const fetchRequest = event.request.clone() | |
return self.fetch(fetchRequest).then(response => { | |
let shouldCache = false | |
if ((response.type === 'basic' || response.type === 'cors') && response.status === 200) { | |
shouldCache = true | |
} else if (response.type === 'opaque') { | |
// if response isn't from our origin / doesn't support CORS, be careful w/ what we cache | |
// https://developer.mozilla.org/en-US/docs/Web/API/Response/type | |
} | |
if (shouldCache) { | |
const responseToCache = response.clone() | |
global.caches.open(CACHE_NAME) | |
.then(cache => { | |
const cacheRequest = event.request.clone() | |
cache.put(cacheRequest, responseToCache) | |
}) | |
} | |
return response | |
}) | |
}) | |
) | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const path = require('path') | |
const CleanWebpackPlugin = require('clean-webpack-plugin') | |
const CopyWebpackPlugin = require('copy-webpack-plugin') | |
const HtmlWebpackPlugin = require('html-webpack-plugin') | |
const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin') | |
const webpack = require('webpack') | |
const production = process.env.NODE_ENV === 'production' | |
// plugins for development builds only | |
const devPlugins = [ | |
// prevent webpack from killing watch on build error | |
new webpack.NoEmitOnErrorsPlugin() | |
] | |
// base plugins | |
const plugins = [ | |
// remove build/client dir before compile time | |
new CleanWebpackPlugin('build/client'), | |
// build vendor bundle (including common code chunks used in other bundles) | |
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'public/js/vendor.[hash].js' }), | |
// define env vars for application (shim for process.env) | |
new webpack.DefinePlugin({ | |
'process.env': { | |
BABEL_ENV: JSON.stringify(process.env.NODE_ENV), | |
NODE_ENV: JSON.stringify(process.env.NODE_ENV), | |
DISABLE_SERVICE_WORKER: JSON.stringify(process.env.DISABLE_SERVICE_WORKER) | |
} | |
}), | |
// interpolate index.ejs to index.html, add assets to html file | |
new HtmlWebpackPlugin({ | |
title: 'MLS Matchcenter', | |
template: 'src/client/index.ejs', | |
inject: 'body', | |
filename: 'index.html' | |
}), | |
// make service worker available to application | |
new ServiceWorkerWebpackPlugin({ | |
entry: path.join(__dirname, 'src/client/sw.js'), | |
filename: 'sw.js', | |
excludes: [ | |
'**/.*', | |
'**/*.map', | |
'*.html' | |
] | |
}), | |
// copy static PWA assets | |
new CopyWebpackPlugin([ | |
// copy manifest.json for app install | |
{ from: 'src/client/manifest.json' }, | |
// copy icon images for save to home screen and splash screen | |
{ from: 'src/client/assets/app-install-icons', to: 'public/img' } | |
]), | |
// you have to put your options in a LoaderOptionsPlugin. can't attach them to config directly | |
new webpack.LoaderOptionsPlugin({ | |
debug: !production | |
}) | |
] | |
// plugins for production builds only | |
const prodPlugins = [ | |
// make sure we don't create too small chunks, merge together chunks smaller than 10kb | |
new webpack.optimize.MinChunkSizePlugin({ minChunkSize: 10240 }), | |
// minify the crap out of this thing | |
new webpack.optimize.UglifyJsPlugin({ | |
mangle: true, | |
compress: { | |
// Suppress uglification warnings | |
warnings: false | |
} | |
}) | |
] | |
module.exports = [{ | |
// inline-source-map makes devtools point to source files | |
devtool: production ? false : 'inline-source-map', | |
entry: { | |
app: './src/client/index.js', | |
// third party modules here | |
vendor: [ | |
'debug', | |
'react', | |
'react-dom', | |
'redux', | |
'redux-thunk', | |
'redux-logger', | |
'react-redux', | |
'react-router-dom' | |
] | |
}, | |
module: { | |
rules: [ | |
{ | |
exclude: /node_modules/, | |
loader: 'babel-loader', | |
test: /\.js$/ | |
}, | |
{ | |
use: [ | |
'url-loader', | |
'image-webpack-loader' | |
], | |
test: /\.(png|jpg|svg)$/ | |
} | |
] | |
}, | |
output: { | |
chunkFilename: 'public/js/[name].[hash].js', | |
filename: 'public/js/[name].[hash].js', | |
path: path.join(__dirname, 'build', 'client') | |
}, | |
plugins: production ? plugins.concat(prodPlugins) : plugins.concat(devPlugins) | |
}] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment