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
160
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
85
Micro-Saas for developer who want a new
ragingwind
0
68
How to use The Fourth Language
ragingwind
0
170
Please Use The Fourth Language - WebAssembly
ragingwind
0
150
Head topics in Javascript, 2020
ragingwind
1
640
Recap Modern WebAssembly in CDS 2019
ragingwind
0
270
Today headlines in Javascript, 2019
ragingwind
0
460
Today, The Actions in Javascript
ragingwind
2
760
PWA Updates in Chrome 68
ragingwind
0
180
Featured
See All Featured
Being A Developer After 40
akosma
87
590k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
229
52k
For a Future-Friendly Web
brad_frost
175
9.4k
Optimising Largest Contentful Paint
csswizardry
33
3k
Docker and Python
trallard
41
3.1k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
356
29k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
191
16k
Stop Working from a Prison Cell
hatefulcrawdad
267
20k
Into the Great Unknown - MozCon
thekraken
33
1.5k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
32
2.7k
Java REST API Framework Comparison - PWX 2021
mraible
PRO
28
8.3k
Faster Mobile Websites
deanohume
305
30k
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