Skip to content

Instantly share code, notes, and snippets.

@peggyrayzis
Last active February 7, 2019 03:31
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save peggyrayzis/6f11b39949242a6f74ab356a2760e820 to your computer and use it in GitHub Desktop.
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)
{
"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"
]
}
// @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
// @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
// 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
})
})
)
})
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