
1. 为什么我要自己搭一个 DOTA2 信息站打 DOTA2 的人大概都有过这种体验想查个英雄胜率打开某个数据站先弹一个全屏广告关掉之后又跳一个视频等真正看到数据的时候已经过去十几秒了。更别提有些站点在手机上排版稀烂英雄头像加载半天出不来。我自己是打了十来年 DOTA2 的老玩家同时也是做前端开发的某天晚上被一个数据站的弹窗恶心到之后就冒出一个念头——干脆自己做一个。这个项目的核心目标很明确做一个干净、快速、无广告的 DOTA2 信息站并且把代码完全开源。它能提供英雄列表、英雄详细数据、胜率排行、装备信息这些玩家最常用的功能数据来源是公开的第三方 API。技术上用的是Astro做静态站点生成Cloudflare Workers做边缘计算和数据代理WebSocket做实时数据的推送。整套东西跑下来部署成本几乎为零访问速度还比很多商业站点快。这篇文章适合谁看如果你是一个有一定前端基础、想学 Astro 或者 Cloudflare Workers 的开发者那这篇基本可以当半个教程用。如果你是一个 DOTA2 玩家对技术不太懂但好奇这种站点是怎么跑起来的我也尽量用大白话把原理讲清楚。如果你正在找一个完整的、从零到上线的开源项目来练手那这个项目的架构和踩坑记录应该能帮你省不少时间。我先把整个项目的技术选型逻辑讲清楚再拆解每个核心模块的实现细节然后是完整的部署流程最后是我在实际开发和运维中遇到的那些坑。这些都是文档里不会写、只有真正跑过一遍才知道的东西。2. 整体架构设计与技术选型思路2.1 为什么是 Astro 而不是 Next.js 或者纯静态 HTML做这个项目之前我认真对比过几个方案。最直接的想法是用纯静态 HTML 加一点 JavaScript简单粗暴。但 DOTA2 的数据量不小一百多个英雄每个英雄有技能、天赋、克制关系、出装推荐如果每个页面都手写维护成本会高到离谱。所以需要一个能批量生成页面的框架。Next.js 当然可以但它的 SSR 模式需要一台常驻服务器即使用静态导出构建出来的包也不小。我这个站点的内容更新频率其实不高——英雄数据一天更新一次就够了胜率数据每小时更新一次——所以完全没必要用 SSR 实时渲染。Astro 的核心优势在于它默认输出零 JavaScript 的静态 HTML只有需要交互的组件才会加载 JS。这意味着首屏加载速度极快SEO 也友好。具体来说Astro 的岛屿架构Islands Architecture让我可以把页面拆成两部分静态部分英雄列表、英雄详情、装备说明在构建时生成纯 HTML动态部分实时胜率、在线人数用客户端组件按需加载。这样用户打开页面时看到的是立即可用的内容而不是一个白屏转圈。另一个关键点是 Astro 的内容集合Content Collections。我可以把英雄数据定义成结构化的 Markdown 或 JSONAstro 在构建时会自动校验字段类型生成类型安全的查询接口。这个在项目规模变大之后特别有用比如我想给英雄加一个“上手难度”字段只要在 schema 里定义好所有引用这个字段的地方都会有类型提示不会出现拼写错误。2.2 Cloudflare Workers 在这里扮演什么角色静态站点有个天然的问题数据是构建时写死的。如果我想展示实时胜率就必须在构建时去拉数据然后重新部署。一天部署几十次显然不现实。所以需要一个运行时的数据层这就是 Cloudflare Workers 的用武之地。Workers 是运行在 Cloudflare 边缘节点上的轻量级函数冷启动时间几乎为零免费额度每天十万次请求对于个人项目来说完全够用。我在 Workers 里做了三件事第一代理第三方 API。DOTA2 的公开数据 API 有时候会有跨域限制直接在前端调用会被浏览器拦截。Workers 作为中间层把请求转发出去加上必要的请求头再把结果返回给前端。这样前端只需要请求我自己的域名不存在跨域问题。第二做数据缓存。第三方 API 有频率限制如果每个用户访问都去拉一次很快就会被限流。Workers 的 KV 存储可以缓存 API 响应设置一个合理的过期时间比如胜率数据缓存十分钟这样大部分请求都命中缓存只有缓存过期后才会真正去拉数据。第三处理 WebSocket 连接。这个后面单独讲是项目里比较有意思的一部分。2.3 WebSocket 用在哪里为什么不用轮询一开始我是用轮询的——前端每隔三十秒发一次请求问服务器“有没有新数据”。这个方案简单但问题很明显如果三十秒内数据没变化这次请求就是浪费如果数据在两次轮询之间变了用户最多要等三十秒才能看到。而且轮询会产生大量无效请求对 Workers 的免费额度也是一种消耗。WebSocket 建立的是持久连接服务器可以主动推送数据。在这个项目里我用 WebSocket 做两件事一是实时胜率更新当后台数据变化时主动推送给所有在线用户二是在线人数统计这个功能用轮询做会很别扭用 WebSocket 就非常自然连接建立和断开的时候各发一次消息就行。不过 WebSocket 也不是没有代价。它需要维护连接状态Cloudflare Workers 对 WebSocket 的支持是通过 Durable Objects 实现的这部分有一定的学习成本。而且如果连接数很多内存占用会上升。对于个人项目来说几十到几百个并发连接完全没问题但如果要做成大规模应用就需要考虑分片和负载均衡了。2.4 数据来源与合规性考量这里必须说清楚项目使用的所有数据都来自公开的第三方 API不涉及任何游戏客户端的逆向或者私有接口。我在代码里也做了请求频率限制避免对数据源造成压力。开源的时候我把 API 的调用逻辑抽象成了一个独立的模块如果将来数据源有变化只需要改这一个文件就行。另外项目里不包含任何游戏内的美术资源。英雄头像用的是公开的图片链接装备图标也是从公开渠道获取的。这一点在开源项目里很重要避免版权问题。3. 核心模块拆解与关键实现细节3.1 英雄数据模块从 API 到静态页面的完整链路英雄数据是整个站点的基石。我的处理流程是这样的首先在构建时通过一个 Node.js 脚本从第三方 API 拉取所有英雄的基础信息包括英雄 ID、名称、头像、主要属性、攻击类型、角色定位等。这些数据变化频率很低一个新英雄出来可能几个月才更新一次所以完全可以在构建时固化。拉下来的原始数据是 JSON 格式字段命名比较随意有些是下划线有些是驼峰。我写了一个转换层把它们统一成自己定义的数据结构然后存入 Astro 的内容集合。这个转换层的好处是如果将来 API 字段变了我只需要改转换逻辑页面模板不用动。每个英雄会生成一个独立的详情页路径是/heroes/[heroId]。Astro 的动态路由会在构建时为每个英雄生成一个 HTML 文件。页面内容包括英雄的基本属性、技能列表、天赋树、常见出装、克制关系。其中技能和天赋的数据也是从 API 拉取后转换的出装和克制关系则是我自己整理的一份静态数据因为这部分没有现成的 API而且不同版本变化很大手动维护反而更可控。这里有个细节值得说英雄的技能描述里经常包含特殊符号和变量占位符比如{s:bonus_damage}这种。直接渲染会显示成乱码。我写了一个解析函数把这些占位符替换成实际的数值并且根据技能等级显示不同的数值。这个函数大概一百多行处理了十几种不同的占位符格式是整个项目里最繁琐但也最有价值的部分之一。3.2 实时胜率模块WebSocket 连接的生命周期管理实时胜率是用户最关心的数据之一。我的实现方案是Cloudflare Worker 里有一个定时任务每隔十分钟从数据源拉取一次最新的胜率数据存入 KV。同时Worker 维护一个 WebSocket 端点当有客户端连接时先把当前缓存的数据推送给它然后把这个连接加入一个广播列表。当定时任务更新数据后遍历广播列表把新数据推送给所有连接。客户端的实现要处理几种情况连接建立、连接断开、重连、数据更新。我用了一个简单的状态机来管理这些状态。连接断开后会尝试重连重连间隔采用指数退避策略——第一次等一秒第二次等两秒第三次等四秒最多等三十秒。这样可以避免在网络不稳定的情况下疯狂重连把服务器打挂。注意WebSocket 连接在移动端有个坑当页面进入后台或者手机锁屏时连接可能会被系统断开。我的处理方式是在页面可见性变化时主动检查连接状态如果发现断开了就立即重连而不是等指数退避。这个细节在文档里基本不会提但实际体验差别很大。还有一个问题是数据格式。WebSocket 传输的是 JSON 字符串但如果数据量大每次全量推送会很浪费带宽。我的做法是只推送变化的部分——比如只有三个英雄的胜率变了就只推这三个英雄的数据客户端收到后合并到本地状态里。这个优化让每次推送的数据量从几十 KB 降到了几百字节。3.3 装备与物品模块静态数据的结构化处理装备数据相对简单因为物品的数量比英雄少而且属性变化不频繁。我把装备数据整理成了一份 JSON 文件包含物品 ID、名称、图标、价格、属性加成、合成配方。合成配方是一个树形结构比如“动力鞋”由“速度之靴”和“锁子甲”合成而“速度之靴”又可能由其他基础物品合成。在页面上展示合成树的时候我用了一个递归组件来渲染。Astro 支持组件递归但需要注意避免无限递归——我在数据里加了一个depth字段限制最大深度防止数据错误导致页面崩溃。装备的搜索和筛选是纯前端实现的。因为数据量不大几百个物品全部加载到内存里也就几十 KB用 JavaScript 做实时筛选完全没问题。我用了一个简单的模糊搜索算法支持拼音首字母匹配比如输入“ljd”就能搜到“雷击刀”。这个功能用户反馈很好因为很多玩家记不住装备的全名但记得大概的拼音。3.4 页面性能优化从 3 秒到 0.8 秒的实战记录项目第一版上线后我用 Lighthouse 跑了一下性能得分只有 60 多分首屏加载时间接近 3 秒。这个成绩不能接受于是我做了一轮系统的优化。第一个瓶颈是图片。英雄头像和装备图标都是外链有些图片尺寸很大加载慢。我的解决方案是在构建时把所有图片下载到本地用 Sharp 库统一压缩成 WebP 格式并且生成多种尺寸的缩略图。列表页用 64x64 的小图详情页用 256x256 的大图。这样图片体积从平均 50KB 降到了 8KB 左右。第二个瓶颈是 JavaScript 体积。虽然 Astro 默认输出零 JS但我用了几个客户端组件实时胜率、搜索框这些组件的依赖被打包进了主 bundle。我检查了一下发现主要是 lodash 和 moment.js 这两个库占了大头。lodash 我只用了几个函数换成了手写的工具函数moment.js 换成了 day.js体积从 70KB 降到了 2KB。第三个瓶颈是字体。我一开始用了一个自定义的中文字体文件大小有 3MB。后来改成了系统字体栈只在标题上用一个轻量的英文字体。这个改动直接让首屏时间减少了 1 秒多。优化之后Lighthouse 性能得分到了 95 分以上首屏加载时间稳定在 0.8 秒左右。这个成绩对于个人项目来说已经很满意了。4. 从零到上线的完整部署流程4.1 本地开发环境搭建与项目初始化先把项目 clone 到本地然后安装依赖。我用的包管理器是 pnpm比 npm 快不少而且对 monorepo 支持更好。虽然这个项目不是 monorepo但 pnpm 的硬链接机制能省不少磁盘空间。git clone https://github.com/yourname/dota2-info-site.git cd dota2-info-site pnpm install安装完成后需要配置环境变量。项目根目录下有一个.env.example文件复制成.env然后填入你的 API 密钥和 Cloudflare 账号信息。API 密钥需要去数据源网站申请免费额度对于个人项目完全够用。cp .env.example .env然后运行开发服务器pnpm devAstro 的开发服务器启动很快默认在localhost:4321。热更新也很灵敏改完代码保存后浏览器几乎立刻刷新。这里有个小技巧如果你在开发 WebSocket 相关的功能本地开发服务器默认不支持 WebSocket。你需要用wrangler dev来启动一个本地的 Workers 环境然后把前端的 WebSocket 地址指向本地。具体做法是在.env里设置PUBLIC_WS_URLws://localhost:8787然后开两个终端一个跑pnpm dev一个跑pnpm wrangler:dev。4.2 Cloudflare Workers 的配置与部署Workers 的配置文件是wrangler.toml里面需要填几个关键信息name dota2-info-worker main src/worker/index.ts compatibility_date 2024-01-01 [vars] API_BASE_URL https://api.example.com [[kv_namespaces]] binding CACHE id your-kv-namespace-id [[durable_objects.bindings]] name WEBSOCKET_SERVER class_name WebSocketServerKV 命名空间需要先在 Cloudflare 控制台创建然后把 ID 填进来。Durable Objects 需要在 Workers 的付费计划里才能用但免费额度足够个人项目使用。部署命令很简单pnpm wrangler deploy第一次部署会提示你登录 Cloudflare 账号授权之后就会自动上传代码。部署完成后Wrangler 会输出一个*.workers.dev的域名你可以先在这个域名上测试确认没问题后再绑定自己的域名。注意Workers 的免费额度是每天十万次请求对于个人项目来说绰绰有余。但如果你在本地开发时频繁调用 API可能会消耗额度。建议在本地开发时用 Wrangler 的本地模式它会在本地模拟 KV 和 Durable Objects不消耗线上额度。4.3 前端静态站点的构建与托管Astro 的构建命令是pnpm build构建产物在dist目录下全是静态文件。托管方式有很多种我选的是 Cloudflare Pages因为它和 Workers 在同一个生态里配置简单而且免费额度很大方——每月五百次构建无限带宽。在 Cloudflare Pages 的控制台里连接 GitHub 仓库设置构建命令为pnpm build输出目录为dist每次 push 代码就会自动构建部署。整个流程大概两分钟比手动上传方便得多。如果你不想用 Cloudflare Pages也可以用 Netlify 或者 Vercel甚至直接扔到对象存储里加个 CDN。Astro 的输出是纯静态的不依赖任何特定的托管平台。4.4 域名绑定与 HTTPS 配置域名绑定在 Cloudflare Pages 的控制台里操作添加自定义域名后按照提示在 DNS 里加一条 CNAME 记录就行。Cloudflare 会自动签发 SSL 证书不需要手动配置。整个过程大概五分钟。这里有个细节如果你同时用了 Workers 和 Pages建议把 API 请求走 Workers 的域名静态资源走 Pages 的域名。这样可以利用浏览器的并发连接数限制加快加载速度。我在 Pages 的_headers文件里配置了缓存策略静态资源缓存一年HTML 缓存一小时。5. 开发与运维中踩过的坑5.1 WebSocket 连接不稳定的排查过程项目上线第一周有用户反馈说实时胜率有时候不更新刷新页面就好了。我自己测试的时候没发现问题因为我的网络环境比较稳定。后来用手机 4G 测试发现确实有这个问题。排查过程比较曲折。首先怀疑是 Workers 的 WebSocket 超时查了文档发现 Cloudflare 的 WebSocket 连接在空闲 100 秒后会被断开。我的定时推送是十分钟一次所以连接在两次推送之间会被断开。解决方案是加一个心跳机制客户端每 30 秒发一个 ping服务器回一个 pong保持连接活跃。改完之后问题依然存在只是频率降低了。继续排查发现是移动端浏览器的省电策略——当页面不可见时浏览器会暂停 JavaScript 执行心跳也停了。解决方案是在页面可见性变化时主动重连这个前面提过。还有一个问题是重连时的数据同步。如果连接断开了五分钟期间数据更新了三次重连后客户端只拿到了最新的一次数据中间的变化丢失了。我的处理方式是在重连成功后客户端主动发一个“请求全量数据”的消息服务器收到后推送完整数据。这样虽然多传了一点数据但保证了状态一致。5.2 第三方 API 限流与缓存策略调整第三方 API 的免费额度是每分钟 60 次请求。我一开始的缓存策略是每个请求都缓存十分钟但忽略了不同数据的更新频率不一样。英雄基础数据一天更新一次就够了胜率数据十分钟更新一次比较合理而在线人数是实时变化的不能缓存。我重新设计了缓存策略用不同的 KV 键前缀区分数据类型设置不同的过期时间。同时加了一个请求合并机制——如果多个客户端同时请求同一种数据只发一次 API 请求其他请求等待结果。这个用 Durable Objects 的原子性操作实现稍微有点复杂但效果很好API 请求量下降了 80% 以上。提示KV 的写入有频率限制每秒最多写一次同一个键。如果你的定时任务更新频率很高建议用 Durable Objects 的存储而不是 KV或者把数据拆分成多个键分散写入压力。5.3 构建时间过长的优化项目初期每次构建要花三到四分钟因为要拉取所有英雄的详细数据一百多个英雄每个都要发一次请求。后来我改成了批量请求一次拉取所有英雄的数据构建时间降到了四十秒左右。另一个优化是增量构建。Astro 本身不支持增量构建但我用了一个技巧把英雄数据缓存到本地文件构建时先检查缓存是否过期如果没过期就直接用缓存。这样在开发时改样式或者改文案不需要重新拉数据构建时间可以降到十秒以内。5.4 常见问题速查表问题现象可能原因排查方法解决方案页面白屏JavaScript 报错打开控制台看报错信息检查客户端组件的依赖是否完整实时数据不更新WebSocket 断开在控制台看 WebSocket 状态检查心跳机制和重连逻辑构建失败API 请求超时看构建日志里的错误信息增加请求重试次数和超时时间图片加载慢外链图片未压缩用 Lighthouse 分析下载到本地并压缩成 WebP部署后样式错乱缓存未更新强制刷新页面在_headers里配置正确的缓存策略Workers 报错环境变量未配置看 Workers 的实时日志检查wrangler.toml和.env6. 开源项目的维护与后续扩展思路6.1 代码仓库的结构与贡献指南项目开源之后陆续有几个人提了 issue 和 PR。为了让协作更顺畅我整理了一下仓库结构dota2-info-site/ ├── src/ │ ├── components/ # 可复用的 UI 组件 │ ├── layouts/ # 页面布局 │ ├── pages/ # 路由页面 │ ├── content/ # 内容集合定义 │ ├── worker/ # Cloudflare Workers 代码 │ └── utils/ # 工具函数 ├── scripts/ # 构建时运行的脚本 ├── public/ # 静态资源 └── wrangler.toml # Workers 配置贡献指南里我写了几条规则提交 PR 前先跑一遍pnpm lint和pnpm test新增功能需要附带测试用例修改数据结构的 PR 需要同步更新类型定义。这些规则看起来繁琐但能避免很多低级错误。6.2 后续可以扩展的方向这个项目目前只做了最基础的功能后续可以扩展的地方还有很多。比如英雄对比功能让用户选择两个英雄展示它们的属性对比和克制关系。这个功能需要一个新的页面和一套对比算法但数据都是现成的。另一个方向是个人战绩查询。如果用户愿意授权可以接入公开的战绩 API展示最近的对局记录和胜率趋势。这个功能涉及用户隐私需要谨慎处理但技术上完全可行。还有一个比较有意思的方向是社区出装分享。让用户提交自己的出装方案其他用户可以点赞和评论。这个需要后端存储和用户系统复杂度会高不少但能让站点从工具型变成社区型。6.3 我在维护开源项目中的几点体会维护开源项目和写自己的代码是两回事。自己的代码怎么写都行但开源项目要考虑别人的使用体验。文档要写清楚配置要有默认值错误信息要友好。我一开始没注意这些结果收到好几个 issue 说“跑不起来”后来花了两天时间把文档重写了一遍问题就少了很多。另外不要害怕拒绝不合理的 PR。有些人会提一些和项目方向不符的功能或者代码质量不达标。礼貌地拒绝并说明原因比勉强合并然后后悔要好。开源是协作不是慈善保持项目的方向清晰比接受所有贡献更重要。最后一点不要给自己太大压力。开源项目是业余时间做的不可能像全职工作那样及时响应。我在仓库首页写明了“维护者响应时间可能较慢”这样用户有预期我也不会因为回复慢了而焦虑。项目能帮到别人当然好但首先它得是我自己用得开心的东西。