超强台风"巴威"逼近的那个凌晨,我睡不着。
不是因为怕,是白天当了一天牛马,脑子转不动了,躺床上习惯性打开手机,想看看台风到底什么情况。结果越刷越乱:
- 这个博主说"史上最强,千万别出门",
- 那个博主说"没那么严重,都是标题党",
- 划两下刷到一条三天前的旧闻, 还有人在评论区吵"到底信谁的"。
刷了二十分钟,我一个消息没搞清楚, 倒是学会了三种"台风穿搭"和两首"台风BGM"。 躺在出租屋的床上,我脑子里其实只剩三个问题
- 台风现在在哪、多强?
- 离我家还有多久?
- 我现在到底该干嘛?
就这三个问题,翻遍手机里的软件, 居然没有一个地方能一次讲清楚。
索性爬起来,打开电脑,用 Claude Fable 5 把想法一点点敲了出来。把基于中央气象台、日本气象厅、美国 JTWC、台湾气象署四家的路径数据放在同一张地图上之后,很多事一眼就看明白了——比如此刻,几家的预测登陆点正在往温州附近收敛。
然后顺手加了几样东西:
- 实时新闻聚合。比官方数据更快,分布式数据的二次验证,但不一定准,请自行甄别;
- 城市波及倒计时。大概还有多久,巴威到你所在的城市,基于风速和地图向量估算的,不一定准;
- 应急指南和紧急电话。真到了要用的时候,不用再去搜。
这个网站纯粹是自己做的:没有广告,不收集数据,数据来源全是官方,防灾决策请以官方预警为准(不信谣、不传谣)。
做这个的时候就一个念头:能让身边人早知道一点,心里也能踏实一点。
如果你有江浙沪的朋友,欢迎转给 ta。如果你是开发者,往下读——我会把它怎么运转的、哪里还很粗糙、你能怎么帮忙,都摊开讲清楚。
二、它长什么样
先看图,再聊技术。
打开就是一张会动的地图
实测路径按国标六级强度配色,从深海一路"烧"到近海。左边挂着一列城市倒计时,最紧迫的排最前面。顶上一条预警横幅,告诉你现在谁最危险。
点一下你关心的城市
镜头飞过去,弹出一张小卡片:风圈预计什么时候到、最近距离多少公里、现在该做什么。分享链接也帮你想好了——?city=温州,发给温州的朋友,打开直接聚焦到 ta 家。
四家机构的预报,摆在一起看
中、日、美、台四家的预报虚线可以独立开关。预报本来就有分歧,分歧本身就是信息——四条线收得越拢,说明路径越确定;散得越开,越要多留个心眼。
新闻和指南,都在手边
新闻是媒体视角,比官方报文快,但请自行甄别。指南是静态内容,从"来临前 48 小时"写到"过后安全确认",紧急电话都列好了——真正慌乱的时刻,没人有耐心再去搜索。
三、它是怎么转起来的
架构简单到有点不好意思:一个 Cloudflare Worker,一堆纯函数,没有数据库,没有服务器。
bash
代码解读
复制代码
┌─────────────────────────────────────────────────────────────┐
│ 用户浏览器(MapLibre GL + TypeScript) │
│ · 路径回放 / 风圈动画 / 城市标记 / HUD / 分享深链 │
└──────────────────────────┬──────────────────────────────────┘
│ /api/typhoon/:id /api/news
┌──────────────────────────▼──────────────────────────────────┐
│ Cloudflare Worker(边缘代理 + 缓存 + 容灾) │
│ · normalize.ts 双源归一化 │
│ · news.ts RSS 资讯聚合 │
│ · 缓存:台风 5min / 资讯 10min │
└──────────┬─────────────────────────────┬────────────────────┘
│ │
浙江水利厅 API(主) Google/Bing News RSS
中央气象台 NMC(备)
但简单是刻意的。灾害面前,系统越简单越可靠。 没有数据库可以宕机,没有服务器需要半夜爬起来重启,边缘节点挂了还有下一个。一个防灾工具首先要做到的,是它自己别先倒下。
选型时想的也是同一件事:
| 层级 | 选型 | 为什么 |
|---|---|---|
| 边缘计算 | Cloudflare Workers | 零运维、全球 CDN、国内可访问 |
| 前端地图 | MapLibre GL | 开源、灵活,不依赖 Google |
| 底图 | 高德卫星 + 中文注记 | 国内 CDN 稳,中文标注,合规踏实 |
| 构建 | Vite + TypeScript | 快,且类型兜底 |
| 数据 | 无数据库 | 源站返回全量轨迹,边缘缓存就够了 |
目录一览
python
代码解读
复制代码
worker/
index.ts # 路由、边缘缓存、双源容灾
normalize.ts # CMA/ZJ 数据 → TyphoonData(纯函数)
news.ts # RSS 解析、聚合与快照合并(纯函数)
src/
app.ts # 入口:回放控制、HUD、分享、倒计时刷新
map.ts # MapLibre 图层、城市标记、风圈动画
impact.ts # 城市波及倒计时算法(纯函数)
geo.ts # 球面距离、风圈多边形、轨迹插值
guide.ts # 应对指南内容
news.ts # 资讯面板渲染
mobile.ts # 移动端布局:顶部实况条 + 底部三档抽屉
slogan.ts # 正能量口号轮播
几个写代码时守着的习惯:
- 数据归一化和算法尽量写成纯函数(
normalize.ts、impact.ts、geo.ts),好测、好复用,出了问题也好定位; - 画图和动画都关在
map.ts里,业务逻辑不跟渲染纠缠; - Worker 只做代理和缓存。它越"笨",越不容易坏。
一个踩过的坑,分享给你
新闻聚合上过一次当。原来的逻辑是每次抓取后覆盖缓存快照——某个时刻 Google 被限流、Bing 恰好只回了 1 条,这份贫瘠的结果就把之前积累的 30 条完整快照冲掉了。用户打开一看,资讯页只剩孤零零一条。
修复之后,刷新改成了合并:新条目优先,历史条目去重补位,七天窗口内只增不减。这个教训值得写在这里:对一个防灾工具来说,一次糟糕的抓取,永远不该毁掉已有的积累。
四、如何上手?
2026 年第 9 号台风「巴威 BAVI」实时路径追踪与多机构预报对比系统。 边缘架构:Cloudflare Worker(数据代理 + 缓存 + 双源容灾) + MapLibre GL 动效前端。
人类正面对天灾。一个人力量有限,众志成城,总会集结智慧对抗天灾。 欢迎提 Issue / PR 共建,转发给需要的人,提前预防,减少灾情影响。
线上访问:chinaupdated.com
快速开始(克隆与部署)
bash
代码解读
复制代码
git clone git@github.com:Trade-Offf/typhoon-bavi-tracker.git
cd typhoon-bavi-tracker
npm install
npm run dev # 本地开发
npm run deploy # 构建并发布到 Cloudflare Workers
首次推送到 GitHub(维护者): github.com/cmfat86/inf… github.com/kokenhave/a… github.com/kokenhave/a… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/warren133/j… github.com/warren133/j… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/kokenhave/a… github.com/kokenhave/a… https://githttps://github.com/ianhilli/zeiqbm/blob/main/%E3%80%90%E6%83%98T%20%20L%20%208%20%208%207%E2%80%A2c%20%200%20M%E3%80%919%E8%B4%B5%E5%AE%BE%E4%BC%9A%E4%BC%9A%E5%91%98%E8%B4%A6%E5%8F%B7%E7%99%BB%E5%BD%95%E4%BB%A5%E5%8F%8A%E6%B3%A8%E5%86%8C%E5%AE%98%E6%96%B9%E7%BD%91%E5%9D%80 github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/gemini06fr/… github.com/gemini06fr/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/gemini06fr/… github.com/gemini06fr/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/kokenhave/a… github.com/kokenhave/a… github.com/kokenhave/a… github.com/kokenhave/a… github.com/gemini06fr/… github.com/gemini06fr/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/gemini06fr/… github.com/gemini06fr/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/gemini06fr/… github.com/gemini06fr/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/gemini06fr/… github.com/gemini06fr/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/myounch020/… github.com/myounch020/… github.com/warren133/j… github.com/warren133/j… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/kokenhave/a… github.com/kokenhave/a… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/gemini06fr/… github.com/gemini06fr/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/myounch020/… github.com/myounch020/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/gemini06fr/… github.com/gemini06fr/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/myounch020/… github.com/myounch020/… github.com/warren133/j… github.com/warren133/j… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/gemini06fr/… github.com/gemini06fr/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/myounch020/… github.com/myounch020/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/warren133/j… github.com/warren133/j… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/myounch020/… github.com/myounch020/… github.com/warren133/j… github.com/warren133/j… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/kokenhave/a… github.com/kokenhave/a… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/gemini06fr/… github.com/gemini06fr/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/warren133/j… github.com/warren133/j… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/gemini06fr/… github.com/gemini06fr/… github.com/warren133/j… github.com/warren133/j… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/gemini06fr/… github.com/gemini06fr/… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/warren133/j… github.com/warren133/j… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/warren133/j… github.com/warren133/j… github.com/gemini06fr/… github.com/gemini06fr/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/gemini06fr/… github.com/gemini06fr/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/myounch020/… github.com/myounch020/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/gemini06fr/… github.com/gemini06fr/… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/warren133/j… github.com/warren133/j… github.com/gemini06fr/… github.com/gemini06fr/… github.com/warren133/j… github.com/warren133/j… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/gemini06fr/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/warren133/j… github.com/warren133/j… github.com/gemini06fr/… github.com/gemini06fr/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/gemini06fr/… github.com/kokenhave/a… github.com/gemini06fr/… github.com/gemini06fr/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/warren133/j… github.com/warren133/j… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/warren133/j… github.com/warren133/j… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/gemini06fr/… github.com/gemini06fr/… github.com/myounch020/… github.com/myounch020/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/myounch020/… github.com/myounch020/… 0 github.com/ianhilli/ze… github.com/myounch020/… github.com/myounch020/… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/gemini06fr/… github.com/gemini06fr/… github.com/myounch020/… github.com/myounch020/… github.com/kokenhave/a… github.com/kokenhave/a… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/gemini06fr/… github.com/gemini06fr/… github.com/myounch020/… github.com/myounch020/… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/ianhilli/ze… github.com/ianhilli/ze… github.com/warren133/j… github.com/warren133/j… github.com/kokenhave/a… github.com/kokenhave/a… github.com/cmfat86/inf… github.com/cmfat86/inf… github.com/myounch020/… github.com/myounch020/… github.com/ianhilli/ze… github.com/gemini06fr/…
bash
代码解读
复制代码
git remote add origin git@github.com:Trade-Offf/typhoon-bavi-tracker.git
git branch -M main
git push -u origin main
为什么不是 Python + matplotlib?
原始方案(Python 抓取 → matplotlib/cartopy 出静态 PNG → 定时任务轮询)与两个核心目标冲突:
- Cloudflare 部署:Workers/Pages 无法运行 matplotlib/cartopy 栈;
- 专业动效:静态 PNG 无法承载路径回放、风圈缩放、台风眼旋转等交互动效。
因此改为 Web 原生架构,Python 侧的「抓取/去重/存储/调度」职责全部被以下机制替代:
| 原 Python 方案 | 现方案 |
|---|---|
| APScheduler 每小时抓取 | 用户请求触发 + 边缘缓存 5 分钟(数据源本身 3 小时一报) |
| CSV/SQLite 存储去重 | 数据源返回全量历史轨迹,无需自建存储 |
| matplotlib 静态图 | MapLibre GL 实时渲染 + 路径回放动画 |
| requests 重试/超时 | Worker 内置超时 + 主备双数据源自动切换 |
数据源(已实测验证)
- 主源:浙江省水利厅台风 API(
typhoon.slt.zj.gov.cn),单接口聚合 中央气象台 / 日本气象厅 / JTWC(美国) / 台湾气象署 四家机构的实测与预报; - 备源:中央气象台台风网(
typhoon.nmc.cn)JSONP 接口,主源失效时 Worker 自动切换; - 资讯:Google News / Bing News RSS 聚合(数十家媒体,10 分钟边缘缓存)。
性能说明
- MapLibre 独立分包(
manualChunks),首屏与地图库并行加载 - 台风 API
preload,资讯面板延迟 2.5s 加载,不阻塞地图 - 页面不可见时暂停动画循环,降低后台 CPU
- 已移除 国外 CARTO 底图与 MapLibre 字体服务,消除控制台报错
五、这些事,等你来做
下面的方向按"能多帮到一个人"排序,不按技术难度。在这个项目里,加一个城市比换一个框架更有价值。
防灾价值直接提升
| 方向 | 说明 | 从哪改 |
|---|---|---|
| 增加城市 | 在 CITIES 数组加 { name, lng, lat },就这么简单 | src/impact.ts |
| 10/12 级风圈倒计时 | 扩展 computeImpacts 返回多级 ETA | src/impact.ts |
| 按定位排序城市 | navigator.geolocation + 距离排序 | src/app.ts |
| 多台风切换 | TYPHOON_ID 改成路由参数——台风年年来,这个网站不该只为巴威活一次 | src/app.ts · worker/index.ts |
| 地方化指南 | 各省防汛细则、避险点。你比我更懂你的城市 | src/guide.ts |
体验与传播
| 方向 | 说明 |
|---|---|
| i18n 繁体/英文 | 面向港澳台与外籍居民 |
| 预警等级接入 | 对接中央气象台预警信号 |
| PWA 离线缓存 | 断网前的最后一次数据,离线也能看——台风天最容易断网 |
| 分享卡片动态生成 | OG 图按城市倒计时实时渲染 |
数据源扩展(需合规评估)
| 源 | 现状 | 思路 |
|---|---|---|
| 小红书 | 反爬,无法匿名抓取 | 需用户授权或官方 API |
| 微博 | 需访客 Cookie | Worker 端谨慎实现,注意 ToS |
| 地方气象局 | 多数有公开接口 | 在 worker/ 加 fetcher 模块 |
还未实现的功能(欢迎认领)
- 风圈多边形点数按需简化(低缩放级别)
- Service Worker 缓存最近一次 API 响应
- 截图/OG 图 WebP 压缩
- 单元测试:
impact.ts·normalize.ts·geo.ts
六、提 MR 的姿势
流程很平常,Fork、开分支、自测、发 PR:
bash
代码解读
复制代码
git checkout -b feat/your-feature-name
npm run typecheck # 必须过
npm run build # 必须过
npm run preview # 本地把 Worker + 前端跑通
几个小约定:
- commit message 讲"为什么" ,别只写"做了什么";
- 一个 MR 只做一件事。加城市、修数据源、改样式,分开提;
- 附上验证方式。截图或录屏,让 review 的人两分钟能确认。
比如这样:
diff
代码解读
复制代码
feat(impact): 新增厦门、泉州城市倒计时
- 在 CITIES 数组补充闽南沿海城市
- 验证:打开 /?city=厦门 可看到倒计时弹窗
Review 时我会看类型安全、依赖体积、数据源的超时与容灾。但有一条比所有代码规范都重要:
这个项目的每一行文案,都可能在某个深夜被一个正在担心家人的人读到。 我们提供确定性,不制造焦虑;传递紧迫感,不贩卖恐慌。免责声明必须保留:倒计时是估算,防灾决策以官方预警为准。
七、为什么开源
防灾信息不应该被锁在任何人的服务器里——包括我的。
这个项目诞生于一个睡不着的凌晨,但它不该只属于那个凌晨。台风年年来,数据源会失效,城市名单会过时,指南会需要更新。一个人的热情撑不起一件需要长期存在的事,能撑起它的只有社区。
开源之后,事情就变得可以想象:数据源挂了,有人半小时内提个 fix;福建的朋友补上福建的避险点;有人 fork 一份部署到自己的域名,哪怕有一天 chinaupdated.com 不在了,代码还在,还能救急。
写代码的人常常怀疑自己做的东西有没有意义。但防灾这件事给了一个罕见的、清晰的答案——
技术人的理想主义,有时候就是写一段代码,让多一个人提前看到危险。
你修的每一个 bug,可能就是某个用户在断网前抢到的最后一次刷新;你加的每一个城市,背后都是真实住在那里的人。这里没有 KPI,没有增长曲线,只有一个朴素的度量:有没有人因为它,提前把窗户加固了、把水囤上了、把老人接走了。
欢迎 Star、Fork、提 Issue、提 MR。一个人力量有限,众人拾柴,总能多照亮一段路。
台风会走,代码会留下。愿它下一次被用到时,比这一次更好。
作者:HiSt
链接:juejin.cn/post/765939…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。