[薅羊毛] 在 Cloudflare 免费计划下自定义缓存 key

概要:我做了一个多语言 Emoji 搜索引擎,其中一个特色功能是:检测用户的语言并自动跳转到相应的语言页面。所以我需要自定义缓存 key ,但是 Cloudflare 必须要升级到企业计划才能自定义缓存 key ,那费用不是我能承担的。于是我通过深入的研究文档,成功在转换规则配置里实现了和自定义缓存 key 一样的效果。接下来我将分享我是如何配置的,即使你不需要自定义缓存 key ,也可以从这篇文章的前半部分学习到一些 CDN 及缓存的知识。

开始之前先介绍我的网站,以便你在下文能更好的理解:

  • 🧐 SearchEmoji (searchemoji.app): 支持 30 个语言的 Emoji 搜索引擎,丰富多彩的 Emoji 让你的文章和社交文字更生动
  • Yesicon (yesicon.app) :支持 8 个语言的矢量图标搜索引擎,收录了超过 20 万个高质量图标,开发者和设计师的 ⌘CV 好帮手

让我们开始!首先解释一下为什么需要自定义缓存 key:

多语言的网站一般都会有这样的功能:检测用户的语言并返回对应语言版本的页面,检测逻辑是:如果用户主动选择过语言,则将该需要编码存储于 Cookie 中(假设 key 为 lang),服务器接收到请求时,优先取 Cookie 里存储的 lang,如果没有,则取 Header 中的 Accept-Language 作为用户语言。

如果用户浏览器和服务器直连,不会有什么问题,但为了网站在各个地区的访问速度,我们一般都会使用 CDN 来缓存并分发页面。想象一下:一个中文用户访问首页时,将中文页面缓存到了边缘服务器中,缓存 key 是首页的路径 /,当第二个英文用户来访问首页时,由于边缘服务器已存在 key 为 / 的页面,所以直接就返回了缓存结果,导致英文用户看到了中文的页面,这是不可接受的。

解决这个问题也很简单,所有的 CDN 服务商都会提供自定义缓存 key 的功能,以上面的例子来说,只要将缓存 key 加上 Cookie 中的 langHeader 中的 Accept-Language 即可。比如:原本的缓存 key 是 /,加上之后变成 /?lang=en&acceptLanguage=en-US,en。这样一来,每种语言组合都是一份单独的缓存,就不会再出现串语言的情况了。

坏就坏在,Cloudflare 需要企业计划才能自定义缓存 key 。这也是我第一次使用 Cloudflare 的服务,Yesicon 的 CDN 使用的是 AWS 的 Cloudfront ,它没有任何限制,很轻松就配置了自定义缓存 key 。我也曾一度想将 SearchEmoji 也迁移到 Cloudfront ,但想到 Cloudfront 的免费套餐连 Yesicon 都不够用,我决定深入研究一下如何在 Cloudflare 中实现我的诉求!

功夫不负有心人,我花了 3 个晚上攻读 Cloudflare 的文档,边看边试,终于让我给实现了!原理很简单,我们先看这个图:

当请求进入到边缘服务器时,会先根据我们配置的 URL Rewrites 规则来重写 url ,然后才去走取缓存的逻辑,那我们只需要想办法将 Cookie 中的 langHeader 中的 Accept-Language 想办法重写到 url 中即可。我们来模拟一下这一流程:

用户在浏览器中访问 searchemoji.app/,请求到达边缘服务器时,将 url 重写为 https://searchemoji.app/?acceptLanguage=en-US,en ,当第一个英文用户从原服务器下载到英文版页面后,Cloudflare 会以 url 作为缓存 key 缓存在边缘服务器中,这时其他英语用户再访问时,由于他们的 url 重写之后都一样,所以他们就可以取到刚才缓存的页面。但如果是中文用户,他们的 url 会被重写为https://searchemoji.app/?acceptLanguage=zh-CN,zh,en ,所以他永远不会访问到英文版页面。当用户主动切换语言,我们也只要把 Cookie 中的 lang 加到 query 中即可。

但要将 url 重写成动态的值也不是一件容易的事,你得首先掌握 Cloudflare Rules language。在添加规则页面中,上面是我们要重写的条件,我们的条件非常复杂,直接选择 Edit expression

由于我们只需在首页执行自动检测跳转,所以我们的条件首先是首页。然后我们希望,如果用户主动选择的语言或浏览器语言在我们支持的语言列表中,则重写 url ,因为我们明确知道这些语言会匹配到对应的语言页面。但如果是英语或者其他我们未支持的语言,则不重写 url ,因为英语是默认语言,其他未支持的语言也会回退到英语。所以完整的判断是否要重写 url 的逻辑是这样:

bash 复制代码
# is home page
http.request.uri.path eq "/" and 
# user did not actively select English
not ( http.cookie contains "lang=en") and 
(
	# user actively select a supported language but not English
	http.cookie contains "lang=" or 
	# browser's language is supported language
	substring( http.request.accepted_languages[0], 0, 2) in 
		{ "zh" "es" "de" "ja" "fr" "ko" "pt" "ru" "tr" "ar" "it" "hi" "pl" "bn" "nl" "uk" "id" "ms" "vi" "th" "sv" "el" "he" "fi" "no" "da" "ro" "hu" }
)

由于我们支持的语言有点多,所以这里的条件会有点长,为了方便理解,我将条件做了换行并写了注释,如果你要复制代码,记得删除所有的换行和注释。

现在我们已经确定了什么情况下重写 url ,接下来就是怎么重写 url 。首先我们重写的是 query ,并且要用到动态变量,所以得选择下图的选项:

要取到 Cookielang 的值,我们可以写个正则表达式:

bash 复制代码
regex_replace( http.cookie, "^.*lang=([^;]+).*$|^.*$", "${1}")

为了简化后续的处理,我们这里直接将 langAccept-Language 拼接在一起形成一个参数,完整的重写规则如下:

bash 复制代码
concat(
	# query key
	"cfcache=",
	# lang
	regex_replace( http.cookie, "^.*lang=([^;]+).*$|^.*$", "${1}"),
	# Accept-Language
	http.request.accepted_languages[0]
)

这样如果是一个未主动选择语言的西班牙语用户访问首页,那么 url 就会被重写为: https://searchemoji.app/?cfcache=es-ES ,如果他主动将语言切换为 English ,那么 url 应该是这样:https://searchemoji.app/?cfcache=enes-ES ,当然如果你觉得这样不美观,可以在 en 和 es 之间拼个分隔符。

现在试一下,他能正确的加上参数,但会有两个问题:

  • 如果访问时带着参数,会被重写覆盖导致参数丢失,比如访问 https://searchemoji.app/?v=1 ,重写后就变成了https://searchemoji.app/?cfcache=es-ES ,v=1 这个参数完全丢失。
  • 跳转后的 url 也会带着这个 query ,比如访问 https://searchemoji.app/ 会跳转: https://searchemoji.app/?cfcache=es-ES ,让用户看到了这个丑陋的参数。

第一个问题你应该也想到了方案,没错,重写时把原来带的参数也加上就好啦:

bash 复制代码
concat(
	# query key
	"cfcache=",
	# lang
	regex_replace( http.cookie, "^.*lang=([^;]+).*$|^.*$", "${1}"),
	# Accept-Language
	http.request.accepted_languages[0],
	# origin query
	"&",
	http.request.uri.query
)

但这样也不够完美,如果原 url 就没有 query ,参数最后会多一个 & 符号(https://searchemoji.app/?cfcache=es-ES&),我们来和第二个问题一起解决:

在边缘服务器加的 query 参数 ?cfcache=xxx 只是为了缓存,但边缘服务器会带着这个参数去请求源服务器,原服务器在执行多语言跳转逻辑时也会把这个参数带上,原服务器是通过返回 302 状态码来实现跳转,所以我们需要在程序中处理在返回的 location 里将这个参数删掉,我用的是 Nuxt 3 ,以下是完整的处理逻辑:

csharp 复制代码
nitroApp.hooks.hook('render:response', (response, { event }) => {
  if (response.headers?.location?.includes('cfcache=')) {
    const host = 'https://searchemoji.app'
    const url = new URL(response.headers.location, host)
	// If there is & at the end of the parameter, it will be processed into { cfcache: 'es-ES&' }
    const params = new URLSearchParams(url.search)
	// So we delete this parameter, & will also be deleted
    params.delete('cfcache')
    url.search = params.toString()
    response.headers.location = url.toString().replace(host, '')
	// Let edge servers cache 302 status
    response.headers['Cache-Control'] = 'max-age=86400, must-revalidate'
  }
})

这样一来,我们就完美的解决了所有的问题。成功的模拟了自定义缓存 key (虽然麻烦了点),以下是完整的配置截图:

尽管如此,我还是觉得 Cloudflare 应该给所有计划都提供自定义缓存 key 的功能,即使需要附加费用。

最后,我想邀请你去体验我的两个网站(链接在文章开头),不管你是设计师、开发者,还是创作者,都有可能会用到图标和 Emoji 。年终了,在你的述职 PPT 上放些图标也能提升美观度哦!如果有任何建议或反馈,网站的右上角有反馈入口,或者在此文章下评论我也能看到。

相关推荐
乐多_L41 分钟前
使用vue3框架vue-next-admin导出表格excel(带图片)
前端·javascript·vue.js
纯粹要努力1 小时前
前端跨域问题及解决方案
前端·javascript·面试
csdn_aspnet2 小时前
JavaScript AJAX 库
javascript·ajax
胡桃不是夹子2 小时前
vue登陆下拉菜单
前端·javascript·vue.js
大G哥2 小时前
用DeepSeek来帮助学习three.js加载3D太极模形
开发语言·前端·javascript·学习·ecmascript
锐小制2 小时前
十分钟学习编写Ridge页面脚本
前端·javascript
丁总学Java5 小时前
Mac arm架构使用 Yarn 全局安装 Vue CLI
前端·javascript·vue.js
摸鱼也很难5 小时前
js安全开发值&dom&bom&xss重定向&安全实例
javascript·xss·idm·dom型xss
࿐ཉི༗࿆许七安༗࿆6 小时前
javaScript交互补充
前端·javascript·交互
鱼樱前端6 小时前
元编程模式&Proxy陷阱与反射API的终极指南
前端·javascript