How to handle images offline in a PWA share target? - javascript

I can register my Progressive Web App as a share target for images (supported from Chrome Android 76):
"share_target": {
"action": "/share-target",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"files": [
{
"name": "myImage",
"accept": ["image/jpeg", "image/png"]
}
]
}
}
I can then intercept attempts to share images to the app in a service worker:
self.addEventListener('fetch', e => {
if (e.request.method === 'POST' && e.request.url.endsWith('/share-target')) {
// todo
}
})
How would I display the shared image in my offline PWA?

There are a few different steps to take here.
I put together a working example at https://web-share-offline.glitch.me/, with the source at https://glitch.com/edit/#!/web-share-offline
Ensure your web app works offline
This is a prerequisite, and I accomplished it by generating a service worker that would precache my HTML, JS, and CSS using Workbox.
The JS that runs when the home page is loaded uses the Cache Storage API to read a list of image URLs that have been cached, to creates <img> elements on the page corresponding to each one.
Create a share target handler that will cache images
I also used Workbox for this, but it's a bit more involved. The salient points are:
Make sure that you intercept POST requests for the configured share target URL.
It's up to you to read in the body of the shared images and to write them to your local cache using the Cache Storage API.
After you save the shared image to cache, it's a good idea to respond to the POST with a HTTP 303 redirected response, so that the browser will display the actual home page for your web app.
Here's the Workbox configuration code that I used to handle this:
const shareTargetHandler = async ({event}) => {
const formData = await event.request.formData();
const cache = await caches.open('images');
await cache.put(
// TODO: Come up with a more meaningful cache key.
`/images/${Date.now()}`,
// TODO: Get more meaningful metadata and use it
// to construct the response.
new Response(formData.get('image'))
);
// After the POST succeeds, redirect to the main page.
return Response.redirect('/', 303);
};
module.exports = {
// ... other Workbox config ...
runtimeCaching: [{
// Create a 'fake' route to handle the incoming POST.
urlPattern: '/share-target',
method: 'POST',
handler: shareTargetHandler,
}, {
// Create a route to serve the cached images.
urlPattern: new RegExp('/images/\\d+'),
handler: 'CacheOnly',
options: {
cacheName: 'images',
},
}],
};

Related

Avoiding PWA to reload when using Web Share Target API

I'm working on a PWA that plays music.
It accepts shared URLs from other android apps via Web Share Target API.
The problem :
Whenever something is shared via the API (which uses GET request)I my PWA is reloded (as expected).
And the already playing Music stops because of that.
Is there any way that page doesn't reload ?
Currently fetching shared params with this code.
const parsedUrl = new URL(window.location);
My PWA is a Single Page application
Thanks
One way to prevent reloading is by setting up a fake path in your Web Application Manifest that solely serves for the purpose of receiving shares (note that this uses HTTP POST and "multipart/form-data" encoding [so later you can extend the app to receive entire files]):
{
"share_target": {
"action": "/receive-shares",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "name",
"text": "description",
"url": "link"
}
}
}
Then in your service worker's fetch handler, you deal with the incoming shares and redirect the user to the home:
self.addEventListener('fetch', (e) => {
if ((e.request.url.endsWith('/receive-shares')) &&
(e.request.method === 'POST')) {
return e.respondWith((async () => {
// This function is async.
const formData = await fetchEvent.request.formData();
// Do something with the URL…
const url = formData.get('url');
// Store the URL, process it, communicate it to the clients…
// You need to redirect the user somewhere, since the path
// /receive-shares does not actually exist.
return Response.redirect('/', 303);
})())
}
/* Your regular fetch handler */
})

Vue PWA: Use ServiceWorker to cache remote media assets from array of URLs

I am building a PWA where I would like to optionally download and cache ALL media assets used throughout the application, if the user wants to. The default PWA behaviour only caches the assets when they are requested navigating around the app. I need to offer the functionality to press the button, download the assets, go offline and use the entire app and have all the assets cached witout necessarily visiting the various routes and views while online.
For context, when the app loads, Vuex grabs the JSON content from my API, puts it in state, and saves to IndexedDB.
If the user clicks the 'download everything' button, I want to parse the JSON in $store.state, collect the URLs (images, PDFs and MP4s) into an array and pass that array to the ServiceWorker to deliberately cache all the assets at those URLs for offline use (up to the allowed origin storage limit).
I'm using Vue CLI and the Vue PWA plugin, so I have the registerServiceWorker.js file and precache manifest is generated for production.
I find the info on the internet about this slightly confusing I was thinking to use the approach outlined by https://developers.google.com/web/ilt/pwa/lab-caching-files-with-service-worker but I'm not sure about some of the specifics, and also unsure about how to tackle this in a way that fits in nicely with Vue CLI workflow?
If possible, can you include the following questions in your answer:
Do I use GenerateSW or InjectManifest?
Do I need to write my own Service Worker, or can I hook into the lifecycle methods (registered(), cached(), updated()) in registerServiceWorker.js?
Thank you in advance for your help.
I found a way to cache remote files on Vue 3, Vue CLI PWA app.
Set up the vue.conig.js on the root directory as shown below
# vue.config.js
module.exports = {
// ...other vue-cli plugin options...
pwa: {
name: "my-pwa-app",
themeColor: "#624040",
msTileColor: "#000000",
// configure the workbox plugin
workboxPluginMode: "InjectManifest",
workboxOptions: {
// swSrc is required in InjectManifest mode.
swSrc: "service-worker.js", //path to your own service-worker file
// ...other Workbox options...
},
},
};
Create the service-worker.js with starter code below, add other functionalities you need for caching
# service-worker.js
// workbox functions from the workbox CDN (version 4.3.1)
// Vue 3 PWA automatically adds the CDN
const { precacheAndRoute } = workbox.precaching;
const { registerRoute } = workbox.routing;
const { CacheFirst, StaleWhileRevalidate } = workbox.strategies;
const { Plugin: ExpirationPlugin } = workbox.expiration;
const { Plugin: CacheableResponsePlugin } = workbox.cacheableResponse;
workbox.core.setCacheNameDetails({ prefix: "appname" });
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
/**
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
*/
self.__precacheManifest = [].concat(self.__precacheManifest || []);
precacheAndRoute(self.__precacheManifest, {});
// cache image and render from the cache if it exists or go t the network
registerRoute(
({ request }) => request.destination === "image",
new CacheFirst({
cacheName: "images",
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 2 * 24 * 60 * 60, // cache the images for only 2 Days
}),
],
})
);
// cache API response and serve from cache if exists or go to the network
registerRoute(
({ url }) => url.pathname.startsWith("https://dog.ceo/api/"),
new StaleWhileRevalidate()
);
VueJs takes the service-worker.js and adds its import for the precaching of the files from the /dist folder
Documentation for Workbox could be found here
Notes on workbox 4.3.1 I found useful here

Set minimum network type (WLAN) for Progressive Web App caching

I created a PWA via Vue CLI which is consuming an external API. The API responses should get cached for offline access.
From the docs https://cli.vuejs.org/core-plugins/pwa.html#configuration I created a vue.config.js file in the root directory
module.exports = {
publicPath: '/pwa',
pwa: {
appleMobileWebAppCapable: 'yes',
workboxPluginMode: 'GenerateSW',
workboxOptions: {
runtimeCaching: [
{
urlPattern: new RegExp('^https://example.com/'),
handler: 'networkFirst', // Network first, Cache fallback
options: {
networkTimeoutSeconds: 5,
cacheName: 'api-cache',
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
},
};
The project is working fine but I want to avoid loading data from the API on a very bad connection.
I want to load data on WLAN network connection only. Is there a way I can extend the configuration and set a network type to detect? The configuration should be called
use network first but only with WLAN, otherwise use cache as fallback
You'll need to switch from using Workbox's GenerateSW to using the InjectManifest mode, which gives you more control over the service worker.
The example in https://developers.google.com/web/tools/workbox/guides/get-started can get you started with the basics of what to include in your service worker file.
Just note that the Network Information API isn't always reliable, and isn't supported in all browsers. You should code defensively so that the experience isn't "broken" if the API isn't available.
The specific logic that you can add to your service worker could go something like:
import {CacheableResponsePlugin} from 'workbox-cacheable-response';
import {NetworkFirst, CacheFirst} from 'workbox-strategies';
import {registerRoute} from 'workbox-routing';
// This will be shared by both the strategies.
const cacheableResponsePlugin = new CacheableResponsePlugin({
statuses: [0, 200],
});
const networkFirst = new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 5,
plugins: [cacheableResponsePlugin],
});
const networkFirst = new CacheFirst({
cacheName: 'api-cache',
plugins: [cacheableResponsePlugin],
});
registerRoute(
// Replace with the routing criteria appropriate for your API.
({url}) => url.origin === 'https://example.com',
(params) => {
// navigator.connection might not always be available, and might not
// always be reliable.
// See https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation
if (navigator.connection?.effectiveType === '4g') {
return networkFirst.handle(params);
}
// If the current connection is not flagged as '4g',
// or if navigator.connection isn't set, use CacheFirst
// as a fallback.
return cacheFirst.handle(params);
}
);

serviceworker is it possible to add headers to url request

I have a website which I don't want to make people create accounts. It is a news feed with each news article categorized. I want to allow people to tag the categories they are interested in so that next time they go to the site it only shows news for the categories that are tagged.
I'm saving the tags in an indexedDB which I understand is available in a service worker.
Hence in my service worker I want to "intercept" requests to www.my-url.com, check the indexDB for what categories this person is interested in, and add some headers like 'x-my-customer-header': 'technology,physics,sports' so that my server can respond with a dynamic html of those categories only.
However I'm struggling to get the service worker to properly cache my root response. In my serviceworker.js, I console log every event.request for the onFetch handler. There are no requests that are related to my root url. I'm testing right now on my localhost, but I only see fetch requests to css & js files.
Here is my onFetch:
function onFetch(event) {
console.log('onFetch',event.request.url);
event.request.headers["X-my-custom-header"] = "technology,sports";
event.respondWith(
// try to return untouched request from network first
fetch(event.request).catch(function() {
// if it fails, try to return request from the cache
caches.match(event.request).then(function(response) {
if (response) {
return response;
}
// if not found in cache, return default offline content for navigate requests
if (event.request.mode === 'navigate' ||
(event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
return caches.match('/offline.html');
}
})
})
);
}
I'm using rails so there is no index.html that exists to be cached, when a user hits my url, the page is dynamically served from my news#controller.
I'm actually using the gem serviceworker-rails
What am I doing wrong? How can I have my service worker save a root file and intercept the request to add headers? Is this even possible?
Credit here goes to Jeff Posnick for his answer on constructing a new Request. You'll need to respond with a fetch that creates a new Request to which you can add headers:
self.addEventListener('fetch', event => {
event.respondWith(customHeaderRequestFetch(event))
})
function customHeaderRequestFetch(event) {
// decide for yourself which values you provide to mode and credentials
// Copy existing headers
const headers = new Headers(event.request.headers);
// Set a new header
headers.set('x-my-custom-header', 'The Most Amazing Header Ever');
// Delete a header
headers.delete('x-request');
const newRequest = new Request(event.request, {
mode: 'cors',
credentials: 'omit',
headers: headers
})
return fetch(newRequest)
}

Priming a runtime service worker cache using Workbox

How to use below code in WorkboxSW to register routes for all per-caching urls. This per-cached urls includes ajax that will go to server also!
$.ajax({
url : '/MyAPI/Records',
type : 'GET',
dataType:'json',
success : function(data) {
alert('Records: '+data);
//build urls array to get all records details
var urls = [];
urls.push('/MyAPI/Records')
$(data).each(function (index) {
urls.push('/MyAPI/Records/' + data[index].id + '/data')
});
//add all urls to cache
var requests = urls.map(url => new Request(url));
caches.open('my-cache').then(cache => {
return cache.addAll(requests).then(() => {
// At this point, `cache` will be populated with `Response` objects,
// and `requests` contains the `Request` objects that were used.
}).catch(error => console.error(`Oops! ${error}`));
});
},
error : function(request,error)
{
alert("Request: "+JSON.stringify(request));
}
});
Workbox's precaching relies on having access to a local file representing the resource at build time. This allows it to generate a hash of each resource its managing (based on the local file's contents) and then keep that cached resource up to date whenever the local file changes.
What you're suggestion sounds more like Workbox's support for handling certain routes via runtime caching. You can configure it via something like:
// Replace with your actual API endpoint.
const API_URL = 'https://my-api-server.com/api/restEndpoint';
// Use whichever strategy you'd prefer: networkFirst, staleWhileRevalidate, etc.
const apiCallHandler = workboxSW.strategies.networkFirst({
cacheName: 'my-api'
});
workboxSW.registerRoute(
API_URL,
apiCallHandler
);
This will result in responses from https://my-api-server.com being added to the cache named my-api at runtime, after you make your first request. (In this particular case, using the networkFirst strategy, those cached responses will only be used if the network is unavailable.)
If you're not okay with the runtime cache starting out "cold" and you feel like it needs to be primed, then you could do that by writing your own install event handler alongside your Workbox code:
// Your existing WorkboxSW code goes here.
self.addEventListener('install', event => {
event.waitUntil(
caches.open('my-api')
.then(cache => cache.add(API_URL))
);
});

Categories

Resources