使用Workers自定义CDN缓存策略,而不是Cloudflare页面规则。
Cloudflare一直是我最喜欢的厂商,一个免费的套餐就能提供很好的服务,不过现在坏了,国内的访问速度也没有以前快了,但总的来说还是聊胜于无。
但是免费包Cloudflare CDN有一个致命的缺点,就是不能根据cookie区分访客,从而提供针对性的访问内容(比如不为登录和评论用户提供缓存)。而且我会缓存已登录或已评论用户的信息,这是无法接受的,也是我放弃Cloudflare的重要原因之一。
不过Cloudflare工作人员出来后,这个问题就解决了。我们可以使用workers来绕过这个限制,并编写我们自己的缓存策略。
介绍
Cloudflare Workers的介绍可以自己搜索,通过搜索引擎看到这篇文章的人可能已经有所了解。
我简单说明一下,本文的内容是通过Cloudflare写的一个脚本来实现自定义缓存策略。
边缘缓存HTML
Cloudflare官方写了一个WordPress插件来匹配edge-cache-html,但是这个项目已经快两年没更新了。我认为这个项目会影响他们付费套餐的销售。
https://github . com/cloud flare/worker-examples/tree/master/examples/edge-cache-html
目前直接用这个脚本,在WordPress下无法生效,缺少URL路径规则。我稍微修改了一下,增加了路径排除,支持WordPress下缓存(不包括头缓存判断)。
前期请参考【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页面缓存插件
非常简单,在WordPress中上传插件cloudflare-page-cache即可,这个插件没有图形界面,无需任何设置,在每次触发缓存更新策略时会自动更新html_cache_version。很简单,在WordPress中上传插件cloudflare-page-cache即可。这个插件没有图形界面,不需要任何设置。它会在每次触发缓存更新策略时自动更新html_cache_version。
值得一提的是,这个插件目前有一个缺点。缓存更新触发后,所有页面缓存都会失效,但后面有NGINX缓存,影响不大。
从可用到易用——快速构建高性能WordPress指南
Cloudflare页面规则设置
禁止Cloudflare页面规则缓存,如图设置即可。如果Cloudflare页面规则缓存所有内容,它将缓存用户信息。现在所有的规则都可以交给Edge Cache HTML了。
部署插件
如果以上操作对你来说还是太难的话,下面是另一个部署插件Edge Cache HTML via Cloud Flare Workers。
我通过Cloud Flare Workers对Edge Cache HTML的插件进行了轻微的修改,并添加了我修改后的脚本(Edge-Cache-HTML-Cloud Flare-Workers下载)使其正常缓存。
填写Cloudflare电子邮件和Cloudflare API密钥,然后保存并安装它。
之后添加路由器,和WorkerKV,就可以用了。
附言
我现在已经转到国内服务器了,用不了了。其实如果缓存的话,效果还是挺显著的。我当时的TTFB大概是160ms。如果您的服务器不在中国,强烈建议您使用Cloudflare Edge Cache HTML缓存您的网站。
Via 《sleele的博客,略有改动。
参考文章
[WordPress]使用Cloudflare Workers来缓存博客的HTML页面
cloud flare worker-示例边缘缓存HTML
假的和免费的Bypass-on-Cookie,带有cloud flare edge cache workers for WordPress
通过Workers进行Cloudflare WordPress边缘缓存