Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Progressive Web Apps with React.js and Firebase
Search
Jimmy Moon
May 20, 2017
250
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Progressive Web Apps with React.js and Firebase
Jimmy Moon
May 20, 2017
More Decks by Jimmy Moon
See All by Jimmy Moon
Operating an AI Infrastructure Through MCP Apps on Agents
ragingwind
0
50
Coding as Prompting Since 2025
ragingwind
0
940
Edge Computing for WebApp
ragingwind
0
160
Micro-Saas for developer who want a new
ragingwind
0
130
How to use The Fourth Language
ragingwind
0
250
Please Use The Fourth Language - WebAssembly
ragingwind
0
280
Head topics in Javascript, 2020
ragingwind
1
750
Recap Modern WebAssembly in CDS 2019
ragingwind
0
370
Today headlines in Javascript, 2019
ragingwind
0
570
Featured
See All Featured
Chasing Engaging Ingredients in Design
codingconduct
0
340
Lessons Learnt from Crawling 1000+ Websites
charlesmeaden
PRO
1
1.6k
How to make the Groovebox
asonas
2
2.5k
Digital Projects Gone Horribly Wrong (And the UX Pros Who Still Save the Day) - Dean Schuster
uxyall
1
3.2k
Done Done
chrislema
187
17k
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
250
What the history of the web can teach us about the future of AI
inesmontani
PRO
1
720
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
GraphQLとの向き合い方2022年版
quramy
50
15k
The #1 spot is gone: here's how to win anyway
tamaranovitovic
4
1.2k
The untapped power of vector embeddings
frankvandijk
2
1.9k
[RailsConf 2023] Rails as a piece of cake
palkan
59
7k
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