Progressive Web Apps with React.js and Firebase

687ac25540fe35fcb5e828f75c4a6079?s=47 Jimmy Moon
May 20, 2017
31

Progressive Web Apps with React.js and Firebase

687ac25540fe35fcb5e828f75c4a6079?s=128

Jimmy Moon

May 20, 2017
Tweet

Transcript

  1. PWA w/React.js & Firebase

  2. +JimmyMoon @ragingwind

  3. - Simple PWA with React.js - App Shell Architecture -

    Web Manifest - Service Worker - Firebase - Tuning on PRPL - Auditing by Lighthouse
  4. - create-react-app - (react-scripts < 0.9.5) - Webpack > 2.4.x

    - PWA features - Minimal snippets - One of the app for guiding PWA
  5. None
  6. create-react-app

  7. > yarn init && tree . └── package.json

  8. > touch public/index.html && tree . └── public └── index.html

  9. <!--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
  10. > mkdir -p src/components/ > touch ./src/components/App.js \ ./src/main.js

  11. > tree . └── src ├── components │ └── App.js

    └── main.js
  12. > yarn add react react-dom

  13. import React from 'react'; import ReactDOM from 'react-dom'; import App

    from './App.js'; ReactDOM.render(<App />, document.getElementById('app')); ./src/main.js
  14. import React from 'react'; class App extends React.Component { render()

    { return ( <div><h1>Hello World !! </h1> </div> ); } } ./src/components/App.js
  15. ... <body> <!--root element --> <div id="app"> </div> </body> ...

    ./public/index.htm
  16. > yarn add --dev webpack \ webpack-dev-server > touch webpack.config.js

  17. module.exports = { entry: { main: ['./src/main.js'], }, output: {

    path: path.resolve( __dirname, './build'), filename: '[name].js' } ... }; ./webpack.config.js
  18. module.exports = { ... devServer: { contentBase: './public', inline: true,

    host: 'localhost', port: 8080 } }; ./webpack.config.js
  19. ... <!--hard code main script --> <script type="text/javascript" src="main.js"> </script>

    ... ./public/index.html
  20. > yarn add --dev babel-core \ babel-loader \ babel-preset-react-app >

    touch .babelrc
  21. { // babel preset for react app // made by

    facebook "presets": ["react-app"] } ./.babelrc
  22. module.exports = { ... module: { loaders: [{ test: /\.(js|jsx)$/,

    include: path.resolve( __dirname, './src'), loaders: 'babel-loader' }] } }; ./webpack.config.js
  23. { ... // 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
  24. > yarn start

  25. None
  26. App Shell 
 Architecture

  27. None
  28. None
  29. > yarn add material-ui \ react-tap-event-plugin > touch ./src/components/AppShell.js

  30. import reactTabEventPlugin from 'react-tap-event-plugin'; reactTabEventPlugin(); ./src/main.js

  31. class AppShell extends React.Component { render() { return ( <div

    id="content"> {React.cloneElement(this.props.children)} </div> ); } }; ./src/components/AppShell.js
  32. import {MuiThemeProvider, getMuiTheme} from 'material-ui/styles'; import {AppBar, Drawer, MenuItem} from

    'material-ui'; ./src/components/AppShell.js
  33. constructor(props) { // drawer open flag this.state = {open: false};

    } ./src/components/AppShell.js
  34. render() { return ( <MuiThemeProvider> <div> <AppBar /> <Drawer open={this.state.open}>

    <MenuItem primaryText={'Main'} /> </Drawer> ... </div> </MuiThemeProvider> ); } ./src/components/AppShell.js
  35. ... handleDrawerToggle = () => this.setState({open: !this.state.open}) handleRequestChange = open

    => this.setState({open: open}) ... ./src/components/AppShell.js
  36. class App extends React.Component { render() { return ( <AppShell>

    <div><h1>Hello World !! </h1> </div> </AppShell> ); } } ./src/components/AppShell.js
  37. None
  38. Web Manifest

  39. > npm install -g pwa-manifest-cli > pwa-manifest ./public \ --icons=ICONPATH_LOCAL_OR_HTTP

  40. None
  41. None
  42. None
  43. > tree -L 3 ./public ├── favicon.ico ├── icon-144x144.png ├──

    icon-192x192.png ├── icon-512x512.png ├── index.html └── manifest.json
  44. { "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
  45. <link rel="manifest" href="manifest.json"> ./public/index.html

  46. None
  47. Service Worker

  48. None
  49. > yarn add --dev \ sw-precache-webpack-plugin-loader \ copy-webpack-plugin

  50. const CopyWebpackPlugin = require('copy-webpack-plugin'); const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin-loader'); ./webpack.config.json

  51. plugins: [ new CopyWebpackPlugin([{ context: './public', from: '*.*' }]) ]

    ./webpack.config.json
  52. plugins: [ new SWPrecacheWebpackPlugin({ staticFileGlobs: [CACHED_FILES_GLOBS], logger: function () {},

    filename: 'sw.js' }) ] ./webpack.config.json
  53. <script> if ('serviceWorker' in navigator) { window.addEventListener('load', function() { navigator.serviceWorker.register('sw.js');

    }); } </script> ./public/index.html
  54. None
  55. More views, React Routing

  56. > yarn add react-router-dom > touch ./src/components/Home.js > tree ./src/components

    ├── App.js ├── AppShell.js └── Home.js
  57. import {Card, CardTitle, CardText} from 'material-ui/Card'; ./src/components/Home.js

  58. class Home extends React.Component { render () { return (

    <Card> <CardTitle title="Hello! World" /> <CardText> ... </CardText> </Card> ) } } ./src/components/Home.js
  59. import {HashRouter as Router, Route} from 'react-router-dom'; import Home from

    './Home'; ./src/components/App.js
  60. 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
  61. None
  62. > touch ./src/components/Users.js > touch ./src/components/Notification.js > tree ./src/components ├──

    App.js ├── AppShell.js ├── Home.js ├── Users.js └── Notification.js
  63. import React from 'react'; class Users extends React.Component { render()

    { return ( <div> Users </div> ); } } export default Users; ./src/components/Users.js
  64. import React from 'react'; class Notification extends React.Component { render()

    { return ( <div>Notification </div> ); } } export default Notification; ./src/components/Notification.js
  65. import Users from './Users'; import Notification from './Notification'; ./src/components/App.js

  66. <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
  67. 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
  68. None
  69. Firebase Hosting with HTTPS

  70. None
  71. > npm install -g firebase-tools

  72. None
  73. > tree -L 1 ./ -a -I node_modules ├── .babelrc

    ├── .firebaserc ├── firebase.json ├── package.json ├── public ├── src ├── webpack.config.js └── yarn.lock
  74. { "hosting": { "public": "build" } } ./firebase.json

  75. { "projects": { "default": "YOUR-PROJECT-NAME" } } ./.firebaserc

  76. None
  77. > 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
  78. Firebase Realtime Database

  79. None
  80. None
  81. None
  82. class Users extends React.Component { constructor() { super(); this.state =

    { users: [] }; } } ./src/components/Users.js
  83. 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
  84. 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
  85. None
  86. None
  87. plugins: [ new SWPrecacheWebpackPlugin({ ... runtimeCaching: [{ urlPattersn: /https:\/\/.+.firebaseio.com/, handler:

    'networkFirst' }] }) ] ./webpack.config.js
  88. None
  89. Firebase Cloud Messaging

  90. > yarn add firebase

  91. None
  92. { gcm_sender_id: "103953800507", ... }; ./manifest.json

  93. import firebase from 'firebase'; ./src/components/Notification.js

  94. 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
  95. class Notification extends React.Component { static firebaseApp; constructor(props) { super(props);

    if (!Notification.firebaseApp) { firebase.initializeApp(config); } } } ./src/components/Notification.js
  96. class Notification extends React.Component { constructor(props) { ... this.state =

    { token: '', message: '' }; } } ./src/components/Notification.js
  97. 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
  98. handlePushMessage = noti => { this.setState({ message: `${noti.title}: ${noti.body}` });

    } ./src/components/Notification.js
  99. render() { return ( <div> <Card> <CardHeader title={'Token'} subtitle={this.state.token} />

    <CardHeader title={'Message'} subtitle={this.state.message} /> </Card> </div> ); } ./src/components/Notification.js
  100. > touch ./src/firebase-messaging-sw.js > tree -L 1 ./src -I node_modules

    ├── components ├── firebase-messaging-sw.js └── main.js
  101. 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

  102. 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
  103. plugins: [ new CopyWebpackPlugin([{ ... }, { from: './src/firebase-messaging-sw.js', to:

    'firebase-messaging-sw.js', }]), ] ./webpack.config.js
  104. > npm install -g fcm-cli

  105. None
  106. None
  107. None
  108. > fcm send --server-key SERVER_KEY:bKFlxhMqS_PYGlCw4VTV \ --to TOKEN:A91bFmi_DEQFPQ2FMf_7V8NcEGGqkAZEV- \ --notification.title

    hi \ --notification.body 'Hello World'
  109. None
  110. Tuning on PRPL Pattern

  111. (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
  112. None
  113. None
  114. Kicking Mega Bundle

  115. None
  116. None
  117. Breaking down Common Chunks

  118. module.exports = { entry: { main: ['./src/main.js'], vendor: ['react', 'react-dom']

    }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' }), ] } ./webpack.config.js
  119. <script type="text/javascript" src="vendor.js"> </script> <script type="text/javascript" src="main.js"> </script> ./public/index.html

  120. None
  121. None
  122. Routes based Code-splitting

  123. > yarn add --dev babel-plugin-syntax- dynamic-import

  124. { "plugins": [ "syntax-dynamic-import" ] } ./.babelrc

  125. 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
  126. import asyncComponent from './AsyncComponent'; const Home = asyncComponent(() => {

    return import( /* webpackChunkName: "home" */ './Home') .then(module => module.default); }); const Users = ...' const Notification = ...; ./src/components/App.js
  127. None
  128. None
  129. Tree-shaking, Dead-code elimination Live-code Importing

  130. None
  131. import {Card, CardHeader} from 'material-ui'; import {Card, CardHeader} from 'material-ui/Card';

  132. > yarn add --dev babel-preset-es2015 babel- plugin-transform-imports

  133. { "presets": [["es2015", {"modules": false}], "react-app"] } ./.babelrc

  134. "plugins": [ [ "transform-imports", { "material-ui": { "transform": "material-ui/${member}", "preventFullImport":

    true }, "react-router-dom": { ...}, "lodash": { ...} } ] ] ./.babelrc
  135. None
  136. 494 kB

  137. None
  138. Extract common parts Shared Common Chunk

  139. plugins: [ new webpack.optimize.CommonsChunkPlugin({ children: true, async: 'common', minChunks: 2

    }), ] ./webpack.config.js
  140. <script type="text/javascript" src="common.js"> </script> ./public/index.html

  141. None
  142. None
  143. None
  144. None
  145. Control resources pushing Preload, Prefetch

  146. > yarn add --dev html-webpack-plugin preload-webpack-plugin

  147. const HtmlWebpackPlugin = require('html-webpack-plugin'); const PreloadWebpackPlugin = require('preload-webpack-plugin'); ./webpack.config.js

  148. plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: './public/index.html', favicon: './public/favicon.ico'

    }), new PreloadWebpackPlugin({ include: ['vendor', 'main', 'common', 'home'] }), ] ./webpack.config.js
  149. <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
  150. None
  151. plugins: [ new PreloadWebpackPlugin({ rel: 'prefetch', include: ['users', 'notification'] }),

    ] ./webpack.config.js
  152. None
  153. None
  154. More Optimization

  155. 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> ); } }
  156. render() { return ( ... {<LazySidebarDrawer />} ... ); }

    ./src/components/AppShell.js
  157. None
  158. > 144 kB

  159. None
  160. None
  161. > yarn add react-lite

  162. More plugins for Webpack

  163. Auditing by Lighthouse

  164. None
  165. And more ...

  166. None