
概要:我做了一个多语言 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
中的 lang
和 Header
中的 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
中的 lang
和 Header
中的 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 ,并且要用到动态变量,所以得选择下图的选项:

要取到 Cookie
中 lang
的值,我们可以写个正则表达式:
bash
regex_replace( http.cookie, "^.*lang=([^;]+).*$|^.*$", "${1}")
为了简化后续的处理,我们这里直接将 lang
和 Accept-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 上放些图标也能提升美观度哦!如果有任何建议或反馈,网站的右上角有反馈入口,或者在此文章下评论我也能看到。