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

Progressive Web Apps with React.js and Firebase

Avatar for Jimmy Moon Jimmy Moon
May 20, 2017
190

Progressive Web Apps with React.js and Firebase

Avatar for Jimmy Moon

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> ); } }