Cloudflare一直是我非常喜歡的廠商,免費的套餐就可以提供不錯的服務,只不過現在被玩壞了,國內訪問速度大不如前,但是總體來說有還是比沒有強。
不過免費版套餐的Cloudflare CDN有個非常致命的缺點,就是不能根據cookie來區分訪客,從而針對性提供訪問內容(比如不為登陸、評論過的使用者提供快取)。並且還會把已登入或已評論使用者的資訊快取上去,這點難以接受,也是我拋棄Cloudflare的重要原因之一。
不過在Cloudflare Workers面世後,這個問題就迎刃而解了,我們可以使用Workers來繞過這個限制,自己編寫快取策略。
介紹
關於Cloudflare Workers的介紹可以自行搜尋,通過搜尋引擎看到本文人或許應該已經有所瞭解。
我簡單闡述一下,本文要講的內容,就是通過Cloudflare官方曾編寫的一款指令碼,實現自定義快取策略。
Edge Cache HTML
Cloudflare官方曾編寫的一款WordPress外掛,用以搭配edge-cache-html,不過這個專案已經快兩年沒更新了,應該是覺得這個專案會影響到他們付費套餐的出售。
https://github.com/cloudflare/worker-examples/tree/master/examples/edge-cache-html
目前這個指令碼直接使用,是無法在WordPress下生效的,並且缺失URL路徑規則。我稍作修改,增加了路徑排除和支援在WordPress下是快取(去除header cache判斷)。
前期操作請參考 [WordPress]利用 Cloudflare Workers 來快取部落格的 HTML 網頁 ,寫的非常詳細,只不過這位博主沒有沒有快取成功。
以下是修改後的指令碼
// IMPORTANT: Either A Key/Value Namespace must be bound to this worker script // using the variable name EDGE_CACHE. or the API parameters below should be // configured. KV is recommended if possible since it can purge just the HTML // instead of the full cache. // Default cookie prefixes for bypass const DEFAULT_BYPASS_COOKIES = [ "wp-", "wordpress", "comment_", "woocommerce_" ]; // URL paths to bypass the cache (each pattern is a regex) const BYPASS_URL_PATTERNS = [ /\/wp-admin\/.*/, /\/wp-adminlogin\/.*/ ]; /** * Main worker entry point. */ addEventListener("fetch", event => { const request = event.request; let upstreamCache = request.headers.get('x-HTML-Edge-Cache'); // Only process requests if KV store is set up and there is no // HTML edge cache in front of this worker (only the outermost cache // should handle HTML caching in case there are varying levels of support). let configured = false; if (typeof EDGE_CACHE !== 'undefined') { configured = true; } else if (CLOUDFLARE_API.email.length && CLOUDFLARE_API.key.length && CLOUDFLARE_API.zone.length) { configured = true; } // Bypass processing of image requests (for everything except Firefox which doesn't use image/*) const accept = request.headers.get('Accept'); let isImage = false; if (accept && (accept.indexOf('image/*') !== -1)) { isImage = true; } if (configured && !isImage && upstreamCache === null) { event.passThroughOnException(); event.respondWith(processRequest(request, event)); } }); /** * Process every request coming through to add the edge-cache header, * watch for purge responses and possibly cache HTML GET requests. * * @param {Request} originalRequest - Original request * @param {Event} event - Original event (for additional async waiting) */ async function processRequest(originalRequest, event) { let cfCacheStatus = null; const accept = originalRequest.headers.get('Accept'); const isHTML = (accept && accept.indexOf('text/html') >= 0); let {response, cacheVer, status, bypassCache} = await getCachedResponse(originalRequest); if (response === null) { // Clone the request, add the edge-cache header and send it through. let request = new Request(originalRequest); request.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall|bypass-cookies'); response = await fetch(request); if (response) { const options = getResponseOptions(response); if (options && options.purge) { await purgeCache(cacheVer, event); status += ', Purged'; } bypassCache = bypassCache || shouldBypassEdgeCache(request, response); if ((!options || options.cache) && isHTML && originalRequest.method === 'GET' && response.status === 200 && !bypassCache) { status += await cacheResponse(cacheVer, originalRequest, response, event); } } } else { // If the origin didn't send the control header we will send the cached response but update // the cached copy asynchronously (stale-while-revalidate). This commonly happens with // a server-side disk cache that serves the HTML directly from disk. cfCacheStatus = 'HIT'; if (originalRequest.method === 'GET' && response.status === 200 && isHTML) { bypassCache = bypassCache || shouldBypassEdgeCache(originalRequest, response); if (!bypassCache) { const options = getResponseOptions(response); if (!options) { status += ', Refreshed'; event.waitUntil(updateCache(originalRequest, cacheVer, event)); } } } } if (response && status !== null && originalRequest.method === 'GET' && response.status === 200 && isHTML) { response = new Response(response.body, response); response.headers.set('x-HTML-Edge-Cache-Status', status); if (cacheVer !== null) { response.headers.set('x-HTML-Edge-Cache-Version', cacheVer.toString()); } if (cfCacheStatus) { response.headers.set('CF-Cache-Status', cfCacheStatus); } } return response; } /** * Determine if the cache should be bypassed for the given request/response pair. * Specifically, if the request includes a cookie that the response flags for bypass. * Can be used on cache lookups to determine if the request needs to go to the origin and * origin responses to determine if they should be written to cache. * @param {Request} request - Request * @param {Response} response - Response * @returns {bool} true if the cache should be bypassed */ function shouldBypassEdgeCache(request, response) { let bypassCache = false; // Bypass the cache for all requests to a URL that matches any of the URL path bypass patterns const url = new URL(request.url); const path = url.pathname + url.search; if (BYPASS_URL_PATTERNS.length) { for (let pattern of BYPASS_URL_PATTERNS) { if (path.match(pattern)) { bypassCache = true; break; } } } if (request && response) { const options = getResponseOptions(response); const cookieHeader = request.headers.get('cookie'); let bypassCookies = DEFAULT_BYPASS_COOKIES; if (options) { bypassCookies = options.bypassCookies; } if (cookieHeader && cookieHeader.length && bypassCookies.length) { const cookies = cookieHeader.split(';'); for (let cookie of cookies) { // See if the cookie starts with any of the logged-in user prefixes for (let prefix of bypassCookies) { if (cookie.trim().startsWith(prefix)) { bypassCache = true; break; } } if (bypassCache) { break; } } } } return bypassCache; } const CACHE_HEADERS = ['Cache-Control', 'Expires', 'Pragma']; /** * Check for cached HTML GET requests. * * @param {Request} request - Original request */ async function getCachedResponse(request) { let response = null; let cacheVer = null; let bypassCache = false; let status = 'Miss'; // Only check for HTML GET requests (saves on reading from KV unnecessarily) // and not when there are cache-control headers on the request (refresh) const accept = request.headers.get('Accept'); const cacheControl = request.headers.get('Cache-Control'); let noCache = false; // if (cacheControl && cacheControl.indexOf('no-cache') !== -1) { // noCache = true; // status = 'Bypass for Reload'; // } if (!noCache && request.method === 'GET' && accept && accept.indexOf('text/html') >= 0) { // Build the versioned URL for checking the cache cacheVer = await GetCurrentCacheVersion(cacheVer); const cacheKeyRequest = GenerateCacheRequest(request, cacheVer); // See if there is a request match in the cache try { let cache = caches.default; let cachedResponse = await cache.match(cacheKeyRequest); if (cachedResponse) { // Copy Response object so that we can edit headers. cachedResponse = new Response(cachedResponse.body, cachedResponse); // Check to see if the response needs to be bypassed because of a cookie bypassCache = shouldBypassEdgeCache(request, cachedResponse); // Copy the original cache headers back and clean up any control headers if (bypassCache) { status = 'Bypass Cookie'; } else { status = 'Hit'; cachedResponse.headers.delete('Cache-Control'); cachedResponse.headers.delete('x-HTML-Edge-Cache-Status'); for (header of CACHE_HEADERS) { let value = cachedResponse.headers.get('x-HTML-Edge-Cache-Header-' + header); if (value) { cachedResponse.headers.delete('x-HTML-Edge-Cache-Header-' + header); cachedResponse.headers.set(header, value); } } response = cachedResponse; } } else { status = 'Miss'; } } catch (err) { // Send the exception back in the response header for debugging status = "Cache Read Exception: " + err.message; } } return {response, cacheVer, status, bypassCache}; } /** * Asynchronously purge the HTML cache. * @param {Int} cacheVer - Current cache version (if retrieved) * @param {Event} event - Original event */ async function purgeCache(cacheVer, event) { if (typeof EDGE_CACHE !== 'undefined') { // Purge the KV cache by bumping the version number cacheVer = await GetCurrentCacheVersion(cacheVer); cacheVer++; event.waitUntil(EDGE_CACHE.put('html_cache_version', cacheVer.toString())); } else { // Purge everything using the API const url = "https://api.cloudflare.com/client/v4/zones/" + CLOUDFLARE_API.zone + "/purge_cache"; event.waitUntil(fetch(url,{ method: 'POST', headers: {'X-Auth-Email': CLOUDFLARE_API.email, 'X-Auth-Key': CLOUDFLARE_API.key, 'Content-Type': 'application/json'}, body: JSON.stringify({purge_everything: true}) })); } } /** * Update the cached copy of the given page * @param {Request} originalRequest - Original Request * @param {String} cacheVer - Cache Version * @param {EVent} event - Original event */ async function updateCache(originalRequest, cacheVer, event) { // Clone the request, add the edge-cache header and send it through. let request = new Request(originalRequest); request.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall|bypass-cookies'); response = await fetch(request); if (response) { status = ': Fetched'; const options = getResponseOptions(response); if (options && options.purge) { await purgeCache(cacheVer, event); } let bypassCache = shouldBypassEdgeCache(request, response); if ((!options || options.cache) && !bypassCache) { await cacheResponse(cacheVer, originalRequest, response, event); } } } /** * Cache the returned content (but only if it was a successful GET request) * * @param {Int} cacheVer - Current cache version (if already retrieved) * @param {Request} request - Original Request * @param {Response} originalResponse - Response to (maybe) cache * @param {Event} event - Original event * @returns {bool} true if the response was cached */ async function cacheResponse(cacheVer, request, originalResponse, event) { let status = ""; const accept = request.headers.get('Accept'); if (request.method === 'GET' && originalResponse.status === 200 && accept && accept.indexOf('text/html') >= 0) { cacheVer = await GetCurrentCacheVersion(cacheVer); const cacheKeyRequest = GenerateCacheRequest(request, cacheVer); try { // Move the cache headers out of the way so the response can actually be cached. // First clone the response so there is a parallel body stream and then // create a new response object based on the clone that we can edit. let cache = caches.default; let clonedResponse = originalResponse.clone(); let response = new Response(clonedResponse.body, clonedResponse); for (header of CACHE_HEADERS) { let value = response.headers.get(header); if (value) { response.headers.delete(header); response.headers.set('x-HTML-Edge-Cache-Header-' + header, value); } } response.headers.delete('Set-Cookie'); response.headers.set('Cache-Control', 'public; max-age=315360000'); event.waitUntil(cache.put(cacheKeyRequest, response)); status = ", Cached"; } catch (err) { // status = ", Cache Write Exception: " + err.message; } } return status; } /****************************************************************************** * Utility Functions *****************************************************************************/ /** * Parse the commands from the x-HTML-Edge-Cache response header. * @param {Response} response - HTTP response from the origin. * @returns {*} Parsed commands */ function getResponseOptions(response) { let options = null; let header = response.headers.get('x-HTML-Edge-Cache'); if (header) { options = { purge: false, cache: false, bypassCookies: [] }; let commands = header.split(','); for (let command of commands) { if (command.trim() === 'purgeall') { options.purge = true; } else if (command.trim() === 'cache') { options.cache = true; } else if (command.trim().startsWith('bypass-cookies')) { let separator = command.indexOf('='); if (separator >= 0) { let cookies = command.substr(separator + 1).split('|'); for (let cookie of cookies) { cookie = cookie.trim(); if (cookie.length) { options.bypassCookies.push(cookie); } } } } } } return options; } /** * Retrieve the current cache version from KV * @param {Int} cacheVer - Current cache version value if set. * @returns {Int} The current cache version. */ async function GetCurrentCacheVersion(cacheVer) { if (cacheVer === null) { if (typeof EDGE_CACHE !== 'undefined') { cacheVer = await EDGE_CACHE.get('html_cache_version'); if (cacheVer === null) { // Uninitialized - first time through, initialize KV with a value // Blocking but should only happen immediately after worker activation. cacheVer = 0; await EDGE_CACHE.put('html_cache_version', cacheVer.toString()); } else { cacheVer = parseInt(cacheVer); } } else { cacheVer = -1; } } return cacheVer; } /** * Generate the versioned Request object to use for cache operations. * @param {Request} request - Base request * @param {Int} cacheVer - Current Cache version (must be set) * @returns {Request} Versioned request object */ function GenerateCacheRequest(request, cacheVer) { let cacheUrl = request.url; if (cacheUrl.indexOf('?') >= 0) { cacheUrl += '&'; } else { cacheUrl += '?'; } cacheUrl += 'cf_edge_cache_ver=' + cacheVer; return new Request(cacheUrl); }
在worker上部署好指令碼後,就可以新增域名了,想使用cname接入的Cloudflare,可以參考 針對使用Cloudflare CDN國內網站的速度優化方案 這篇文章。
安裝WordPress Page Cache Plugin
非常簡單,在WordPress中上傳外掛cloudflare-page-cache即可,這個外掛沒有圖形介面,無需任何設定,在每次觸發快取更新策略時會自動更新html_cache_version
。
值得一提的是,目前這個外掛有一個缺點,觸發快取更新後,所有頁面快取都會失效,不過後面有NGINX Cache頂著,到也影響不大。
Cloudflare Page Rule設定
需要禁止Cloudflare Page Rule快取,直接如圖設定即可,讓Cloudflare Page Rule快取所有的話,會把使用者資訊快取上去,現在所有的規則交給Edge Cache HTML就可以了。
一件部署外掛
如果上述操作對於你來說還是太難了,這裡還有一件部署外掛Edge Cache HTML via Cloudflare Workers。
我對Edge Cache HTML via Cloudflare Workers這款外掛稍作修改,新增了我修改的指令碼(edge-cache-html-cloudflare-workers下載),使之能夠正常快取。
填寫Cloudflare E-mail
和Cloudflare API Key
,然後儲存、安裝即可。
之後新增router,和Workers KV就可以使用了。
後記
我現在已經轉移到國內伺服器了,用不上了。實際上,如果快取上了,效果還是相當顯著的,我當時的TTFB大概是160ms。如果你的伺服器不在國內,相當建議你使用Cloudflare Edge Cache HTML 快取你的網站。
via 《sleele的部落格》,稍有改動。
評論留言