1. 项目概述边缘新闻聚合的挑战与机遇最近在做一个挺有意思的尝试我把它叫做“News — At The Edge”。这个名字听起来有点玄乎其实内核很简单就是尝试在离用户最近的地方也就是所谓的“网络边缘”去聚合、处理和分发新闻内容。这个想法的源头是源于我自己作为一个重度新闻阅读者的痛点。每天打开各种新闻App要么是加载慢尤其是在信号不好的地方刷半天出不来要么是内容同质化严重几个平台推的新闻大同小异更别提偶尔还会遇到因为某些原因某个新闻源突然访问不了的情况。我就想能不能有一个方案它足够快、足够稳定并且能让我在一个地方看到经过整合和初步处理的多元信息流“At The Edge”这个概念在技术圈并不新鲜云计算厂商们早就把边缘计算炒得火热。但把边缘计算和新闻阅读结合起来做一个个人化、轻量级的实践这里面就有不少值得琢磨的地方了。它不是一个要取代今日头条或者谷歌新闻的庞然大物而更像是一个极客的玩具一个可以完全由自己掌控的新闻“前哨站”。这个项目代号“7/7”意味着我希望它具备高可用性7天24小时虽然个人项目很难真正做到但这是架构设计时的一个目标。接下来我会详细拆解这个项目的设计思路、技术选型、实现细节以及一路踩过来的坑希望能给同样对信息获取效率有要求又喜欢折腾的朋友们一些参考。2. 核心架构设计为何选择边缘优先2.1 从中心化到边缘化的思维转变传统的新闻应用无论是客户端还是网页端其核心模式都是“中心化聚合-下发”。你的手机App作为一个“瘦客户端”主要负责展示UI和发起请求真正的新闻抓取、内容分析、个性化推荐这些重逻辑都在远方的云端服务器完成。这个模式的优势是功能强大、更新统一但劣势也很明显延迟受网络质量影响大所有流量都要经过中心节点对服务器压力大并且在单一节点故障或网络出现局部问题时用户体验会骤降。“News — At The Edge”的思路是反过来的。它的核心思想是“边缘化处理与缓存”。我设想有一个轻量级的服务部署在离我物理位置或者网络位置非常近的地方。这个地方可以是我家里的树莓派、一台始终开机的旧电脑甚至可以是一个云服务商的边缘函数如Cloudflare Workers。这个边缘节点承担起核心职责定时去我预设的、可信的新闻源如特定媒体的RSS、一些公开的API、甚至精心筛选的网页抓取内容。抓取后立即在边缘节点进行初步处理比如清洗HTML标签、提取正文、统一格式、去除重复项然后存储起来。当我打开我的新闻阅读客户端可能是一个简单的网页也可能是一个轻量级App时客户端不再去请求遥远的新闻平台API而是直接请求我这个部署在“边缘”的服务。因为距离近网络跳转少延迟极低几乎是瞬间加载。同时由于内容已经预处理并缓存即使原始新闻源暂时无法访问或者访问速度很慢我依然能从边缘缓存中读到最新的已抓取内容。这相当于为我的新闻获取路径建立了一个“本地加油站”和“缓冲池”。2.2 技术栈选型与考量确定了边缘化的方向接下来就是技术选型。我的原则是轻量、高效、可维护、低成本最好是零成本。边缘运行时环境这是最关键的决策。我考虑了以下几个选项树莓派 Docker完全自控数据物理上在自己家隐私性好。但需要家庭网络有公网IP或做内网穿透维护成本稍高且受家庭宽带上传速度和稳定性的制约。云服务器轻量应用比如腾讯云/阿里云的轻量服务器性能有保障网络好。但每月有固定成本虽然不高且服务器位置相对固定可能不是最优“边缘”。Serverless边缘函数Cloudflare Workers。这是我最終选择的方向。理由如下它拥有全球数百个边缘节点我的服务代码会被自动部署到离访问者最近的节点真正实现了“边缘”。它有免费的额度每日10万次请求对于个人新闻抓取和阅读来说完全够用。它基于V8隔离环境启动速度极快适合定时触发和API响应。无需管理服务器省心。后端核心逻辑运行在边缘函数语言选择JavaScript/TypeScript。因为Cloudflare Workers原生支持生态好特别是处理网络请求和文本HTML解析有丰富的库。抓取器使用fetchAPI 配合cheerio。fetch用于获取新闻源HTML或RSS XMLcheerio是一个服务器端的jQuery实现能非常方便地从HTML中提取需要的标题、链接、正文内容。对于RSS源则使用rss-parser这类库。缓存与存储Cloudflare Workers 提供了KV (Key-Value) 存储。虽然免费额度有限但用于存储处理后的新闻条目JSON格式和元数据绰绰有余。KV的读写速度很快并且也分布在边缘网络。我的策略是将每个新闻源的最新抓取结果以源ID为Key存入KV并设置一个合理的过期时间如30分钟。客户端请求时直接返回KV中的缓存数据。同时设置一个定时触发器Cron Trigger每隔一段时间如15分钟执行一次抓取和缓存更新任务这样缓存就能保持相对新鲜。前端客户端为了极致的轻量和通用性我选择了一个纯静态的单页面应用(SPA)。使用简单的HTML/CSS/JavaScriptVue.js或React的轻量版本编写。它只做一件事通过Fetch API调用我部署在Cloudflare Workers上的服务接口获取处理好的新闻JSON数据然后渲染成列表和详情页。这个前端可以部署在任意静态托管服务上如Cloudflare Pages、Vercel、GitHub Pages甚至直接本地打开HTML文件运行需处理CORS。它的UI极其简洁专注于内容本身。新闻源管理我设计了一个简单的配置文件如sources.json里面以数组的形式定义了我需要抓取的新闻源。每个源包含名称、URL、类型RSS/HTML、以及用于提取内容的cheerio选择器规则。这个配置文件直接放在Worker的代码中或者存储在KV里方便增删改。注意在实施抓取时务必尊重目标网站的robots.txt规则控制抓取频率避免给对方服务器造成压力。我的策略是定时触发间隔至少在10分钟以上并且只抓取有限的、真正需要的页面。2.3 数据流与核心工作流程整个系统的工作流程可以清晰地分为两条线缓存预热线和客户端读取线。缓存预热线由Cron Trigger驱动边缘函数被定时触发例如每15分钟一次。函数读取预设的新闻源配置列表。遍历列表对每个新闻源使用fetch发起请求获取原始内容HTML/XML。根据源类型使用cheerio或rss-parser解析内容提取出新闻条目标题、链接、摘要、发布时间等。将提取出的条目列表按统一格式整理成JSON对象。将这个JSON对象以新闻源ID为Key存储到Cloudflare KV中并设置过期时间TTL。对下一个新闻源重复步骤3-6直至所有源处理完毕。客户端读取线由用户访问驱动用户打开前端静态页面。页面加载的JavaScript立即向我的Cloudflare Worker服务地址发起一个GET请求。Worker接收到请求直接从Cloudflare KV中读取所有已缓存的新闻源数据。Worker将这些数据整合例如按时间倒序排列返回一个统一的、结构清晰的JSON响应给前端。前端接收到JSON数据动态渲染生成新闻列表页面。用户点击某条新闻前端可能会直接跳转到原始链接也可能先通过Worker代理获取纯文本内容再展示取决于设计。这个架构的优势在于用户的每一次阅读请求都不需要触发真实的网络抓取仅仅是一次对边缘KV存储的快速读取速度极快可靠性极高。内容的更新由后台定时任务默默完成实现了读写分离。3. 关键实现细节与避坑指南3.1 使用Cloudflare Workers实现边缘抓取与缓存首先你需要一个Cloudflare账户。Workers的免费套餐是起点。创建一个新的Worker我们将把主要逻辑放在这里。核心代码结构概览// 新闻源配置可以存储在KV中这里为示例直接写在代码里 const newsSources [ { id: tech_crunch, name: TechCrunch, url: https://techcrunch.com/feed/, type: rss, }, { id: hacker_news, name: Hacker News, url: https://news.ycombinator.com/rss, type: rss, }, { id: example_news, name: Example News Site, url: https://example.com/news, type: html, selector: { list: .article-list .item, // 文章列表项选择器 title: h2 a, // 标题选择器相对于list link: h2 ahref, // 链接选择器 time: .timedatetime, // 时间选择器 } } ]; // KV绑定假设你的KV命名空间绑定变量名为 NEWS_CACHE const CACHE NEWS_CACHE; // 处理客户端请求从KV返回缓存数据 async function handleApiRequest(request) { const allNews []; for (const source of newsSources) { const cachedData await CACHE.get(source.id, { type: json }); if (cachedData) { allNews.push(...cachedData.items.map(item ({...item, source: source.name}))); } } // 按时间排序 allNews.sort((a, b) new Date(b.pubDate || b.time) - new Date(a.pubDate || a.time)); return new Response(JSON.stringify({ news: allNews }), { headers: { Content-Type: application/json, Cache-Control: public, max-age60 }, }); } // 定时任务抓取并更新缓存 async function scheduledCronJob(event) { for (const source of newsSources) { let items []; try { if (source.type rss) { items await fetchRSS(source.url); } else if (source.type html) { items await fetchHTML(source.url, source.selector); } // 将抓取到的条目存入KV设置30分钟过期 await CACHE.put(source.id, JSON.stringify({ lastUpdated: new Date().toISOString(), items }), { expirationTtl: 1800 }); } catch (err) { console.error(Failed to fetch ${source.name}:, err.message); // 可选保留旧的缓存不更新 } } } // 主请求路由器 addEventListener(fetch, event { event.respondWith(handleApiRequest(event.request)); }); // 定时触发器 addEventListener(scheduled, event { event.waitUntil(scheduledCronJob(event)); });fetchRSS和fetchHTML是两个关键的函数需要自己实现。这里以fetchHTML为例展示如何使用cheerioasync function fetchHTML(url, selector) { const response await fetch(url); const html await response.text(); const $ cheerio.load(html); const items []; $(selector.list).each((index, elem) { const title $(elem).find(selector.title).text().trim(); const link $(elem).find(selector.title).attr(href); // 处理相对链接 const absoluteLink new URL(link, url).href; const time $(elem).find(selector.time).text().trim() || new Date().toISOString(); if (title absoluteLink) { items.push({ title, link: absoluteLink, time }); } }); return items.slice(0, 15); // 只取最新15条 }实操心得一选择器稳定性网页结构经常改版你的cheerio选择器可能会失效。因此新闻源尽量优先选择提供标准RSS/Atom订阅的网站其结构稳定得多。对于只能HTML抓取的源要选择相对稳定、语义化的CSS类名或标签如article,h1,h2,time避免使用.div123这种可能随样式调整而变化的类名。最好为每个重要的HTML源写一个简单的健康检查当连续几次抓取失败时发出通知。3.2 处理反爬机制与提升健壮性任何抓取项目都无法回避反爬虫机制。我们的边缘Worker虽然IP是Cloudflare的优质IP但过于频繁或特征明显的请求仍可能被限制。设置合理的请求间隔与频率不要在定时任务中一次性并发请求所有源。可以在循环中加入随机延迟await new Promise(resolve setTimeout(resolve, Math.random() * 2000 1000));模拟人类浏览的间隔。Cloudflare Workers的Cron最小粒度是1分钟我们设置为15分钟或更长已经很低频了。完善请求头User-Agent使用常见的浏览器User-Agent让请求看起来更像普通浏览器访问。const fetchOptions { headers: { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, }, }; const response await fetch(url, fetchOptions);错误处理与重试机制网络请求可能失败。必须用try...catch包裹每个源的抓取逻辑。对于非200状态码或超时可以实现简单的重试逻辑例如最多重试2次每次间隔递增。async function fetchWithRetry(url, options, maxRetries 2) { for (let i 0; i maxRetries; i) { try { const response await fetch(url, options); if (response.ok) return response; throw new Error(HTTP ${response.status}); } catch (err) { if (i maxRetries) throw err; console.log(Retrying (${i1}/${maxRetries})...); await new Promise(r setTimeout(r, 1000 * (i 1))); // 指数退避 } } }尊重robots.txt在代码逻辑中可以集成一个简单的robots.txt解析器或者至少人工检查你关注的网站是否允许抓取其feed或新闻列表页。这是良好的网络公民行为。3.3 前端展示与用户体验优化前端的目标是极简和快。我使用Vue 3的Composition API写了一个简单的组件。核心逻辑页面加载即请求在onMounted生命周期中调用Worker API。状态管理使用ref或reactive管理加载状态、新闻列表数据和错误信息。虚拟滚动如果新闻条目非常多比如超过100条可以考虑使用虚拟滚动列表库如vue-virtual-scroller只渲染可视区域内的DOM元素保证滚动流畅。离线支持PWA利用Service Worker可以将这个静态页面和Worker API的响应缓存起来实现离线阅读。这是“边缘缓存”思想的进一步延伸——缓存到了浏览器本地。自定义与过滤可以在前端添加简单的过滤功能比如按新闻源筛选、按关键词过滤标题。这些过滤操作完全在前端进行不增加后端压力。实操心得二时间处理不同新闻源的时间格式千奇百怪RFC 2822, ISO 8601, 自定义字符串如“3 hours ago”。在Worker处理数据时尽量将它们统一转换为ISO 8601格式或时间戳再存入KV。前端显示时可以使用Intl.DateTimeFormat或像day.js这样的轻量库来格式化为本地时间。统一的时间处理能极大提升排序和显示的准确性。4. 部署、监控与维护实战4.1 部署流程详解准备代码将上述Worker代码包括sources.json配置整理好。使用wrangler(Cloudflare的官方CLI工具) 进行开发和部署。创建KV命名空间在Cloudflare Dashboard的Workers页面创建一个KV命名空间记下其ID。配置wrangler.toml在你的项目根目录创建这个文件用于绑定KV和配置Cron触发器。name news-at-the-edge type javascript account_id 你的账户ID workers_dev true kv_namespaces [ { binding NEWS_CACHE, id 你创建的KV命名空间ID } ] [triggers] crons [*/15 * * * *] # 每15分钟执行一次部署运行npx wrangler deploy。首次部署会要求登录认证。成功后你会获得一个*.workers.dev的域名。部署前端将前端静态文件HTML, CSS, JS部署到任意静态托管服务。例如可以直接使用Cloudflare Pages它与Workers同属一个生态部署简单并且自动享受全球CDN。在Pages中关联你的前端代码仓库构建命令留空因为是静态文件输出目录指定为.或包含HTML文件的目录即可。4.2 监控与日志排查个人项目虽小但监控能让你知道它是否在健康运行。利用Cloudflare Workers自身的日志在Worker代码中关键位置如抓取开始、成功、失败使用console.log或console.error。然后可以在Cloudflare Dashboard的Workers详情页的“日志”选项卡下查看近期的日志。免费套餐有每日10万次请求的日志限额对于监控完全足够。健康检查端点可以在Worker中额外添加一个路由例如/health。当访问这个端点时它检查KV的连接状态并返回各新闻源最后一次成功更新的时间戳。你可以用一个第三方监控服务如UptimeRobot定期调用这个端点如果失败或长时间未更新就发送邮件或短信告警。前端错误监控前端可以使用window.onerror或更现代的window.addEventListener(error, ...)来捕获运行时错误并通过一个简单的API发送到你的日志服务甚至可以发到另一个Worker做记录。4.3 内容更新与源管理新闻源不是一成不变的。网站改版、RSS地址变更、或者你发现了新的优质源都需要更新。动态配置将newsSources配置从硬编码改为从KV中读取。这样你可以写一个简单的管理页面密码保护通过调用Worker的另一个管理接口来动态增删改新闻源而无需重新部署Worker代码。版本控制将整个项目Worker代码和前端代码放入Git仓库。任何配置的更改都通过提交代码、审核、再部署的方式进行便于回滚和追踪历史。定期审查每隔一两个月手动检查一下各个新闻源的抓取情况。看看是否有源失效了或者抓取到的内容格式变了需要调整选择器。5. 遇到的典型问题与解决方案在开发和运行这个项目的过程中我遇到了不少典型问题这里记录下来供大家参考。问题现象可能原因排查步骤与解决方案Worker部署失败提示“绑定错误”wrangler.toml中的KV命名空间ID填写错误或该命名空间不属于当前账户。1. 在Dashboard确认KV命名空间ID。2. 检查wrangler.toml中的account_id和kv_namespaces配置。3. 运行npx wrangler kv:namespace list查看当前账户下的命名空间。定时任务Cron没有执行Cron表达式配置错误Worker有未处理的致命错误导致启动失败免费套餐的Cron可能有一定延迟。1. 检查wrangler.toml中的crons表达式语法。2. 查看Worker的“日志”中是否有在预定时间点的执行记录或错误信息。3. 手动触发一次Worker通过访问其URL看功能是否正常。4. 耐心等待Cloudflare的Cron不是精确到秒的。抓取特定网站返回403错误网站有反爬机制识别出是Cloudflare IP或非浏览器请求。1. 检查并完善请求头特别是User-Agent。2. 尝试添加Referer头设为目标网站域名。3. 大幅降低抓取频率。4. 考虑是否该网站明确禁止抓取检查robots.txt若是则应移除该源。KV中读取的数据为null数据未成功写入Key名称不对数据已过期TTL到期。1. 检查定时任务日志确认抓取和CACHE.put是否成功执行。2. 确认读取时使用的Keysource.id与写入时完全一致。3. 检查设置的expirationTtl是否太短导致数据在下次读取前已过期。4. 通过wrangler kv:key get KEY --binding NEWS_CACHE命令直接查看KV中该Key的值。前端页面打开空白或报跨域CORS错误Worker的响应头中没有设置正确的CORS头前端页面与Worker域名不同。在Worker处理API请求的返回响应中务必添加CORS头headers: { Access-Control-Allow-Origin: *, // 或你的前端域名 Access-Control-Allow-Methods: GET, OPTIONS, ... }。对于OPTIONS预检请求也要正确处理。抓取到的中文等非ASCII字符显示乱码目标网页编码与解析时假设的编码默认UTF-8不一致。在fetch到响应后可以先通过response.headers.get(content-type)查看编码信息。如果不是UTF-8可能需要使用iconv-lite这样的库进行转码然后再交给cheerio解析。前端列表渲染大量数据时卡顿一次性渲染数百条DOM元素性能开销大。1. 在Worker端或前端进行分页每次只加载固定数量如20条。2. 实现虚拟滚动只渲染可视区域内的元素。这是解决此类性能问题最有效的方法。避坑技巧测试先行在将一个新新闻源加入正式配置前强烈建议先写一个独立的测试脚本可以就在浏览器的开发者工具Console里或一个单独的Node.js脚本用你的选择器逻辑去抓取和解析确认能稳定拿到想要的数据。这能避免有问题的源污染你的整个抓取流程。6. 性能优化与扩展思路项目基本跑起来后还可以从以下几个方面进行优化和扩展增量抓取与去重目前的策略是每次全量抓取并替换缓存。对于更新不频繁的源这样效率低。可以设计为增量抓取记录上次抓取的最新文章ID或时间戳下次只抓取比这个时间新的内容然后与旧缓存合并去重。这需要更复杂的状态管理。内容预处理与摘要生成在边缘Worker抓取到全文后可以调用简单的NLP库在Worker受限环境下可以考虑使用WebAssembly版本的轻量库为长文生成摘要或者提取关键词。这样返回给前端的数据就包含了摘要用户无需跳转即可了解大意。多边缘节点同步高级虽然Cloudflare Workers本身是全球边缘但KV存储默认是全局一致的。如果你希望在不同地理位置有更极致的缓存可以探索使用Durable ObjectsCloudflare的另一项服务来协调多个边缘节点的数据但这会显著增加复杂性和成本。个性化推荐雏形在前端记录用户的点击行为本地存储。将行为数据加密匿名化后传回WorkerWorker可以基于简单的协同过滤或标签匹配在整合新闻列表时进行初步的权重排序将用户可能更感兴趣的内容排在前列。这需要谨慎处理隐私问题。支持更多内容类型除了新闻这个架构可以很容易地扩展为聚合博客、论坛热帖、视频更新通知等。只需定义新的源类型和对应的解析器即可。这个“News — At The Edge”项目从构思到实现花了我几个周末的时间。它现在安静地运行在Cloudflare的全球网络上为我提供着一个快速、干净、不受干扰的新闻入口。最大的成就感不在于技术有多复杂而在于它完美地解决了我自己的真实需求并且整个系统完全在我的理解和控制范围之内。它可能没有商业新闻App那样华丽的UI和精准的推荐算法但它快如闪电稳定可靠并且最重要的是它只服务于我。对于开发者而言有时候用一些简单的技术组合为自己打造一件称手的工具这种乐趣远大于使用现成的产品。如果你也对信息过载和平台依赖感到厌倦不妨试试动手搭建一个属于自己的“边缘信息站”这个过程本身就是一种极好的学习和享受。
基于边缘计算与Cloudflare Workers构建个人新闻聚合系统
发布时间:2026/6/1 6:51:06
1. 项目概述边缘新闻聚合的挑战与机遇最近在做一个挺有意思的尝试我把它叫做“News — At The Edge”。这个名字听起来有点玄乎其实内核很简单就是尝试在离用户最近的地方也就是所谓的“网络边缘”去聚合、处理和分发新闻内容。这个想法的源头是源于我自己作为一个重度新闻阅读者的痛点。每天打开各种新闻App要么是加载慢尤其是在信号不好的地方刷半天出不来要么是内容同质化严重几个平台推的新闻大同小异更别提偶尔还会遇到因为某些原因某个新闻源突然访问不了的情况。我就想能不能有一个方案它足够快、足够稳定并且能让我在一个地方看到经过整合和初步处理的多元信息流“At The Edge”这个概念在技术圈并不新鲜云计算厂商们早就把边缘计算炒得火热。但把边缘计算和新闻阅读结合起来做一个个人化、轻量级的实践这里面就有不少值得琢磨的地方了。它不是一个要取代今日头条或者谷歌新闻的庞然大物而更像是一个极客的玩具一个可以完全由自己掌控的新闻“前哨站”。这个项目代号“7/7”意味着我希望它具备高可用性7天24小时虽然个人项目很难真正做到但这是架构设计时的一个目标。接下来我会详细拆解这个项目的设计思路、技术选型、实现细节以及一路踩过来的坑希望能给同样对信息获取效率有要求又喜欢折腾的朋友们一些参考。2. 核心架构设计为何选择边缘优先2.1 从中心化到边缘化的思维转变传统的新闻应用无论是客户端还是网页端其核心模式都是“中心化聚合-下发”。你的手机App作为一个“瘦客户端”主要负责展示UI和发起请求真正的新闻抓取、内容分析、个性化推荐这些重逻辑都在远方的云端服务器完成。这个模式的优势是功能强大、更新统一但劣势也很明显延迟受网络质量影响大所有流量都要经过中心节点对服务器压力大并且在单一节点故障或网络出现局部问题时用户体验会骤降。“News — At The Edge”的思路是反过来的。它的核心思想是“边缘化处理与缓存”。我设想有一个轻量级的服务部署在离我物理位置或者网络位置非常近的地方。这个地方可以是我家里的树莓派、一台始终开机的旧电脑甚至可以是一个云服务商的边缘函数如Cloudflare Workers。这个边缘节点承担起核心职责定时去我预设的、可信的新闻源如特定媒体的RSS、一些公开的API、甚至精心筛选的网页抓取内容。抓取后立即在边缘节点进行初步处理比如清洗HTML标签、提取正文、统一格式、去除重复项然后存储起来。当我打开我的新闻阅读客户端可能是一个简单的网页也可能是一个轻量级App时客户端不再去请求遥远的新闻平台API而是直接请求我这个部署在“边缘”的服务。因为距离近网络跳转少延迟极低几乎是瞬间加载。同时由于内容已经预处理并缓存即使原始新闻源暂时无法访问或者访问速度很慢我依然能从边缘缓存中读到最新的已抓取内容。这相当于为我的新闻获取路径建立了一个“本地加油站”和“缓冲池”。2.2 技术栈选型与考量确定了边缘化的方向接下来就是技术选型。我的原则是轻量、高效、可维护、低成本最好是零成本。边缘运行时环境这是最关键的决策。我考虑了以下几个选项树莓派 Docker完全自控数据物理上在自己家隐私性好。但需要家庭网络有公网IP或做内网穿透维护成本稍高且受家庭宽带上传速度和稳定性的制约。云服务器轻量应用比如腾讯云/阿里云的轻量服务器性能有保障网络好。但每月有固定成本虽然不高且服务器位置相对固定可能不是最优“边缘”。Serverless边缘函数Cloudflare Workers。这是我最終选择的方向。理由如下它拥有全球数百个边缘节点我的服务代码会被自动部署到离访问者最近的节点真正实现了“边缘”。它有免费的额度每日10万次请求对于个人新闻抓取和阅读来说完全够用。它基于V8隔离环境启动速度极快适合定时触发和API响应。无需管理服务器省心。后端核心逻辑运行在边缘函数语言选择JavaScript/TypeScript。因为Cloudflare Workers原生支持生态好特别是处理网络请求和文本HTML解析有丰富的库。抓取器使用fetchAPI 配合cheerio。fetch用于获取新闻源HTML或RSS XMLcheerio是一个服务器端的jQuery实现能非常方便地从HTML中提取需要的标题、链接、正文内容。对于RSS源则使用rss-parser这类库。缓存与存储Cloudflare Workers 提供了KV (Key-Value) 存储。虽然免费额度有限但用于存储处理后的新闻条目JSON格式和元数据绰绰有余。KV的读写速度很快并且也分布在边缘网络。我的策略是将每个新闻源的最新抓取结果以源ID为Key存入KV并设置一个合理的过期时间如30分钟。客户端请求时直接返回KV中的缓存数据。同时设置一个定时触发器Cron Trigger每隔一段时间如15分钟执行一次抓取和缓存更新任务这样缓存就能保持相对新鲜。前端客户端为了极致的轻量和通用性我选择了一个纯静态的单页面应用(SPA)。使用简单的HTML/CSS/JavaScriptVue.js或React的轻量版本编写。它只做一件事通过Fetch API调用我部署在Cloudflare Workers上的服务接口获取处理好的新闻JSON数据然后渲染成列表和详情页。这个前端可以部署在任意静态托管服务上如Cloudflare Pages、Vercel、GitHub Pages甚至直接本地打开HTML文件运行需处理CORS。它的UI极其简洁专注于内容本身。新闻源管理我设计了一个简单的配置文件如sources.json里面以数组的形式定义了我需要抓取的新闻源。每个源包含名称、URL、类型RSS/HTML、以及用于提取内容的cheerio选择器规则。这个配置文件直接放在Worker的代码中或者存储在KV里方便增删改。注意在实施抓取时务必尊重目标网站的robots.txt规则控制抓取频率避免给对方服务器造成压力。我的策略是定时触发间隔至少在10分钟以上并且只抓取有限的、真正需要的页面。2.3 数据流与核心工作流程整个系统的工作流程可以清晰地分为两条线缓存预热线和客户端读取线。缓存预热线由Cron Trigger驱动边缘函数被定时触发例如每15分钟一次。函数读取预设的新闻源配置列表。遍历列表对每个新闻源使用fetch发起请求获取原始内容HTML/XML。根据源类型使用cheerio或rss-parser解析内容提取出新闻条目标题、链接、摘要、发布时间等。将提取出的条目列表按统一格式整理成JSON对象。将这个JSON对象以新闻源ID为Key存储到Cloudflare KV中并设置过期时间TTL。对下一个新闻源重复步骤3-6直至所有源处理完毕。客户端读取线由用户访问驱动用户打开前端静态页面。页面加载的JavaScript立即向我的Cloudflare Worker服务地址发起一个GET请求。Worker接收到请求直接从Cloudflare KV中读取所有已缓存的新闻源数据。Worker将这些数据整合例如按时间倒序排列返回一个统一的、结构清晰的JSON响应给前端。前端接收到JSON数据动态渲染生成新闻列表页面。用户点击某条新闻前端可能会直接跳转到原始链接也可能先通过Worker代理获取纯文本内容再展示取决于设计。这个架构的优势在于用户的每一次阅读请求都不需要触发真实的网络抓取仅仅是一次对边缘KV存储的快速读取速度极快可靠性极高。内容的更新由后台定时任务默默完成实现了读写分离。3. 关键实现细节与避坑指南3.1 使用Cloudflare Workers实现边缘抓取与缓存首先你需要一个Cloudflare账户。Workers的免费套餐是起点。创建一个新的Worker我们将把主要逻辑放在这里。核心代码结构概览// 新闻源配置可以存储在KV中这里为示例直接写在代码里 const newsSources [ { id: tech_crunch, name: TechCrunch, url: https://techcrunch.com/feed/, type: rss, }, { id: hacker_news, name: Hacker News, url: https://news.ycombinator.com/rss, type: rss, }, { id: example_news, name: Example News Site, url: https://example.com/news, type: html, selector: { list: .article-list .item, // 文章列表项选择器 title: h2 a, // 标题选择器相对于list link: h2 ahref, // 链接选择器 time: .timedatetime, // 时间选择器 } } ]; // KV绑定假设你的KV命名空间绑定变量名为 NEWS_CACHE const CACHE NEWS_CACHE; // 处理客户端请求从KV返回缓存数据 async function handleApiRequest(request) { const allNews []; for (const source of newsSources) { const cachedData await CACHE.get(source.id, { type: json }); if (cachedData) { allNews.push(...cachedData.items.map(item ({...item, source: source.name}))); } } // 按时间排序 allNews.sort((a, b) new Date(b.pubDate || b.time) - new Date(a.pubDate || a.time)); return new Response(JSON.stringify({ news: allNews }), { headers: { Content-Type: application/json, Cache-Control: public, max-age60 }, }); } // 定时任务抓取并更新缓存 async function scheduledCronJob(event) { for (const source of newsSources) { let items []; try { if (source.type rss) { items await fetchRSS(source.url); } else if (source.type html) { items await fetchHTML(source.url, source.selector); } // 将抓取到的条目存入KV设置30分钟过期 await CACHE.put(source.id, JSON.stringify({ lastUpdated: new Date().toISOString(), items }), { expirationTtl: 1800 }); } catch (err) { console.error(Failed to fetch ${source.name}:, err.message); // 可选保留旧的缓存不更新 } } } // 主请求路由器 addEventListener(fetch, event { event.respondWith(handleApiRequest(event.request)); }); // 定时触发器 addEventListener(scheduled, event { event.waitUntil(scheduledCronJob(event)); });fetchRSS和fetchHTML是两个关键的函数需要自己实现。这里以fetchHTML为例展示如何使用cheerioasync function fetchHTML(url, selector) { const response await fetch(url); const html await response.text(); const $ cheerio.load(html); const items []; $(selector.list).each((index, elem) { const title $(elem).find(selector.title).text().trim(); const link $(elem).find(selector.title).attr(href); // 处理相对链接 const absoluteLink new URL(link, url).href; const time $(elem).find(selector.time).text().trim() || new Date().toISOString(); if (title absoluteLink) { items.push({ title, link: absoluteLink, time }); } }); return items.slice(0, 15); // 只取最新15条 }实操心得一选择器稳定性网页结构经常改版你的cheerio选择器可能会失效。因此新闻源尽量优先选择提供标准RSS/Atom订阅的网站其结构稳定得多。对于只能HTML抓取的源要选择相对稳定、语义化的CSS类名或标签如article,h1,h2,time避免使用.div123这种可能随样式调整而变化的类名。最好为每个重要的HTML源写一个简单的健康检查当连续几次抓取失败时发出通知。3.2 处理反爬机制与提升健壮性任何抓取项目都无法回避反爬虫机制。我们的边缘Worker虽然IP是Cloudflare的优质IP但过于频繁或特征明显的请求仍可能被限制。设置合理的请求间隔与频率不要在定时任务中一次性并发请求所有源。可以在循环中加入随机延迟await new Promise(resolve setTimeout(resolve, Math.random() * 2000 1000));模拟人类浏览的间隔。Cloudflare Workers的Cron最小粒度是1分钟我们设置为15分钟或更长已经很低频了。完善请求头User-Agent使用常见的浏览器User-Agent让请求看起来更像普通浏览器访问。const fetchOptions { headers: { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, }, }; const response await fetch(url, fetchOptions);错误处理与重试机制网络请求可能失败。必须用try...catch包裹每个源的抓取逻辑。对于非200状态码或超时可以实现简单的重试逻辑例如最多重试2次每次间隔递增。async function fetchWithRetry(url, options, maxRetries 2) { for (let i 0; i maxRetries; i) { try { const response await fetch(url, options); if (response.ok) return response; throw new Error(HTTP ${response.status}); } catch (err) { if (i maxRetries) throw err; console.log(Retrying (${i1}/${maxRetries})...); await new Promise(r setTimeout(r, 1000 * (i 1))); // 指数退避 } } }尊重robots.txt在代码逻辑中可以集成一个简单的robots.txt解析器或者至少人工检查你关注的网站是否允许抓取其feed或新闻列表页。这是良好的网络公民行为。3.3 前端展示与用户体验优化前端的目标是极简和快。我使用Vue 3的Composition API写了一个简单的组件。核心逻辑页面加载即请求在onMounted生命周期中调用Worker API。状态管理使用ref或reactive管理加载状态、新闻列表数据和错误信息。虚拟滚动如果新闻条目非常多比如超过100条可以考虑使用虚拟滚动列表库如vue-virtual-scroller只渲染可视区域内的DOM元素保证滚动流畅。离线支持PWA利用Service Worker可以将这个静态页面和Worker API的响应缓存起来实现离线阅读。这是“边缘缓存”思想的进一步延伸——缓存到了浏览器本地。自定义与过滤可以在前端添加简单的过滤功能比如按新闻源筛选、按关键词过滤标题。这些过滤操作完全在前端进行不增加后端压力。实操心得二时间处理不同新闻源的时间格式千奇百怪RFC 2822, ISO 8601, 自定义字符串如“3 hours ago”。在Worker处理数据时尽量将它们统一转换为ISO 8601格式或时间戳再存入KV。前端显示时可以使用Intl.DateTimeFormat或像day.js这样的轻量库来格式化为本地时间。统一的时间处理能极大提升排序和显示的准确性。4. 部署、监控与维护实战4.1 部署流程详解准备代码将上述Worker代码包括sources.json配置整理好。使用wrangler(Cloudflare的官方CLI工具) 进行开发和部署。创建KV命名空间在Cloudflare Dashboard的Workers页面创建一个KV命名空间记下其ID。配置wrangler.toml在你的项目根目录创建这个文件用于绑定KV和配置Cron触发器。name news-at-the-edge type javascript account_id 你的账户ID workers_dev true kv_namespaces [ { binding NEWS_CACHE, id 你创建的KV命名空间ID } ] [triggers] crons [*/15 * * * *] # 每15分钟执行一次部署运行npx wrangler deploy。首次部署会要求登录认证。成功后你会获得一个*.workers.dev的域名。部署前端将前端静态文件HTML, CSS, JS部署到任意静态托管服务。例如可以直接使用Cloudflare Pages它与Workers同属一个生态部署简单并且自动享受全球CDN。在Pages中关联你的前端代码仓库构建命令留空因为是静态文件输出目录指定为.或包含HTML文件的目录即可。4.2 监控与日志排查个人项目虽小但监控能让你知道它是否在健康运行。利用Cloudflare Workers自身的日志在Worker代码中关键位置如抓取开始、成功、失败使用console.log或console.error。然后可以在Cloudflare Dashboard的Workers详情页的“日志”选项卡下查看近期的日志。免费套餐有每日10万次请求的日志限额对于监控完全足够。健康检查端点可以在Worker中额外添加一个路由例如/health。当访问这个端点时它检查KV的连接状态并返回各新闻源最后一次成功更新的时间戳。你可以用一个第三方监控服务如UptimeRobot定期调用这个端点如果失败或长时间未更新就发送邮件或短信告警。前端错误监控前端可以使用window.onerror或更现代的window.addEventListener(error, ...)来捕获运行时错误并通过一个简单的API发送到你的日志服务甚至可以发到另一个Worker做记录。4.3 内容更新与源管理新闻源不是一成不变的。网站改版、RSS地址变更、或者你发现了新的优质源都需要更新。动态配置将newsSources配置从硬编码改为从KV中读取。这样你可以写一个简单的管理页面密码保护通过调用Worker的另一个管理接口来动态增删改新闻源而无需重新部署Worker代码。版本控制将整个项目Worker代码和前端代码放入Git仓库。任何配置的更改都通过提交代码、审核、再部署的方式进行便于回滚和追踪历史。定期审查每隔一两个月手动检查一下各个新闻源的抓取情况。看看是否有源失效了或者抓取到的内容格式变了需要调整选择器。5. 遇到的典型问题与解决方案在开发和运行这个项目的过程中我遇到了不少典型问题这里记录下来供大家参考。问题现象可能原因排查步骤与解决方案Worker部署失败提示“绑定错误”wrangler.toml中的KV命名空间ID填写错误或该命名空间不属于当前账户。1. 在Dashboard确认KV命名空间ID。2. 检查wrangler.toml中的account_id和kv_namespaces配置。3. 运行npx wrangler kv:namespace list查看当前账户下的命名空间。定时任务Cron没有执行Cron表达式配置错误Worker有未处理的致命错误导致启动失败免费套餐的Cron可能有一定延迟。1. 检查wrangler.toml中的crons表达式语法。2. 查看Worker的“日志”中是否有在预定时间点的执行记录或错误信息。3. 手动触发一次Worker通过访问其URL看功能是否正常。4. 耐心等待Cloudflare的Cron不是精确到秒的。抓取特定网站返回403错误网站有反爬机制识别出是Cloudflare IP或非浏览器请求。1. 检查并完善请求头特别是User-Agent。2. 尝试添加Referer头设为目标网站域名。3. 大幅降低抓取频率。4. 考虑是否该网站明确禁止抓取检查robots.txt若是则应移除该源。KV中读取的数据为null数据未成功写入Key名称不对数据已过期TTL到期。1. 检查定时任务日志确认抓取和CACHE.put是否成功执行。2. 确认读取时使用的Keysource.id与写入时完全一致。3. 检查设置的expirationTtl是否太短导致数据在下次读取前已过期。4. 通过wrangler kv:key get KEY --binding NEWS_CACHE命令直接查看KV中该Key的值。前端页面打开空白或报跨域CORS错误Worker的响应头中没有设置正确的CORS头前端页面与Worker域名不同。在Worker处理API请求的返回响应中务必添加CORS头headers: { Access-Control-Allow-Origin: *, // 或你的前端域名 Access-Control-Allow-Methods: GET, OPTIONS, ... }。对于OPTIONS预检请求也要正确处理。抓取到的中文等非ASCII字符显示乱码目标网页编码与解析时假设的编码默认UTF-8不一致。在fetch到响应后可以先通过response.headers.get(content-type)查看编码信息。如果不是UTF-8可能需要使用iconv-lite这样的库进行转码然后再交给cheerio解析。前端列表渲染大量数据时卡顿一次性渲染数百条DOM元素性能开销大。1. 在Worker端或前端进行分页每次只加载固定数量如20条。2. 实现虚拟滚动只渲染可视区域内的元素。这是解决此类性能问题最有效的方法。避坑技巧测试先行在将一个新新闻源加入正式配置前强烈建议先写一个独立的测试脚本可以就在浏览器的开发者工具Console里或一个单独的Node.js脚本用你的选择器逻辑去抓取和解析确认能稳定拿到想要的数据。这能避免有问题的源污染你的整个抓取流程。6. 性能优化与扩展思路项目基本跑起来后还可以从以下几个方面进行优化和扩展增量抓取与去重目前的策略是每次全量抓取并替换缓存。对于更新不频繁的源这样效率低。可以设计为增量抓取记录上次抓取的最新文章ID或时间戳下次只抓取比这个时间新的内容然后与旧缓存合并去重。这需要更复杂的状态管理。内容预处理与摘要生成在边缘Worker抓取到全文后可以调用简单的NLP库在Worker受限环境下可以考虑使用WebAssembly版本的轻量库为长文生成摘要或者提取关键词。这样返回给前端的数据就包含了摘要用户无需跳转即可了解大意。多边缘节点同步高级虽然Cloudflare Workers本身是全球边缘但KV存储默认是全局一致的。如果你希望在不同地理位置有更极致的缓存可以探索使用Durable ObjectsCloudflare的另一项服务来协调多个边缘节点的数据但这会显著增加复杂性和成本。个性化推荐雏形在前端记录用户的点击行为本地存储。将行为数据加密匿名化后传回WorkerWorker可以基于简单的协同过滤或标签匹配在整合新闻列表时进行初步的权重排序将用户可能更感兴趣的内容排在前列。这需要谨慎处理隐私问题。支持更多内容类型除了新闻这个架构可以很容易地扩展为聚合博客、论坛热帖、视频更新通知等。只需定义新的源类型和对应的解析器即可。这个“News — At The Edge”项目从构思到实现花了我几个周末的时间。它现在安静地运行在Cloudflare的全球网络上为我提供着一个快速、干净、不受干扰的新闻入口。最大的成就感不在于技术有多复杂而在于它完美地解决了我自己的真实需求并且整个系统完全在我的理解和控制范围之内。它可能没有商业新闻App那样华丽的UI和精准的推荐算法但它快如闪电稳定可靠并且最重要的是它只服务于我。对于开发者而言有时候用一些简单的技术组合为自己打造一件称手的工具这种乐趣远大于使用现成的产品。如果你也对信息过载和平台依赖感到厌倦不妨试试动手搭建一个属于自己的“边缘信息站”这个过程本身就是一种极好的学习和享受。