家庭数据中心系列 cloudflare教程(七) CF worker功能介绍及基于worker实现”乞丐版APO for WordPress”功能加速网站访问的实操、验证及相关技术原理研究

前言

CF系列教程第7部,写CF worker教程的时机终于成熟了。

为什么这么说呢?之所以将CF worker留到后面来写,是因为”要让某种流量最终能被对应的worker接受并处理,其前提条件是该流量不能被CF流量序列中位于worker之前的节点功能模块的规则命中并处理,而不同类型的worker实现的功能可能需要不同的节点功能模块的规则相配合:例如,优化http访问的worker要能正常处理访问请求,就需要访问请求不能被流量序列中位置在worker前面的Cache Rules的缓存规则命中,否则,Cache Rules会直接使用CDN中的内容对访问请求进行响应,这样一来造成的后果就是,访问请求根本到不了worker,自然也就不能被worker优化访问了;同时,访问请求也不能被其他节点功能模块命中并处理,例如,不能被WAF规则中处理行为是阻止的规则命中。

在上面绿色字体部分内容的描述中,如果我不提前写清楚什么是”流量系列”、什么是”节点功能模块”、什么是”Cache Rules”、什么是缓存规则命中、什么是”WAF”以及什么是”WAF规则中处理行为是阻止的规则”等概念,大部分人只看这段描述估计都是懵逼的,但是这段话偏偏是任何worker能正常工作的基本前提,避都避不过,更别说文章后面还需要涉及到诸如边缘网络之类的诸多概念了。

为了避免同一篇文章中涉及太多概念而显得逻辑杂乱无章,同时要写worker的教程需要涉及到那么多前置知识,所以我干脆就把worker的教程安排在后面写,而在这之前,先以系列教程的方式从CF的整体方案、相关基础概念开始,并慢慢梳理流量序列中位于worker之前的重要节点功能,到现在,前置知识基本都理清楚了,也终于可以开始写worker的教程了。

注1:其他前置知识请参看CF系列教程的”一”到”六”,特别是”二”(家庭数据中心系列 cloudflare教程(二) CF整体方案流量序列中各技术节点功能简介)和”六”(家庭数据中心系列 cloudflare教程(六) CF Cache Rules(缓存规则)功能介绍及详细配置教程)。

注2:本篇文章为了梳理一些相关概念(也有一些是我的突发奇想),会涉及到大量的跨多个领域的专业知识,不感兴趣的朋友可以直接跳到后面的实操部分(我也没想到会牵扯那么多,只不过想到了就干脆想清楚,继而干脆就记录下来,不知不觉就越写越多~)。

不过那些对技术真感兴趣的朋友,不妨读一读,相信会有些许收获。

CF worker相关概念介绍

什么是CF worker?

简单来说,CF worker是CF提供的一种对用户而言能够serverless(无服务器)运行JavaScript代码的方式(可以简单理解为CF使用自己的边缘网络服务器的cpu和内存帮用户运行JS代码,并将结果传回给用户),除了正常运行JS代码去实现代码本身编写的功能外(比如一些非常规的正向代理、反向代理,不过这些本文中不涉及),最关键的是,通过worker能够控制一些CF的功能,例如边缘缓存。这种通过worker控制边缘缓存的方式,和原本Free计划中通过Cache Rules(或者页面规则)只能粗糙控制CDN的方式(只有绕过缓存和符合缓存条件2个选项,最多还有个边缘TTL和浏览器TTL的时间控制选项)比起来,能够提供更多、更精细化的缓存控制,所以相比之下能提供更快的网站的访问速度。

注1:其实,即便原本Free计划中通过Cache Rules(或者页面规则)只能粗糙控制CDN、且不支持基于Cookie的高级定向服务或个性化功能,但是CF的基本功能仍然能够通过Anycast IP技术和其全球网络,将用户的请求路由到距离他们最近的可用数据中心,只是因为众所周知的原因,国内访问者无法享受而已(国外访问者就没说过有负优化~),所以,这和CF无关。

注2:使用worker控制边缘缓存能提高网站访问速度的效果不假,不过实现原理上和常说的优选IP完全不同(优选IP相关知识我下一节会单独讲),要说加速的原理,倒是和CF另一个付费功能一样:APO for WordPress,所以我称之为”乞丐版APO for WordPress”功能。

注3:由于worker本来就是建立在边缘网络服务器上,所以天然具备着优势,比如,可以在靠近用户的CF边缘网络上运行JS代码来优化和定制 HTTP 请求和响应,提升应用性能、减少延迟并增强安全性,最终实现提升用户访问体验的目标。

注4:除了JavaScript,worker还支持Rust 或其他 WebAssembly 语言。

注5:对于Free计划而言,worker每天的免费额度为100000次请求。

附加知识:优选IP

关于”优选IP”概念的澄清

1、首先需要明确的一点是,优选 IP这个说法并不是CF官方的正式名称,而是一些用户和第三方服务商为描述CF的 China Network(中国网络) 功能所使用的非正式称呼,很可能是因为该功能在中国大陆通过优化的 IP 路径(主要是使用国内云合作伙伴的网络和数据中心),提升了访问速度,所以用户习惯称其为”优选 IP”,这种表达方式并没有在CF的官方文档或产品页面中出现,所以只能算是一个非正式的民间说法。

2、”中国网络”功能并不能单独提供,需要企业级套餐(Enterprise plan)才能获得,并且价格还只能”定制”:
image.png

3、该功能在国外对应的官方功能是Argo Smart Routing,不过还是贵:
image.png

另:原则上,通过优选IP(中国大陆访问)功能在国内实现的网站访问加速和本文通过worker控制边缘缓存的方式实现的网站加速,在底层逻辑上风马牛不相关(优选IP适用性更广,更高级,而本文所说的方式则限制性较大,只适合HTML类型的应用),但是,鉴于部分朋友一提到网站加速就喜欢往优选IP功能上靠的习惯(例如:通过worker实现优选IP功能这种说法),我觉得还是先把优选IP功能说透比较好。

注:为了大家的阅读习惯,我后面仍旧会保留优选IP这种民间说法,但是大家要记住其所指的真正含义。

优选IP(中国网络访问)的工作原理


CF企业级套餐用户在国内的优选IP功能一直让广大习惯白嫖的朋友流口水,可惜的是,Free计划的用户只能远观而不可亵玩。不过,虽然对优选IP功能眼热,但是我相信绝大部分的朋友只想着如何白嫖,却没有过多关注过优选IP功能的底层技术原理到底是什么(怎么有点像在说渣男?)。


CF的优选IP功能是通过优化CF内部网络路由,选择最佳路径和数据中心来处理用户请求,从而提升访问速度、降低延迟。这一功能对用户是透明的,不会改变用户解析域名时获得的IP地址,但能显著改善网站的性能和稳定性,它基于以下几个判断依据:

  1. 智能路由:动态评估网络状况,包括延迟、带宽和拥塞情况,选择最快的路径传输数据。
  2. 负载均衡:根据服务器的负载情况,将请求分配到性能最佳的服务器,避免单点过载。
  3. 地理位置:根据用户的地理位置,将请求路由到最近的数据中心,减少延迟。
  4. 实时监控:持续监控网络和服务器状态,自动调整路由策略,以应对网络变化和突发情况。

上面的描述比较官方,不容易理解,那么我从用户视角把上面的话再梳理一遍。

  1. 用户解析域名的IP地址

• 当用户通过DNS解析一个使用CF的域名时,他们会得到CF分配的一个IP地址,通常是CF的Anycast IP地址(Anycast IP技术出现在1989年,在1993年之后开始广泛普及,国内各地运营商的Local DNS地址基本都是基于Anycast IP技术的,只不过一般是在某个区域网络内有效,最多了不起国内有效,CF牛逼的地方在于全球有效),这个IP地址通常是全球范围内通用的,和”优选IP”这个功能没有直接联系。举例说明:支持优选IP功能的域名和不支持优选IP功能的域名解析出来可以是同一个IP地址,也就是说,其实”优选IP”这个功能和解析的IP地址没有半毛钱关系,要准确表述的话,其实更应该是”优选路由”或者”路由优化”更贴切(所以CF官方功能”Argo Smart Routing“的名字才是对的)。


注1:Anycast IP技术需要BGP动态路由协议作为支撑,所以如果你有一块自己能够完全控制的大型网络(并且有独立的ASN号、IPv4或者IPv6的公网IP地址段),同时通过EBGP和外网相连,那么只要在所控制的网络内部合理配置路由(内部动态路由协议+IBGP),就可以在让你的某些公网IP在外网眼中实现Anycast IP了。

注2:而如果这时候你在自己的网络内再配合特定的隧道协议(比如MPLS)创建对于多个出口之间的最佳路径(假设你的大型网络有2个进出口,正常数据包从进出口1进入你的网络,然后按照正常路由从进出口2出去要走20跳,但是如果有基于类似MPLS技术的隧道连接了进出口1和2,并且可以用特殊方式,例如专线,使从进出口1出来的包能直接到进出口2的话,那么,这样经过的网络节点就只有2跳),然后使其对访问目标域名”blog.tangwudi.com”对应的后续包序列生效的话,那么,当访问”blog.tangwudi.com”的请求从进入口1进入,就会从这条隧道的直接到达进出口2之后出去,这优化减少了多少跳?而如果你能控制的网络遍布全球呢(比如cloudflare)?这还只是从路由优化的角度,如果再加上遍布全球的数据中心可以就近回源呢?所以优选IP功能贵完全可以理解。

注3:理论上这里画个图更容易理解,不过太累人了,算了。。


  1. CF内部路由优化


• 优选IP功能主要是为了优化CF的内部路由,使得请求在CF的全球骨干网络(在国内就是中国网络)中传输时更高效。它通过选择最佳的网络路径和数据中心来加速请求的处理,降低延迟并提升整体性能。
• 这种优化是透明的,即用户解析域名得到的IP地址保持不变,变化的只是CF如何在内部处理和传输这些请求。举例说明,如果是国内CF企业套餐用户的网站,虽然其域名解析出来的IP和其他Free计划用户的网站域名解析出来的IP是一样的,但是,企业级用户网站的访问者却能用这个IP直接从CF国内合作伙伴的数据中心(目前是京东云)进入CF边缘网络:

image.png

这所谓的位于中国大陆的30个数据中心,就是其目前国内合作伙伴(京东云)的数据中心:
image.png

而Free计划用户的访问者,就只能老老实实从美国西部数据中心进入CF边缘网络,这差距,可不是一星半点。

  1. CF在国内的优选IP功能


• 在CF没有直接控制网络的区域(比如国内。国外CF基本都是自建骨干网直接铺到各个国家和城市以及和各运营商直连),优选IP功能可能受到一些限制,尤其是当合作伙伴的网络基础设施或路由策略不能完全支持CF的优化需求时。
• 由于这些依赖关系,CF在这些区域提供的优选IP效果很可能不如在自己控制的全球骨干网区域中那样显著(但是也要比访问Free计划的用户网站需要从美国数据中心走快得多)。

注:其实,优选IP功能除了上面提到的路由优化,还需要和很多关键技术组件,比如”协议层优化”、”TLS优化”、”智能DNS解析”、”应用层路由”、”实时性能监控”等等相配合,要说技术含量,起码还是有三、四层楼那么高的。


很多Free计划的朋友采用自己使用软件手动测试的方式来寻找”最优的自选IP”(其实这个准确上来说应该描述为”在当时的测试结果中寻找综合看起来最佳的IP”),但是这种方式有显著的缺点:

  1. 手动操作复杂且耗时:手动测试和选择最佳IP需要大量时间和精力,尤其是需要频繁测试和调整时,用户需要不断监控网络状况并进行测试,以确保选择的IP地址始终是最佳的。
  2. 实时性不足:网络状况是动态变化的(同一个IP,可能这一刻测试最佳,但是几分钟后就不是最佳了),手动测试的结果可能很快就过时,无法及时反映当前的网络状态。这意味着即使选中了一个最佳IP,也可能很快失去其优势。
  3. 缺乏全局视角:个体用户的测试通常局限于他们所在的地理位置和网络环境,无法全面反映不同地区和网络条件下的最佳IP选择,而CF的优选IP功能则能够基于全球网络状况进行优化。

我去年下半年刚开始学习搭建博客的时候,是使用备案域名搭建的(那时候还很天真和单纯,还不知道在国内搭建个人博客的坑有多深,对这个感兴趣的可以参看文章:家庭数据中心系列 独立个人博客搭建及避坑指南),就尝试过使用CF自定义主机名+手动选择最佳IP的方式,当时的感觉就是不稳定,刚开始的时候感觉好像还行,但是过一段时间就变慢了,这让我不得不经常关注访问速度的变化,甚是累人,后来国内访问就直接用腾讯云的CDN了。


为什么使用worker控制边缘缓存能够达到比CF常规CDN更好的访问效果?

这主要因为worker具备以下几个关键特性:

  1. 灵活的编程能力

worker 允许开发者编写自定义的 JavaScript 代码来处理请求,这种灵活性意味着你可以编写逻辑来选择和路由流量到不同的 IP 地址,从而实现更好的的效果。

  1. 边缘计算优势

CF worker 在全球分布的边缘节点上运行,能够快速响应并处理用户请求。通过 worker,你可以根据实时网络状况、地理位置、服务器负载等动态选择最佳IP地址。

  1. 绕过基础功能限制

虽然 Free 计划中的默认功能集可能不支持一些高级路由和负载均衡功能,但 workers 提供了一个编程接口,让你能够自己实现这些功能。例如,通过 worker,你可以编写逻辑来检测网络状况并选择最佳路径,而不依赖于基础计划的默认设置。

  1. 自定义HTTP请求处理

使用 worker,你可以拦截和修改 HTTP 请求和响应。这允许你根据自定义的优选IP逻辑来转发请求到不同的目标IP地址,从而实现更好的性能和可靠性。

因此,也不能说CF在Free计划中只提供了的常规的CDN功能,准确的描述应该是:在Free计划中以比较含蓄、不易被发现、有一定技术门槛的、有一定额度的方式(worker)悄悄提供了更精细的边缘缓存的功能~。当然,这和优选IP功能还是没法比的,只能说比Cache Rules(或者页面规则)实现的缓存效果更好一些。

附加知识:worker KV(Key-Value)

我在文章前面提到”worker可以简单理解为CF使用自己的边缘网络服务器的cpu和内存帮用户运行JS代码,并将结果传回给用户”,那么这里就引出了一个问题,有些代码的运行没有缓存、持久化存储、会话管理等需求,因此可以直接运行在CF边缘服务器的内存中,例如:

  1. HTTP 请求处理: 处理、修改和响应 HTTP 请求。
  2. 边缘计算: 在请求到达原始服务器之前,在边缘节点执行逻辑。
  3. API 网关: 路由和处理 API 请求。
  4. 静态内容服务: 提供静态文件或生成动态内容。
  5. 请求转发: 将请求转发到其他服务器,进行负载均衡等。

但是,有些代码的运行却有缓存、持久化存储、会话管理等需求,而这些需求仅仅依靠CF边缘服务器内存无法实现,所以,针对这些需求,就需要worker的一个配套功能:KV(Key-Value,键值存储)来辅助实现(类似于docker run命令使用-v参数挂载外部存储来实现特定文件或者文件夹内容的持久化)。

CF worker KV 是CF提供的一种全托管的键值存储服务,专为 CF worker设计。它允许开发者在全球分布的CF边缘服务器上存储和访问数据,这些数据可以被 worker使用,使得你可以在边缘执行更复杂的逻辑和处理请求。

一些关键的特点包括:

  1. 全球分布: 数据存储在全球多个地点的边缘服务器上,可以实现低延迟的访问。
  2. 快速读写: 提供快速的读写访问,适合处理高吞吐量和低延迟要求的应用程序。
  3. 可扩展性: 支持大规模数据存储和高并发访问,适合处理大量的并发请求。
  4. 版本控制: 支持版本控制和事务操作,使得数据更新更加安全和可控。
  5. 安全性: 数据在传输和存储时都经过加密,确保数据的安全性和隐私保护。

而刚好,使用worker实现”乞丐版APO for WordPress”功能就需要KV来配合完成边缘缓存部分的操作。

注:CF worker KV 在 Free 计划中有一些使用限制,这些限制包括:

  1. 存储限制: 在 Free 计划中,KV 存储的总容量限制为 1 GB。这意味着你可以存储的键值对总大小不能超过 1 GB。
  2. 读取次数: Free 计划对读取操作(reads)有每月 100,000 次的限制。超过这个限额之后,额外的读取操作将会收费。
  3. 写入次数: Free 计划对写入操作(writes)有每月 1,000 次的限制。超过这个限额之后,额外的写入操作将会收费。
  4. 列表操作: 列表操作(listing keys)每月限制在 1,000 次。
  5. 性能: Free 计划的 KV 数据同步到全球所有数据中心的速度较慢,延迟较高。高级计划(如 Pro 或 Enterprise)则提供更快的同步速度和更低的延迟。
  6. API 请求速率: 在 Free 计划中,每秒允许的 API 请求速率较低,高级计划则允许更高的速率。

总体来讲,这些限制使得 Free 计划的KV只适合小型项目开发和测试使用,不过嘛,对个人博客而言足够了。

使用CF worker为WordPress实现”乞丐版APO for WordPress”的功能

背景介绍:APO For WordPress 与 Edge Cache HTML

CF于2020 年 10 月 1 日正式推出的 Automatic Platform Optimization (APO) for WordPress 功能,该功能旨在通过CF的全球内容分发网络(CDN)来优化 WordPress 网站的性能,减少加载时间,提升用户体验。APO 自动缓存和优化 WordPress 网站的内容,包括 HTML、JavaScript、CSS 和图像,从而加速页面加载并减轻服务器负载,而这项优化,CF官方博客曾经提到过,其实就是通过worker来完成的:
image.png

APO for WordPress本身是收费服务(5美金/月),这个对大部分个人站长而言还是偏贵(毕竟现在的个人站长大部分都是为爱发电,能省就省,且平时流量也不大,付费有点不划算),不过,APO for WordPress能使用worker来优化,那对于Free计划的用户而言,能不能也使用worker来优化WordPress站点的访问呢?

答案是可以,这归功于之前CF官方曾经出过一个JS脚本示范模板:edge-cache-html.js
image.png

该JS脚本可以与支持 x-HTML-Edge-Cache 标头的内容管理系统(如wordpress)通过插件配合,也可以直接和大部分静态类型站点配合,使用边缘缓存为未登录用户边缘缓存 HTML,所以后续要使用worker实现乞丐版APO功能,就需要以该JS示范脚本为模板进行修改了。另外,CF还为wordpress提供了配合的插件(为了支持 x-HTML-Edge-Cache 标头),贴心不贴心?

注1:APO for For WordPress也是采用的插件+worker的模式,只不过插件是官方插件cloudflare,而worker是系统后台自动生效,无需人工设置和干预。

注2:要使用该JS脚本,必须使用 EDGE_CACHE 变量将键/值命名空间绑定到此 Worker 脚本,该变量在以下代码中可以看到,大家可以留意一下,之后会用到。

注3:以下代码只是针对wordpress的,针对一般静态网站的JS代码参见倒数第2章。

以下是针对wordpress站点修改后的脚本注释版,相对官方模板,我对变量进行了一些调整并针对wordpress可缓存内容进行了优化(wordpress哪些内容需要跳过缓存我在系列教程六中已经讲过,这里就不再重复了):

// 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.

// 定义需要处理的缓存头部信息
const CACHE_HEADERS = ['Cache-Control', 'Expires', 'Pragma'];

// 默认的绕过缓存的 Cookie 前缀
const DEFAULT_BYPASS_COOKIES = [
    "wp-",
    "wordpress",
    "_logged_in_",
    "comment_",
    "woocommerce_"
];

// 使用正则表达式定义需要绕过缓存的URL
const BYPASS_URL_PATTERNS = [
    /\/wp-admin\/.*/,
    /\/wp-adminlogin\/.*/,
    /\/wp-login\/.*/,
    /\/wp-comment\/.*/,
    /xmlrpc.*/,
    /preview=true.*/,
    /[?&]s=.*/
];

// 监听 fetch 事件,处理每个请求
addEventListener("fetch", event => {
    // 在发生异常时继续传递事件
    event.passThroughOnException();
    // 处理请求
    event.respondWith(handleRequest(event));
});

// 主请求处理函数
async function handleRequest(event) {
    const { request } = event;

    // 如果请求应绕过缓存,则直接获取请求
    if (shouldBypassRequest(request)) {
        return fetch(request);
    }

    // 获取缓存的响应
    let { response, cacheVer, status } = await getCachedResponse(request);

    // 如果没有缓存响应,则获取并缓存响应
    if (!response) {
        response = await fetchAndCache(request, cacheVer, event);
    } else {
        status = 'HIT';
        // 更新缓存(如果需要)
        event.waitUntil(updateCacheIfNeeded(request, cacheVer, event));
    }

    // 增强响应头
    return enhanceResponse(response, status, cacheVer);
}

// 判断请求是否应绕过缓存
function shouldBypassRequest(request) {
    const accept = request.headers.get('Accept');
    const isImage = accept && accept.includes('image/*');
    return isImage || !isConfigured() || isUpstreamCachePresent(request);
}

// 检查配置是否完整
function isConfigured() {
    return typeof <font color="#dd0000">HTML、JavaScript、CSS 和图像</font>EDGE_CACHE !== 'undefined' || (
        CLOUDFLARE_API.email.length && CLOUDFLARE_API.key.length && CLOUDFLARE_API.zone.length
    );
}

// 判断请求头中是否有上游缓存信息
function isUpstreamCachePresent(request) {
    return request.headers.get('x-HTML-Edge-Cache') !== null;
}

// 获取缓存的响应
async function getCachedResponse(request) {
    if (!shouldCacheRequest(request)) {
        return { response: null, cacheVer: null, status: 'Miss' };
    }

    const cacheVer = await getCurrentCacheVersion(null);
    const cacheKeyRequest = generateCacheRequest(request, cacheVer);
    const cache = caches.default;

    let response = await cache.match(cacheKeyRequest);
    if (response) {
        response = new Response(response.body, response);
        if (shouldBypassEdgeCache(request, response)) {
            return { response: null, cacheVer, status: 'Bypass Cookie' };
        }
        response = cleanupCacheHeaders(response);
        return { response, cacheVer, status: 'Hit' };
    }

    return { response: null, cacheVer, status: 'Miss' };
}

// 判断请求是否应缓存
function shouldCacheRequest(request) {
    const accept = request.headers.get('Accept');
    return request.method === 'GET' && accept && accept.includes('text/html');
}

// 清理缓存头部信息
function cleanupCacheHeaders(response) {
    CACHE_HEADERS.forEach(header => {
        const value = response.headers.get(x-HTML-Edge-Cache-Header-${header});
        if (value) {
            response.headers.set(header, value);
            response.headers.delete(x-HTML-Edge-Cache-Header-${header});
        }
    });
    return response;
}

// 获取并缓存响应
async function fetchAndCache(request, cacheVer, event) {
    const response = await fetch(modifyRequest(request));
    if (shouldCacheResponse(request, response)) {
        const status = await cacheResponse(cacheVer, request, response, event);
        response.headers.set('x-HTML-Edge-Cache-Status', status);
    }
    return response;
}

// 修改请求头部以包含缓存支持信息
function modifyRequest(request) {
    const newRequest = new Request(request);
    newRequest.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall|bypass-cookies');
    return newRequest;
}

// 判断响应是否应缓存
function shouldCacheResponse(request, response) {
    const accept = request.headers.get('Accept');
    return request.method === 'GET' && response.status === 200 && accept && accept.includes('text/html');
}

// 缓存响应
async function cacheResponse(cacheVer, request, response, event) {
    const cache = caches.default;
    const cacheKeyRequest = generateCacheRequest(request, cacheVer);
    const clonedResponse = new Response(response.body, response);

    CACHE_HEADERS.forEach(header => {
        const value = clonedResponse.headers.get(header);
        if (value) {
            clonedResponse.headers.set(x-HTML-Edge-Cache-Header-${header}, value);
            clonedResponse.headers.delete(header);
        }
    });

    clonedResponse.headers.set('Cache-Control', 'public; max-age=315360000');
    event.waitUntil(cache.put(cacheKeyRequest, clonedResponse));
    return "Cached";
}

// 根据需要更新缓存
async function updateCacheIfNeeded(request, cacheVer, event) {
    if (shouldCacheRequest(request)) {
        const response = await fetch(modifyRequest(request));
        if (shouldCacheResponse(request, response)) {
            await cacheResponse(cacheVer, request, response, event);
        }
    }
}

// 增强响应头部信息
function enhanceResponse(response, status, cacheVer) {
    const newResponse = new Response(response.body, response);
    if (status) {
        newResponse.headers.set('x-HTML-Edge-Cache-Status', status);
    }
    if (cacheVer !== null) {
        newResponse.headers.set('x-HTML-Edge-Cache-Version', cacheVer.toString());
    }
    return newResponse;
}

// 获取当前缓存版本
async function getCurrentCacheVersion(cacheVer) {
    if (cacheVer === null) {
        if (typeof EDGE_CACHE !== 'undefined') {
            cacheVer = await EDGE_CACHE.get('html_cache_version') || 0;
            await EDGE_CACHE.put('html_cache_version', cacheVer.toString());
        } else {
            cacheVer = -1;
        }
    }
    return cacheVer;
}

// 生成缓存请求
function generateCacheRequest(request, cacheVer) {
    const url = new URL(request.url);
    url.searchParams.append('cf_edge_cache_ver', cacheVer);
    return new Request(url.toString());
}

// 判断是否应绕过边缘缓存
function shouldBypassEdgeCache(request, response) {
    const url = new URL(request.url);
    if (BYPASS_URL_PATTERNS.some(pattern => pattern.test(url.pathname + url.search))) {
        return true;
    }

    const options = getResponseOptions(response);
    const bypassCookies = options ? options.bypassCookies : DEFAULT_BYPASS_COOKIES;
    const cookieHeader = request.headers.get('cookie');
    if (cookieHeader) {
        return bypassCookies.some(prefix => cookieHeader.split(';').some(cookie => cookie.trim().startsWith(prefix)));
    }
    return false;
}

// 获取响应选项
function getResponseOptions(response) {
    const header = response.headers.get('x-HTML-Edge-Cache');
    if (!header) return null;

    const options = {
        purge: header.includes('purgeall'),
        cache: header.includes('cache'),
        bypassCookies: header.split(',')
            .filter(command => command.trim().startsWith('bypass-cookies'))
            .flatMap(command => command.split('=')[1].split('|').map(cookie => cookie.trim()))
    };

    return options;
}

注释说明

    •   缓存头部常量:定义需要处理的缓存头部信息。
    •   默认绕过缓存的 Cookie 前缀:定义默认的绕过缓存的 Cookie 前缀列表。
    •   绕过缓存的 URL 模式:定义需要绕过缓存的 URL 模式。
    •   事件监听器:监听 fetch 事件,并处理每个请求。
    •   主请求处理函数:处理每个请求,判断是否需要绕过缓存,并获取或缓存响应。
    •   绕过缓存判断:判断请求是否需要绕过缓存。
    •   配置检查:检查配置是否完整。
    •   上游缓存检查:检查请求头中是否包含上游缓存信息。
    •   获取缓存响应:获取缓存中的响应,如果没有缓存响应则返回空。
    •   缓存请求判断:判断请求是否应缓存。
    •   清理缓存头部信息:清理响应中的缓存头部信息。
    •   获取并缓存响应:获取并缓存响应。
    •   修改请求头部:修改请求头部以包含缓存支持信息。
    •   缓存响应判断:判断响应是否应缓存。
    •   缓存响应:将响应缓存到边缘缓存中。
    •   根据需要更新缓存:根据需要更新缓存内容。
    •   增强响应头部信息:增强响应头部信息,添加缓存状态和版本信息。
    •   获取当前缓存版本:获取当前的缓存版本号。
    •   生成缓存请求:生成缓存请求,附加缓存版本号。
    •   判断是否绕过边缘缓存:判断是否应绕过边缘缓存,根据 URL 模式和 Cookie 前缀。
    •   获取响应选项:获取响应中的选项信息,包括缓存和绕

以下是没有注释的纯净代码版,后面创建worker的时候会用到(似乎转成html后格式有问题,直接复制到worker里会报错,大家可以先试试(如果有问题,可以直接让chatgpt优化一下格式问题)我这里也提供一下我自己的下载地址,大家可以直接下txt文件,链接如下:无敌的文件分享,访问密码:”blog.tangwudi.com”):

// IMPORTANT: Ensure that a Key/Value Namespace (EDGE_CACHE) is bound to this worker script.
// This KV store is recommended as it allows purging only HTML instead of the entire cache.

const CACHE_HEADERS = ['Cache-Control', 'Expires', 'Pragma'];

const DEFAULT_BYPASS_COOKIES = [
    "wp-", 
    "wordpress", 
    "_logged_in_", 
    "comment_", 
    "woocommerce_"
];

const BYPASS_URL_PATTERNS = [
    /\/wp-admin\/.*/, 
    /\/wp-login\/.*/, 
    /xmlrpc.*/, 
    /preview=true.*/, 
    /[?&]s=.*/
];

addEventListener("fetch", event => {
    event.passThroughOnException();
    event.respondWith(handleRequest(event));
});

async function handleRequest(event) {
    const { request } = event;

    // Check if the request should bypass the cache
    if (shouldBypassRequest(request)) {
        return fetch(request);
    }

    // Attempt to get the cached response
    let { response, cacheVer, status } = await getCachedResponse(request);

    if (!response) {
        response = await fetchAndCache(request, cacheVer, event);
    } else {
        status = 'HIT';
        event.waitUntil(updateCacheIfNeeded(request, cacheVer, event));
    }

    return enhanceResponse(response, status, cacheVer);
}

function shouldBypassRequest(request) {
    const accept = request.headers.get('Accept');
    const isImage = accept && accept.includes('image/*');

    return isImage || !isConfigured() || isUpstreamCachePresent(request);
}

function isConfigured() {
    return typeof EDGE_CACHE !== 'undefined'; // Ensure KV is configured
}

function isUpstreamCachePresent(request) {
    return request.headers.get('x-HTML-Edge-Cache') !== null;
}

async function getCachedResponse(request) {
    if (!shouldCacheRequest(request)) {
        return { response: null, cacheVer: null, status: 'Miss' };
    }

    const cacheVer = await getCurrentCacheVersion();
    const cacheKeyRequest = generateCacheRequest(request, cacheVer);
    const cache = caches.default;

    let response = await cache.match(cacheKeyRequest);
    if (response) {
        response = new Response(response.body, response);

        if (shouldBypassEdgeCache(request, response)) {
            return { response: null, cacheVer, status: 'Bypass Cookie' };
        }

        response = cleanupCacheHeaders(response);
        return { response, cacheVer, status: 'Hit' };
    }

    return { response: null, cacheVer, status: 'Miss' };
}

function shouldCacheRequest(request) {
    const accept = request.headers.get('Accept');
    return request.method === 'GET' && accept && accept.includes('text/html');
}

function cleanupCacheHeaders(response) {
    CACHE_HEADERS.forEach(header => {
        const value = response.headers.get(x-HTML-Edge-Cache-Header-${header});
        if (value) {
            response.headers.set(header, value);
            response.headers.delete(x-HTML-Edge-Cache-Header-${header});
        }
    });
    return response;
}

async function fetchAndCache(request, cacheVer, event) {
    const response = await fetch(modifyRequest(request));
    if (shouldCacheResponse(request, response)) {
        const status = await cacheResponse(cacheVer, request, response, event);
        response.headers.set('x-HTML-Edge-Cache-Status', status);
    }
    return response;
}

function modifyRequest(request) {
    const newRequest = new Request(request);
    newRequest.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall|bypass-cookies');
    return newRequest;
}

function shouldCacheResponse(request, response) {
    const accept = request.headers.get('Accept');
    return request.method === 'GET' && response.status === 200 && accept && accept.includes('text/html');
}

async function cacheResponse(cacheVer, request, response, event) {
    const cache = caches.default;
    const cacheKeyRequest = generateCacheRequest(request, cacheVer);
    const clonedResponse = new Response(response.body, response);

    CACHE_HEADERS.forEach(header => {
        const value = clonedResponse.headers.get(header);
        if (value) {
            clonedResponse.headers.set(x-HTML-Edge-Cache-Header-${header}, value);
            clonedResponse.headers.delete(header);
        }
    });

    clonedResponse.headers.set('Cache-Control', 'public; max-age=315360000'); // 10 years cache
    event.waitUntil(cache.put(cacheKeyRequest, clonedResponse));
    return "Cached";
}

async function updateCacheIfNeeded(request, cacheVer, event) {
    if (shouldCacheRequest(request)) {
        const response = await fetch(modifyRequest(request));
        if (shouldCacheResponse(request, response)) {
            await cacheResponse(cacheVer, request, response, event);
        }
    }
}

function enhanceResponse(response, status, cacheVer) {
    const newResponse = new Response(response.body, response);
    if (status) {
        newResponse.headers.set('x-HTML-Edge-Cache-Status', status);
    }
    if (cacheVer !== null) {
        newResponse.headers.set('x-HTML-Edge-Cache-Version', cacheVer.toString());
    }
    return newResponse;
}

async function getCurrentCacheVersion() {
    let cacheVer = 0;

    if (typeof EDGE_CACHE !== 'undefined') {
        cacheVer = await EDGE_CACHE.get('html_cache_version') || 0;
    } else {
        cacheVer = -1; // Fallback if KV isn't bound
    }

    return cacheVer;
}

function generateCacheRequest(request, cacheVer) {
    const url = new URL(request.url);
    url.searchParams.append('cf_edge_cache_ver', cacheVer);
    return new Request(url.toString());
}

function shouldBypassEdgeCache(request, response) {
    const url = new URL(request.url);
    if (BYPASS_URL_PATTERNS.some(pattern => pattern.test(url.pathname + url.search))) {
        return true;
    }

    const options = getResponseOptions(response);
    const bypassCookies = options ? options.bypassCookies : DEFAULT_BYPASS_COOKIES;
    const cookieHeader = request.headers.get('cookie');
    if (cookieHeader) {
        return bypassCookies.some(prefix => cookieHeader.split(';').some(cookie => cookie.trim().startsWith(prefix)));
    }
    return false;
}

function getResponseOptions(response) {
    const header = response.headers.get('x-HTML-Edge-Cache');
    if (!header) return null;

    const options = {
        purge: header.includes('purgeall'),
        cache: header.includes('cache'),
        bypassCookies: header.split(',')
            .filter(command => command.trim().startsWith('bypass-cookies'))
            .flatMap(command => command.split('=')[1].split('|').map(cookie => cookie.trim()))
    };

    return options;
}

CF配置步骤

第一步:创建KV

如前面所说,要使用worker控制边缘缓存,需要配合KV功能,所以第一步是先创建一个KV命名空间,按照如下流程创建KV空间。

从”Workers和Pages”-“KV”中,点击”创建命名空间”:

image.png

填写命名空间名称并点击”添加”:
image.png

完成:
image.png

第二步:创建worker

从”Workers和Pages”-“概述”中,点击”创建”:

image.png

点击”创建Worker”:
image.png

填写worker名称中点击右下角的”部署”:
image.png

然后点击右上红框中的”编辑代码”:
image.png

将左边红框中的默认内容删除,然后将前面的无注释版纯净代码粘贴进去,最后点击右上角的”部署”:
image.png

点击”保存并部署”:
image.png

第三步:将创建的worker和KV空间绑定

从”Workers和Pages”-“概述”中,点击进入刚才创建的worker:

image.png

在左上方菜单的”设置”-“变量”-“KV命名空间绑定”选项中,点击”添加绑定”:
image.png

变量名称填写:

EDGE_CACHE

KV命名空间在下拉菜单中选择之前创建的KV空间,然后点击右下角的”部署”:
image.png

第四步、添加worker路由

在创建了KV、worker并完成绑定之后,需要告诉CF这个worker、KV的组合用来处理哪些流量,这就需要使用worker的路由功能(将访问目标是特定URL的访问流量转发往特定worker)。

注:为啥这功能叫worker路由?想想传统路由的定义:将目标IP属于特定目标网段的包转发往特定的网关,是不是差不多?

在左上方菜单的”设置”-“触发器”-“路由”选项中,点击”添加路由”:

image.png

image.png

完成:
image.png


另外,worker还有一个选项,自定义域:

image.png

该功能允许你将自定义的域名(就是你在CF上的托管域名)绑定到worker上,从而让你可以直接使用自己的域名来访问worker脚本,通常适用于使用worker实现”xx代理”之类的功能而需要被直接访问场景。

而worker路由和自定义域不同,worker路由涉及的worker不需要被直接访问,只是需要CF根据特定的URL路径、查询参数等条件将对应的访问流量转入worker进行相应的处理。

为什么我忽然提到自定义域?因为虽然在本文中用不上,但是在其他worker的使用场景中却经常遇到,并且worker路由和自定义域这两个选项虽然用处不同,却有相似点:都提供了对请求如何被处理的控制,worker路由是基于路径和条件的控制,而worker自定义域是基于域名的控制。


第五步:将需要worker处理的请求绕过缓存

在前言中我提到过,要让worker能处理对应的流量,前提条件是该流量不能被流量序列中位置位于worker前面的节点功能模块的规则命中并处理,本文中主要指缓存相关的规则(当然也包括WAF,不能有阻止访问请求的规则),那么,要保证访问目标URL(本文中是blog.tangwudi.xyz/*)的请求都绕过缓存,最简单的方式就是通过页面规则(页面规则在系列教程第六部中讲Cache Rules时简单提到过,这里就不多说了),按如下方式配置:

image.png

image.png

至此,CF上的配置就完成了。

在wordpress上安装”Cloudflare Page Cache”插件

先下载插件,可以在github的官方地址直接下载:github官方下载地址,如果不能访问github,也可以在我的博客专用分享站上下载:无敌文件分享(访问密码:blog.tangwudi.com):

image.png

然后在wordpress上安装并启用该插件即可,不用做任何设置:
image.png

image.png

验证缓存效果

验证边缘缓存是否生效

再次提醒:要在未登录wordpress的状态测试,登录状态会被绕过缓存。

使用edge或者chrome浏览器,打开开发者工具,选择”网络”,勾选”禁用缓存”,然后打开目标网址,本文中是”blog.tangwudi.xyz”:

image.png

上图中的缓存版本号在CF控制台对应的KV命名空间中查看(wordpress站点内容发生变化的时候,插件会通知CF,此时边缘缓存版本号就会变化):
image.png

image.png

验证站点页面打开时间是否缩短

分别在3种CF设置下查看开发工具中的计时。
1、基于worker的乞丐版APO for WordPress方式
等待服务器响应时间(Waiting TTFB) 332.52毫秒:

image.png

2、不使用任何缓存,直接回源
如果不使用worker优化是多长时间呢?验证一下,在CF上打开”开发模式”:
image.png

重新访问https://blog.tangwudi.xyz
image.png

等待服务器响应时间(Waiting TTFB) 2.74秒:
image.png

3、使用Cache Rules
那么,worker缓存和使用Cache Rules缓存的效果比起来如何呢?
等待服务器响应时间(Waiting TTFB) 1.14秒:
image.png

不使用缓存直接回源:2.74秒,使用Cache Rules:1.14秒,使用基于worker的乞丐版APO for WordPress:332.52毫秒,乞丐版APO for WordPress完胜,不过Cache Rules的1.14秒,也比2.74秒好多了,勉强可以一战。

不过,每个人的源站所在位置、回源方式和使用的宽带都不一样,3种方式的测试结果肯定各不相同,总的来说,电信、联通还是要比移动好多了。

适用于静态站点的JS代码

文章前面的代码只适合wordpress,对于一般的静态站点就不太合适了,所以这里也附上适合静态站点的代码模板。

适合大部分常规静态站点的JS代码模板

主要实现HTML、JavaScript、CSS 和图像的缓存(不包括需要登录,特别是涉及用户会话或个性化内容的静态站点):

const CACHE_HEADERS = ['Cache-Control', 'Expires', 'Pragma'];

const BYPASS_URL_PATTERNS = [
    /\/admin\/.*/,  // 绕过后台管理部分页面的缓存,可以根据需要修改或移除
];

addEventListener("fetch", event => {
    event.passThroughOnException();
    event.respondWith(handleRequest(event));
});

async function handleRequest(event) {
    const { request } = event;
    if (shouldBypassRequest(request)) {
        return fetch(request);
    }

    let { response, cacheVer, status } = await getCachedResponse(request);

    if (!response) {
        response = await fetchAndCache(request, cacheVer, event);
    } else {
        status = 'HIT';
        event.waitUntil(updateCacheIfNeeded(request, cacheVer, event));
    }

    return enhanceResponse(response, status, cacheVer);
}

function shouldBypassRequest(request) {
    const accept = request.headers.get('Accept');
    const isImage = accept && accept.includes('image/*');
    return isImage || isUpstreamCachePresent(request);
}

function isUpstreamCachePresent(request) {
    return request.headers.get('x-HTML-Edge-Cache') !== null;
}

async function getCachedResponse(request) {
    if (!shouldCacheRequest(request)) {
        return { response: null, cacheVer: null, status: 'Miss' };
    }

    const cacheVer = await getCurrentCacheVersion(null);
    const cacheKeyRequest = generateCacheRequest(request, cacheVer);
    const cache = caches.default;

    let response = await cache.match(cacheKeyRequest);
    if (response) {
        response = new Response(response.body, response);
        response = cleanupCacheHeaders(response);
        return { response, cacheVer, status: 'Hit' };
    }

    return { response: null, cacheVer, status: 'Miss' };
}

function shouldCacheRequest(request) {
    const accept = request.headers.get('Accept');
    return request.method === 'GET' && (
        accept.includes('text/html') ||
        accept.includes('application/javascript') ||
        accept.includes('text/css') ||
        accept.includes('image/*')
    );
}

function cleanupCacheHeaders(response) {
    CACHE_HEADERS.forEach(header => {
        const value = response.headers.get(x-HTML-Edge-Cache-Header-${header});
        if (value) {
            response.headers.set(header, value);
            response.headers.delete(x-HTML-Edge-Cache-Header-${header});
        }
    });
    return response;
}

async function fetchAndCache(request, cacheVer, event) {
    const response = await fetch(modifyRequest(request));
    if (shouldCacheResponse(request, response)) {
        const status = await cacheResponse(cacheVer, request, response, event);
        response.headers.set('x-HTML-Edge-Cache-Status', status);
    }
    return response;
}

function modifyRequest(request) {
    const newRequest = new Request(request);
    newRequest.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall');
    return newRequest;
}

function shouldCacheResponse(request, response) {
    const accept = request.headers.get('Accept');
    return request.method === 'GET' && response.status === 200 && (
        accept.includes('text/html') ||
        accept.includes('application/javascript') ||
        accept.includes('text/css') ||
        accept.includes('image/*')
    );
}

async function cacheResponse(cacheVer, request, response, event) {
    const cache = caches.default;
    const cacheKeyRequest = generateCacheRequest(request, cacheVer);
    const clonedResponse = new Response(response.body, response);

    CACHE_HEADERS.forEach(header => {
        const value = clonedResponse.headers.get(header);
        if (value) {
            clonedResponse.headers.set(x-HTML-Edge-Cache-Header-${header}, value);
            clonedResponse.headers.delete(header);
        }
    });

    clonedResponse.headers.set('Cache-Control', 'public; max-age=315360000');
    event.waitUntil(cache.put(cacheKeyRequest, clonedResponse));
    return "Cached";
}

async function updateCacheIfNeeded(request, cacheVer, event) {
    if (shouldCacheRequest(request)) {
        const response = await fetch(modifyRequest(request));
        if (shouldCacheResponse(request, response)) {
            await cacheResponse(cacheVer, request, response, event);
        }
    }
}

function enhanceResponse(response, status, cacheVer) {
    const newResponse = new Response(response.body, response);
    if (status) {
        newResponse.headers.set('x-HTML-Edge-Cache-Status', status);
    }
    if (cacheVer !== null) {
        newResponse.headers.set('x-HTML-Edge-Cache-Version', cacheVer.toString());
    }
    return newResponse;
}

async function getCurrentCacheVersion(cacheVer) {
    if (cacheVer === null) {
        cacheVer = -1;
    }
    return cacheVer;
}

function generateCacheRequest(request, cacheVer) {
    const url = new URL(request.url);
    url.searchParams.append('cf_edge_cache_ver', cacheVer);
    return new Request(url.toString());
}

适合需要登录、涉及用户会话或个性化内容的静态站点的JS代码模板

const CACHE_HEADERS = ['Cache-Control', 'Expires', 'Pragma'];

const BYPASS_URL_PATTERNS = [
    /\/admin\/.*/,
    /\/login\/.*/,
    /\/account\/.*/
];

const BYPASS_COOKIES = [
    'session_id',  // 假设使用这个Cookie来标识登录状态
    'auth_token'
];

addEventListener("fetch", event => {
    event.passThroughOnException();
    event.respondWith(handleRequest(event));
});

async function handleRequest(event) {
    const { request } = event;
    if (shouldBypassRequest(request)) {
        return fetch(request);
    }

    let { response, cacheVer, status } = await getCachedResponse(request);

    if (!response) {
        response = await fetchAndCache(request, cacheVer, event);
    } else {
        status = 'HIT';
        event.waitUntil(updateCacheIfNeeded(request, cacheVer, event));
    }

    return enhanceResponse(response, status, cacheVer);
}

function shouldBypassRequest(request) {
    const accept = request.headers.get('Accept');
    const isImage = accept && accept.includes('image/*');
    if (isImage || shouldBypassCookies(request)) {
        return true;
    }

    return BYPASS_URL_PATTERNS.some(pattern => pattern.test(request.url));
}

function shouldBypassCookies(request) {
    const cookieHeader = request.headers.get('Cookie');
    if (cookieHeader) {
        return BYPASS_COOKIES.some(cookie => cookieHeader.includes(cookie));
    }
    return false;
}

async function getCachedResponse(request) {
    if (!shouldCacheRequest(request)) {
        return { response: null, cacheVer: null, status: 'Miss' };
    }

    const cacheVer = await getCurrentCacheVersion(null);
    const cacheKeyRequest = generateCacheRequest(request, cacheVer);
    const cache = caches.default;

    let response = await cache.match(cacheKeyRequest);
    if (response) {
        response = new Response(response.body, response);
        response = cleanupCacheHeaders(response);
        return { response, cacheVer, status: 'Hit' };
    }

    return { response: null, cacheVer, status: 'Miss' };
}

function shouldCacheRequest(request) {
    const accept = request.headers.get('Accept');
    return request.method === 'GET' && (
        accept.includes('text/html') ||
        accept.includes('application/javascript') ||
        accept.includes('text/css') ||
        accept.includes('image/*')
    ) && !shouldBypassCookies(request);
}

function cleanupCacheHeaders(response) {
    CACHE_HEADERS.forEach(header => {
        const value = response.headers.get(x-HTML-Edge-Cache-Header-${header});
        if (value) {
            response.headers.set(header, value);
            response.headers.delete(x-HTML-Edge-Cache-Header-${header});
        }
    });
    return response;
}

async function fetchAndCache(request, cacheVer, event) {
    const response = await fetch(modifyRequest(request));
    if (shouldCacheResponse(request, response)) {
        const status = await cacheResponse(cacheVer, request, response, event);
        response.headers.set('x-HTML-Edge-Cache-Status', status);
    }
    return response;
}

function modifyRequest(request) {
    const newRequest = new Request(request);
    newRequest.headers.set('x-HTML-Edge-Cache', 'supports=cache|purgeall');
    return newRequest;
}

function shouldCacheResponse(request, response) {
    const accept = request.headers.get('Accept');
    return request.method === 'GET' && response.status === 200 && (
        accept.includes('text/html') ||
        accept.includes('application/javascript') ||
        accept.includes('text/css') ||
        accept.includes('image/*')
    ) && !shouldBypassCookies(request);
}

async function cacheResponse(cacheVer, request, response, event) {
    const cache = caches.default;
    const cacheKeyRequest = generateCacheRequest(request, cacheVer);
    const clonedResponse = new Response(response.body, response);

    CACHE_HEADERS.forEach(header => {
        const value = clonedResponse.headers.get(header);
        if (value) {
            clonedResponse.headers.set(x-HTML-Edge-Cache-Header-${header}, value);
            clonedResponse.headers.delete(header);
        }
    });

    clonedResponse.headers.set('Cache-Control', 'public; max-age=315360000');
    event.waitUntil(cache.put(cacheKeyRequest, clonedResponse));
    return "Cached";
}

async function updateCacheIfNeeded(request, cacheVer, event) {
    if (shouldCacheRequest(request)) {
        const response = await fetch(modifyRequest(request));
        if (shouldCacheResponse(request, response)) {
            await cacheResponse(cacheVer, request, response, event);
        }
    }
}

function enhanceResponse(response, status, cacheVer) {
    const newResponse = new Response(response.body, response);
    if (status) {
        newResponse.headers.set('x-HTML-Edge-Cache-Status', status);
    }
    if (cacheVer !== null) {
        newResponse.headers.set('x-HTML-Edge-Cache-Version', cacheVer.toString());
    }
    return newResponse;
}

async function getCurrentCacheVersion(cacheVer) {
    if (cacheVer === null) {
        cacheVer = -1;
    }
    return cacheVer;
}

function generateCacheRequest(request, cacheVer) {
    const url = new URL(request.url);
    url.searchParams.append('cf_edge_cache_ver', cacheVer);
    return new Request(url.toString());
}

关键点解释:

    1.  BYPASS_COOKIES:定义了一组会话或身份验证相关的Cookie,如session_id或auth_token,如果请求中包含这些Cookie,则不进行缓存。
    2.  shouldBypassCookies:检查请求是否包含这些会话Cookie。如果包含,则跳过缓存,并直接从源服务器获取内容。
    3.  公共资源的缓存:对于不涉及用户个性化或会话的资源(如CSS、JS、图片),仍然可以进行缓存,以提升站点性能。

worker方式缓存静态站点的注意事项

对于一般的静态站点来说,不需要像WordPress那样安装插件来支持x-HTML-Edge-Cache。只要站点的文件结构和路径设计合理,缓存策略可以直接在worker脚本中处理。不过,以下几点需要注意:

  1. URL路径设计:确保静态资源(HTML、CSS、JavaScript、图像等)的URL路径是固定且不依赖于动态参数的。这样可以确保缓存命中率更高。

  2. 响应头设置:虽然不需要像WordPress那样安装插件,但在服务器端,可以考虑设置适当的缓存控制头(如Cache-Control),以配合worker的缓存策略。如果静态站点使用了服务端(如NGINX、Apache),可以手动配置这些头。

  3. 清理缓存:当静态资源更新时,可以通过Worker提供的API或CF的面板来清理缓存,以确保用户获取最新内容(还是没有wordpress使用插件自动更新来得方便)。

  4. 无需特殊支持的特性:对于不依赖用户会话或个性化内容的静态站点,worker脚本的基本缓存逻辑已经足够,不需要像在wordpress中那样安装额外的插件来处理特定的缓存头。

总结

使用worker、KV搭配Edge Cache HTML脚本来实现的乞丐版APO for WordPress功能虽然说可勘一战,但是要和正版APO功能相比,不管是访问速度、兼容性、性能等方面都有一定的差距,不过嘛,对于使用Free计划的朋友而言,也算是一个福音了。

但是,这种方式最大的限制其实来源于Free计划中worker的1天10万次请求免费额度,这个额度其实对于一般的个人站长而言完全足够了,根本用不完,但是最怕一件事:DDos攻击,一遇到一会功夫这10万次免费额度就消耗完了,然后就只能直接回源(2.74秒),APO则无限制。

当然,也有应急的解决方法:保留常规Cache Rules的相关配置(参见文章:家庭数据中心系列 cloudflare教程(六) CF Cache Rules(缓存规则)功能介绍及详细配置教程),平时使用页面规则绕过缓存(因为页面规则在流量序列中优先级高于Cache Rules,所以只要命中页面规则绕过了缓存,就不会命中后续的Cache Rules规则)。当遇到DDos攻击导致worker的免费额度被消耗完之后,只要手动关闭页面规则的开关,访问流量就会命中后续的Cache Rules(Cache Rules在流量序列中优先级高于worker,1.14秒,总要比直接回源的2.74秒好),等撑到了第二天,重新打开页面规则就又是一条好汉了。

另1:不要以为DDos离自己很远,我昨天又被打了,虽然没啥感觉:
image.png

另2:从这篇文章大家应该能看出网络基础的重要性吧?如果网络基础不扎实,很多时候分析问题都没有办法深入,更别说涉及到一些稀奇古怪问题时候的故障排查了,不过,现在有多少搞网络相关工作的朋友还会看”TCP/IP详解三部曲”呢?

另3:写这篇文章好累,短期内不想再写涉及这么多技术点及细节的学术型文章了,随便写写、水水文章顺便休息一下吧。不过好在这篇关于worker的文章目前看来应该是CF系列教程中最麻烦的一篇了,之后的都没这么折腾了。

博客内容均系原创,转载请注明出处!更多博客文章,可以移步至网站地图了解。博客的RSS地址为:https://blog.tangwudi.com/feed,欢迎订阅;如有需要,可以加入Telegram群一起讨论问题。

评论

  1. Windows Chrome 128.0.0.0
    3 月前
    2024-10-07 18:21:15

    ai提示有语法错误正确写法:
    应该将 x-HTML-Edge-Cache-Header-{header} 放入反引号(“),并使用{} 引用模板变量。

    • 博主
      不留名
      Macintosh Chrome 129.0.0.0
      3 月前
      2024-10-07 20:26:54

      这段代码之前也是chatgpt优化过的。。感觉每过一段时间它的想法就会改变。这段代码运行起来肯定是没问题的,我验证过,不过你也可以让ai再优化一下,最后以能正常运行为准。有了AI之后,代码随便优化,能用就行,也可以直接用github上官方的脚本为蓝本让AI进行优化。

  2. knbn
    Linux Edge 129.0.0.0
    4 月前
    2024-10-04 9:07:32

    佬,你的CF worker脚本有问题啊,复制粘贴进去会有一大堆报错,然后CF就不给保存了

    • 博主
      knbn
      Macintosh Chrome 129.0.0.0
      4 月前
      2024-10-04 9:12:01

      不会呀,我验证过的啊,文章中的测试效果都是基于脚本来的。

    • 博主
      knbn
      Macintosh Chrome 129.0.0.0
      4 月前
      2024-10-04 9:24:17

      可能是格式问题,我本地的代码直接拷到worker上没问题,但是网站上代码的确是拷进去会报错,我来调整一下。

    • 博主
      knbn
      Macintosh Chrome 129.0.0.0
      4 月前
      2024-10-04 9:54:05

      我临时在文章中提供了代码的下载地址,wordpress版和html版代码的txt文件都放上去了(也放评论里:链接如下:无敌的文件分享,访问密码:”blog.tangwudi.com”),你可以直接下载,格式问题等我空了来研究研究。

  3. Windows Chrome 118.0.0.0
    5 月前
    2024-8-28 10:38:07

    worker是在边缘节点上的,cache也是在边缘节点上的,为什么worker作为缓存来用就是比cache快?

    • 博主
      ere
      Macintosh Chrome 128.0.0.0
      4 月前
      2024-9-09 6:12:19

      现在好麻烦,有评论我收不到邮件提醒了~~今天才看到。我在文章里”为什么使用worker控制边缘缓存能够达到比CF常规CDN更好的访问效果”部分说过了,其实就是worker这个功能有一些特权,突破了常规缓存的一些限制,能够免费蹭到付费功能APO的一些特权。

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇
       

本站已禁用鼠标右键和各种快捷键,代码块内容可以直接在右上角点击复制按钮进行复制

zh_CN