Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Progressive Web Apps with React.js and Firebase

Jimmy Moon
May 20, 2017
130

Progressive Web Apps with React.js and Firebase

Jimmy Moon

May 20, 2017
Tweet

Transcript

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

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

    - PWA features - Minimal snippets - One of the app for guiding PWA
  3. <!--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
  4. import React from 'react'; import ReactDOM from 'react-dom'; import App

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

    { return ( <div><h1>Hello World !! </h1> </div> ); } } ./src/components/App.js
  6. module.exports = { entry: { main: ['./src/main.js'], }, output: {

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

    host: 'localhost', port: 8080 } }; ./webpack.config.js
  8. { // babel preset for react app // made by

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

    include: path.resolve( __dirname, './src'), loaders: 'babel-loader' }] } }; ./webpack.config.js
  10. { ... // 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
  11. class AppShell extends React.Component { render() { return ( <div

    id="content"> {React.cloneElement(this.props.children)} </div> ); } }; ./src/components/AppShell.js
  12. render() { return ( <MuiThemeProvider> <div> <AppBar /> <Drawer open={this.state.open}>

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

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

    <div><h1>Hello World !! </h1> </div> </AppShell> ); } } ./src/components/AppShell.js
  15. > tree -L 3 ./public ├── favicon.ico ├── icon-144x144.png ├──

    icon-192x192.png ├── icon-512x512.png ├── index.html └── manifest.json
  16. { "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
  17. > yarn add react-router-dom > touch ./src/components/Home.js > tree ./src/components

    ├── App.js ├── AppShell.js └── Home.js
  18. class Home extends React.Component { render () { return (

    <Card> <CardTitle title="Hello! World" /> <CardText> ... </CardText> </Card> ) } } ./src/components/Home.js
  19. 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
  20. > touch ./src/components/Users.js > touch ./src/components/Notification.js > tree ./src/components ├──

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

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

    { return ( <div>Notification </div> ); } } export default Notification; ./src/components/Notification.js
  23. <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
  24. 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
  25. > tree -L 1 ./ -a -I node_modules ├── .babelrc

    ├── .firebaserc ├── firebase.json ├── package.json ├── public ├── src ├── webpack.config.js └── yarn.lock
  26. > 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
  27. 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
  28. 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
  29. 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
  30. class Notification extends React.Component { static firebaseApp; constructor(props) { super(props);

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

    { token: '', message: '' }; } } ./src/components/Notification.js
  32. 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
  33. render() { return ( <div> <Card> <CardHeader title={'Token'} subtitle={this.state.token} />

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

    ├── components ├── firebase-messaging-sw.js └── main.js
  35. 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
  36. (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
  37. module.exports = { entry: { main: ['./src/main.js'], vendor: ['react', 'react-dom']

    }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' }), ] } ./webpack.config.js
  38. 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
  39. import asyncComponent from './AsyncComponent'; const Home = asyncComponent(() => {

    return import( /* webpackChunkName: "home" */ './Home') .then(module => module.default); }); const Users = ...' const Notification = ...; ./src/components/App.js
  40. plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: './public/index.html', favicon: './public/favicon.ico'

    }), new PreloadWebpackPlugin({ include: ['vendor', 'main', 'common', 'home'] }), ] ./webpack.config.js
  41. 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> ); } }