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:
/// <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
.js
and.css
files at runtime with passedstrategy
option (default is stale while revalidate strategy) - limit cache size and ttl with
maxEntries
andmaxAgeSeconds
options - cache only
200
or opaque responses - allows to precache assets with
precacheManifest
option (simple way to control this assets stillpwa.workbox.include
parameter 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, .svg
files runtime with passedstrategy
option (default is stale while revalidate strategy) - limit cache size and ttl with
maxEntries
andmaxAgeSeconds
options - cache only
200
or opaque responses - allows to precache assets with
precacheManifest
option (simple way to control this assets stillpwa.workbox.include
parameter 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, .ttf
files runtime with passedstrategy
option (default is stale while revalidate strategy) - limit cache size and ttl with
maxEntries
andmaxAgeSeconds
options - cache only
200
or opaque responses - allows to precache assets with
precacheManifest
option (simple way to control this assets stillpwa.workbox.include
parameter 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.scope
parameter fromtramvai.json
, with passedstrategy
option (default is network first strategy) - with timeout for network request provided in
networkTimeoutSeconds
option - limit cache size and ttl with
maxEntries
andmaxAgeSeconds
options - cache only
200
or opaque responses - allows to precache pages with
precacheManifest
option (simple way to control this assets stillpwa.workbox.include
parameter 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$"
]
}
}
}
}