Add service worker for PWA functionality.
This commit is contained in:
parent
d0eaabf8e1
commit
4fad57a1b3
|
@ -6,13 +6,16 @@ use Slim\App;
|
|||
use Slim\Routing\RouteCollectorProxy;
|
||||
|
||||
return function (App $app) {
|
||||
$app->get('/sw.js', Controller\Frontend\PWA\ServiceWorkerAction::class)
|
||||
->setName('public:sw');
|
||||
|
||||
$app->group(
|
||||
'/public/{station_id}',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get('[/{embed:embed|social}]', Controller\Frontend\PublicPages\PlayerAction::class)
|
||||
->setName('public:index');
|
||||
|
||||
$group->get('/app.webmanifest', Controller\Frontend\PublicPages\AppManifestAction::class)
|
||||
$group->get('/app.webmanifest', Controller\Frontend\PWA\AppManifestAction::class)
|
||||
->setName('public:manifest');
|
||||
|
||||
$group->get('/embed-requests', Controller\Frontend\PublicPages\RequestsAction::class)
|
||||
|
|
|
@ -482,6 +482,35 @@ class Assets
|
|||
return $compiled_attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] The paths to all currently loaded files.
|
||||
*/
|
||||
public function getLoadedFiles(): array
|
||||
{
|
||||
$this->sort();
|
||||
|
||||
$result = [];
|
||||
foreach ($this->loaded as $item) {
|
||||
if (!empty($item['files']['js'])) {
|
||||
foreach ($item['files']['js'] as $file) {
|
||||
if (isset($file['src'])) {
|
||||
$result[] = $this->getUrl($file['src']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($item['files']['css'])) {
|
||||
foreach ($item['files']['css'] as $file) {
|
||||
if (isset($file['href'])) {
|
||||
$result[] = $this->getUrl($file['href']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the URI of the resource, whether local or remote/CDN-based.
|
||||
*
|
||||
|
@ -495,7 +524,7 @@ class Assets
|
|||
$resource_uri = $this->versioned_files[$resource_uri];
|
||||
}
|
||||
|
||||
if (preg_match('/^(https?:)?\/\//', $resource_uri)) {
|
||||
if (str_starts_with($resource_uri, 'http')) {
|
||||
$this->addDomainToCsp($resource_uri);
|
||||
return $resource_uri;
|
||||
}
|
||||
|
@ -528,13 +557,17 @@ class Assets
|
|||
|
||||
// CSP JavaScript policy
|
||||
// Note: unsafe-eval included for Vue template compiling
|
||||
$csp_script_src = $this->getCspDomains();
|
||||
$csp_script_src[] = "'self'";
|
||||
$csp_script_src[] = "'unsafe-eval'";
|
||||
$csp_script_src[] = "'nonce-" . $this->getCspNonce() . "'";
|
||||
$cspScriptSrc = $this->getCspDomains();
|
||||
$cspScriptSrc[] = "'self'";
|
||||
$cspScriptSrc[] = "'unsafe-eval'";
|
||||
$cspScriptSrc[] = "'nonce-" . $this->getCspNonce() . "'";
|
||||
$csp[] = 'script-src ' . implode(' ', $cspScriptSrc);
|
||||
|
||||
$csp[] = 'script-src ' . implode(' ', $csp_script_src);
|
||||
$csp[] = 'worker-src blob:';
|
||||
$cspWorkerSrc = [];
|
||||
$cspWorkerSrc[] = "blob:";
|
||||
$cspWorkerSrc[] = "'self'";
|
||||
|
||||
$csp[] = 'worker-src ' . implode(' ', $cspWorkerSrc);
|
||||
|
||||
return $response->withHeader('Content-Security-Policy', implode('; ', $csp));
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Frontend\PublicPages;
|
||||
namespace App\Controller\Frontend\PWA;
|
||||
|
||||
use App\Environment;
|
||||
use App\Exception\StationNotFoundException;
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Frontend\PWA;
|
||||
|
||||
use App\Assets;
|
||||
use App\Environment;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use DI\FactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class ServiceWorkerAction
|
||||
{
|
||||
public const SW_BASE = <<<'JS'
|
||||
const cacheName = 'assets'; // Change value to force update
|
||||
const cacheFiles = [];
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
// Kick out the old service worker
|
||||
self.skipWaiting();
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(cacheName).then(cache => {
|
||||
return cache.addAll(cacheFiles);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
// Delete any non-current cache
|
||||
event.waitUntil(
|
||||
caches.keys().then(keys => {
|
||||
Promise.all(
|
||||
keys.map(key => {
|
||||
if (![cacheName].includes(key)) {
|
||||
return caches.delete(key);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Offline-first, cache-first strategy
|
||||
// Kick off two asynchronous requests, one to the cache and one to the network
|
||||
// If there's a cached version available, use it, but fetch an update for next time.
|
||||
// Gets data on screen as quickly as possible, then updates once the network has returned the latest data.
|
||||
self.addEventListener('fetch', event => {
|
||||
event.respondWith(
|
||||
caches.open(cacheName).then(cache => {
|
||||
return cache.match(event.request).then(response => {
|
||||
return response || fetch(event.request).then(networkResponse => {
|
||||
cache.put(event.request, networkResponse.clone());
|
||||
return networkResponse;
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
JS;
|
||||
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
Environment $environment,
|
||||
FactoryInterface $factory
|
||||
): ResponseInterface {
|
||||
$assets = $factory->make(
|
||||
Assets::class,
|
||||
[
|
||||
'request' => $request,
|
||||
]
|
||||
);
|
||||
|
||||
$swContents = self::SW_BASE;
|
||||
|
||||
$findString = 'const cacheFiles = [];';
|
||||
if (!str_contains($swContents, $findString)) {
|
||||
throw new \RuntimeException('Service worker template does not contain proper placeholder.');
|
||||
}
|
||||
|
||||
$cacheFiles = [];
|
||||
|
||||
// Cache the compiled assets that would be used on public players.
|
||||
$assets->load('minimal')
|
||||
->load('Vue_PublicFullPlayer');
|
||||
|
||||
$loadedFiles = $assets->getLoadedFiles();
|
||||
|
||||
foreach ($loadedFiles as $file) {
|
||||
if (!str_starts_with($file, 'http')) {
|
||||
$cacheFiles[] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache images and icons
|
||||
$staticBase = $environment->getBaseDirectory() . '/web' . $environment->getAssetUrl();
|
||||
|
||||
$otherStaticFiles = Finder::create()
|
||||
->files()
|
||||
->in($staticBase)
|
||||
->depth('>=1')
|
||||
->exclude(['dist', 'api']);
|
||||
|
||||
foreach($otherStaticFiles as $file) {
|
||||
$cacheFiles[] = $environment->getAssetUrl().'/'.$file->getRelativePathname();
|
||||
}
|
||||
|
||||
$replaceString = 'const cacheFiles = ' . json_encode($cacheFiles) . ';';
|
||||
|
||||
$swContents = str_replace($findString, $replaceString, $swContents);
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'text/javascript')
|
||||
->write($swContents);
|
||||
}
|
||||
}
|
|
@ -38,6 +38,19 @@ if ($customization->useWebSocketsForNowPlaying()) {
|
|||
$assets
|
||||
->addVueRender('Vue_PublicFullPlayer', '#public-radio-player', $props);
|
||||
|
||||
// Register PWA service worker
|
||||
$swJsRoute = (string)$router->named('public:sw');
|
||||
|
||||
$assets->addInlineJs(
|
||||
<<<JS
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('${swJsRoute}');
|
||||
});
|
||||
}
|
||||
JS
|
||||
);
|
||||
|
||||
$this->push('head');
|
||||
?>
|
||||
<link rel="manifest" href="<?=(string)$router->fromHere('public:manifest')?>">
|
||||
|
|
Loading…
Reference in New Issue