家庭資料中心系列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每天的免費額度為100,000次請求。

附加知識:優選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
    3 個月前
    2024-10-04 9:07:32

    佬,你的CF worker腳本有問題啊,複製貼上進去會有一大堆報錯,然後CF就不給保存了

    • 部落客
      knbn
      Macintosh Chrome 129.0.0.0
      3 個月前
      2024-10-04 9:12:01

      不會呀,我驗證過的啊,文章中的測試效果都是基於腳本來的。

    • 部落客
      knbn
      Macintosh Chrome 129.0.0.0
      3 個月前
      2024-10-04 9:24:17

      可能是格式問題,我本地的程式碼直接拷到worker上沒問題,但是網站上程式碼的確是拷貝進去會報錯,我來調整一下。

    • 部落客
      knbn
      Macintosh Chrome 129.0.0.0
      3 個月前
      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_HK