Skip to content

Commit

Permalink
Update README and implement Workbox Service Worker
Browse files Browse the repository at this point in the history
Improved structure and clarity in README for better understanding of manifest configuration. Also added implementation of a Workbox-based Service Worker, providing offline capabilities and push notifications, improving overall robustness of the web application.
  • Loading branch information
Spomky committed Dec 18, 2023
1 parent c0fc5b5 commit bdb2cb3
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 9 deletions.
62 changes: 56 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ This project follows the [semantic versioning](http://semver.org/) strictly.

# Documentation

## Manifest Configuration
A Progressive Web Application (PWA) is a web application that has a manifest and can be installed on a device.
The manifest is a JSON file that describes the application and its assets (icons, screenshots, etc.).

A Service Worker can be used to provide offline capabilities to the application or to provide push notifications.

## Manifest

### Manifest Configuration

The bundle is able to generate a manifest from a configuration file.
The manifest members defined in the [Web app manifests](https://developer.mozilla.org/en-US/docs/Web/Manifest) are supported
Expand Down Expand Up @@ -57,7 +64,7 @@ phpwa:
'application/json': ['.json']
```
## Manifest Generation
### Manifest Generation
When the configuration is set, you can generate the manifest with the following command:
Expand Down Expand Up @@ -98,7 +105,7 @@ For instance, if your application root URL is https://example.com/foo/bar, set t
}
```

## Manifest Icons
### Manifest Icons

The bundle is able to generate icons from a source image.
The icons must be square and the source image should be at best quality as possible.
Expand All @@ -109,7 +116,7 @@ Depending on your system, you may have to install one extension or the other.
```yaml
# config/packages/phpwa.yaml
phpwa:
icon_processor: 'pwa.icon_processor.gd' # or 'pwa.icon_processor.imagick'
image_processor: 'pwa.image_processor.gd' # or 'pwa.image_processor.imagick'
icons:
- src: "%kernel.project_dir%/assets/images/logo.png"
sizes: [48, 57, 60, 72, 76, 96, 114, 128, 144, 152, 180, 192, 256, 384, 512, 1024]
Expand All @@ -126,14 +133,15 @@ With the configuration above, the bundle will generate
* 16 icons from the `mask.png` image. The format will be `png` and the purpose will be `maskable`.
* And 1 icon from the `logo.svg` image. The format will be `svg` and the size will be `any`.

## Manifest Screenshots
### Manifest Screenshots

The bundle is able to generate screenshots from a source image.
Similar to icons, the source image should be at best quality as possible.

```yaml
# config/packages/phpwa.yaml
phpwa:
image_processor: 'pwa.image_processor.gd' # or 'pwa.image_processor.imagick'
screenshots:
- src: "%kernel.project_dir%/assets/screenshots/narrow.png"
label: "View of the application home page"
Expand All @@ -149,14 +157,15 @@ The bundle will automatically generate screenshots from the source images and ad
such as the `sizes` and the `form_factor` (`wide` or `narrow`).
The `format` parameter is optional. It indicates the format of the generated image. If not set, the format will be the same as the source image.

## Manifest Shortcuts
### Manifest Shortcuts

The `shortcuts` member may contain a list of icons.
The parameters are very similar to the `icons` member.

```yaml
# config/packages/phpwa.yaml
phpwa:
image_processor: 'pwa.image_processor.gd' # or 'pwa.image_processor.imagick'
shortcuts:
- name: "Shortcut 1"
short_name: "shortcut-1"
Expand All @@ -176,6 +185,47 @@ phpwa:
format: 'webp'
```

### Using the Manifest

The manifest can be used in your HTML pages with the following code in the `<head>` section.
In you customized the output filename or the public folder, please replace `pwa.json` with the path to your manifest file.

```html
<link rel="manifest" href="{{ asset('pwa.json') }}">
```

## Service Worker

The following command will generate a Service Worker in the `public` directory.

```bash
symfony console pwa:sw
```

You can change the output file name and the output folder with the following options:

* `--output` or `-o` to change the output file name (default: `sw.js`)
* `--public_folder` or `-p` to change the public folder (default: `%kernel.project_dir%/public`)

Next, you have to register the Service Worker in your HTML pages with the following code in the `<head>` section.
It can also be done in a JavaScript file such as `app.js`.
In you customized the output filename or the public folder, please replace `sw.js` with the path to your Service Worker file.

```html
<script>
if (navigator.serviceWorker) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/sw.js", {scope: '/'});
})
}
</script>
```

### Service Worker Configuration

The Service Worker uses Workbox and comes with predefined configuration and recipes.
You are free to change the configuration and the recipes to fit your needs.
In particular, you can change the cache strategy, the cache expiration, the cache name, etc.

# Support

Expand Down
4 changes: 1 addition & 3 deletions src/Command/GenerateManifestCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,15 @@ final class GenerateManifestCommand extends Command
{
private readonly MimeTypes $mime;

private readonly Filesystem $filesystem;

public function __construct(
private readonly null|ImageProcessor $imageProcessor,
#[Autowire('%spomky_labs_pwa.config%')]
private readonly array $config,
#[Autowire('%kernel.project_dir%')]
private readonly string $rootDir,
private readonly Filesystem $filesystem,
) {
$this->mime = MimeTypes::getDefault();
$this->filesystem = new Filesystem();
parent::__construct();
}

Expand Down
72 changes: 72 additions & 0 deletions src/Command/WorkboxInitCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;
use Symfony\Component\HttpKernel\Config\FileLocator;

#[AsCommand(
name: 'pwa:sw',
description: 'Initializes the Workbox-based Service Worker.',
)]
class WorkboxInitCommand extends Command
{
public function __construct(
#[Autowire('%kernel.project_dir%')]
private readonly string $rootDir,
private readonly Filesystem $filesystem,
private readonly FileLocator $fileLocator,
) {
parent::__construct();
}

protected function configure(): void
{
$this
->addOption(
'public_folder',
'p',
InputOption::VALUE_OPTIONAL,
'Public folder',
$this->rootDir . '/public'
)
->addOption('output', 'o', InputOption::VALUE_OPTIONAL, 'Output file', 'sw.js')
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Workbox Service Worker');

$publicFolder = Path::canonicalize($input->getOption('public_folder'));
$outputFile = '/' . trim((string) $input->getOption('output'), '/');

if (! $this->filesystem->exists($publicFolder)) {
$this->filesystem->mkdir($publicFolder);
}

$resourcePath = $this->fileLocator->locate('@SpomkyLabsPwaBundle/Resources/workbox.js', null, false);
if (count($resourcePath) !== 1) {
$io->error('Unable to find the Workbox resource.');
return self::FAILURE;
}
$resourcePath = $resourcePath[0];
$this->filesystem->copy($resourcePath, $publicFolder . $outputFile);


$io->success('Workbox is ready to use!');

return self::SUCCESS;
}
}
2 changes: 2 additions & 0 deletions src/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use SpomkyLabs\PwaBundle\Command\GenerateManifestCommand;
use SpomkyLabs\PwaBundle\Command\WorkboxInitCommand;
use SpomkyLabs\PwaBundle\ImageProcessor\GDImageProcessor;
use SpomkyLabs\PwaBundle\ImageProcessor\ImagickImageProcessor;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
Expand All @@ -16,6 +17,7 @@
;

$container->set(GenerateManifestCommand::class);
$container->set(WorkboxInitCommand::class);

if (extension_loaded('imagick')) {
$container
Expand Down
65 changes: 65 additions & 0 deletions src/Resources/workbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js'
);

const {
pageCache, // Cache pages with a network-first strategy.
imageCache, // Cache images with a cache-first strategy.
staticResourceCache, // Cache CSS, JS, and Web Worker requests with a cache-first strategy for 1 year.
offlineFallback, // Serve an offline fallback page when the user is offline and try to revalidate the request when the user is online.
warmStrategyCache, // Warm the cache with URLs that are likely to be visited next or during offline navigation.
} = workbox.recipes;
const { CacheFirst } = workbox.strategies;
const { registerRoute } = workbox.routing;
const { CacheableResponsePlugin } = workbox.cacheableResponse;
const { ExpirationPlugin } = workbox.expiration;

const PAGE_CACHE_NAME = 'pages';
const FONT_CACHE_NAME = 'fonts';
const STATIC_CACHE_NAME = 'assets';
const IMAGE_CACHE_NAME = 'images';
const OFFLINE_URI = '/offline'; // URI of the offline fallback page.
const warmCacheUrls = [ // URLs to warm the cache with.
'/',
];

// *** Recipes ***
// Cache pages with a network-first strategy.
pageCache({
cacheName: PAGE_CACHE_NAME
});
// Cache CSS, JS, and Web Worker requests with a cache-first strategy.
staticResourceCache({
cacheName: STATIC_CACHE_NAME,
});
// Cache images with a cache-first strategy.
imageCache({
cacheName: IMAGE_CACHE_NAME,
maxEntries: 60, // Default 60 images
maxAgeSeconds: 60 * 60 * 24 * 30, // Default 30 days
});
// Serve an offline fallback page when the user is offline and try to revalidate the request when the user is online.
offlineFallback({
pageFallback: OFFLINE_URI,
});

// Cache the underlying font files with a cache-first strategy.
registerRoute(
({request}) => request.destination === 'font',
new CacheFirst({
cacheName: FONT_CACHE_NAME,
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxAgeSeconds: 60 * 60 * 24 * 365,
maxEntries: 30,
}),
],
}),
);

// Warm the cache with URLs that are likely to be visited next or during offline navigation.
const strategy = new CacheFirst();
warmStrategyCache({urls: warmCacheUrls, strategy});

0 comments on commit bdb2cb3

Please sign in to comment.