PWA (Progressive Web App)
Introduction
tramvai provides complete support for Progressive Web Apps (PWA) features.
Main PWA features separated into a few modules:
Explanation
Workbox
Workbox module based on awesome workbox library.
This module responsible for Service Worker (SW) generation and registration. For SW generation, we use InjectManifest workbox webpack plugin. On the client-side, we use workbox-window library for SW registration.
Main purpose of InjectManifest integration is to inject information about application assets to source SW file. It allows us to precache all critical application assets.
Service Worker generation process is integrated with @tramvai/cli development server. It means that you don't need always to run build command to generate SW, but this behavior is configurable.
For production build, SW will be generated in client build directory with other assets.
Webmanifest
Another important part of PWA is Web Application Manifest.
This module allows you to generate webmanifest file as part of the build process. Generated file can have .json or .webmanifest extension.
On application pages, webmanifest will be automatically connected through link tag with rel manifest.
All webmanifest configuration is placed in tramvai.json configuration file.
Meta
This module simplifies the process of adding PWA specific meta tags to the application pages.
All PWA meta configuration basically is placed in tramvai.json configuration file.
Icons
PWA can be installed on endless set of devices, and all of them can have different requirements for installed app or startup screen icons.
This module allows you to generate all required icons for PWA and automatically connect them to the webmanifest.
For source image processing sharp library is used.
Prerequisites
First, you need to install @tramvai/module-progressive-web-app module:
npx tramvai add @tramvai/module-progressive-web-app
Then, connect TramvaiPwaModule from this package to createApp function:
import { createApp } from '@tramvai/core';
import { TramvaiPwaModule } from '@tramvai/module-progressive-web-app';
createApp({
  name: 'tincoin',
  modules: [TramvaiPwaModule],
});
At last, you need to create source Service Worker file, by default it should be named src/sw.ts, and contain minimum boilerplate for better typings and workbox integration:
/// <reference lib="webworker" />
declare const self: ServiceWorkerGlobalScope;
// `self.__WB_MANIFEST` type is provided by `workbox-precaching` package, so `any` cast can we removed after this package import
const manifest = (self as any).__WB_MANIFEST;
Usage
Service Worker
For SW generation, you need to provide experiments.pwa.workbox.enabled option in tramvai.json configuration file:
{
  "experiments": {
    "pwa": {
      "workbox": {
        "enabled": true
      }
    }
  }
}
Hot refresh is not working correctly with InjectManifest plugin. If you want to devlop SW locally, better to disable hot refresh in tramvai.json:
{
  "hotRefresh": {
    "enabled": false
  }
}
Registration scope
Default Service Worker scope is /. Registration scope can be changed by providing experiments.pwa.sw.scope option:
{
  "experiments": {
    "pwa": {
      "sw": {
        "scope": "/myapp/"
      },
      "workbox": {
        "enabled": true
      }
    }
  }
}
This parameter will be used for SW registration and injected in generated webmanifest file.
Source and output filenames
By default, from src/sw.ts file will be generated ${output.client}/sw.js. SW source and output filenames can be changed by providing experiments.pwa.sw.src and experiments.pwa.sw.dest options:
{
  "experiments": {
    "pwa": {
      "sw": {
        "src": "service-worker.ts",
        "dest": "service-worker.js"
      },
      "workbox": {
        "enabled": true
      }
    }
  }
}
Precaching
Simple way to precache all application assets is to use workbox-precaching package:
/// <reference lib="webworker" />
import { precacheAndRoute } from 'workbox-precaching';
declare const self: ServiceWorkerGlobalScope;
precacheAndRoute(self.__WB_MANIFEST);
By default, self.__WB_MANIFEST will contain all application processed assets - JS and CSS files, fonts, images.
Control precached assets
For large applications, it is unnecessary to precache all assets.
exclude and include options allow you to pass regexp for assets filtering, for example include only JS and CSS files or exclude all images:
{
  "experiments": {
    "pwa": {
      "workbox": {
        "include": ["\\.js$", "\\.css$"],
        // do not use include and exclude options together
        "exclude": ["\\.{png|jpg|jpeg|svg|gif}$"]
      }
    }
  }
}
Also you can specify JS and CSS chunks that should be included (chunks) or excluded (excludeChunks):
Only JS and CSS assets will be included with chunks option
{
  "experiments": {
    "pwa": {
      "workbox": {
        "chunks": ["react", "platform"],
        // do not use chunks and excludeChunks options together
        "excludeChunks": ["some-lazy-chunk"]
      }
    }
  }
}
For custom assets, which is not included in build process, additionalManifestEntries option is available:
{
  "experiments": {
    "pwa": {
      "workbox": {
        "additionalManifestEntries": [
          "static/offline.html",
          // better way to pass object with file revision, it is important for cache invalidation
          {
            "url": "static/offline.html",
            "revision": "1234567890"
          }
        ]
      }
    }
  }
}
Webmanifest
For webmanifest generation, you need to provide experiments.pwa.webmanifest.enabled option in tramvai.json configuration file:
{
  "experiments": {
    "pwa": {
      "webmanifest": {
        "enabled": true
      }
    }
  }
}
Output filename
By default, will be generated ${output.client}/manifest.json file in development mode and ${output.client}/manifest.[hash].json for production. Filename can be changed by providing experiments.pwa.webmanifest.dest option:
{
  "experiments": {
    "pwa": {
      "webmanifest": {
        "enabled": true,
        "dest": "manifest.webmanifest"
      }
    }
  }
}
If [hash] pattern will be used in filename, it will be replaced with manifest content hash, only for production build, it is useful for cache invalidation.
Content
All another experiments.pwa.webmanifest options will be passed directly to generated webmanifest. For example, from this configuration:
{
  "experiments": {
    "pwa": {
      "webmanifest": {
        "enabled": true,
        "name": "tincoin",
        "start_url": "/",
        "display": "standalone"
      }
    }
  }
}
This webmanifest will be created:
{
  // scope borrowed from `experiments.pwa.sw.scope`
  "scope": "/",
  "name": "tincoin",
  "start_url": "/",
  "display": "standalone"
}
Also, theme_color property will be borrowed from experiments.pwa.meta.themeColor, and icons will be automatically generated from experiments.pwa.icons configuration.
Icons
First, you need to install sharp library:
- npm
- Yarn
npm install --save-dev sharp
yarn add --dev sharp
Then, provide path to your source icon in experiments.pwa.icons.src:
{
  "experiments": {
    "pwa": {
      "icons": {
        // relative to "root" directory
        "src": "images/pwa-icon.png"
      }
    }
  }
}
After application build, this set of icons will be generated and added to webmanifest:
[
  {
    "src": "ASSETS_PREFIX/dist/client/pwa-icons/36x36.png",
    "sizes": "36x36",
    "type": "image/png"
  },
  {
    "src": "ASSETS_PREFIX/dist/client/pwa-icons/48x48.png",
    "sizes": "48x48",
    "type": "image/png"
  },
  {
    "src": "ASSETS_PREFIX/dist/client/pwa-icons/72x72.png",
    "sizes": "72x72",
    "type": "image/png"
  },
  {
    "src": "ASSETS_PREFIX/dist/client/pwa-icons/96x96.png",
    "sizes": "96x96",
    "type": "image/png"
  },
  {
    "src": "ASSETS_PREFIX/dist/client/pwa-icons/144x144.png",
    "sizes": "144x144",
    "type": "image/png"
  },
  {
    "src": "ASSETS_PREFIX/dist/client/pwa-icons/192x192.png",
    "sizes": "192x192",
    "type": "image/png"
  },
  {
    "src": "ASSETS_PREFIX/dist/client/pwa-icons/512x512.png",
    "sizes": "512x512",
    "type": "image/png"
  }
]
Sizes
Default set of icon sizes - [36, 48, 72, 96, 144, 192, 512].
You can change it by providing experiments.pwa.icons.sizes option:
{
  "experiments": {
    "pwa": {
      "icons": {
        "src": "images/pwa-icon.png",
        "sizes": [512]
      }
    }
  }
}
Output directory
Default output directory is ${output.client}/pwa-icons, it can be changed by providing experiments.pwa.icons.dest option:
{
  "experiments": {
    "pwa": {
      "icons": {
        "src": "images/pwa-icon.png",
        "dest": "icons"
      }
    }
  }
}
Meta
Meta tags depends on experiments.pwa.meta limited set of options, e.g.:
{
  "experiments": {
    "pwa": {
      "meta": {
        "themeColor": "#ffdd2d",
        "viewport": "width=device-width, initial-scale=1"
      }
    }
  }
}
PWA Recipes
Special for tramvai applications we have created a set of utility functions with popular PWA patterns, based on workbox-recipes library - @tramvai/pwa-recipes.
Installation
You need to install @tramvai/pwa-recipes and use recipes from it in your service worker:
npx tramvai add @tramvai/pwa-recipes
Cache static assets
Application may have a lot of static assets - JS and CSS files. Optimal solution for PWA is to cache this assets at runtime instead of precache all assets on application startup, and precache only critical assets.
Recipe cacheApplicationStaticAssets works like this:
- cache all .jsand.cssfiles at runtime with passedstrategyoption (default is stale while revalidate strategy)
- limit cache size and ttl with maxEntriesandmaxAgeSecondsoptions
- cache only 200or opaque responses
- allows to precache assets with precacheManifestoption (simple way to control this assets stillpwa.workbox.includeparameter fromtramvai.json)
Prefer cacheApplicationStaticAssets method over the precacheAndRoute from workbox if you want cache all used assets
Usage example:
/// <reference lib="webworker" />
import { cacheApplicationStaticAssets } from '@tramvai/pwa-recipes';
declare const self: ServiceWorkerGlobalScope;
const precacheManifest = self.__WB_MANIFEST;
cacheApplicationStaticAssets({ precacheManifest });
Cache images
Recipe cacheApplicationImages works like this:
- cache all .png, .jpg, .jpeg, .webp, .avif, .svgfiles runtime with passedstrategyoption (default is stale while revalidate strategy)
- limit cache size and ttl with maxEntriesandmaxAgeSecondsoptions
- cache only 200or opaque responses
- allows to precache assets with precacheManifestoption (simple way to control this assets stillpwa.workbox.includeparameter fromtramvai.json)
Usage example:
/// <reference lib="webworker" />
import { cacheApplicationImages } from '@tramvai/pwa-recipes';
declare const self: ServiceWorkerGlobalScope;
const precacheManifest = self.__WB_MANIFEST;
cacheApplicationImages({ precacheManifest });
Cache fonts
Recipe cacheApplicationFonts works like this:
- cache all .woff, .woff2, .otf, .ttffiles runtime with passedstrategyoption (default is stale while revalidate strategy)
- limit cache size and ttl with maxEntriesandmaxAgeSecondsoptions
- cache only 200or opaque responses
- allows to precache assets with precacheManifestoption (simple way to control this assets stillpwa.workbox.includeparameter fromtramvai.json)
Usage example:
/// <reference lib="webworker" />
import { cacheApplicationFonts } from '@tramvai/pwa-recipes';
declare const self: ServiceWorkerGlobalScope;
const precacheManifest = self.__WB_MANIFEST;
cacheApplicationFonts({ precacheManifest });
Cache pages
Recipe cacheApplicationPages works like this:
- cache all HTML pages in runtime, started with pwa.sw.scopeparameter fromtramvai.json, with passedstrategyoption (default is network first strategy)
- with timeout for network request provided in networkTimeoutSecondsoption
- limit cache size and ttl with maxEntriesandmaxAgeSecondsoptions
- cache only 200or opaque responses
- allows to precache pages with precacheManifestoption (simple way to control this assets stillpwa.workbox.includeparameter fromtramvai.json)
Usage example:
/// <reference lib="webworker" />
import { cacheApplicationPages } from '@tramvai/pwa-recipes';
declare const self: ServiceWorkerGlobalScope;
const precacheManifest = self.__WB_MANIFEST;
cacheApplicationPages({ precacheManifest });
How to
How to use custom service worker or webmanifest file
Sometimes you may want to register your own custom service worker or webmanifest. In this case you will need TramvaiPwaLightModule.
import { createApp } from '@tramvai/core';
import { TramvaiPwaLightModule } from '@tramvai/module-progressive-web-app';
createApp({
  name: 'tincoin',
  modules: [TramvaiPwaLightModule],
  providers: [
    provide({
      provide: PWA_SW_URL_TOKEN,
      useValue: '/sw.js',
    }),
    provide({
      provide: PWA_MANIFEST_URL_TOKEN,
      useValue: '/manifest.webmanifest',
    }),
  ],
});
For local development put your service worker to /public folder or use ServerModule to request proxying. Example:
import { createApp } from '@tramvai/core';
import { TramvaiPwaLightModule } from '@tramvai/module-progressive-web-app';
import { ServerModule } from '@tramvai/module-server';
createApp({
  name: 'tincoin',
  modules: [TramvaiPwaLightModule, ServerModule],
  providers: [
    provide({
      provide: PWA_SW_URL_TOKEN,
      useValue: '/sw.js',
    }),
    ...(process.env.NODE_ENV === 'development'
      ? [
          provide({
            provide: PROXY_CONFIG_TOKEN,
            scope: Scope.SINGLETON,
            useValue: {
              context: ['/sw.js', '/manifest.webmanifest'],
              target: 'https://cdn.example.com',
            },
          }),
        ]
      : []),
  ],
});
How to send messages to Service Worker?
workbox-window library provides messageSW method as a wrapper around postMessage API.
Workbox instance can be obtained from PWA_WORKBOX_TOKEN, but use it with caution, because it will be available only in browser environment, and not all browsers support Service Workers.
workbox-window will register Service Worker at commandLineListTokens.init stage, so you can use it only after this stage
import { provide, optional, commandLineListTokens } from '@tramvai/core';
// import this provider only in browser environment
const provider = provide({
  provide: commandLineListTokens.listen,
  useFactory: ({ workbox }) => {
    return async function sendMessageToSW() {
      const wb = await workbox?.();
      // wb can be `null` if Service Worker is not supported or registration failed
      const swVersion = await wb?.messageSW({ type: 'GET_VERSION' });
      console.log('Service Worker version:', swVersion);
    };
  },
  deps: {
    workbox: optional(PWA_WORKBOX_TOKEN),
  },
});
And appropriate message handler in Service Worker:
/// <reference lib="webworker" />
import { precacheAndRoute } from 'workbox-precaching';
declare const self: ServiceWorkerGlobalScope;
const SW_VERSION = '1.0.0';
self.addEventListener('message', (event) => {
  if (event.data.type === 'GET_VERSION') {
    event.ports[0].postMessage(SW_VERSION);
  }
});
precacheAndRoute(self.__WB_MANIFEST);
How to disable Service Worker generation in development mode?
You may want to disable Service Worker in development mode most of time, except when you develop SW specific features. You can pass this object to experiments.pwa.workbox.enabled option, and SW will be generated only for production build:
{
  "experiments": {
    "pwa": {
      "workbox": {
        "enabled": {
          "production": true,
          "development": false
        }
      }
    }
  }
}
How to precache webmanifest and critical assets?
You can use experiments.pwa.workbox.include option to precache webmanifest and critical assets. experiments.pwa.workbox.chunks will always exclude webmanifest, so we need to use include with some boilerplate regexp for assets hashes and without:
{
  "experiments": {
    "pwa": {
      "workbox": {
        "include": [
          // react framework chunk
          "react\\.([\\w\\d]+?\\.)?js$",
          // tramvai framework chunk
          "platform\\.([\\w\\d]+?\\.)?(js|css)$",
          // workbox-window chunk
          "tramvai-workbox-window\\.([\\w\\d]+?\\.)?chunk.js$",
          // webmanifest
          "manifest\\.([\\w\\d]+?\\.)?webmanifest$"
        ]
      }
    }
  }
}