☰
H5天气效果开发实战:从天气API接入到Canvas动效完整指南
2026/10/14 4:22:52 网站建设 项目流程

做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很关键。它支持城市拼音形式,例如location=demo_city这种(实际开发时填目标城市拼音或城市ID);也可以用经纬度,形式通常是纬度:经度,例如location=12.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:30+08: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_AGE

5.3 让动画省电的几条建议

移动端Canvas粒子动画最容易遇到的问题就是掉帧和发热。分享几个实测有效的手段:

  • 控制粒子数量。中低端手机雨滴粒子数降到40左右,中高端机再提到80。
  • Canvas画布尺寸不要超过物理像素太多。按devicePixelRatio缩放是必要的,但不要无脑设为3。
  • 页面不可见时暂停动画。监听visibilitychange事件,在后台时cancelAnimationFrame,切回前台再恢复,既能省电也能减少发热。
  • 动画属性优先用transform和opacity,这些属性走GPU合成,避免用filter: blur()这种代价较高的效果。
  • 如果检测到低端机型,直接降级为静态背景图,不做粒子。

这些优化做完,中端Android机上基本能保持流畅,不会因为一个天气动效把页面整体拖垮。

6. 收个尾:这类H5天气页面的几个心得

把整个流程跑完之后,我最大的感受是:天气动效项目的技术重点不在于“画得好看”,而在于数据和效果之间那层映射关系是否稳。接口返回的code写死前,一定要对照服务商文档逐条确认;渲染动效前,一定要考虑低端机的情况;定位失败时,一定要给用户一条体面的退路。

另外,密钥和跨域这类工程细节最好在项目启动第一天就处理好,不要等到联调阶段才被CORS和权限弹窗卡住。缓存策略也值得多花半小时设计,它不仅能省API配额,还能让你的页面在弱网环境下显得更可靠。

如果你之后要做类似的项目,我的建议是:先花一天时间把接口文档读透,再花半天做一张完整的天气code映射表,剩下的时间其实都花在动效和兜底上。把“数据到视觉”这条管线理顺,H5天气效果就没有想象中那么复杂了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询