使用 Cloudflare Workers 搭建一个获取必应每日美图的API
以前在自己的服务器上用PHP代码做了一个接口,服务器过期后就失效了。最近接触到 Cloudflare Workers,因为不用部署在服务器上,而且支持 HTTPS,每天有免费的10万次请求数,于是又在 Cloudflare Workers 上做了一个。
以下步骤及代码均来源于阿里云·通义大模型:
要使用 Cloudflare Workers 搭建一个获取必应每日美图的 API,你可以按照以下步骤进行:
注册并登录 Cloudflare 账户:
- 如果还没有 Cloudflare 账户,首先需要注册一个。
创建一个新的 Worker:
- 登录 Cloudflare 控制台后,进入 Workers 部分,点击“Create a service”按钮。
- 输入你的 Worker 名称,然后点击“Create service”。
编写 Worker 代码:
- 在 Worker 编辑器中,你可以编写 JavaScript 代码来实现获取必应每日美图的功能。
- 必应每日美图的 URL 是
https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=en-US,你可以通过这个 URL 获取图片信息。
示例代码:
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)) })
async function handleRequest(request) { const bingDailyImageURL = 'https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN';
const response = await fetch(bingDailyImageURL); const data = await response.json(); const baseUrl = 'https://s.cn.bing.net'; const imageUrl = `${baseUrl}${data.images[0].url}`; return Response.redirect(imageUrl, 302); }
|
部署 Worker:
- 编写完代码后,点击“Save and Deploy”按钮将你的 Worker 部署到 Cloudflare 的边缘网络。
测试 API:
- 部署完成后,你可以通过浏览器或者工具(如 Postman)访问你的 Worker URL 来测试 API 是否正常工作。
设置自定义域名(可选):
- 如果你有自己的域名,可以在 Cloudflare 中添加域名,并将 Worker 关联到这个域名上,以便通过自定义域名访问你的 API。
通过以上步骤,你就可以成功搭建一个使用 Cloudflare Workers 获取必应每日美图的 API。
上面的代码我替换了国内访问速度较快的必应地址。
然后又想增加通过参数来获取不同分辨率的图片,于是修改了一下代码:
初步优化的代码
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
| addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)) }) async function handleRequest(request) { const supportedResolutions = { '1080': '1920', '720': '1280', '768': '1024', '600': '800', '480': '640' }; const url = new URL(request.url); const h = url.searchParams.get('h'); const defaultResolution = '1080'; const height = h && supportedResolutions[h] ? h : defaultResolution; const width = supportedResolutions[height]; const bingDailyImageDataURL = 'https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN'; const response = await fetch(bingDailyImageDataURL); const data = await response.json(); const baseUrl = 'https://s.cn.bing.net/'; let imageUrl = `${baseUrl}${data.images[0].url}`; const defaultResolutionStr = '1920x1080'; const requestedResolutionStr = `${width}x${height}`; if (imageUrl.includes(defaultResolutionStr)) { imageUrl = imageUrl.replace(defaultResolutionStr, requestedResolutionStr); } else { const imageBase = imageUrl.split('_').slice(0, -1).join('_'); imageUrl = `${imageBase}_${requestedResolutionStr}.jpg`; } return Response.redirect(imageUrl, 302); }
|
这个代码首先定义了一个对象,用于存储支持的分辨率映射。然后,它从请求的查询参数中获取’h’参数,并检查它是否存在于支持的分辨率映射中。如果不存在,则使用默认的分辨率。
接着优化:每天第一次访问时,会请求必应的API,获取今日美图的信息,然后根据请求的分辨率参数,构建完整的图片URL,然后把构建好的URL作为重定向目标返回给客户端,同时缓存到KV中,下次再访问时,直接从KV中读取图片URL,减少对必应的请求次数。但是这样一来,跟必应的API更新时间有一点差距,但无伤大雅。
优化完成的代码
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); });
const kvNamespace = globalThis['bing.worker'];
const supportedResolutions = { '1080': '1920', '720': '1280', '768': '1024', '600': '800', '480': '640', '2160': '3840', '1920': '1080', '1280': '720' };
const specialResolutionIdentifiers = { '3840x2160': 'UHD' };
async function handleRequest(request) { try { const url = new URL(request.url); const h = url.searchParams.get('h') || '1080'; const requestedResolutionStr = `${supportedResolutions[h] || '1920'}x${h}`;
const cachedImageUrl = await kvNamespace.get(requestedResolutionStr, 'text');
if (cachedImageUrl) { return Response.redirect(cachedImageUrl, 302); }
const imageLinks = await fetchAndCacheBingDailyImage();
const imageUrl = imageLinks[requestedResolutionStr] || imageLinks['1920x1080'];
return Response.redirect(imageUrl, 302); } catch (error) { console.error('Error handling request:', error); return new Response('Internal Server Error', { status: 500 }); } }
async function fetchAndCacheBingDailyImage() { try { const bingDailyImageDataURL = 'https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN'; const response = await fetch(bingDailyImageDataURL);
if (!response.ok) { throw new Error(`Failed to fetch Bing daily image: ${response.statusText}`); }
const data = await response.json(); const baseUrl = 'https://cn.bing.com'; let originalImageUrl = `${baseUrl}${data.images[0].url}`;
const cleanedImageUrl = cleanImageUrl(originalImageUrl);
const imageLinks = Object.entries(supportedResolutions).reduce((acc, [height, width]) => { const resolutionStr = `${width}x${height}`; const replacement = specialResolutionIdentifiers[resolutionStr] || resolutionStr; acc[resolutionStr] = cleanedImageUrl.replace('1920x1080', replacement); return acc; }, {});
await Promise.all( Object.keys(imageLinks).map(resolution => kvNamespace.put(resolution, imageLinks[resolution], { expirationTtl: 8 * 60 * 60 }) ) );
return imageLinks; } catch (error) { console.error('Error fetching and caching Bing daily image:', error); throw error; } }
function cleanImageUrl(url) { try { const urlObj = new URL(url); urlObj.searchParams.delete('rf'); urlObj.searchParams.delete('pid');
return urlObj.toString(); } catch (error) { console.error('Error cleaning URL:', error); throw error; } }
|
使用/?h=720请求这个API,就可以得到一个302重定向到1280x720分辨率的图片。
效果如下:
API 已部署在 Cloudflare 上,可以通过访问这个链接来获取不同分辨率的图片。如果不附带参数或请求的参数不在支持的列表中,则返回默认的分辨率(1920x1080)。
| 参数 | 对应分辨率 |
|---|
| 2160 | 3840x2160 |
| 1920 | 1080x1920 |
| 1280 | 720x1280 |
| 1080 | 1920x1080 |
| 720 | 1280x720 |
| 768 | 1024x768 |
| 600 | 800x600 |
| 480 | 640x480 |
PS:workers的默认域名workers.dev在国内无法直接访问,需要设置自定义域名后才能访问。
2026年回顾:时隔两年的二次优化
距离写这篇文章已经过去一年半了。最近在整理 Cloudflare 账户时重新审视了这个 Worker,发现虽然它一直在稳定跑着,但代码里藏着几个小毛病,干脆趁这个机会给它做了一次大保健。
发现了什么?横屏 vs 竖屏
先说一下必应壁纸的一个特点:同一张图有横屏和竖屏两个版本:
1 2 3 4
| https://cn.bing.com/...SichuanTea_1920x1080.jpg → 200 ✓
https://cn.bing.com/...SichuanTea_1080x1920.jpg → 200 ✓
|
必应的 CDN 会为每张图生成不同裁切方向的版本,横屏给桌面用,竖屏给手机用。
所以代码里原有的 '1920': '1080' 和 '1280': '720' 不是 bug,而是竖屏模式的映射。只不过原来的命名把横竖混在一起,看起来有点乱。
做了哪些改进
我把横屏和竖屏分成了两组,加上注释说明:
| 优化项 |
原来的问题 |
改了什么 |
| 🗂️ 分辨率分组 |
横竖混在一起看不明白 |
LANDSCAPE / PORTRAIT 分开定义,再合并 |
| 💬 参数提示 |
传错只返回支持列表 |
多加一句提示:>=1080 是竖屏,<1080 是横屏 |
| 🐛 4K 标识符 |
原来的特殊映射逻辑不够直观 |
统一处理:3840x2160 → UHD |
现在的参数说明
| 参数 |
分辨率 |
方向 |
适用场景 |
?h=2160 |
3840×2160 |
横屏 🖥️ |
4K 桌面壁纸 |
?h=1080 |
1920×1080 |
横屏 🖥️ |
Full HD 桌面壁纸 |
?h=720 |
1280×720 |
横屏 🖥️ |
HD 桌面壁纸 |
?h=768 |
1024×768 |
横屏 🖥️ |
XGA |
?h=600 |
800×600 |
横屏 🖥️ |
SVGA |
?h=480 |
640×480 |
横屏 🖥️ |
VGA |
?h=1920 |
1080×1920 |
竖屏 📱 |
手机壁纸 FHD |
?h=1280 |
720×1280 |
竖屏 📱 |
手机壁纸 HD |
改了哪些地方
| 优化项 |
原来的问题 |
改了什么 |
| 🐛 分辨率映射 |
最后三条是竖屏/重复映射 |
去掉有问题的条目,只保留标准横屏分辨率 |
| 🛡️ 参数校验 |
传 ?h=999 静默回退到 1080 |
传错参数返回 400 + 提示支持的分辨率列表 |
| 🔄 容错降级 |
Bing API 挂了直接 500 |
自动用 KV 里的旧缓存降级,实在没有才 503 |
| ⚡ 缓存优化 |
302 重定向没有 Cache-Control |
加了 max-age=21600,浏览器/CDN 都能缓存 |
| 📦 写入优化 |
逐个分辨率串行写入 KV |
Promise.all 并行写入,快了一点点 |
| 📝 代码结构 |
注释较少,逻辑混在一起 |
分区清晰,每段功能都有注释说明 |
优化后的效果
1 2 3 4 5 6 7 8 9
| $ curl -I https://bing.worker.huanzhi.wang/?h=1080 HTTP/2 302 location: https://cn.bing.com/th?id=OHR.SichuanTea_ZH-CN...1920x1080.jpg cache-control: public, max-age=21600
$ curl https://bing.worker.huanzhi.wang/?h=999 {"error":"Unsupported height. Supported: 480, 600, 720, 768, 1080, 2160"}
|
最终完整代码
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
|
const BING_KV = globalThis['bing.worker'];
const LANDSCAPE = { '2160': '3840', '1080': '1920', '720': '1280', '768': '1024', '600': '800', '480': '640', };
const PORTRAIT = { '1920': '1080', '1280': '720', };
const ALL_RESOLUTIONS = { ...LANDSCAPE, ...PORTRAIT }; const DEFAULT_HEIGHT = '1080'; const KV_CACHE_TTL = 8 * 60 * 60; const EDGE_CACHE_TTL = 6 * 60 * 60;
addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); });
async function handleRequest(request) { try { const url = new URL(request.url); const h = url.searchParams.get('h') || DEFAULT_HEIGHT;
if (!ALL_RESOLUTIONS[h]) { const supported = Object.keys(ALL_RESOLUTIONS).join(', '); return new Response( JSON.stringify({ error: `Unsupported height. Supported: ${supported}`, note: 'Values >= 1080 are portrait; < 1080 are landscape' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); }
const width = ALL_RESOLUTIONS[h]; const resolutionKey = `${width}x${h}`; let imageUrl = await BING_KV.get(resolutionKey, 'text');
if (!imageUrl) { const allLinks = await fetchAndCacheAllResolutions(); if (!allLinks) { const fallback = await getAnyCachedImage(); if (fallback) return redirectWithCache(fallback); return new Response('Service temporarily unavailable', { status: 503 }); } imageUrl = allLinks[resolutionKey] || allLinks['1920x1080']; }
return redirectWithCache(imageUrl); } catch (err) { console.error('Fatal error:', err); return new Response('Internal Server Error', { status: 500 }); } }
function redirectWithCache(imageUrl) { return new Response(null, { status: 302, headers: { Location: imageUrl, 'Cache-Control': `public, max-age=${EDGE_CACHE_TTL}`, }, }); }
async function fetchAndCacheAllResolutions() { try { const apiUrl = 'https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN'; const resp = await fetch(apiUrl); if (!resp.ok) return null;
const data = await resp.json(); if (!data?.images?.[0]?.url) return null;
const baseUrl = 'https://cn.bing.com'; let originalUrl = `${baseUrl}${data.images[0].url}`;
try { const u = new URL(originalUrl); u.searchParams.delete('rf'); u.searchParams.delete('pid'); originalUrl = u.toString(); } catch (_) {}
const imageLinks = {}; const kvPromises = [];
for (const [height, width] of Object.entries(ALL_RESOLUTIONS)) { const res = `${width}x${height}`; const replacement = res === '3840x2160' ? 'UHD' : res; imageLinks[res] = originalUrl.replace('1920x1080', replacement); kvPromises.push( BING_KV.put(res, imageLinks[res], { expirationTtl: KV_CACHE_TTL }) ); }
await Promise.all(kvPromises); return imageLinks; } catch (err) { console.error('fetchAndCacheAllResolutions error:', err); return null; } }
async function getAnyCachedImage() { try { const keys = await BING_KV.list({ limit: 1 }); if (keys.keys.length > 0) { return await BING_KV.get(keys.keys[0].name, 'text'); } } catch (_) {} return null; }
|
更新后的分辨率表也重新整理一下,横屏竖屏分开:
| 参数 |
分辨率 |
方向 |
适用场景 |
| 2160 |
3840×2160 |
横屏 🖥️ |
4K 桌面 |
| 1080 |
1920×1080 |
横屏 🖥️ |
Full HD |
| 720 |
1280×720 |
横屏 🖥️ |
HD |
| 768 |
1024×768 |
横屏 🖥️ |
XGA |
| 600 |
800×600 |
横屏 🖥️ |
SVGA |
| 480 |
640×480 |
横屏 🖥️ |
VGA |
| 1920 |
1080×1920 |
竖屏 📱 |
手机壁纸 FHD |
| 1280 |
720×1280 |
竖屏 📱 |
手机壁纸 HD |
写在最后
这个 Worker 从 2024 年 12 月写完第一版到现在,默默跑了快一年半,中间没出过什么岔子。这次优化更多是”既然发现了就顺手修一下”的心态,也顺便给自己提个醒——代码写完当时觉得没问题,过段时间回来看可能就能发现一些之前没注意到的细节。
必应的每日壁纸质量一直在线,我的博客封面图到现在也还在用这个接口。如果你也在用,可以试试传个不同的 ?h= 参数看看效果。😊