H5天气效果开发实战:从天气API接入到Canvas动效完整指南 做H5的天气效果很多人的第一反应是“找一张好看的天气背景图再放个温度数字”但实际上手后会发现天气功能真正的难点不在视觉而在数据。实时气温从哪来、接口挂了页面怎么兜底、雨雪粒子在不同手机上性能差多少这些都是躲不开的问题。我最近在“心知天气”这套方案上完整跑了一版H5天气效果这里把申请密钥、请求封装、天气代码映射、动效实现、定位兜底和踩坑记录全部拆开讲适合要做活动落地页、旅游页面或办公小工具天气模块的开发者参考。1. 为什么直接用天气数据服务而不是自己造1.1 自建气象数据的坑比想象中多先说一个容易上头的事看起来“天气数据”不就是温度、风速、天气现象吗网上抓一份回来存起来不就完了我见过不少团队一开始就是这种思路结果全都倒在稳定性和合法性上。第一气象数据源本身不稳定。你从一个天气网站抓HTML人家改一次前端结构你的解析逻辑就得重写再严重点对方上了验证码或访问频率限制你的服务器IP直接被封页面就会开始显示上个月的数据。第二数据精度和更新频率对不上。很多公开页面展示的是“整点更新”的数据你抓下来之后H5页面里显示的和用户手机自带天气App差一大截用户立刻就觉得“你这是假天气”。第三合规问题。气象数据属于专业领域数据商用和公开发布是有规范和授权要求的自己爬取展示一旦流量起来后续风险很高。所以天气这个功能自建数据源的成本远不止“写个爬虫”这么简单。比较理性的做法是把数据获取交给专业的天气API服务前端专注做展示和交互。H5场景下你需要的不是“自己拥有气象网络”而是“稳定地拿一份干净、结构化的JSON”。1.2 商业天气API到底省了哪些事接商业天气API之后最直接的变化是数据契约变得清晰了。你请求一个接口它返回固定结构的JSON里面有天气现象code、温度、体感温度、湿度、更新时间甚至空气质量。字段什么时候更新、超时怎么处理、配额怎么算都有文档说清楚。稳定性方面也有明显差别。头部天气服务商一般有多级数据源和容灾不会因为某一个天气网站改版就断供。多语言、多单位摄氏度/华氏度、城市拼音/经纬度查询这些都是现成的省掉了翻译和换算的麻烦。我做个简单对比维度自建/爬虫商业天气API数据稳定性受目标站点影响大有服务等级保障返回格式需要维护解析逻辑结构固定文档齐全更新频率取决于抓取策略接口自带实时更新合规性高风险授权明确可商用开发效率需要长期维护一次接入长期可用1.3 “心知天气”这个选型具体好在哪“心知天气”本质上是面向开发者的天气数据服务接入方式简单注册后拿key通过HTTP接口取数据。我选它的原因有几点一是文档中文且参数直观location、unit、language这几个参数一眼就懂对H5团队来说上手成本低。二是免费配额够用做活动页和开发调试阶段可以不花钱先跑通。三是它提供实时天气、逐日预报、空气质量等多个接口后续要扩展“穿衣指数”之类功能也顺路。缺点也要说清楚免费配额有请求频率限制如果你的H5页面流量很大建议换付费档或者让服务端转发另外浏览器直连的话跨域和域名白名单需要提前配置不然上线第一天就会被CORS报错卡住。2. 接入前的准备密钥、参数与跨域2.1 注册应用与密钥管理接入的第一步是在服务商控制台创建一个应用拿到一串密钥一般是key字段。创建应用的时候会让你填域名信息本地开发可以先填localhost上线前再改成正式域名。密钥管理建议注意两点不要把key直接提交到Git仓库。前端代码里经常需要通过环境变量注入比如.env文件并且.env要进.gitignore。线上H5如果直接在前端代码里带key用户打开控制台就能看到。对活动页这种低敏场景还能接受但如果是长期产品最好让后端或云函数做一层转发key只存在于服务端。2.2 常用接口和参数说明我用得最多的是这三个接口用途接口路径以服务商文档为准关键参数实时天气/v3/weather/now.jsonlocation、unit、language逐日预报/v3/weather/daily.jsonlocation、days、start空气质量/v3/airquality/now.jsonlocation参数细节上location很关键。它支持城市拼音形式例如locationdemo_city这种实际开发时填目标城市拼音或城市ID也可以用经纬度形式通常是纬度:经度例如location12.3456:78.9000这里只是一个示例坐标实际使用时由定位结果返回当前经纬度。用经纬度查询的好处是更精准坏处是“示例城市”这种城市名没法直接用必须走坐标。unit取值c或f国内H5页面基本用c。language取zh-Hans时返回的text字段直接就是中文天气现象比如“晴”“多云”“小到中雨”。实时天气接口返回的JSON结构大概是这样的{ results: [ { location: { name: 示例城市, country: CN }, now: { text: 晴, code: 0, temperature: 26, feels_like: 28, humidity: 45 }, last_update: 2025-01-06T10:3008:00 } ] }注意看code字段它是天气现象的数字编码前端动画就是靠它来决定展示晴天、下雨还是下雪。后面的天气现象映射表会详细说。2.3 开发环境的跨域处理浏览器直接请求天气API域名会发生跨域。如果你本地用Vite开发最简单的做法是配一个代理把/api开头的请求转发到天气服务域名。// vite.config.js export default { server: { proxy: { /api: { target: https://api.weather-service.example.com, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } }这样前端代码里请求统一写成/api/v3/weather/now.json不用在代码里写完整的外网地址。生产环境的话一般是在Nginx层做反向代理location /api/ { proxy_pass https://api.weather-service.example.com/; proxy_set_header Host api.weather-service.example.com; }除了代理还要确认服务商的域名白名单里加上了你的H5域名。有些天气服务要求先在控制台配置“允许访问的域名”否则就算代理通了也会返回权限错误。3. 核心实现从接口数据到天气动效3.1 先封装一个最小请求模块拿到接口后我习惯先做一个请求函数把key、参数、错误处理统一封装起来。这样页面其他模块调用时不需要关心接口细节。const API_KEY 这里填你申请的key const BASE_URL /api/v3/weather/now.json export async function getNowWeather({ location demo_city, unit c } {}) { const params new URLSearchParams({ key: API_KEY, location, unit, language: zh-Hans }) const response await fetch(${BASE_URL}?${params.toString()}) if (!response.ok) { throw new Error(天气接口HTTP异常${response.status}) } const data await response.json() // 部分天气服务在出错时会返回 status_code 或 status 字段 if (data.status_code data.status_code ! ok) { throw new Error(data.status || 天气接口返回异常) } const first data.results?.[0] if (!first) { throw new Error(天气数据为空) } return first }这里我用URLSearchParams来拼参数它会自动处理特殊字符。如果你习惯手拼字符串记得对中文和冒号做encodeURIComponent不然城市名带中文时很容易出乱码。另一个小细节是防止重复请求。页面可能同时有几个组件都需要天气数据如果各自发一次请求不仅浪费配额还容易触发频率限制。我加了一个简单的请求锁let weatherPromise null function fetchWeatherOnce(params) { if (!weatherPromise) { weatherPromise getNowWeather(params).finally(() { weatherPromise null }) } return weatherPromise }这样同一时间只有一个请求在飞其他调用方直接复用这个Promise。3.2 天气代码映射这张表得做好天气服务返回的code字段是驱动动画的核心。不同的服务商编码可能略有不同我在项目里整理了一份映射把code转成业务语义再绑定到对应的样式类code中文含义页面主题0晴艳阳高照1多云云层移动2阴灰暗色调3阵雨雨滴稀疏4雷阵雨闪电雨幕7小雨雨滴缓慢8中雨雨滴加速9大雨雨幕变密14小雪雪花飘落15中雪雪花加速16大雪大雪白雾30雾迷雾粒子23沙尘暴黄褐色调实际开发时我先建一个weather-theme的枚举对象const WEATHER_THEME_MAP { 0: sunny, 1: cloudy, 2: overcast, 3: shower, 4: thunderstorm, 7: light-rain, 8: moderate-rain, 9: heavy-rain, 14: light-snow, 15: moderate-snow, 16: heavy-snow, 30: fog, 23: sandstorm } export function getWeatherTheme(code) { return WEATHER_THEME_MAP[code] || unknown }页面拿到天气数据后把返回的code传给getWeatherTheme得到主题类名然后设置到页面的根节点上。CSS里定义.weather-sunny、.weather-light-rain这些类名对应的背景和动效。3.3 用Canvas做雨雪粒子动效天气动效里最重头的就是雨和雪。我最初试过纯CSS模拟雨丝但效果一般后来改成Canvas粒子方案。简单说下原理在页面上叠加一块全屏透明的Canvas根据天气type生成若干个粒子对象每一帧更新粒子坐标并重绘。以雨滴为例最简版可以这样写function createRainPainter(canvas, { wind 1, speed 8 } {}) { const ctx canvas.getContext(2d) let raf null const drops [] const count 80 for (let i 0; i count; i) { drops.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height }) } function frame() { ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.strokeStyle rgba(160, 190, 220, 0.65) ctx.lineWidth 1 drops.forEach((d) { d.x wind d.y speed if (d.y canvas.height) { d.y -8 d.x Math.random() * canvas.width } if (d.x canvas.width) { d.x 0 } ctx.beginPath() ctx.moveTo(d.x, d.y) ctx.lineTo(d.x 0.6, d.y 12 * (speed / 8)) ctx.stroke() }) raf requestAnimationFrame(frame) } frame() return function stop() { if (raf) { cancelAnimationFrame(raf) raf null } } }核心思路就是每个雨滴在每次帧刷新时向下移动speed个像素向侧面偏移wind个像素超出底部就重置到顶部。requestAnimationFrame会匹配屏幕刷新率动画看起来会比较顺滑。雪花的实现类似区别是雪花下落更慢、有左右摇摆绘制时可以用小圆形代替线段。晴天和阴天则不需要Canvas用CSS动画就够了。比如晴天的太阳光晕呼吸效果.weather-sunny .sun { animation: breathe 6s ease-in-out infinite; } keyframes breathe { 0%, 100% { transform: scale(1); opacity: 0.9; } 50% { transform: scale(1.08); opacity: 1; } }如果目标设备不支持Canvas可以做一个降级直接显示一张静态天气背景图至少不会出现空白页。3.4 把数据组装到页面数据到页面的组装一般包含这几块信息城市名显示当前城市定位失败时显示默认城市天气图标/大背景根据天气主题展示不同画面温度实时温度单位是摄氏度天气现象文字晴、多云、小雨等辅助信息体感温度、湿度、更新时间以温度为例接口里返回的temperature是字符串前端需要转成数值再渲染const weather await getNowWeather({ location: demo_city }) document.querySelector(.city-name).textContent weather.location.name document.querySelector(.temperature).textContent ${Math.round(Number(weather.now.temperature))}° document.querySelector(.weather-text).textContent weather.now.text注意数据加载完成后再设置内容不要让页面先展示一个空壳。加载中的状态我用一个骨架屏来撑住后面会讲。4. 定位、缓存、异常兜底4.1 定位逻辑能定位就定位不行就给默认城市H5页面在城市定位上最容易踩坑。浏览器有navigator.geolocation接口但使用有限制必须在HTTPS环境下本地开发用http://localhost一般可以线上如果还是HTTP接口会被浏览器拦截。用户必须授权地理位置权限拒绝权限后就拿不到坐标。内置浏览器的定位能力不稳定有些WebView不会对网页暴露定位能力。我的流程是这样的先尝试navigator.geolocation.getCurrentPosition拿坐标拿不到就回退到城市选择器用户可以从热门城市列表里手动选。function getPosition() { return new Promise((resolve, reject) { if (!navigator.geolocation) { reject(new Error(当前浏览器不支持地理定位)) return } navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 8000, maximumAge: 5 * 60 * 1000 }) }) }拿到坐标后转成纬度:经度的格式传给天气接口。这里要注意不同天气服务的经纬度顺序可能不同有的要求纬度:经度有的要求经度:纬度接入前一定看文档确认。如果定位失败我会用一个静态数组保存20个热门城市做成一个横向滚动选择条。用户点哪个城市就查哪个城市交互很简单也规避了搜索接口的额外对接成本。4.2 缓存策略与弱网兜底天气数据属于“短时数据”但它不是“实时秒级数据”。你完全可以在一段时间内复用上一次的数据减少API调用量。我用本地缓存做了一个30分钟的过期时间const CACHE_PREFIX h5_weather_ const MAX_AGE 30 * 60 * 1000 function getCache(location) { const key CACHE_PREFIX location const raw localStorage.getItem(key) if (!raw) return null const parsed JSON.parse(raw) if (Date.now() - parsed.timestamp MAX_AGE) return null return parsed.data } function setCache(location, data) { const key CACHE_PREFIX location localStorage.setItem(key, JSON.stringify({ timestamp: Date.now(), data })) }当接口请求失败时我会优先读取缓存里的旧数据展示同时在页面上显示一个“数据更新失败”的轻提示而不是直接让用户看到空白。重试按钮也要给用户准备。最简单的做法是加一个遮罩层提示“天气信息加载失败了点我重试”点击后重新走一遍获取流程。4.3 骨架屏、可访问性与机型适配天气数据请求有网络延迟页面会经历“空白期”。我用一个骨架屏撑着用灰色色块模拟天气图标和文字的位置数据返回后再替换成真实内容。这样用户观感会好很多不会误以为页面坏了。可访问性也值得注意。H5页面如果会被读屏软件读取最好给天气容器加上aria-label比如“当前示例城市晴26摄氏度湿度百分之四十五”。这个成本很低但体验提升明显。机型适配方面我遇到最多的是刘海屏和全面屏的安全区问题。天气信息通常放在页面顶部需要适配env(safe-area-inset-top)否则在部分手机上信息会被挖孔区域遮住。5. 实战中踩过的坑与排查速查表5.1 跨域、HTTPS定位失效的典型现场第一个坑本地直接打开一个index.html用file://协议去请求天气API结果控制台报CORS错误。这个不用想file://协议没有origin头很多接口直接拒绝。正确做法是本地起一个静态服务器再用代理转发。第二个坑线上H5配了Nginx代理但还是偶尔报502。排查后发现目标天气服务域名只能从指定出口IP访问服务器IP不在白名单里。解决办法是在服务商后台把服务器IP加到白名单或者确认代理转发时没有丢弃必要的请求头。第三个坑内置浏览器里位置权限弹窗不出现。最常见的场景是某些超级App的网页容器没有给HTML5定位授权getCurrentPosition方法直接走error回调。这种情况下不要硬扛回退到IP定位或者城市选择器即可。第四个坑API key暴露。我在开发版里把key写死在代码里结果被同事提交到了一个公开代码仓库当天就被人盗刷了配额。后来整改成key走服务端转发前端再用短时效的临时token去换取数据。5.2 请求返回异常速查表现象可能原因处理方式返回“认证失败”key错误或已被删除检查控制台里的key是否一致返回“域名不允许”未配置域名白名单在控制台加上当前H5域名请求正常但数据为空location拼写错误或城市不存在用接口提供的城市搜索确认频繁提示“请求频率超出”超过免费配额加缓存节流或升级套餐温度显示“-”unit参数传了不支持的值统一传c或f数据是上一天的本地缓存过期时间太久检查缓存逻辑缩短MAX_AGE5.3 让动画省电的几条建议移动端Canvas粒子动画最容易遇到的问题就是掉帧和发热。分享几个实测有效的手段控制粒子数量。中低端手机雨滴粒子数降到40左右中高端机再提到80。Canvas画布尺寸不要超过物理像素太多。按devicePixelRatio缩放是必要的但不要无脑设为3。页面不可见时暂停动画。监听visibilitychange事件在后台时cancelAnimationFrame切回前台再恢复既能省电也能减少发热。动画属性优先用transform和opacity这些属性走GPU合成避免用filter: blur()这种代价较高的效果。如果检测到低端机型直接降级为静态背景图不做粒子。这些优化做完中端Android机上基本能保持流畅不会因为一个天气动效把页面整体拖垮。6. 收个尾这类H5天气页面的几个心得把整个流程跑完之后我最大的感受是天气动效项目的技术重点不在于“画得好看”而在于数据和效果之间那层映射关系是否稳。接口返回的code写死前一定要对照服务商文档逐条确认渲染动效前一定要考虑低端机的情况定位失败时一定要给用户一条体面的退路。另外密钥和跨域这类工程细节最好在项目启动第一天就处理好不要等到联调阶段才被CORS和权限弹窗卡住。缓存策略也值得多花半小时设计它不仅能省API配额还能让你的页面在弱网环境下显得更可靠。如果你之后要做类似的项目我的建议是先花一天时间把接口文档读透再花半天做一张完整的天气code映射表剩下的时间其实都花在动效和兜底上。把“数据到视觉”这条管线理顺H5天气效果就没有想象中那么复杂了。