Skip to main content

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. If you have enabled modern build, then SW will be generated for both modern (with .modern.js suffix) and legacy bundles.

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:

src/sw.ts
/// <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
}
}
}
}
caution

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:

src/sw.ts
/// <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):

caution

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 install --save-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 .js and .css files at runtime with passed strategy option (default is stale while revalidate strategy)
  • limit cache size and ttl with maxEntries and maxAgeSeconds options
  • cache only 200 or opaque responses
  • allows to precache assets with precacheManifest option (simple way to control this assets still pwa.workbox.include parameter from tramvai.json)
tip

Prefer cacheApplicationStaticAssets method over the precacheAndRoute from workbox if you want cache all used assets

Usage example:

sw.ts
/// <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, .svg files runtime with passed strategy option (default is stale while revalidate strategy)
  • limit cache size and ttl with maxEntries and maxAgeSeconds options
  • cache only 200 or opaque responses
  • allows to precache assets with precacheManifest option (simple way to control this assets still pwa.workbox.include parameter from tramvai.json)

Usage example:

sw.ts
/// <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, .ttf files runtime with passed strategy option (default is stale while revalidate strategy)
  • limit cache size and ttl with maxEntries and maxAgeSeconds options
  • cache only 200 or opaque responses
  • allows to precache assets with precacheManifest option (simple way to control this assets still pwa.workbox.include parameter from tramvai.json)

Usage example:

sw.ts
/// <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.scope parameter from tramvai.json, with passed strategy option (default is network first strategy)
  • with timeout for network request provided in networkTimeoutSeconds option
  • limit cache size and ttl with maxEntries and maxAgeSeconds options
  • cache only 200 or opaque responses
  • allows to precache pages with precacheManifest option (simple way to control this assets still pwa.workbox.include parameter from tramvai.json)

Usage example:

sw.ts
/// <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.

tip

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:

src/sw.ts
/// <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$"
]
}
}
}
}