Custom Plugins in Google Workbox - javascript

i am pretty new to google workbox and try to add a custom plugin.
(Google workbox guide: Using Plugins)
I have registered routes - example:
/**
* Caching routes .de .com .test and .html
*/
workbox.routing.registerRoute(
new RegExp('\\.(?:html|de/|com/|test/)$'),
new workbox.strategies.StaleWhileRevalidate({
// Use a custom cache name.
cacheName: 'html-cache',
plugins: [
... <- here should be my plugin
new workbox.expiration.ExpirationPlugin({
// Cache for a maximum of 3 days.
maxAgeSeconds: 3 * 24 * 60 * 60,
}),
],
})
);
and the example plugin file from google:
const myPlugin = {
cacheWillUpdate: async ({request, response, event}) => {
// Return `response`, a different `Response` object, or `null`.
return response;
},
cacheDidUpdate: async ({cacheName, request, oldResponse, newResponse, event}) => {
// No return expected
// Note: `newResponse.bodyUsed` is `true` when this is called,
// meaning the body has already been read. If you need access to
// the body of the fresh response, use a technique like:
// const freshResponse = await caches.match(request, {cacheName});
},
cacheKeyWillBeUsed: async ({request, mode}) => {
// `request` is the `Request` object that would otherwise be used as the cache key.
// `mode` is either 'read' or 'write'.
// Return either a string, or a `Request` whose `url` property will be used as the cache key.
// Returning the original `request` will make this a no-op.
return request;
},
cachedResponseWillBeUsed: async ({cacheName, request, matchOptions, cachedResponse, event}) => {
// Return `cachedResponse`, a different `Response` object, or null.
return cachedResponse;
},
requestWillFetch: async ({request}) => {
// Return `request` or a different `Request` object.
return request;
},
fetchDidFail: async ({originalRequest, request, error, event}) => {
// No return expected.
// NOTE: `originalRequest` is the browser's request, `request` is the
// request after being passed through plugins with
// `requestWillFetch` callbacks, and `error` is the exception that caused
// the underlying `fetch()` to fail.
},
fetchDidSucceed: async ({request, response}) => {
// Return `response` to use the network response as-is,
// or alternatively create and return a new `Response` object.
return response;
}
};
My problem is now that i cant add this to the plugins section.
I tried several notations and nothing works.
What would i like to do?
I want to compare the old content from the cache with the newer response.
(I know there is a class who can do this -> BroadcastCacheUpdate).
But it also doesnt work for me. If anyone has some configuration examples (for version 5) i would appreciate it.
I hope you guys can help me.
Greetings
Simon

I'm fairly sure that you're going through the right steps, so I'm not sure what's not working. But basically, with just the relevant bits, it should look like:
const customPlugin = {
cacheDidUpdate: async (args) => {
console.debug('cacheDidUpdate called with', args);
// Do something with args.oldResponse and args.newResponse
},
};
workbox.routing.registerRoute(
new RegExp('\\.(?:html|de/|com/|test/)$'),
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'html-cache',
plugins: [
customPlugin,
],
})
);
Assuming that works for you, and you see the logged message, that should help put you on the right path.

Related

Vue prefetch data from separate backend

I have some queries from an API-Server that returns a json object that will be static over a user session, but not static forever.
It's a one-pager with Vue router.
How can I achieve that I:
can access this.myGlobals (or similar eg window.myGlobals) in all components, where my prefetched json-data from API-Server is stored.
My approach that is already working is to embed help.js via a mixin.
Oddly enough, I get hundreds of calls to this query. At first I thought that it only happened in the frontend and is chached, but the requests are actually sent hundreds of times to the server. I think it is a mistake of my thinking, or a systematic mistake.
i think the problem is, that the helper.js is not static living on the vue instance
main.js:
import helpers from './helpers'
Vue.mixin(helpers)
helpers.js:
export default {
data: function () {
return {
globals: {},
}
}, methods: {
//some global helper funktions
},
}, mounted() {
let url1 = window.datahost + "/myDataToStore"
this.$http.get(url1).then(response => {
console.log("call")
this.globals.myData = response.data
});
}
}
log in console:
call
SomeOtherStuff
(31) call
SomeOtherStuff
(2) call
....
log on server:
call
call
call (pew pew)
My next idea would be to learn vuex, but since its a easy problem, im not sure if i really need that bomb ?
You can use plugin to achieve this.
// my-plugin.js
export default {
install (Vue, options) {
// start fetching data right after install
let url1 = window.datahost + "/myDataToStore"
let myData
Vue.$http.get(url1).then(response => {
console.log("call")
myData = response.data
})
// inject via global mixin
Vue.mixin({
computed: {
myData () {
return myData
}
}
})
// or inject via instance property
Vue.prototype.$myData = myData
// or if you want to wait until myData is available
Vue.prototype.$myData = Vue.$http.get(url1)
.then(response => {
console.log("call")
myData = response.data
})
}
}
and use it:
Vue.use(VueResource)
Vue.use(myPlugin)

How to concat return of two axios get calls, Nuxt.js Sitemap

Hey guys hope u are doing well :) I have this situation that I dont know how to solve, so Im trying to fill nuxt.js sitemap of my dynamic websites, so when I had only one call all worked fine and I got what I needed in sitemap.xml but when I add other one I guess I need to concat results, but I dont know how...
sitemap: {
routes: async () => {
let { data } = await axios.get("http://localhost:1337/articles");
return data.map(v => `${v.slug}`)
}
},
I need to add same stuff just with diferent api "http://localhost:1337/faq/slug"
And with one result of both api calls, I need to fill sitempa.xml :)
Using the spread syntax, you can do this
sitemap: {
routes: async () => {
let { data: articlesData } = await axios.get("http://localhost:1337/articles");
const articlesArray = articlesData.map(v => `${v.slug}`)
let { data: slugData } = await axios.get("http://localhost:1337/faq/slug");
const slugsArray = slugData.map(v => `${v.slug}`)
return [...articlesArray, ...slugsArray]
}
},
This should work fine if you want to join 2 arrays. Otherwise, give us an example of what is the end structure we need here.
PS: articlesData and slugData are renamed on the fly here to avoid redefining the same variable and keep things clean.
I had this problem in my project.
To solve this problem I did make one request for my api. In Api I created a method in which I processed all requests.
sitemap: {
path: '/sitemap.xml',
cacheTime: 1000 * 60 * 1440,
hostname: process.env.SITEURL,
routes: async () => {
const {data} = await axios.get(`${process.env.BACKADDR}/getSiteMapRoutes`);
return data.routers
}
},

Error `Template parameter names *must* be separated`

I am using mithril.js in frontend application and backend application is running on ipv6 environment.
Calling post ajax request to backend using mithril.js.
async post(url, body = {}) {
return new Promise((resolve, reject) => {
m.request({method: 'POST', url, body}).then((data) => {
resolve(data);
}).catch((err) => {
reject(err.message);
});
});
}
Backed url is like this: http://[340f:c0e0:1d1:5gc0:g4fs:2::]:22923/backend.
But getting this error Template parameter names *must* be separated while calling backend api.
Explanation of the error
Based on the documentation of m.request(), you can specify dynamic URLs:
Request URLs may contain interpolations:
m.request({
method: "GET",
url: "/api/v1/users/:id",
params: {id: 123},
}).then(function(user) {
console.log(user.id) // logs 123
})
In the code above, :id is populated with the data from the params object, and the request becomes GET /api/v1/users/123.
Interpolations are ignored if no matching data exists in the params property.
m.request({
method: "GET",
url: "/api/v1/users/foo:bar",
params: {id: 123},
})
In the code above, the request becomes GET /api/v1/users/foo:bar?id=123
Since your backend URL contains colons, it's interpreted as being a dynamic URL.
According to the documentation of m.buildPathname(), m.request() uses m.buildPathname() internally to process dynamic URLs.
The beginning of m.buildPathname() contains the following check regarding parameters of a path template (dynamic URL = path template populated with path parameters):
if ((/:([^\/\.-]+)(\.{3})?:/).test(template)) {
throw new SyntaxError("Template parameter names *must* be separated")
}
(Source: https://github.com/MithrilJS/mithril.js/blob/v2.0.4/mithril.js#L1288-L1292)
And, again, since your backend URL contains colons, this is where you are getting the error. (You can verify this by trying to run m.buildPathname('http://[340f:c0e0:1d1:5gc0:g4fs:2::]:22923/backend') – you'll get the same error.)
How to fix it
Since you can't get away from that regex check at the beginning of m.buildPathname(), your best bet might be to use a dynamic URL. Like so:
m.buildPathname(':url...', { url: 'http://[340f:c0e0:1d1:5gc0:g4fs:2::]:22923/backend' })
// => http://[340f:c0e0:1d1:5gc0:g4fs:2::]:22923/backend
Or when applied to your code:
async post(url, body = {}) {
return new Promise((resolve, reject) => {
m.request({method: 'POST', url: ':url...', body, params: {url}}).then((data) => {
resolve(data);
}).catch((err) => {
reject(err.message);
});
});
}
Or alternatively you can specify the (dynamic) URL as the first argument of m.request():
async post(url, body = {}) {
return new Promise((resolve, reject) => {
m.request(':url...', {method: 'POST', body, params: {url}}).then((data) => {
resolve(data);
}).catch((err) => {
reject(err.message);
});
});
}
Notice that there are three dots after the path parameter :url. Otherwise its value would be escaped/encoded. This is mentioned in the documentation of path handling. Example:
m.buildPathname(':url', { url: 'http://[340f:c0e0:1d1:5gc0:g4fs:2::]:22923/backend' })
// => http%3A%2F%2F%5B340f%3Ac0e0%3A1d1%3A5gc0%3Ag4fs%3A2%3A%3A%5D%3A22923%2Fbackend
Handling URL parameters
As mentioned in the other answer, if the URL contains parameters, the question mark will be duplicated:
m.buildPathname(':url...', { url: 'https://example.com/foo?bar=baz' })
// => https://example.com/foo??bar=baz
// ^^
One way to solve that would be to include the parameters in the path template:
const url = 'https://example.com/foo?bar=baz'
const [baseUrl, params] = url.split('?')
const template = ':baseUrl...' + (params ? `?${params}` : '')
m.buildPathname(template, { baseUrl })
// => https://example.com/foo?bar=baz
However, if there are colons in the URL parameters, there's a possibility that you'll get the same error as originally ("Template parameter names *must* be separated").
There might be a way to solve this, but the previous code sample is already quite complex for this relatively simple use case. Which leads us to:
Alternative solution: don't use m.request()
m.request() is just "a thin wrapper around XMLHttpRequest." It "returns a promise and triggers a redraw upon completion of its promise chain."
If m.request() is difficult to work with due to using IPv6 URLs (or for other reasons), it can be easier to use something else for doing XHR requests. You could for example use fetch() – just remember to call m.redraw() at the end (m.request() does this automatically).
Sure, m.request() does more than just calls m.redraw() at the end (see the docs), but it's also okay to use something else.
Thanks mts knn for the reply. We have implemented your solution however we faced below issues.
Question mark is passing two times in http url of api. Please find attached below screenshot.
In order to fix this problem, please find the updated code below
async post(url, body = {}) {
var queryIndex = url.indexOf('?');
var httpPart = url.slice(0,queryIndex);
var finalUrl = url.replace(httpPart,":url...");
return new Promise((resolve, reject) => {
m.request({method: 'POST', url: finalUrl, body, params: {url: httpPart}}).then((data) => {
resolve(data);
}).catch((err) => {
reject(err.message);
});
});
}
You can also provide efficient solution If any.

Cypress, response body as BLOB instead of JSON, but JSON in chrome devtools

i've been struggling with this behaviour of Cypress that i do not understand and i need help.
When i set route and wait for the request i can see that the response body is in BLOB, when in chrome devtools response body arrives as JSON, so is in application. I have Content-type set to application/vnd.api+json. Cypress version 3.7.0. I also disabled Fetch because Cypress have problems with that Cypress documentation #wait
cy.server();
cy.route('POST', '**/services').as('postService');
cy.get('[data-cy=AddServices_submit]').click();
cy.wait('#postService').then((xhr) => {
//xhr.response.body is BLOB
//xhr.responseBody is BLOB
})
Found similar question: Stackoverflow Similar question but this is not helpful for me.
Did any one had similar problems with response arriving as BLOB?
Any help would be great, if you need more information feel free to ask. Thanks
EDIT
I have a workaround to this problem if anyone needed one. But the problem Still occurs
cy.wait('#postService').then(async (xhr) => {
const response = await new Response(xhr.responseBody).text();
const jsonResponse = JSON.parse(response);
// jsonResponse is real json
});
I got the same problem and it was solved by adding cypress fetch polyfill as here
If the link won't be available, I copy the content here:
In directory cypress/support/ in file hooks.js add this code:
// Cypress does not support listening to the fetch method
// Therefore, as a workaround we polyfill `fetch` with traditional XHR which
// are supported. See: https://github.com/cypress-io/cypress/issues/687
enableFetchWorkaround();
// private helpers
function enableFetchWorkaround() {
let polyfill;
before(() => {
console.info('Load fetch XHR polyfill')
cy.readFile('./cypress/support/polyfills/unfetch.umd.js').then((content) => {
polyfill = content
})
});
Cypress.on('window:before:load', (win) => {
delete win.fetch;
// since the application code does not ship with a polyfill
// load a polyfilled "fetch" from the test
win.eval(polyfill);
win.fetch = win.unfetch;
})
}
In directory cypress/support/ in file index.js import hooks.js
import './hooks'
In directory cypress/support/ add directory polyfills and add there file unfetch.umd.js with this code:
// cypress/support/polyfills/unfetch.umd.js
// Version: 4.1.0
// from: https://unpkg.com/unfetch/dist/unfetch.umd.js
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):e.unfetch=n()}(this,function(){return function(e,n){return n=n||{},new Promise(function(t,o){var r=new XMLHttpRequest,s=[],u=[],i={},f=function(){return{ok:2==(r.status/100|0),statusText:r.statusText,status:r.status,url:r.responseURL,text:function(){return Promise.resolve(r.responseText)},json:function(){return Promise.resolve(JSON.parse(r.responseText))},blob:function(){return Promise.resolve(new Blob([r.response]))},clone:f,headers:{keys:function(){return s},entries:function(){return u},get:function(e){return i[e.toLowerCase()]},has:function(e){return e.toLowerCase()in i}}}};for(var a in r.open(n.method||"get",e,!0),r.onload=function(){r.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm,function(e,n,t){s.push(n=n.toLowerCase()),u.push([n,t]),i[n]=i[n]?i[n]+","+t:t}),t(f())},r.onerror=o,r.withCredentials="include"==n.credentials,n.headers)r.setRequestHeader(a,n.headers[a]);r.send(n.body||null)})}});
So, it worked for me
Same problem here...
I manage to get the data as JSON when I use cy.request() but I can't when I use an alias with cy.wait()
Could you try this as a workaround ?
const setBodyAsJson = async (xhr) => ({ ...xhr, body: JSON.parse(String.fromCharCode.apply(null, new Uint8Array(await xhr.response.body.arrayBuffer()))) })
cy.server();
cy.route('POST', '**/services').as('postService');
cy.get('[data-cy=AddServices_submit]').click();
cy.wait('#postService').then(setBodyAsJson).then((res) => {
// res should contain body as JSON
})
This does not explain why but in case your response.body is a Blob but responseBody is null, you can use this to read it:
cy.wait('#postService', TIMEOUT)
.its('response.body')
.then(body => {
return new Promise(done => {
const reader = new FileReader();
reader.onload = function() {
done(JSON.parse(this.result));
};
reader.readAsText(body);
});
})
.then(object => {
expect(typeof object).to.equal('object')
});

How to use Workbox setDefaultHandler

I'm switching from sw-toolbox to Workbox and I can't figure out how to use setDefaultHandler().
If I try (as stated in the documentation linked above):
workboxSW.router.setDefaultHandler({
handler: new workbox.runtimeCaching.CacheFirst()
});
I get an error that runtimeCaching is undefined:
Uncaught ReferenceError: router is not defined
So.. how do I use it and configure it in a way similar to how I could configure sw-toolbox:
toolbox.options.cache = {
name: "default",
maxEntries: 128,
maxAgeSeconds: (60*60*24), // 24hrs
};
toolbox.router.default = toolbox.cacheFirst;
I would like to be able to do something like this:
workboxSW.router.setDefaultHandler({
handler: workboxSW.strategies.cacheFirst({
cacheName: 'default',
cacheExpiration: {
maxEntries: 128,
},
cacheableResponse: {statuses: [0, 200]},
})
});
..which doesn't throw compile errors but when I use it I get this:
Uncaught (in promise) TypeError: Request method 'POST' is unsupported
..and my Cache Storage for 'default' remains empty..?
Since my edits for Jeff's first solution were rejected I'll just go ahead and submit an answer myself.
Jeff's sample came close. He suggested:
You could check for the request type in the default handler, and only
apply the cache-first strategy to GET requests:
workboxSW.router.setDefaultHandler({
handler: (args) => {
if (args.event.request.method === 'GET') {
return workboxSW.strategies.cacheFirst(args);
}
return fetch(args.event.request);
},
});
It's the right approach but the example code he provided didn't work. The handler argument needs a handler, not a strategy. Luckily, strategies have exactly one (public) method, called "handle".
So I modified his code a little; First, I create a strategy called defaultStrategy with all the options I need. Then, in the setDefaultHandler call, I return defaultStrategy.handle(args) instead of the CacheFirst constructor. That's it!
// Register 'default'
var defaultStrategy = workboxSW.strategies.cacheFirst({
cacheName: "default",
cacheExpiration: {
maxEntries: 128,
// more options..
},
cacheableResponse: {statuses: [0, 200]},
});
workboxSW.router.setDefaultHandler({
handler: (args) => {
if (args.event.request.method === 'GET') {
return defaultStrategy.handle(args);
}
return fetch(args.event.request);
},
});
UPDATE: Workbox v3
As I pointed out in the comments below, the above code doesn't work with Workbox v3. Use this instead:
// Register 'default'
var defaultStrategy = workbox.strategies.cacheFirst ({
cacheName: "your.cache.name",
plugins: [
new workbox.expiration.Plugin({
maxEntries: 128,
maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week
purgeOnQuotaError: true, // Opt-in to automatic cleanup
}),
new workbox.cacheableResponse.Plugin({
statuses: [0, 200] // for opague requests
}),
],
});
workbox.routing.setDefaultHandler(
(args) => {
if (args.event.request.method === 'GET') {
return defaultStrategy.handle(args); // use default strategy
}
return fetch(args.event.request);
}
);
workboxSW.router.setDefaultHandler({
handler: workboxSW.strategies.cacheFirst({...})
});
is the right syntax in general.
I believe that you're seeing
Uncaught (in promise) TypeError: Request method 'POST' is unsupported
because the default handler is triggered for all HTTP requests that don't match any explicit route, including HTTP POST requests. But a HTTP POST request can't be used with the Cache Storage API, and an exception similar to what you're seeing will be thrown when the cache-first strategy attempts to store the request/response pair in the cache.
In this particular case, when you know that your web app is going to make HTTP POST requests, you could take one of two approaches.
You could check for the request type in the default handler, and only apply the cache-first strategy to GET requests:
workboxSW.router.setDefaultHandler({
handler: (args) => {
if (args.event.request.method === 'GET') {
return workboxSW.strategies.cacheFirst(args);
}
return fetch(args.event.request);
},
});
Alternatively, you could create a wildcard route that matches all requests, and take advantage of the fact that by default, routes will only match HTTP GET:
workboxSW.router.registerRoute(
/./, // This should match all requests.
workboxSW.strategies.cacheFirst({...}),
'GET' // This is the default, and can be left out.
);

Categories