许多非REST API甚至可以用于读取数据的POST请求:典型的例子是graphql、soap和其他rpcpapi。但是,Post请求不能在一个现成的渐进式Web应用程序中缓存和脱机使用。浏览器的缓存API不会接受它们。下面是一个在IncedB中使用自定义缓存的解决方案。
幸运的是Service Worker可以截获任何异步请求,因此我们处理POST请求没有问题。缺少的是在离线时缓存它们并检索相应的响应的可能性。
在下面的示例中,我们将在indexeddb中创建一个简单的缓存,并将其用作回退(networkfirst策略)。注意,我们将使用整个请求的字符串化版本作为缓存键。这是非常安全的,但会为具有长主体和/或头的请求生成大密钥。如果这对您来说是一个问题,您可以选择另一种方法来构建键,或者只使用lz字符串或类似的库来压缩它。
一句警告
当然,有一些原因可以解释为什么post请求不属于当前Service Worker规范的一部分。以下是其中一些:
从理论上讲,POSTs将修改数据。当然,您不能使用正在尝试修改的数据缓存版本。所以不要缓存任何类型的修改请求!
这些API使用文章进行阅读,通常不会为每个可能的结果提供单独的URL。相反,它们只有一个URL来检索不同的数据集,并接受请求主体中某种类型的查询。这意味着,您不能缓存基于URL的响应(就像缓存API一样),而是需要使用自己的组合键,包括主体和可能的一些头。
所以在将post请求缓存到您的数据之前,请仔细考虑!如果您正在寻找一种使修改post请求离线工作的方法,我建议您查看用于延迟发布的Mozilla代码配方。
如何在服务工作进程中缓存发布请求
对于缓存所有post请求的工作示例,请将此代码放在serviceworker.js中。我在这里使用dexie.js访问indexeddb,但是任何其他方法也可以。
importScripts('your/path/to/dexie/dist/dexie.min.js'); // Listen to fetch requests self.addEventListener('fetch', function(event) { // We will cache all POST requests, but in the real world, you will probably filter for // specific URLs like if(... || event.request.url.href.match(...)) if(event.request.method === "POST"){ // Init the cache. We use Dexie here to simplify the code. You can use any other // way to access IndexedDB of course. var db = new Dexie("post_cache"); db.version(1).stores({ post_cache: 'key,response,timestamp' }) event.respondWith( // First try to fetch the request from the server fetch(event.request.clone()) .then(function(response) { // If it works, put the response into IndexedDB cachePut(event.request.clone(), response.clone(), db.post_cache); return response; }) .catch(function() { // If it does not work, return the cached response. If the cache does not // contain a response for our request, it will give us a 503-response return cacheMatch(event.request.clone(), db.post_cache); }) ); } }) /** * Serializes a Request into a plain JS object. * * @param request * @returns Promise */ function serializeRequest(request) { var serialized = { url: request.url, headers: serializeHeaders(request.headers), method: request.method, mode: request.mode, credentials: request.credentials, cache: request.cache, redirect: request.redirect, referrer: request.referrer }; // Only if method is not `GET` or `HEAD` is the request allowed to have body. if (request.method !== 'GET' && request.method !== 'HEAD') { return request.clone().text().then(function(body) { serialized.body = body; return Promise.resolve(serialized); }); } return Promise.resolve(serialized); } /** * Serializes a Response into a plain JS object * * @param response * @returns Promise */ function serializeResponse(response) { var serialized = { headers: serializeHeaders(response.headers), status: response.status, statusText: response.statusText }; return response.clone().text().then(function(body) { serialized.body = body; return Promise.resolve(serialized); }); } /** * Serializes headers into a plain JS object * * @param headers * @returns object */ function serializeHeaders(headers) { var serialized = {}; // `for(... of ...)` is ES6 notation but current browsers supporting SW, support this // notation as well and this is the only way of retrieving all the headers. for (var entry of headers.entries()) { serialized[entry[0]] = entry[1]; } return serialized; } /** * Creates a Response from it's serialized version * * @param data * @returns Promise */ function deserializeResponse(data) { return Promise.resolve(new Response(data.body, data)); } /** * Saves the response for the given request eventually overriding the previous version * * @param data * @returns Promise */ function cachePut(request, response, store) { var key, data; getPostId(request.clone()) .then(function(id){ key = id; return serializeResponse(response.clone()); }).then(function(serializedResponse) { data = serializedResponse; var entry = { key: key, response: data, timestamp: Date.now() }; store .add(entry) .catch(function(error){ store.update(entry.key, entry); }); }); } /** * Returns the cached response for the given request or an empty 503-response for a cache miss. * * @param request * @return Promise */ function cacheMatch(request) { return getPostId(request.clone()) .then(function(id) { return store.get(id); }).then(function(data){ if (data) { return deserializeResponse(data.response); } else { return new Response('', {status: 503, statusText: 'Service Unavailable'}); } }); } /** * Returns a string identifier for our POST request. * * @param request * @return string */ function getPostId(request) { return new Promise((resolve, reject) => { return JSON.stringify(serializeRequest(request.clone())); }); }