許多非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())); }); }