使用Workers自定義CDN快取策略取代Cloudflare Page Rule

Cloudflare一直是我非常喜歡的廠商,免費的套餐就可以提供不錯的服務,只不過現在被玩壞了,國內訪問速度大不如前,但是總體來說有還是比沒有強。

不過免費版套餐的Cloudflare CDN有個非常致命的缺點,就是不能根據cookie來區分訪客,從而針對性提供訪問內容(比如不為登陸、評論過的使用者提供快取)。並且還會把已登入或已評論使用者的資訊快取上去,這點難以接受,也是我拋棄Cloudflare的重要原因之一。

不過在Cloudflare Workers面世後,這個問題就迎刃而解了,我們可以使用Workers來繞過這個限制,自己編寫快取策略。

使用Workers自定義CDN快取策略

介紹

關於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 網頁 ,寫的非常詳細,只不過這位博主沒有沒有快取成功。

以下是修改後的指令碼

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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);
}
// 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); }
// 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_versioncloudflare頁面快取外掛

值得一提的是,目前這個外掛有一個缺點,觸發快取更新後,所有頁面快取都會失效,不過後面有NGINX Cache頂著,到也影響不大。

從能用到好用-快速搭建高效能WordPress指南

 

Cloudflare Page Rule設定

需要禁止Cloudflare Page Rule快取,直接如圖設定即可,讓Cloudflare Page Rule快取所有的話,會把使用者資訊快取上去,現在所有的規則交給Edge Cache HTML就可以了。

禁止Cloudflare Page Rule快取

一件部署外掛

如果上述操作對於你來說還是太難了,這裡還有一件部署外掛Edge Cache HTML via Cloudflare Workers

 

我對Edge Cache HTML via Cloudflare Workers這款外掛稍作修改,新增了我修改的指令碼(edge-cache-html-cloudflare-workers下載),使之能夠正常快取。

填寫Cloudflare E-mailCloudflare API Key,然後儲存、安裝即可。

Cloudflare Edge快取設定

之後新增router,和Workers KV就可以使用了。

HTML頁面edge快取狀態

後記

我現在已經轉移到國內伺服器了,用不上了。實際上,如果快取上了,效果還是相當顯著的,我當時的TTFB大概是160ms。如果你的伺服器不在國內,相當建議你使用Cloudflare Edge Cache HTML 快取你的網站。

via 《sleele的部落格》,稍有改動。

參考文章

  1. [WordPress]利用 Cloudflare Workers 來快取部落格的 HTML 網頁
  2. cloudflare worker-examples Edge Cache HTML
  3. Fake and free Bypass-on-Cookie, with CloudFlare edge cache workers for WordPress
  4. Cloudflare WordPress Edge Caching via Workers

評論留言