超强台风"巴威"逼近的那个凌晨,我睡不着。

0 阅读11分钟

超强台风"巴威"逼近的那个凌晨,我睡不着。

不是因为怕,是白天当了一天牛马,脑子转不动了,躺床上习惯性打开手机,想看看台风到底什么情况。结果越刷越乱:

  • 这个博主说"史上最强,千万别出门",
  • 那个博主说"没那么严重,都是标题党",
  • 划两下刷到一条三天前的旧闻, 还有人在评论区吵"到底信谁的"。

刷了二十分钟,我一个消息没搞清楚, 倒是学会了三种"台风穿搭"和两首"台风BGM"。 躺在出租屋的床上,我脑子里其实只剩三个问题

  1. 台风现在在哪、多强?
  2. 离我家还有多久?
  3. 我现在到底该干嘛?

就这三个问题,翻遍手机里的软件, 居然没有一个地方能一次讲清楚。

索性爬起来,打开电脑,用 Claude Fable 5 把想法一点点敲了出来。把基于中央气象台、日本气象厅、美国 JTWC、台湾气象署四家的路径数据放在同一张地图上之后,很多事一眼就看明白了——比如此刻,几家的预测登陆点正在往温州附近收敛。

然后顺手加了几样东西:

  • 实时新闻聚合。比官方数据更快,分布式数据的二次验证,但不一定准,请自行甄别;
  • 城市波及倒计时。大概还有多久,巴威到你所在的城市,基于风速和地图向量估算的,不一定准;
  • 应急指南和紧急电话。真到了要用的时候,不用再去搜。

这个网站纯粹是自己做的:没有广告,不收集数据,数据来源全是官方,防灾决策请以官方预警为准(不信谣、不传谣)。

做这个的时候就一个念头:能让身边人早知道一点,心里也能踏实一点。

如果你有江浙沪的朋友,欢迎转给 ta。如果你是开发者,往下读——我会把它怎么运转的、哪里还很粗糙、你能怎么帮忙,都摊开讲清楚。


二、它长什么样

先看图,再聊技术。

打开就是一张会动的地图

01-overview-city-countdown.png

实测路径按国标六级强度配色,从深海一路"烧"到近海。左边挂着一列城市倒计时,最紧迫的排最前面。顶上一条预警横幅,告诉你现在谁最危险。

点一下你关心的城市

02-city-popup.png

镜头飞过去,弹出一张小卡片:风圈预计什么时候到、最近距离多少公里、现在该做什么。分享链接也帮你想好了——?city=温州,发给温州的朋友,打开直接聚焦到 ta 家。

四家机构的预报,摆在一起看

截屏2026-07-07 12.09.07.png

中、日、美、台四家的预报虚线可以独立开关。预报本来就有分歧,分歧本身就是信息——四条线收得越拢,说明路径越确定;散得越开,越要多留个心眼。

新闻和指南,都在手边

03-news-panel.png

截屏2026-07-09 11.41.12.png

新闻是媒体视角,比官方报文快,但请自行甄别。指南是静态内容,从"来临前 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.tsimpact.tsgeo.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 → 定时任务轮询)与两个核心目标冲突:

  1. Cloudflare 部署:Workers/Pages 无法运行 matplotlib/cartopy 栈;
  2. 专业动效:静态 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 返回多级 ETAsrc/impact.ts
按定位排序城市navigator.geolocation + 距离排序src/app.ts
多台风切换TYPHOON_ID 改成路由参数——台风年年来,这个网站不该只为巴威活一次src/app.ts · worker/index.ts
地方化指南各省防汛细则、避险点。你比我更懂你的城市src/guide.ts

体验与传播

方向说明
i18n 繁体/英文面向港澳台与外籍居民
预警等级接入对接中央气象台预警信号
PWA 离线缓存断网前的最后一次数据,离线也能看——台风天最容易断网
分享卡片动态生成OG 图按城市倒计时实时渲染

数据源扩展(需合规评估)

现状思路
小红书反爬,无法匿名抓取需用户授权或官方 API
微博需访客 CookieWorker 端谨慎实现,注意 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…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。