简介:这是一份微信小程序天气预报项目的完整源码,附带运行截图,适合正在学习小程序开发或需要快速搭建天气类应用的初中级开发者参考。源码基于微信小程序原生框架,包含页面布局、交互逻辑与样式配置,可帮助读者理解从城市选择、天气数据请求到界面渲染的完整流程。资源共47个文件,以png图片、wxml模板、js脚本、wxss样式、json配置及jpg图片为主,其中wxml与wxss负责页面结构及视觉样式,js承担事件处理和数据逻辑,json用于项目与页面配置,结构清晰,便于按模块查阅。压缩包仅928KB,体量轻量,下载后可快速解压导入微信开发者工具运行。目前已有1390人学习下载,经社区验证具备较高参考价值。通过阅读源码,读者可掌握小程序网络请求封装、生命周期函数运用及多页面数据传递技巧,也可基于现有界面直接替换数据源,扩展为实际可用项目。
1. 微信小程序天气预报源码,为什么值得自己落地一版
在微信小程序的开源资源和各类网盘资料里,"天气预报"是出现频率最高的项目类型之一,"含截图"三个字是这类资源最常见的信任背书——证明源码真的跑起来过。但我接手过的类似源码里,十个有七个打开就报错:天气 API 的 Key 失效,页面字段和接口返回对不上,截图里的城市在代码里根本查不到。问题不在需求本身——天气数据流清晰,非常适合入门——而在源码包分发前没做过环境重建。
下面按一线工程师做同类微信小程序项目实例的流程来写:先拆数据源和请求链,再给一套能直接跑通的原生微信小程序天气预报代码,然后讲截图怎么重新生成、报错怎么定位,最后落到交付前的核对清单。这套路径也适合准备拿天气预报做微信小程序毕业设计的同学,以及想看到完整请求闭环的初学者。
2. 天气预报小程序架构拆解:数据源选型与目录设计
2.1 天气 API 怎么选:免费额度、字段结构与请求链
写天气预报小程序,第一件事不是写页面,而是定数据源。常见做法是"和风天气 + 地图逆地理编码"组合:和风天气提供实时天气、逐日预报和生活指数,微信内置的 wx.getLocation 负责拿经纬度,再用和风天气的 GeoAPI 把经纬度换算成城市信息。这样整套链路里不需要自建城市代码表,城市名和 LocationID 都由接口返回。
| 数据源 | 免费额度(个人订阅) | 接口特点 | 适配场景 |
|---|---|---|---|
| 和风天气 QWeather | 每日约 1000 次请求 | now/7d/indices/GeoAPI 齐全,字段规范 | 本示例与绝大多数毕设、练习项目 |
| 心知天气 | 免费体验次数较少 | 中文文档友好,字段精简 | 只做最小闭环学习 |
| 高德/腾讯地图 | 定位与逆地理编码免费 | 只返回行政区划,不含天气 | 单独做定位,仍需拼天气源 |
请求链是两跳而不是一跳:先wx.getLocation拿经纬度,把经度,纬度传给 GeoAPI 的城市查询接口拿到 LocationID,再用 LocationID 请求实时天气、7 天预报和生活指数。QWeather 的鉴权方式是请求头携带 API Key,免费订阅对应的域名是 devapi 和 geoapi,升级付费后才切成 api 域名。这个细节最容易踩:把付费域名的示例代码直接黏到自己的 Key 上,请求必然 401。
提示:免费订阅每日约 1000 次请求,单用户配合缓存后远用不完;但把 Key 写在源码里分发,别人只要反编译小程序包就能提取,属于这类源码无法根治的固有问题,交付时在文档里写明即可。
2.2 原生微信小程序与 uniapp 的取舍:看清你要的源码形态
下载天气类源码时先分清是原生还是 uniapp 微信小程序。原生项目的标志是目录下有 app.json、app.js,可以直接用微信开发者工具打开;uniapp 项目的标志是 pages.json 和 main.js,页面代码写得再像 Vue,也得经 HBuilderX 或 CLI 编译成 mp-weixin 产物才能在开发者工具里跑。
从源码交付角度看,原生形态最稳:没有编译步骤,任何人在新机器上导入目录就能预览,截图也可以在同一个工具里复现,"含截图"的广告要成立,靠的就是这个可复现性。如果你打算改造成 uniapp 版本,通常的迁移策略是保留接口封装层,把 WXML 换成 Vue 模板,其他逻辑基本可以平移。判断一份源码属于哪种形态,打开目录看有没有 app.json 就够了。
2.3 请求封装:把 wx.request 包成 Promise,让调用链扁平化
原生 wx.request 是回调式写法,一次天气首页要串"定位→城市搜索→三个天气接口",回调嵌套会很乱。我一般先把请求封装成 Promise,两个域名分别暴露给不同模块:
// utils/request.js // 免费订阅用 devapi/geoapi,升级付费订阅后换成对应 api 域名 const HOSTS = { weather: 'https://devapi.qweather.com/v7', geo: 'https://geoapi.qweather.com/v2' } function request(hostKey, path, data = {}) { return new Promise((resolve, reject) => { wx.request({ url: `${HOSTS[hostKey]}${path}`, data, method: 'GET', // 和风天气鉴权:请求头携带 API Key header: { 'X-QW-Api-Key': '你的Key' }, success(res) { // HTTP 200 不代表业务成功,需再判断 res.data.code === '200' if (res.statusCode >= 200 && res.statusCode < 300 && res.data.code === '200') { resolve(res.data) } else { reject(new Error(res.data.code || res.statusCode)) } }, fail(err) { reject(err) } }) }) } module.exports = { request }注意两点:一是 QWeather 的业务状态码是字符串'200',HTTP 状态码只是传输层成功,两层都要判断;二是fail回调在断网、域名未配置时触发,错误信息要透传给页面,方便后续在 Network 面板定位。基于这个封装,接口层可以写成只关心参数和返回值的纯函数,页面里用Promise.all并行拉三个接口即可,代码量能少三分之一。
// utils/weather.js const { request } = require('./request') // GeoAPI 城市搜索:传经纬度 "116.41,39.92",返回 LocationID 和城市名 function getCityId(location) { return request('geo', '/city/lookup', { location }) } // v7 实时天气 function getNow(cityId) { return request('weather', '/weather/now', { location: cityId }) } // v7 未来 7 天逐日预报 function get7d(cityId) { return request('weather', '/weather/7d', { location: cityId }) } // 生活指数:type=1穿衣 2洗车 3感冒 5运动 function getIndices(cityId) { return request('weather', '/indices/1d', { location: cityId, type: '1,2,3,5' }) } module.exports = { getCityId, getNow, get7d, getIndices }3. 用原生微信小程序写天气预报:页面渲染与交互实现
3.1 首页今日天气:wx.getLocation 授权与数据回填
首页是这套天气预报源码的核心页面。onLoad 里的正确顺序是先读缓存再定位刷新,避免用户每次进入都看到空白期;定位成功后走"经纬度→城市搜索→并行拉取三个接口"的链路,最后用 setData 回填。完整逻辑:
// pages/index/index.js const { getCityId, getNow, get7d, getIndices } = require('../../utils/weather') const cache = require('../../utils/cache') Page({ data: { now: null, // 实时天气 daily: [], // 7 天预报 indices: [], // 生活指数 cityName: '定位中', loading: true }, onLoad() { this.loadFromCache() this.fetchByLocation() }, loadFromCache() { const cached = cache.get() if (cached) { this.setData({ ...cached, loading: false }) } }, fetchByLocation() { wx.getLocation({ type: 'gcj02', // gcj02 是国测局坐标,地图与天气接口都认 success: (loc) => { this.fetchWeather(`${loc.longitude},${loc.latitude}`) }, fail: () => { // 用户拒绝授权时用一个默认城市兜底,避免白屏 this.fetchWeather('101010100') } }) }, fetchWeather(location) { getCityId(location) .then((res) => { const city = res.location && res.location[0] if (!city) throw new Error('city not found') this.setData({ cityName: city.name }) // 三个接口互相独立,并行请求比串行快一个 RTT return Promise.all([ getNow(city.id), get7d(city.id), getIndices(city.id) ]) }) .then(([now, daily, indices]) => { const data = { now: now.now, daily: daily.daily, indices: indices.daily, loading: false, updateTime: this.formatTime(new Date()) } this.setData(data) cache.set(data) }) .catch((err) => { wx.showToast({ title: '天气加载失败', icon: 'none' }) console.error('fetchWeather', err) }) }, formatTime(date) { const h = String(date.getHours()).padStart(2, '0') const m = String(date.getMinutes()).padStart(2, '0') return `${h}:${m}` } })这段代码里最值得记住的是授权兜底:wx.getLocation 的 fail 分支不能只弹 toast,要给一个固定城市继续跑,否则首页直接白屏,真机调试时很容易误判成接口问题。另外新版基础库要求在 app.json 里声明定位用途,否则授权弹窗不会出现:
{ "pages": ["pages/index/index"], "permission": { "scope.userLocation": { "desc": "用于获取当前城市的实时天气" } }, "requiredPrivateInfos": ["getLocation"], "window": { "navigationBarTitleText": "天气预报" } }requiredPrivateInfos是隐私接口收紧后的硬性要求,老源码报"getLocation:fail api scope is not declared"基本就是缺这一项。
3.2 未来 7 天预报:scroll-view 横向滚动渲染
7 天预报用横向滚动列表呈现,容器必须是 scroll-view 而不是 view,并开 scroll-x。横向滚动在微信小程序里有个经典坑:子元素要用 inline-block 并给固定宽度,容器设 white-space: nowrap,否则子项会被压缩换行:
<!-- pages/index/index.wxml --> <scroll-view scroll-x enhanced show-scrollbar="{{false}}" class="daily-scroll"> <view class="daily-item" wx:for="{{daily}}" wx:key="fxDate"> <text class="date">{{item.fxDate.slice(5)}}</text> <text class="weather">{{item.textDay}}</text> <text class="temp">{{item.tempMax}}°/{{item.tempMin}}°</text> </view> </scroll-view>.daily-scroll { width: 100%; white-space: nowrap; } .daily-item { display: inline-block; width: 130rpx; margin-right: 20rpx; text-align: center; }渲染层面有两个要点:wx:key 用 fxDate,因为日期在 7 天里是唯一且稳定的主键;日期直接对字符串 slice,不要 new Date('2025-06-01') 再取 getDate——JS 会按本地时区解析纯日期字符串,在部分时区会偏移一天。天气图标方面,接口返回的是 iconDay/iconNight 这类代码,最省事的做法是首页只显示 textDay 文字,想要图标就把 code 映射到本地图片目录,别依赖第三方图床的图标地址。
3.3 城市切换:picker 与生活指数联动
城市切换用 picker 的 selector 模式,数据源是一份内置城市数组,字段包含 name 和 location。这里有个出现频率极高的下标陷阱:e.detail.value 是字符串"2",直接拿去做数组下标,js 会自动转成数字所以看着没毛病,但一旦和 Number() 混用就容易出 undefined;规范做法是显式转换:
<picker mode="selector" range="{{cityList}}" range-key="name" bindchange="onCityChange"> <view class="city-picker">{{cityName}} ▾</view> </picker>onCityChange(e) { const index = Number(e.detail.value) const city = this.data.cityList[index] if (city) { this.fetchWeather(city.location) } }生活指数和城市是同一份数据流:fetchWeather 内部已经把 indices 一并拉回来,页面里用 wx:for 渲染名称和建议文案即可。要扩充城市列表时,维护 cityList 数组就行,但注意数组长度和 picker 的滚动体验相关,超过 30 个城市建议改成城市列表页,用搜索接口动态查询——这正好也是第 5 章做分包时的一个候选页面。
4. 截图生成与真机排错:让"含截图"经得起验证
4.1 三种截图方式:工具截图、真机截图、自动化脚本
"含截图"要成为可靠的交付物,前提是截图能随时重新生成,而不是依赖分发时附带的旧图。换过 Key、改过字段之后,旧截图立刻失真。我一般按三种场景处理:第一种是微信开发者工具自带截图,模拟器右上角有相机图标,点击后保存当前模拟器画面,适合快速留档;第二种是真机截图,用真机预览时直接用手机系统截图,优势是能看到真实网络和真实授权弹窗,劣势是每换一次城市就要手动截一张,量大了很累;第三种是自动化截图,用官方提供的 miniprogram-automator 在构建后自动产出成套页面截图,适合页面多、截图要成套交付的场景:
// scripts/shot.js,先 npm i miniprogram-automator const automator = require('miniprogram-automator') automator.launch({ projectPath: './project', // 开发者工具导入的项目路径 port: 9420 }).then(async (miniProgram) => { const page = await miniProgram.reLaunch('/pages/index/index') await page.waitFor(800) // 等天气接口返回、骨架屏退场 await miniProgram.screenshot({ path: './screenshots/index.png' }) await miniProgram.close() })自动化截图的前置条件是开发者工具开启服务端口(设置-安全设置-服务端口)。脚本里 waitFor 的时长决定了截到的是骨架屏还是真实数据,天气接口通常几百毫秒返回,800ms 是稳妥值;更严谨的做法是轮询页面里某个绑定数据的字段,数据出现再截。
4.2 高频报错排查:请求失败、授权拒绝、时间错位
排查微信小程序的请求问题,第一现场是开发者工具自带的 Network 面板,能看到 URL、请求头和状态码,不需要额外装抓包工具。对比接口返回和页面渲染,八成报错都能落到这张表:
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| request:fail url not in domain list | 真机请求域名不在白名单 | 开发阶段工具勾选"不校验合法域名",上线前在 mp 后台配置 request 合法域名 |
| 返回 code 401/403 | Key 无效、订阅到期、配额耗尽 | 控制台重新复制 Key;免费订阅注意每日请求上限,缓存能显著降低消耗 |
| getLocation:fail api scope is not declared | 隐私接口声明缺失 | 在 app.json 配 permission 和 requiredPrivateInfos |
| 日期比当地早一天 | 对纯日期字符串做了 new Date 再取本地时间 | 直接字符串处理,fxDate.slice(5) |
| 页面白屏但 Network 正常 | 接口字段名与渲染模板不一致 | 对比 Network 里实际 JSON 的 key,大小写和命名都要核对 |
这类源码最常见的老化点是真实接口字段和旧代码假设不一致。比如早期版本用 tmp 表示温度,现在用 temp;用 cond_txt 表示天气现象,现在用 text。拿到任何天气预报源码,第一件事不是看页面,而是把 Network 面板里的原始 JSON 和 WXML 里的绑定字段逐一对一遍。
5. 天气预报源码的进阶优化:缓存、骨架屏与分包
5.1 30 分钟缓存策略:setStorageSync 与过期时间
天气数据不是实时变化的,逐分钟刷新既费 API 配额又没有任何体验收益。我一般在工具层封装一个带过期时间的缓存:读缓存时先判断时间戳,超过 30 分钟直接清掉并返回 null,避免返回脏数据:
// utils/cache.js const KEY = 'weather_cache' const TTL = 30 * 60 * 1000 // 30 分钟过期 function get() { const cache = wx.getStorageSync(KEY) if (!cache) return null if (Date.now() - cache.time > TTL) { wx.removeStorageSync(KEY) return null } return cache.data } function set(data) { wx.setStorageSync(KEY, { time: Date.now(), data }) } module.exports = { get, set }缓存写在哪一层很关键:不能只缓存最终页面数据,因为城市切换后旧缓存会污染新城市;正确的做法是缓存以 LocationID 为维度,set 时把 cityId 一起存进去,get 时先比对 cityId。上面的示例是单城市简化版,多城市改造时优先把这一层补上。
5.2 骨架屏替代 loading:"修改刚进入的加载页面"的常规解法
"修改刚进入的加载页面"是天气类小程序被提得最多的优化点。默认的 wx.showLoading 只有一个转圈,接口慢时显得整个页面死了。骨架屏的常规做法:loading 为 true 时渲染灰色占位块,数据返回后整块替换成真实内容:
<!-- 骨架屏占位,wx:if 控制;数据到达后 wx:elif 渲染真实卡片 --> <view class="skeleton" wx:if="{{loading}}"> <view class="skeleton skeleton-temp"></view> <view class="skeleton skeleton-line"></view> </view> <view class="now-card" wx:elif="{{now}}"> <!-- 真实天气内容 --> </view>.skeleton { background: #f2f3f5; border-radius: 16rpx; } .skeleton-temp { height: 120rpx; margin: 24rpx; } .skeleton-line { height: 32rpx; margin: 24rpx; }骨架屏配合 5.1 节的缓存策略有一个联动收益:缓存命中时 loading 直接为 false,骨架屏连出现都不出现;缓存未命中时骨架屏替代白屏,让首次加载有"页面在动"的反馈。视觉上再给占位块加一个透明度渐变的 CSS 动画即可,不需要引入任何第三方组件库。
5.3 分包:把城市列表和详情页拆出去
小程序主包有 2MB 硬限制,整个项目在多数基础库上限制 20MB(新版本放宽到 30MB)。天气预报这个体量本身不大,但一旦加了城市选择页、温度图表页、图标资源包,主包很快会逼近上限。常见做法是只把入口页留在主包,城市列表和详情页拆进 subpackages,用户点进去时才按需下载:
{ "pages": ["pages/index/index"], "subpackages": [ { "root": "pages/city", "pages": ["list"] }, { "root": "pages/detail", "pages": ["index"] } ] }分包后有一个隐性收益:首页 onLoad 的初始化路径更短,加载更接近秒开。注意分包里的页面如果要复用 utils 目录里的请求和缓存模块,这些公共代码必须留在主包,分包通过相对路径引用主包模块不会额外重复打包。
6. 源码交付前必做的验收清单
6.1 截图与代码一致性核对
拿到或交付一份"含截图"的天气预报源码,验收不是打开截图看一眼就完事。三件事必须做:第一,核对截图里的日期是否和接口返回的 fxDate 对应,天气现象文字和 Network 里的 text 字段是否一致,城市名是否能在 cityList 或 GeoAPI 返回里找到;第二,把 Key 换成自己的之后重新截图,对比新旧截图差异,差异只允许在温度和现象文字这类正常波动上,不允许出现页面布局错位;第三,如果截图里有时间、电量这类状态栏信息,标注清楚这是工具模拟器还是真机截图,避免交付后被人拿真机截图来对质。
6.2 运行环境与密钥管理
源码交付时最容易被忽略的是运行环境说明。一份能跑的天气预报源码,至少要交代清楚三件事:开发者工具的基础库版本、AppID 用测试号还是正式号、Key 放在哪个配置文件。基础库版本决定 requiredPrivateInfos 这类字段是否生效,AppID 类型决定真机预览和定位权限能不能开,Key 的位置决定换环境时改哪一行。建议把 Key 单独抽成 config.js,不要散落在 request.js 和各个页面里。
网上还流传着各种小程序一键反编译工具,能还原别人线上包的大半逻辑,但反编译产物没有注释、组件路径错乱、接口字段经常拼错,只能拿来参考页面结构,不能当交付物直接改进自己项目里。真要认真交付一个微信小程序天气预报项目,按上面这套结构重写一遍,比对着反编译代码修修补补一整天更快,而且截图、字段、运行环境全部可控。
本文还有配套的精品资源,点击获取