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
190
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
95
Micro-Saas for developer who want a new
ragingwind
0
74
How to use The Fourth Language
ragingwind
0
180
Please Use The Fourth Language - WebAssembly
ragingwind
0
180
Head topics in Javascript, 2020
ragingwind
1
670
Recap Modern WebAssembly in CDS 2019
ragingwind
0
290
Today headlines in Javascript, 2019
ragingwind
0
490
Today, The Actions in Javascript
ragingwind
2
790
PWA Updates in Chrome 68
ragingwind
0
200
Featured
See All Featured
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
26k
Thoughts on Productivity
jonyablonski
69
4.6k
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
233
17k
The Art of Programming - Codeland 2020
erikaheidi
53
13k
The World Runs on Bad Software
bkeepers
PRO
67
11k
Exploring the Power of Turbo Streams & Action Cable | RailsConf2023
kevinliebholz
32
5.1k
Keith and Marios Guide to Fast Websites
keithpitt
411
22k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
194
16k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
331
21k
RailsConf 2023
tenderlove
30
1.1k
The Illustrated Children's Guide to Kubernetes
chrisshort
48
49k
Art, The Web, and Tiny UX
lynnandtonic
298
20k
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