我一直觉得,做天气类的小程序是练手性价比最高的项目类型之一。它不像后台管理系统那样全是列表表单,也不像电商项目那样纠缠支付和售后,但它把微信小程序日常开发里最常碰到的环节全部串起来了:用户授权、定位获取、网络请求、数据解析、页面渲染、下拉刷新、异常兜底、提审发布。而且做完之后自己每天都会打开,给朋友演示也不丢人。
这篇就围绕“微信小程序天气预报”这个项目,从 0 到 1 完整走一遍:先讲清楚技术选型为什么这么定,再给可直接抄走的源码结构和关键模块实现,最后把上线阶段容易踩的坑、以及后续值得做的优化方向一起理顺。内容定位很明确:给刚学完小程序基础语法、想拿真实项目练手的人,以及做了一些静态页面但没跑通过完整数据链路的开发者。目标只有一个,让你看完之后能真正跑起来一个属于自己的天气小程序,而不是停留在“看教程都会,一动手就废”的状态。
先打个预防针:这个小程序看起来简单,但核心链路里每一步都有细节。定位授权失败怎么处理,接口返回字段不稳定怎么办,下拉刷新时请求竞态怎么避免,真机预览为什么报“域名不在白名单”,这些问题比写几行页面代码更容易劝退新手。我会把常见问题单独放在一节里,按“现象-原因-排查方案”来讲,建议你卡住的时候直接跳到那一节对照查。
1. 先把项目拆清楚:定位、选型与数据流
1.1 为什么天气项目是最好的“练手完整件”
天气预报小程序的最大特点是:业务闭环完整,但复杂度可控。用户打开页面,拿到定位,请求天气数据,渲染结果,刷新数据,这一条链路几乎覆盖了小程序所有基础能力。你不需要考虑复杂的表单校验、多角色权限、支付回调,一门心思把“数据从小程序端到接口再回到界面”这件事跑通就够了。
更关键的是,它的数据是实时变化的。温度、风力、天气现象每隔一段时间就变一次,这倒逼着你去处理加载状态、刷新逻辑、接口异常。如果是做一个静态展示页,根本不会遇到这些问题。而天气数据天然适合演示授权逻辑、缓存策略、域名配置,这些恰恰是真实商业项目里躲不掉的硬骨头。
从另一个角度看,天气类小程序也很适合作为作品集里的点缀。它的 UI 能做得很精致、很可视化,功能又能讲清楚技术链路,放在简历里比“仿电商首页”更有记忆点。面试官问起来,你可以从定位授权讲到接口封装,从渲染讲到发布,一整条知识链都是你自己的。
1.2 技术选型背后的实际考量
技术选型这块,我给的建议是:老老实实用原生小程序,别一上来就上跨端框架。原生框架的调试体验最直接,wx 系列的 API 支持最完整,报错信息也最容易看懂。等你把原生的坑踩过一遍,再去看跨端框架的适配方案,会容易理解得多。项目结构也简单,不引入第三方框架,包体积控制起来更轻松。
天气数据源我建议选“免费的、返回 JSON 格式的开放天气接口”,不要选需要复杂签名或者文档极其残缺的那种。判断一个天气接口能不能用,先看三件事:有没有免费的日请求额度,接口是 HTTP 还是 HTTPS,支不支持根据经纬度查询实时天气。很多接口还提供城市名称查询,但小程序里最可靠的路径是先拿坐标再查天气,因为城市名称可能因为重名产生歧义,而坐标是全球唯一的。
关于逆地理编码,也就是把经纬度转换成城市名,用的通常是地图服务提供方的位置解析接口。不过也可以偷懒:如果天气接口里直接返回了当前城市字段,那就不需要再请一个逆地理解析服务,少一次请求就少一个失败点。我的实现方案是按“坐标查天气 + 接口返回城市字段”来设计的,整个链路只有一次外部请求,非常好排查问题。
1.3 整体数据流与页面结构
这个小程序的实际数据流是:
页面加载 → 请求定位权限 → 拿到经纬度 → 组装天气接口请求 → 解析返回数据 → setData 驱动视图更新 → 下拉刷新再次触发同一流程。
如果用户拒绝授权定位,流程会走一个降级分支:使用默认城市坐标发起请求,保证页面至少能显示天气,而不是白屏一片。
页面结构也不用复杂,初期一个页面就能搞定,页面内从上到下分为三块:顶部城市和更新时间,中间大号温度数字,下方天气现象和风力描述。这种单页结构特别适合新手理解,等后期要加城市管理,再扩展一个搜索页面也来得及。
2. 从工具链起步:注册、建工程与目录规划
2.1 注册 AppID 与开发者工具初始化
做小程序的第一步不是写代码,而是去微信公众平台注册一个小程序账号,拿到 AppID。用测试号也能在开发者工具里跑,但没法真机预览,也没法体验完整的定位授权流程,所以别嫌麻烦,注册一个自己的 AppID 是必须的。
装好微信开发者工具之后,新建项目时选择“不使用模板”,直接把项目类型定为小程序。这里有个容易被忽略的设置:在开发者工具的“详情 - 本地设置”里,把“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”这个选项勾上,本地开发时才能用任意 HTTPS 接口去调试。如果不勾,请求会直接报“url not in domain list”。
第一印象非常重要。新建项目成功后,先在模拟器里把这个空项目跑通,然后再往里面加代码。很多新手一上来就往 app.json 里堆配置,结果页面路径配错,打开就是白屏报错,整个开发心情都毁了。
2.2 项目目录结构与全局配置
我推荐的项目目录非常克制:
miniprogram/ app.js app.json app.wxss utils/ api.js pages/ index/ index.js index.json index.wxml index.wxss一个工具函数目录加一个页面目录,页面内部四个文件按模板生成,不花哨,但每一层职责都清清楚楚。后续要加城市搜索页,就在 pages 下加一个 search 目录,不会影响现有结构。
app.json 是小程序的全局配置入口,也是最容易出问题的地方。我给的完整配置如下:
{ "pages": [ "pages/index/index" ], "window": { "navigationBarTitleText": "天气预报", "navigationBarBackgroundColor": "#4A90D9", "navigationBarTextStyle": "white", "backgroundColor": "#E8F4FD" }, "permission": { "scope.userLocation": { "desc": "你的位置信息将用于获取所在城市的实时天气" } }, "sitemapLocation": "sitemap.json" }pages 数组第一项就是小程序启动后打开的第一个页面。window 里配置顶栏标题和颜色,注意 navigationBarTextStyle 只支持 black 和 white。permission 这一项是我特别想强调的:从基础库某个版本开始,如果 app.json 里没有声明 scope.userLocation 的用途说明,真机上调用定位接口可能不会弹出授权框,直接走 fail 回调,排查起来非常隐蔽。
2.3 公共样式与页面骨架
app.wxss 里我一般只放全局的基础样式,比如页面背景、默认字体色。页面内部的具体排版放在对应页面的 wxss 里,这样不会互相污染。
天气页面的布局核心是纵向 flex:头部一行放城市和更新时间,中间一块用大字号突出温度,下面放天气现象和风力。这样安排符合用户看天气的第一直觉:先看温度数字,再看天气描述。
这里有一个关于 rpx 的理解要点:rpx 是微信小程序的响应式单位,iPhone 上 750rpx 等于屏幕宽度,所以设计稿按 750 宽来做,切图标 rpx 基本不会出大错。温度数字这种核心视觉元素,我喜欢直接给 96rpx 甚至更夸张的字号,让它和下方描述文字形成明显层级。
3. 核心功能模块实操:定位、请求、渲染、刷新
3.1 定位授权与城市回退方案
定位是整个小程序的第一道关卡。我使用 wx.getLocation 获取用户当前位置,type 传 gcj02,这是国内地图通用的坐标系,能直接用于天气接口和地图服务。
getLocationAndWeather() { wx.getLocation({ type: 'gcj02', success: (res) => { this.fetchWeather(res.latitude, res.longitude); }, fail: () => { wx.showToast({ title: '定位失败,使用默认城市', icon: 'none' }); this.fetchWeather(39.9042, 116.4074); } }); }这段代码里最值得说的是 fail 分支。真实场景中,用户第一次打开小程序点“拒绝授权”的比例比你想象的高得多,尤其是在用户对小程序还不信任的情况下。如果拒绝授权就什么都不显示,这个产品就废了。所以我在 fail 分支里使用了一组默认城市的坐标兜底,保证页面永远有数据可看。
更完善的做法是,当定位失败时,给用户一个引导去设置页打开定位权限的入口。可以配合 wx.showModal 提示“定位权限未开启,是否去设置页开启”,用户确认后调 wx.openSetting 跳转。注意这个引导不能每次都弹,可以在全局用一个标记控制频率,否则会变成对用户的打扰。
3.2 天气接口请求封装
网络请求这块必须独立封装,我放在 utils/api.js 里。原因很简单:如果以后换了天气服务商,你只需要改这一个文件,而不是跑到每个页面里去改 wx.request。
const BASE_URL = 'https://api.example.com/weather'; const API_KEY = '这里替换成你自己的key'; function requestWeather({ lat, lng }) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL, data: { key: API_KEY, lat: lat, lng: lng }, timeout: 8000, success: (res) => { if (res.statusCode === 200 && res.data && res.data.code === 200) { resolve(res.data); } else { reject(new Error(res.data && res.data.msg ? res.data.msg : '天气接口返回异常')); } }, fail: (err) => { reject(err); } }); }); } module.exports = { requestWeather };封装成 Promise 算是一个标准的行业习惯。wx.request 本身是回调式的,一旦页面里要串多个请求,回调嵌套会迅速膨胀。Promise 可以让你在页面里用 .then 或者 async/await 写请求逻辑,阅读顺序和业务顺序完全一致。
关于 timeout,我强烈建议显式设置。天气接口一旦被请求方限流或者网络抖动,默认超时时间可能过长,用户对着加载状态转圈两分钟,早就关掉小程序了。我设的是 8000 毫秒,体感上比较合理。
这里必须强调一个上线前的大坑:小程序在生产环境要求 wx.request 的接口域名必须配置到微信公众平台的“开发管理 - 开发设置 - 服务器域名 - request 合法域名”里,而且要求 HTTPS。本地调试可以勾选不校验域名,但真机上不配置白名单就一定会请求失败。这个属于微信的平台安全策略,理解并遵守就行。
3.3 页面渲染与数据绑定
页面入口的逻辑很简单,onLoad 里调用定位和请求函数,数据回来之后更新 data。关键点在于把接口的返回字段“翻译”成页面需要的展示字段,比如把温度和天气现象放到 data.temp、data.weather 里,然后 WXML 里做插值。
fetchWeather(lat, lng) { this.setData({ loading: true }); requestWeather({ lat, lng }) .then((res) => { const now = res.now; this.setData({ temp: now.temp, weather: now.text, wind: now.windDir + ' ' + now.windScale, city: res.city || '当前位置', updateTime: this.formatTime(new Date()), loading: false }); }) .catch((err) => { console.error('天气请求失败', err); this.setData({ loading: false }); }); }WXML 部分我给了最小可运行的结构:
<view class="page"> <view class="header"> <text class="city">{{city}}</text> <text class="update">{{updateTime}} 更新</text> </view> <view class="main"> <text class="temp">{{temp}}°</text> <text class="desc">{{weather}} · {{wind}}</text> </view> <view wx:if="{{loading}}" class="loading">加载中...</view> </view>注意温度数字后面拼接“°”,这是个很小的交互细节。很多天气 App 喜欢把数字和单位做成一体的视觉,看起来更有设计感。另外加载状态我用 wx:if 控制,请求期间展示“加载中”,请求完成就消失。
还有一个细节:setData 不要一整个大对象直接丢进去,最好按字段名更新。小程序 setData 的 diff 机制是基于 key 的,按字段更新能减少渲染开销。我这个例子里数据量小,区分不明显,但养成好习惯以后做复杂页面会感谢自己。
3.4 下拉刷新与请求竞态
下拉刷新在页面配置文件 index.json 里开启:
{ "enablePullDownRefresh": true, "backgroundColor": "#E8F4FD" }然后在页面里监听 onPullDownRefresh。这个函数会在用户下拉时触发,我在里面重新走一遍定位和请求流程,请求结束后调用 wx.stopPullDownRefresh 收起动画。
但这里有一个很容易踩的性能坑:请求竞态。用户快速连续下拉三四次,会同时发出多个请求。网络返回顺序不一定是发出顺序,旧请求晚返回时,就会把新请求的数据覆盖掉,页面显示的温度反而是过期的。天气数据时效性极强,这个问题不能忍。
我的解决方案是在 fetchWeather 里加一个请求锁:
onPullDownRefresh() { if (this._requesting) { wx.stopPullDownRefresh(); return; } this.getLocationAndWeather(() => { wx.stopPullDownRefresh(); }); } fetchWeather(lat, lng, callback) { if (this._requesting) return; this._requesting = true; this.setData({ loading: true }); requestWeather({ lat, lng }) .then((res) => { // 处理数据 }) .catch((err) => { // 错误处理 }) .finally(() => { this._requesting = false; if (callback) callback(); }); }核心思路就是:同一时间只允许一个请求在途。后来的请求除非在锁释放之后触发,否则直接忽略。这个技巧在真实项目里非常常用,不只是天气小程序,凡是涉及“用户高频操作触发网络请求”的场景都需要这种保护。
4. 源码组装、真机调试与发布避坑
4.1 最小可运行项目的源码清单
前面各节的代码片段,其实已经覆盖了这个小程序 80% 的逻辑。我再把剩余的几个小文件补上,组合起来就是一个完整可运行的项目。把 app.js 做成最简即可:
App({ onLaunch() { // 这里可以放全局初始化逻辑,比如登录态检查 } });页面配置文件 index.json 只做两件事:设置标题和开启下拉刷新:
{ "navigationBarTitleText": "当前天气", "enablePullDownRefresh": true, "backgroundColor": "#E8F4FD" }页面样式我给了核心部分,背景用浅蓝色渐变,营造天空的清爽感:
.page { min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 40rpx 30rpx; background: linear-gradient(180deg, #6FA6E8 0%, #9DC6F0 40%, #E8F4FD 100%); box-sizing: border-box; } .header { width: 100%; display: flex; justify-content: space-between; align-items: center; color: #FFFFFF; font-size: 30rpx; margin-top: 20rpx; } .main { flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; color: #FFFFFF; } .temp { font-size: 120rpx; font-weight: 300; line-height: 120rpx; } .desc { font-size: 32rpx; margin-top: 20rpx; opacity: 0.9; }组装好后,把天气 API 的 key 换成你自己的,在开发者工具里点编译,就可以看到当前城市的温度。注意我前面代码里 BASE_URL 是示例域名,实际开发时需要替换成你注册到的天气接口地址。
4.2 本地联调与真机预览
本地联调的核心是看调试器里的 Network 面板。wx.request 发出的请求会被记录在这里,你可以直接看到请求参数、返回状态码、返回的 JSON 数据。接口字段不确定时,第一件事永远是打开 Network 面板看原始返回,而不是去改 WXML。
开发环境的坑主要在域名校验。工具里如果不勾选“不校验合法域名”,请求会报错;勾选之后接口通了,但真机又可能报域名不在白名单。所以我的习惯是:本地开发阶段勾选跳过校验,确认逻辑没问题之后,马上到后台配置合法域名,再在工具里取消跳过校验,用“真机预览”验证一遍完整链路。
真机预览时,确保手机和电脑处于可正常联网状态,扫码后小程序会在手机微信里打开。如果遇到定位获取不到,检查你的 AppID 是否真实注册,测试号在真机上的定位能力是非常受限的。
4.3 提审发布前要处理的小细节
发布前有一个最容易被忽视的环节:在公众平台后台填写“用户隐私保护指引”,其中有一项必须声明“位置信息”的用途。如果不声明,审核员或系统会以“违规收集用户位置信息”为由驳回,很多新手在这个地方卡上一周。
小程序类目选择也很重要,天气类一般归到“工具 - 信息查询”,在提交审核时匹配对应的类目,能减少很多不必要的审核沟通。提审前建议自己按用户路径走一遍:首次打开授权弹窗、正常展示天气、拒绝授权后的展示、下拉刷新,至少这四个场景都要测一遍。
版本号管理也要养成习惯。小程序后台发布时要填版本号和项目备注,我一般用 1.0.0 这种语义化版本号,备注写“新增天气查询功能”,方便以后在后台看版本历史。
5. 高频问题与排查心得速查
5.1 常见报错速查表
做这个小程序的过程中,我踩过不少坑,也帮朋友排查过很多次,把最常见的现象、原因、解决方式整理成了一张表,你可以直接当速查手册用。
| 现象 | 常见原因 | 排查与解决 |
|---|---|---|
| getLocation 直接走 fail | 用户拒绝授权,或 app.json 未声明 permission | 走默认城市兜底;声明 scope.userLocation 用途;必要时引导 openSetting |
| 真机请求报 url not in domain list | 接口域名未配置到后台合法域名 | 公众平台后台配置 request 合法域名,必须 HTTPS |
| 请求超时或一直 loading | 接口响应慢,或 API key 失效 | 设置 timeout;先到浏览器手测接口地址;检查 key 是否过期 |
| 页面白屏,console 无报错 | app.json pages 路径写错 | 检查 pages 第一项路径是否真实存在且匹配文件名 |
| setData is not a function | this 作用域丢失 | 检查回调是否用了普通 function,需要箭头函数或提前保存 this |
| 数据返回 undefined | 接口字段名与代码不一致 | 打开 Network 面板打印原始数据结构,按实际字段名修改解析代码 |
| 下拉刷新后数据未变化 | onPullDownRefresh 未配置或未调用 stop | 页面 json 开启 enablePullDownRefresh;请求结束后调 wx.stopPullDownRefresh |
| 温度和描述闪一下变空白 | loading 状态切换导致视图抖动 | 不要整个替换显示节点,或增加最小加载时间、使用旧数据占位 |
这张表是实战经验的高度浓缩,绝大多数新手卡住半天的问题,都能在里面找到答案。
5.2 三个特别容易踩的隐性坑
第一个坑是首次授权时机。很多人喜欢在首页 onLoad 里立刻 wx.getLocation,用户刚看到页面还没建立信任就被要求授权,拒绝率很高。更稳妥的做法是,先展示默认城市的数据,同时通过一个友好的按钮提示“点击获取当前位置天气”,用户主动点击后再发起定位授权,成功率会高很多。
第二个坑是 loading 状态闪烁。小程序的 setData 是异步渲染的,频繁把 loading 设为 true 和 false,如果接口响应时间只有一两百毫秒,用户会看到加载占位一闪而过,视觉上很廉价。我的做法是:页面已有旧数据时不显示 loading,只有首次加载才展示,同时给一个最小加载时长,比如 300 毫秒,让过渡更平滑。
第三个坑是接口返回的城市名称层级问题。按经纬度查询时,很多天气接口返回的城市名是区县级,比如“朝阳区”,而不是用户认知里的“北京市”。如果直接展示会显得很怪。处理方式是拼接显示:用“省-市-区县”呈现,或者干脆显示“当前位置”,让用户不必纠结行政区域名称。
5.3 调试利器:模拟定位与断网测试
开发者工具里的“模拟定位”功能非常实用。在工具菜单的“位置模拟”里,可以直接填一组经纬度,模拟自己跑到任意地方。这对测试默认城市兜底、查接口在不同坐标下的返回结构极为方便。
更值得养成习惯的是断网测试。打开工具右上角的“模拟器 - 网络 - Offline”,然后重新加载页面,你会发现页面能不能正常容错、loading 是否卡死、有没有报错,这一套测试能筛出很多仅有网络环境下才暴露的问题。很多开发者在正常网络下测不出来问题,上线后被用户抱怨“加载不出来”,就是因为没有做断网状态的自测。
6. 面向后续迭代的优化方向
6.1 功能层面扩展:7 日预报、城市搜索、生活指数
当前实时天气是一个最小闭环,但如果想让产品有使用粘性,第一优先级是把“实时天气”升级成“多日天气预报”。大多数免费天气接口都提供 3 天或 7 天预报,前端只需要在页面下方增加一个横向滚动的日期列表,展示每日最高最低温度,代码量不大但信息量提升明显。
第二个值得加的功能是城市搜索。用户可以主动搜索并选择其他城市来查看天气,这涉及一个城市名称和经纬度对应的数据源。比较轻量的实现是接入一个城市关键字搜索的接口,也有天气服务商会直接提供城市列表文件,打包在小程序里做本地搜索。后者响应更快,还省一次外部请求。
生活指数是第三个亮点,包括穿衣指数、紫外线指数、运动指数等。这类数据能明显提升页面内容的丰富度,让用户觉得“不只是看一个温度数字”。接口能返回结构化字段的话,前端做成几张小卡片即可,交互逻辑完全复用现有请求方式。
6.2 性能与包体积控制
功能变多以后,包体积会膨胀,性能优化就必须提上日程。一个核心思路是“能本地缓存就不重复请求”。天气数据不需要每次打开都重新拉取,本地存储里存一份上次结果,设置一个 10 到 15 分钟的过期时间(TTL),过期了再重新请求,既能省流量又让二次打开秒开。
另一个优化点是组件与代码包的拆分。如果新增了城市搜索、生活指数等页面,可以按页面分包加载,把次要页面放到 subpackages 里,主包的体积控制住,小程序的启动速度会明显更快。图片资源也尽量压缩或换成图标字体,不要让几张装饰图吃掉几十 KB 的包体积。
还有一个小技巧:开启开发者工具里的“上传代码时压缩文件”和微信后台的“代码加固”,在节省流量的同时提高一点点安全性,代价可以忽略不计。
6.3 体验优化与数据兜底
体验优化的方向,我建议先做骨架屏和深色模式。骨架屏就是在数据未返回时先用灰色块占位,让用户知道页面正在加载而不是空白;深色模式则需要在 wxss 里用媒体查询适配 systemTheme,天气页面的大色块背景在夜间模式下的观感差异很大,值得专门调一版深色样式。
数据兜底这块常常被人忽略,但恰恰是拉开完成度的关键。我在项目里做了三层兜底:第一层,接口请求失败时展示上一次缓存的数据,并在页面上提示“数据可能不是最新”;第二层,接口超时后提供重新加载按钮;第三层,请求频率被限流时,用指数退避策略逐渐拉长重试间隔。这三层看起来不起眼,能把很多线上事故变成“用户无感自愈”。
后续如果要接云开发,还可以把天气接口的请求逻辑挪到云函数里,前端只调用云函数,API key 就不会暴露在小程序代码包中,也不需要考虑手机端域名白名单。这是一套更工程化的方案,适合想往全栈方向走的同学深入研究。
我个人做下来的体会是:这种“小而全”的项目,最值钱的部分往往不是某一行代码,而是你把边界情况处理顺的过程。定位被拒绝怎么优雅降级,接口返回慢怎么不让用户干等,快速下拉怎么防止数据错乱,这些问题想清楚一次,以后做任何小程序都能少踩好多坑。最后再分享一个小技巧:开发期间每写完一个模块,就切到真机预览跑一遍完整链路,别只在模拟器里自我感动。模拟器正常不代表真机正常,域名白名单和授权弹窗这些硬约束,只有真机才会如实暴露。把这条链路跑顺了,你这台“天气预报”小程序就不只是练手作品,而是一个真正能天天打开用的工具了。