搭建一个Gravatar镜像站可以有效减少访问延迟,特别是对于那些位于Gravatar服务器较远地区的用户。使用Cloudflare Workers来实现这个目标是非常合适的,因为Workers可以在全球分布的边缘节点上执行代码,从而显著降低图片加载时间。下面是如何使用Cloudflare Workers创建一个简单的Gravatar镜像站的步骤。

步骤 1: 创建 Cloudflare Worker

首先,你需要在Cloudflare控制面板中创建一个新的Worker脚本。

步骤 2: 编写 Worker 脚本

接下来,在Worker脚本中编写代码以处理请求并代理到Gravatar服务。以下是一个基本示例:

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
// 解析请求中的查询参数
const url = new URL(request.url);
const emailHash = url.searchParams.get('email') || ''; // 获取email参数
const size = url.searchParams.get('size') || '80'; // 默认大小为80x80像素

if (!emailHash) {
return new Response('No email hash provided.', { status: 400 });
}

// 构建Gravatar URL
const gravatarUrl = `https://secure.gravatar.com/avatar/${emailHash}?s=${size}&d=identicon`;

// 从Gravatar获取图片
const response = await fetch(gravatarUrl);

// 返回响应给客户端
return new Response(response.body, {
status: response.status,
headers: {
...response.headers,
'Cache-Control': 'public, max-age=31536000, immutable' // 设置缓存策略
}
});
}

这段代码会根据请求中的emailsize参数构建Gravatar URL,并将请求代理到Gravatar服务。它还设置了适当的缓存头,以便图像可以在用户的浏览器和其他CDN节点上长期缓存。

步骤 3: 部署 Worker

保存并部署你的Worker脚本。你可以通过Cloudflare控制面板或者使用Wrangler CLI工具来完成这一步。

步骤 4: 配置 DNS 和 CNAME

确保你有一个域名指向Cloudflare,并且为该域名配置了CNAME记录,指向你刚才创建的Worker。例如,如果你希望镜像站在gravatar.yourdomain.com下运行,则需要设置一个CNAME记录,将其指向your-worker-name.workers.dev

步骤 5: 测试你的镜像站

现在,你可以通过访问类似https://gravatar.yourdomain.com/?email=hash&size=80这样的URL来测试你的Gravatar镜像站是否正常工作。

可选增强功能

  • HTTPS 强制:确保所有的请求都是通过HTTPS进行的,以保护数据传输的安全。
  • 默认头像支持:除了identicon外,Gravatar还支持其他类型的默认头像(如monsteridwavatar等)。你可以添加额外的查询参数来支持这些选项。
  • 图片格式转换:如果需要,可以通过修改Worker脚本来支持不同的图片格式(如WebP),以提高页面加载性能。
  • 错误处理:改进错误处理逻辑,比如当Gravatar返回非200状态码时提供备用图片或自定义错误消息。

通过上述步骤,你可以快速地利用Cloudflare Workers搭建一个高效、低延迟的Gravatar镜像站。这不仅有助于提升网站性能,还可以改善用户体验。


优化思路

gravatar的请求参数并非一成不变,通过解析请求参数构建访问url的形式,很容易失效,因此,我考虑修改为直接透传参数的形式,即:

优化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
// 获取原始URL和查询参数
const url = new URL(request.url);

// 构建Gravatar URL,保持所有查询参数不变
const gravatarUrl = new URL(url.pathname + url.search, 'https://secure.gravatar.com');

// 设置缓存选项
const cache = caches.default;
let response = await cache.match(gravatarUrl);

if (!response) {
// 如果没有命中缓存,则从Gravatar获取图片并缓存
response = await fetch(gravatarUrl, {
cf: {
cacheEverything: true,
cacheTtl: 60 * 60 * 24 * 365, // 缓存一年
bypassCacheOnCookie: false,
ttlOverride: 60 * 60 * 24 * 365 // 强制设置缓存时间
}
});

// 将响应存储到缓存中
cache.put(gravatarUrl, response.clone());
}

// 返回响应给客户端
return response;
}

在这个脚本中并没有对请求中的查询参数进行任何解析或修改,而是直接使用了原始的pathname和search部分来构建新的Gravatar URL。这保证了所有查询参数都能被完整地传递给Gravatar服务。这种方法不仅简化了开发过程,还充分利用了Cloudflare的边缘网络优势。

PS:workers的默认域名workers.dev在国内无法直接访问,需要设置自定义域名。

20241214更新


使用过程中发现,上面的脚本可以直接访问gravatar.com的首页,为了避免不必要的麻烦,对代码进行一下优化,访问首页的请求直接返回500错误。

优化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
// 获取原始URL和查询参数
const url = new URL(request.url);

// 检查是否为首页请求
if (url.pathname === '/') {
return new Response('Home page access not allowed.', { status: 500 });
}

// 构建Gravatar URL,保持所有查询参数不变
const gravatarUrl = new URL(url.pathname + url.search, 'https://secure.gravatar.com');

// 设置缓存选项
const cache = caches.default;
let response = await cache.match(gravatarUrl);

if (!response) {
// 如果没有命中缓存,则从Gravatar获取图片并缓存
response = await fetch(gravatarUrl, {
cf: {
cacheEverything: true,
cacheTtl: 60 * 60 * 24 * 365, // 缓存一年
bypassCacheOnCookie: false,
ttlOverride: 60 * 60 * 24 * 365 // 强制设置缓存时间
}
});

// 将响应存储到缓存中
cache.put(gravatarUrl, response.clone());
}

// 返回响应给客户端
return response;
}

继续优化

用KV存储代替浏览器缓存,用于保存图片,减少源站请求次数,缓存时间1年。就不考虑换头像了,服务自用,不再公开链接。

优化后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});

// 使用绑定的KV命名空间 "gravatar"
const GRAVATAR_KV = globalThis.gravatar;

// 定义一年的秒数
const ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365;

async function handleRequest(request) {
// 获取原始URL和查询参数
const url = new URL(request.url);

// 检查是否为首页请求
if (url.pathname === '/') {
return new Response('Home page access not allowed.', { status: 500 });
}

// 构建Gravatar URL,保持所有查询参数不变
const gravatarUrl = new URL(url.pathname + url.search, 'https://secure.gravatar.com');

// 获取Gravatar哈希作为键名
const gravatarHash = gravatarUrl.pathname.split('/').pop();

// 尝试从KV获取图片及其元数据
const { metadata, value } = await GRAVATAR_KV.getWithMetadata(gravatarHash, 'arrayBuffer'); // 尝试从KV获取图片数据及元数据

let contentType;
let kvResponse = value;

if (kvResponse) {
// 如果从KV获取到图片,则创建一个Response对象并确定内容类型
contentType = metadata?.contentType || 'application/octet-stream'; // 使用存储的内容类型或默认类型
} else {
// 如果KV中没有缓存,则从Gravatar获取图片
const response = await fetch(gravatarUrl);

// 确定内容类型
contentType = response.headers.get('content-type') || 'application/octet-stream';

// 将响应克隆一份用于存储到KV中
const buffer = await response.arrayBuffer();

// 将图片存储到KV中,设置适当的过期时间(1年)
await GRAVATAR_KV.put(gravatarHash, buffer, { metadata: { contentType }, expirationTtl: ONE_YEAR_IN_SECONDS });

kvResponse = buffer; // 更新kvResponse以便直接返回
}

// 返回响应给客户端
const headers = new Headers();
headers.set('Content-Type', contentType);
return new Response(kvResponse, { headers });
}