主要观点总结
本文介绍了作者如何使用HTTP特性实现简单的实时更新技术,特别是在公交实时追踪的网页应用中的实践。作者探索了不同的更新策略,如轮询、服务器发送事件(SSE)以及使用If-Modified-Since和Last-Modified头优化轮询。文章还涵盖了工程优化,如封装Hook提高复用性、服务端判断逻辑抽象为工具函数、容错与错误日志处理等。最后,作者提到了在DigitalOcean App Platform和Cloudflare CDN中遇到的挑战,并描述了如何通过自定义请求头绕过这些挑战。此外,文章还讨论了寻找合适平台的考量因素。
关键观点总结
关键观点1: 实时更新技术的探索和实践
关键观点2: 使用HTTP头优化轮询
关键观点3: 工程优化和封装
关键观点4: 在DigitalOcean App Platform和Cloudflare CDN中的挑战和解决方案
关键观点5: 寻找合适平台的考量因素
正文
,
}
,
}
)
;
}
最初的问题其实是 React 的重新
渲染
。我只想在数据真的更新了时才更新 UI。虽然可以在前端判断数据是否有变化,但需要做一次深比较,还是有点麻烦。而公交系统给我的数据里本来就带了一个更新时间戳,所以我就把这个时间戳一起存入 Redis,然后前端拿这个时间戳来判断是否更新。
function VehiclePositions(){
const [vehicles, setVehicles]= useState<VehiclePosition[]>([]);
const lastUpdatedRef = useRef(0);// Start at 0 so the first one will always be newer
useEffect(()=>{
const interval = setInterval(async()=>{
const resp = awaitfetch("/api/vehicle-positions");
if(!resp.ok) return;
const{ lastUpdated, vehiclePositions } = await resp.json();
// If the update time is older than or the same as the previous
// Do nothing
if(lastUpdated <= lastUpdatedRef.current) return;
setVehicles(vehiclePositions);
},1_000);// Poll every second
return()=>clearInterval(interval);
},[]);
return(
<>
{vehicles.map(({ vehicleId, position })=>(
<Marker
key={vehicleId}
position={position}
/>
))}
</>
);
}
这个方案效果已经很好了。但我还是觉得可以更进一步。虽然请求很轻,但大多数情况下我还是在传输和解析一些不会用到的数据。于是我想:既然客户端有更新时间戳,能不能把它发给服务器,服务器只有在数据更新时才返回内容?
最开始我想把时间戳放在 URL 的查询参数里,始终返回 200 状态码,如果响应体为空我就不更新。但总觉得这种做法不太优雅。于是我把想法讲给 ChatGPT,问有没有更标准的做法。我不想让这变成一篇夸 ChatGPT 的文章,但确实在你不知道要查什么关键词的时候,LLM 是很好的工具。ChatGPT 给我指出了 If-Modified-Since 和 Last-Modified 这两个标准 HTTP 头,我查了文档后发现:这不正是我需要的吗!
原理和我之前的
方案
类似,但这是浏览器已经实现好的标准方式。现在我可以把更新时间戳放在响应头里的
Last-Modified
,而浏览器会自动在下次请求时带上
If-Modified-Since
。如果数据没更新,服务器返回一个 304 Not Modified 状态码,表示不需要更新内容。
这个方式比我自定义的方案好太多了。浏览器全自动处理,数据与元数据分离更清晰。而且它的效果跟
SSE
有点像:服务器决定什么时候更新,只在数据变化时才返回内容。而因为时间戳来自公交系统,不是我生成的,也避免了时钟偏差的问题。
整合起来后,我把 “是否更新” 的逻辑从客户端挪到了服务器端,代码还变得更简单了。不仅避免了不必要的渲染,还省下了带宽。我还可以用同一个时间戳来提前结束后台更新任务。虽然车辆位置数据更新本来也不重,但到站时间的数据更复杂,这种优化能节省更多资源。
前端代码:
function VehiclePositions(){
const[vehicles, setVehicles]= useState<VehiclePosition[]>([]);
useEffect(()=>{
const interval = setInterval(async()=>{
const resp = awaitfetch("/api/vehicle-positions");
if(!resp.ok)return;
if