Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Progressive Web Apps with React.js and Firebase
Search
Jimmy Moon
May 20, 2017
0
150
Progressive Web Apps with React.js and Firebase
Jimmy Moon
May 20, 2017
Tweet
Share
More Decks by Jimmy Moon
See All by Jimmy Moon
Edge Computing for WebApp
ragingwind
0
83
Micro-Saas for developer who want a new
ragingwind
0
65
How to use The Fourth Language
ragingwind
0
170
Please Use The Fourth Language - WebAssembly
ragingwind
0
140
Head topics in Javascript, 2020
ragingwind
1
630
Recap Modern WebAssembly in CDS 2019
ragingwind
0
260
Today headlines in Javascript, 2019
ragingwind
0
450
Today, The Actions in Javascript
ragingwind
2
750
PWA Updates in Chrome 68
ragingwind
0
170
Featured
See All Featured
Speed Design
sergeychernyshev
24
610
A Philosophy of Restraint
colly
203
16k
The Pragmatic Product Professional
lauravandoore
31
6.3k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
226
22k
Building Flexible Design Systems
yeseniaperezcruz
327
38k
Building an army of robots
kneath
302
43k
Designing for humans not robots
tammielis
250
25k
Unsuck your backbone
ammeep
668
57k
The Straight Up "How To Draw Better" Workshop
denniskardys
232
140k
Navigating Team Friction
lara
183
14k
How to train your dragon (web standard)
notwaldorf
88
5.7k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
38
1.8k
Transcript
PWA w/React.js & Firebase
+JimmyMoon @ragingwind
- Simple PWA with React.js - App Shell Architecture -
Web Manifest - Service Worker - Firebase - Tuning on PRPL - Auditing by Lighthouse
- create-react-app - (react-scripts < 0.9.5) - Webpack > 2.4.x
- PWA features - Minimal snippets - One of the app for guiding PWA
None
create-react-app
> yarn init && tree . └── package.json
> touch public/index.html && tree . └── public └── index.html
<!--fit in mobile-optimized --> <meta name="viewport" content="width=device-width, initial-scale=1" /> <!--support
tool bar color --> <meta name="theme-color" content="#0A0A0A" /> ./public/index.html
> mkdir -p src/components/ > touch ./src/components/App.js \ ./src/main.js
> tree . └── src ├── components │ └── App.js
└── main.js
> yarn add react react-dom
import React from 'react'; import ReactDOM from 'react-dom'; import App
from './App.js'; ReactDOM.render(<App />, document.getElementById('app')); ./src/main.js
import React from 'react'; class App extends React.Component { render()
{ return ( <div><h1>Hello World !! </h1> </div> ); } } ./src/components/App.js
... <body> <!--root element --> <div id="app"> </div> </body> ...
./public/index.htm
> yarn add --dev webpack \ webpack-dev-server > touch webpack.config.js
module.exports = { entry: { main: ['./src/main.js'], }, output: {
path: path.resolve( __dirname, './build'), filename: '[name].js' } ... }; ./webpack.config.js
module.exports = { ... devServer: { contentBase: './public', inline: true,
host: 'localhost', port: 8080 } }; ./webpack.config.js
... <!--hard code main script --> <script type="text/javascript" src="main.js"> </script>
... ./public/index.html
> yarn add --dev babel-core \ babel-loader \ babel-preset-react-app >
touch .babelrc
{ // babel preset for react app // made by
facebook "presets": ["react-app"] } ./.babelrc
module.exports = { ... module: { loaders: [{ test: /\.(js|jsx)$/,
include: path.resolve( __dirname, './src'), loaders: 'babel-loader' }] } }; ./webpack.config.js
{ ... // add dev server and build script to
package.json "scripts": { "start": "NODE_ENV=development webpack-dev-server", "build": "rm -rf build && NODE_ENV=production webpack -p" }, ... } ./package.json
> yarn start
None
App Shell Architecture
None
None
> yarn add material-ui \ react-tap-event-plugin > touch ./src/components/AppShell.js
import reactTabEventPlugin from 'react-tap-event-plugin'; reactTabEventPlugin(); ./src/main.js
class AppShell extends React.Component { render() { return ( <div
id="content"> {React.cloneElement(this.props.children)} </div> ); } }; ./src/components/AppShell.js
import {MuiThemeProvider, getMuiTheme} from 'material-ui/styles'; import {AppBar, Drawer, MenuItem} from
'material-ui'; ./src/components/AppShell.js
constructor(props) { // drawer open flag this.state = {open: false};
} ./src/components/AppShell.js
render() { return ( <MuiThemeProvider> <div> <AppBar /> <Drawer open={this.state.open}>
<MenuItem primaryText={'Main'} /> </Drawer> ... </div> </MuiThemeProvider> ); } ./src/components/AppShell.js
... handleDrawerToggle = () => this.setState({open: !this.state.open}) handleRequestChange = open
=> this.setState({open: open}) ... ./src/components/AppShell.js
class App extends React.Component { render() { return ( <AppShell>
<div><h1>Hello World !! </h1> </div> </AppShell> ); } } ./src/components/AppShell.js
None
Web Manifest
> npm install -g pwa-manifest-cli > pwa-manifest ./public \ --icons=ICONPATH_LOCAL_OR_HTTP
None
None
None
> tree -L 3 ./public ├── favicon.ico ├── icon-144x144.png ├──
icon-192x192.png ├── icon-512x512.png ├── index.html └── manifest.json
{ "name": "react-pwa", "short_name": "react-pwa", "icons": [ { "src": "icon-144x144.png",
"sizes": "144x144", "type": "image/png" }, ... ], "start_url": "./?utm_source=web_app_manifest", "display": "standalone", "orientation": "natural", "background_color": "#FFFFFF", "theme_color": "#3F51B5" } ./public/manifest.json
<link rel="manifest" href="manifest.json"> ./public/index.html
None
Service Worker
None
> yarn add --dev \ sw-precache-webpack-plugin-loader \ copy-webpack-plugin
const CopyWebpackPlugin = require('copy-webpack-plugin'); const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin-loader'); ./webpack.config.json
plugins: [ new CopyWebpackPlugin([{ context: './public', from: '*.*' }]) ]
./webpack.config.json
plugins: [ new SWPrecacheWebpackPlugin({ staticFileGlobs: [CACHED_FILES_GLOBS], logger: function () {},
filename: 'sw.js' }) ] ./webpack.config.json
<script> if ('serviceWorker' in navigator) { window.addEventListener('load', function() { navigator.serviceWorker.register('sw.js');
}); } </script> ./public/index.html
None
More views, React Routing
> yarn add react-router-dom > touch ./src/components/Home.js > tree ./src/components
├── App.js ├── AppShell.js └── Home.js
import {Card, CardTitle, CardText} from 'material-ui/Card'; ./src/components/Home.js
class Home extends React.Component { render () { return (
<Card> <CardTitle title="Hello! World" /> <CardText> ... </CardText> </Card> ) } } ./src/components/Home.js
import {HashRouter as Router, Route} from 'react-router-dom'; import Home from
'./Home'; ./src/components/App.js
class App extends React.Component { render() { return ( <div><h1>Hello
World !! </h1> </div> <Router> <AppShell> <div> <Route exact path="/" component={Home} /> </div> </AppShell> </Router> ); } } ./src/components/App.js
None
> touch ./src/components/Users.js > touch ./src/components/Notification.js > tree ./src/components ├──
App.js ├── AppShell.js ├── Home.js ├── Users.js └── Notification.js
import React from 'react'; class Users extends React.Component { render()
{ return ( <div> Users </div> ); } } export default Users; ./src/components/Users.js
import React from 'react'; class Notification extends React.Component { render()
{ return ( <div>Notification </div> ); } } export default Notification; ./src/components/Notification.js
import Users from './Users'; import Notification from './Notification'; ./src/components/App.js
<Router> <AppShell> <div> <Route exact path="/" component={Home} /> <Route path="/users"
component={Users} /> <Route path="/notification" component={Notification} /> </div> </AppShell> </Router> ./src/components/App.js
import {Link} from 'react-router-dom'; <Drawer> ... <MenuItem primaryText={'Users'} containerElement={<Link to={'/Users'}
/>} onClick={this.handleDrawerToggle} /> <MenuItem primaryText={'Notification'} containerElement={<Link to={'/Notification'} />} onClick={this.handleDrawerToggle} /> </Drawer> ./src/components/AppShell.js
None
Firebase Hosting with HTTPS
None
> npm install -g firebase-tools
None
> tree -L 1 ./ -a -I node_modules ├── .babelrc
├── .firebaserc ├── firebase.json ├── package.json ├── public ├── src ├── webpack.config.js └── yarn.lock
{ "hosting": { "public": "build" } } ./firebase.json
{ "projects": { "default": "YOUR-PROJECT-NAME" } } ./.firebaserc
None
> yarn build > tree -L 1 ./ -a -I
node_modules ├── favicon.ico ├── icon-144x144.png ├── icon-192x192.png ├── icon-512x512.png ├── index.html ├── main.js ├── manifest.json ├── notification.js └── sw.js
Firebase Realtime Database
None
None
None
class Users extends React.Component { constructor() { super(); this.state =
{ users: [] }; } } ./src/components/Users.js
componentDidMount() { const databaseURL = 'https: //YOUR_DATABASE_URL'; fetch(`${databaseURL}/data.json/`).then(res => {
if (res.status !== 200) { throw new Error(res.statusText); } return res.json(); }).then(users => this.setState({users: users})) } ./src/components/Users.js
render() { const users = () => { return Object.keys(this.state.users).map(id
=> { const user = this.state.users[id]; return ( <User key={id} name={user.name} email={user.email} /> ); }); } return (<div>{users()} </div>); } ./src/components/Users.js
None
None
plugins: [ new SWPrecacheWebpackPlugin({ ... runtimeCaching: [{ urlPattersn: /https:\/\/.+.firebaseio.com/, handler:
'networkFirst' }] }) ] ./webpack.config.js
None
Firebase Cloud Messaging
> yarn add firebase
None
{ gcm_sender_id: "103953800507", ... }; ./manifest.json
import firebase from 'firebase'; ./src/components/Notification.js
var config = { apiKey: "AIzasdakagskgei@#9412i8123WWEeFwyuOk4af3vhYFw", authDomain: "YOURPROJECT.firebaseapp.com", databaseURL: "https:
//YOURPROJECT.firebaseio.com", projectId: "YOURPROJECT", storageBucket: "YOURPROJECT.appspot.com", messagingSenderId: "2595534347235" }; ./src/components/Notification.js
class Notification extends React.Component { static firebaseApp; constructor(props) { super(props);
if (!Notification.firebaseApp) { firebase.initializeApp(config); } } } ./src/components/Notification.js
class Notification extends React.Component { constructor(props) { ... this.state =
{ token: '', message: '' }; } } ./src/components/Notification.js
componentDidMount() { const messaging = firebase.messaging(); messaging.onMessage(this.handlePushMessage); messaging.requestPermission() .then(() =>
messaging.getToken()) .then(token => this.setState({token: token})) .catch(err => { throw new Error(err); }); } ./src/components/Notification.js
handlePushMessage = noti => { this.setState({ message: `${noti.title}: ${noti.body}` });
} ./src/components/Notification.js
render() { return ( <div> <Card> <CardHeader title={'Token'} subtitle={this.state.token} />
<CardHeader title={'Message'} subtitle={this.state.message} /> </Card> </div> ); } ./src/components/Notification.js
> touch ./src/firebase-messaging-sw.js > tree -L 1 ./src -I node_modules
├── components ├── firebase-messaging-sw.js └── main.js
importScripts('https: // www.gstatic.com/firebasejs/3.5.2/firebase-app.js'); importScripts('https: // www.gstatic.com/firebasejs/3.5.2/firebase-messaging.js'); ./src/firebase-messaging-sw.js
var firebaseConfig = { ... }; firebase.initializeApp(firebaseConfig); const messaging =
firebase.messaging(); messaging.setBackgroundMessageHandler(({data} = {}) => { const title = data.title || 'Title'; const opts = Object.assign({body: data.body || 'Body'}, data); return self.registration.showNotification(title, opts); }); ./src/firebase-messaging-sw.js
plugins: [ new CopyWebpackPlugin([{ ... }, { from: './src/firebase-messaging-sw.js', to:
'firebase-messaging-sw.js', }]), ] ./webpack.config.js
> npm install -g fcm-cli
None
None
None
> fcm send --server-key SERVER_KEY:bKFlxhMqS_PYGlCw4VTV \ --to TOKEN:A91bFmi_DEQFPQ2FMf_7V8NcEGGqkAZEV- \ --notification.title
hi \ --notification.body 'Hello World'
None
Tuning on PRPL Pattern
(P)ush critical resources for the initial route (R)ender initial route
and get it interactive as soon as possible, (P)re-cache components for the remaining routes (L)azy-load, and create next routes on demand by user
None
None
Kicking Mega Bundle
None
None
Breaking down Common Chunks
module.exports = { entry: { main: ['./src/main.js'], vendor: ['react', 'react-dom']
}, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' }), ] } ./webpack.config.js
<script type="text/javascript" src="vendor.js"> </script> <script type="text/javascript" src="main.js"> </script> ./public/index.html
None
None
Routes based Code-splitting
> yarn add --dev babel-plugin-syntax- dynamic-import
{ "plugins": [ "syntax-dynamic-import" ] } ./.babelrc
export default (getComponent) => ( class AsyncComponent extends React.Component {
componentWillMount() { if (!this.state.Component) { getComponent().then(Component => { AsyncComponent.Component = Component this.setState({ Component }) }); } }. ... } ); ./src/components/AsyncComponent.js
import asyncComponent from './AsyncComponent'; const Home = asyncComponent(() => {
return import( /* webpackChunkName: "home" */ './Home') .then(module => module.default); }); const Users = ...' const Notification = ...; ./src/components/App.js
None
None
Tree-shaking, Dead-code elimination Live-code Importing
None
import {Card, CardHeader} from 'material-ui'; import {Card, CardHeader} from 'material-ui/Card';
> yarn add --dev babel-preset-es2015 babel- plugin-transform-imports
{ "presets": [["es2015", {"modules": false}], "react-app"] } ./.babelrc
"plugins": [ [ "transform-imports", { "material-ui": { "transform": "material-ui/${member}", "preventFullImport":
true }, "react-router-dom": { ...}, "lodash": { ...} } ] ] ./.babelrc
None
494 kB
None
Extract common parts Shared Common Chunk
plugins: [ new webpack.optimize.CommonsChunkPlugin({ children: true, async: 'common', minChunks: 2
}), ] ./webpack.config.js
<script type="text/javascript" src="common.js"> </script> ./public/index.html
None
None
None
None
Control resources pushing Preload, Prefetch
> yarn add --dev html-webpack-plugin preload-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin'); const PreloadWebpackPlugin = require('preload-webpack-plugin'); ./webpack.config.js
plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: './public/index.html', favicon: './public/favicon.ico'
}), new PreloadWebpackPlugin({ include: ['vendor', 'main', 'common', 'home'] }), ] ./webpack.config.js
<script type="text/javascript" src="vendor.js"> </script> <script type="text/javascript" src="main.js"> </script> <script type="text/javascript"
src="common.js"> </script> ./public/index.html
None
plugins: [ new PreloadWebpackPlugin({ rel: 'prefetch', include: ['users', 'notification'] }),
] ./webpack.config.js
None
None
More Optimization
class LazySidebarDrawer extends React.Component { componentDidMount() { let frameCount =
0; const open = () => (frameCount ++ > 0) ? this.props.onMounted() : requestAnimationFrame(open); requestAnimationFrame(open); } render() { return ( <Drawer open={this.props.open} docked={false} onRequestChange={this.props.onRequestChange}> ... </Drawer> ); } }
render() { return ( ... {<LazySidebarDrawer />} ... ); }
./src/components/AppShell.js
None
> 144 kB
None
None
> yarn add react-lite
More plugins for Webpack
Auditing by Lighthouse
None
And more ...
None