Home Navigation

Tuesday 26 November 2019

Implement PWA Service worker with google WorkBox

What is Workbox?

From google workbox site, Workbox is a library that bakes in a set of best practices and removes the boilerplate every developer writes when working with service workers.
  • Precaching
  • Runtime caching
  • Strategies
  • Request routing
  • Background sync
  • Helpful debugging
  • Greater flexibility and feature set than sw-precache and sw-toolbox
To create service worker with workbox follow the below steps

Step 1:
Create a react app with your preferred tool like create-react-app or npx or yarn

Step 2:
        install workbox cli
        $npm install workbox-cli --global

Step 3:
       Go to the react project directory and then run the below commands

       $npm run build // it will compile and create the build folder

       $workbox wizard

       Then follow the options it asks. ( if you are not sure what to choose pick the default option and            hit enter)

       You will be presented the below options

? What is the root of your web app (i.e. which directory do you deploy)? (Use ar
row keys)
> build/
  public/
  src/
  ──────────────
  Manually enter path

? Which file types would you like to precache? (Press <space> to select, <a> to
toggle all, <i> to invert selection)
>(*) json
 (*) ico
 (*) html
 (*) png
 (*) js
 (*) txt
 (*) css
(Move up and down to reveal more choices)
  
? Where would you like your service worker file to be saved? (build\sw.js)  
? Where would you like to save these configuration options? (workbox-config.js)

Step 4:
       To generate service worker, run

       $workbox generateSW workbox-config.js

Step 5:
create a service worker file in /src dirctory name: workbox-sw.js and add the below contents

importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");

const precacheManifest = [];


console.log("[Workbox] ######################## Installing ############################")

if (workbox) {
    console.log('[Workbox] Yay! Workbox is loaded 🎉');
} else {
    console.log('[Workbox] Boo! Workbox did not load 😬');
}

console.log("[Workbox] #################################################################")


workbox.precaching.precacheAndRoute(precacheManifest);



Step 6:
Modify the workbox-config.js located in the root directory of the project


module.exports = {
  "globDirectory": "build/",
  "globPatterns": [
    "**/*.{json,ico,html,js,css}"
  ],
  "swDest": "build/sw.js",
  "swSrc": "src/workbox-sw.js",
  "injectionPointRegexp": /(const precacheManifest = )\[\](;)/
};


Step 7:
      Register service worker, edit index.html file in public/ directory and add the below scripts

h
<script>
    console.log('%NODE_ENV%');

    const isProduction = '%NODE_ENV%' === 'production';
    if (isProduction) {
      console.log('This is a production environment :-|');
    } else {
      console.log('This is a development environment o-o');
    }

    if (isProduction && 'serviceWorker' in navigator) {
      navigator.serviceWorker.register('sw.js')
        .then(registration => console.log('[ service workder ] - Service Worker registered'))
        .catch(err => '[ service workder ] - SW registration failed');
    }
  </script>


Step 8:
Modify package.json and add the below script line star-sw


"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "start-sw": "react-scripts build && workbox copyLibraries build/ && workbox injectManifest workbox-config.js"
  }


Step 9:
run the service worker script which will generate and precache and build the project.

$npm run start-sw

Step 10:
run the compiled and generated project ( if you don't have serve installed run command
        $npm  install serve -g )

$serve -s build

Step 11:
Open your project http://localhost:5000, turn off the network and reload the page and see the                magic, it works offline

to see all cached contents go to application tab on your browser,





Additional: 
Add your caching strategy in src/workbox-sw.js , for reference how to add strategy follow the below links

https://developers.google.com/web/tools/workbox/modules/workbox-strategies
https://developers.google.com/web/tools/workbox/guides/common-recipes

 A sample workbox-sw.js with graphql implementation

importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
const precacheManifest = [];
console.log("[Workbox] ############## Installing #############################")
if (workbox) {
    console.log('[Workbox] Yay! Workbox is loaded 🎉');
} else {
    console.log('[Workbox] Boo! Workbox did not load 😬');
}
console.log("[Workbox] ########################################################")
workbox.precaching.precacheAndRoute(precacheManifest);

// You might want to use a cache-first strategy for images
workbox.routing.registerRoute(
    /\.(?:png|gif|jpg|jpeg|webp|svg)$/,
    new workbox.strategies.CacheFirst({
        cacheName: IMAGE_CACHE,
        plugins: [
            new workbox.expiration.Plugin({
                maxEntries: 60,
                maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
            }),
        ],
    })
);

// Cache the Google Fonts stylesheets with a stale-while-revalidate strategy.
workbox.routing.registerRoute(
    /^https:\/\/fonts\.googleapis\.com/,
    new workbox.strategies.StaleWhileRevalidate({
        cacheName: GOOGLE_FONT_STYLE_CACHE,
    })
);

// Cache the underlying font files with a cache-first strategy for 1 year.
workbox.routing.registerRoute(
    /^https:\/\/fonts\.gstatic\.com/,
    new workbox.strategies.CacheFirst({
        cacheName: GOOGLE_FONT_WEBAPI_CACHE,
        plugins: [
            new workbox.cacheableResponse.Plugin({
                statuses: [0, 200],
            }),
            new workbox.expiration.Plugin({
                maxAgeSeconds: 60 * 60 * 24 * 365,
                maxEntries: 30,
            }),
        ],
    })
);

// broadcast channel to load new updates
self.addEventListener('install', (event) => {
    const updateChannel = new BroadcastChannel('sw-precache-channel');
    updateChannel.postMessage({ promptToReload: true });

    updateChannel.onmessage = (message) => {
        if(message.data.skipWaiting){
            self.skipWaiting();
        }
    };
});

// Workbox with custom handler to use IndexedDB for cache.

workbox.routing.registerRoute(
    new RegExp('/graphql(/)?'),
    async ({ event }) => {
        return staleWhileRevalidate(event);
    },
    'POST'
);

// Return cached response when possible, and fetch new results from server in chnage
// the background and update the cache.
self.addEventListener('fetch', async (event) => {
    if (event.request.method === 'POST') {
        event.respondWith(staleWhileRevalidate(event));
    }
    // TODO: Handles other types of requests.
});

async function staleWhileRevalidate(event) {
    let promise = null;
    let cachedResponse = await getCache(event.request.clone());
    let fetchPromise = fetch(event.request.clone())
        .then((response) => {
            setCache(event.request.clone(), response.clone());
            return response;
        })
        .catch((err) => {
            console.error(err);
        });
    return cachedResponse ? Promise.resolve(cachedResponse) : fetchPromise;
}

async function serializeResponse(response) {
    let serializedHeaders = {};
    for (var entry of response.headers.entries()) {
        serializedHeaders[entry[0]] = entry[1];
    }
    let serialized = {
        headers: serializedHeaders,
        status: response.status,
        statusText: response.statusText
    };
    serialized.body = await response.json();
    return serialized;
}

async function setCache(request, response) {
    var key, data;
    let body = await request.json();
    let id = CryptoJS.MD5(body.query).toString();

    var entry = {
        query: body.query,
        response: await serializeResponse(response),
        timestamp: Date.now()
    };
    idbKeyval.set(id, entry, store);
}

async function getCache(request) {
    let data;
    try {
        let body = await request.json();
        let id = CryptoJS.MD5(body.query).toString();
        data = await idbKeyval.get(id, store);
        if (!data) return null;

        // Check cache max age.
        let cacheControl = request.headers.get('Cache-Control');
        let maxAge = cacheControl ? parseInt(cacheControl.split('=')[1]) : 3600;
        if (Date.now() - data.timestamp > maxAge * 1000) {
            console.log(`Cache expired. Load from API endpoint.`);
            return null;
        }

        console.log(`Load response from cache.`);
        return new Response(JSON.stringify(data.response.body), data.response);
    } catch (err) {
        return null;
    }
}

async function getPostKey(request) {
    let body = await request.json();
    return JSON.stringify(body);
}

Monday 25 November 2019

React manage different environment variable with .env file



React web application has two values environment variable NODE_ENV, it is either production or development. You can not modify the variable NODE_ENV, this is an international setting to protect the production environment from an accidental development.

  "scripts": {
    "start": "react-scripts start", // the value of NODE_ENV is development
    "build": "react-scripts build", // the value of NODE_ENV is production
...
}


.env: Default.
.env.local: Local overrides. This file is loaded for all environments except test.
.env.development, .env.test, .env.staging, .env.production: Environment-specific settings.
.env.development.local, .env.test.local, .env.production.local: Local overrides of environment-specific settings.

.env file will be use used for runing by defualt
.env.development file will be used for running script npm start
.env.production file will be used for running script npm build

To create different environmental variable and use them in react code create the below files in the root directory of the project

filename: .env
contents:  REACT_APP_PAGE_TITLE = "My React app application"

filename: .env.development
contents:  REACT_APP_MY_API = "https://development-my-api.com/"
  REACT_APP_ENV=dev

filename: .env.staging
contents:  REACT_APP_MY_API = "https://staging-my-api.com/"
  REACT_APP_ENV=staging

filename: .env.production
contents:  REACT_APP_MY_API = "https://prod-my-api.com/"
  REACT_APP_ENV=prod

install the below package:

$ npm install env-cmd --save
or
$ yarn add env-cmd


Modify script in package.json and it should be look like below

"scripts": {
    "start": "react-scripts start", // the value of NODE_ENV is development
    "build": "react-scripts build", // the value of NODE_ENV is production
"build:staging": "env-cmd -f .env.staging react-scripts build", // the value of NODE_ENV is still production
...
}

To test the application if it works, add the below tags in your app.js

<div>
      <h1>{process.env.REACT_APP_PAGE_TITLE}</h1> 
      <small>You are running this application in <b>{process.env.REACT_APP_ENV}</b> mode.</small>
      <p>{process.env.REACT_APP_MY_API}</p>
  </div>


Run application
development:
npm start


Staging:
npm run build:staging // build the application for staging
serve -s build // run the application compiled for staging


production:
npm run build // build the application for production
serve -s build // run the application compiled for production