最近好多朋友在问小程序开发怎么入门,我翻了翻自己仓库里一个完整的小程序项目,正好拿它当例子讲讲。这个项目就是微信小程序天气预报,从0到1做完大概花了两个周末,麻雀虽小但五脏俱全,涉及页面布局、网络请求、数据缓存、地图选点、定位刷新这些小程序开发几乎必踩的点。这篇就按我实际开发的顺序,把项目拆开讲清楚,代码片段直接贴在下面,完整可运行版本我也做了整理,你可以照着还原一遍。
先说说这个项目适合谁。如果你已经会一点 HTML/CSS/JavaScript,但还没完整做过小程序,或者做过简单页面但没接触过真实接口联调,那这篇就是给你准备的。我会把每一步为什么这么做、数据从哪来、真机调试容易踩哪些坑都交代清楚,尽量让你学完能自己再做一个类似的工具类小程序。
1. 项目整体拆解与技术选型
1.1 需求范围界定:先做MVP,别一开始就上全套
小程序天气预报这个需求很容易失控,因为天气数据能做的功能太多了:当前天气、逐小时预报、15天预报、空气质量、生活指数、台风路径、降雨雷达……如果把这些全塞进第一版,项目大概率烂尾。我第一版只定了四项核心功能:
- 首页展示当前城市的实时天气、温度、风力、湿度、空气质量
- 展示未来24小时逐小时预报和未来7天每日预报
- 支持切换城市、搜索城市、删除城市、默认城市管理
- 进入页面自动定位,支持下拉手动刷新
这些功能覆盖了小程序开发最核心的几条链路:页面生命周期、网络请求、本地存储、地图选点、下拉刷新。像生活指数、极端天气预警这些,放在后续版本迭代里更合理。MVP阶段把主流程跑通,比堆砌功能更重要。
1.2 技术方案选型:原生小程序还是跨端框架
这个项目我选的是原生小程序,没有用 uni-app 或者 Taro。原因有三点:
第一,项目复杂度不高,用跨端框架反而多一层构建和编译的认知成本。原生小程序开发者工具打开就能调试,wxml、wxss、js、json 四个文件组织清晰,对新手更友好。
第二,原生小程序有能力边界,但天气类项目要求的定位、地图、网络请求、存储这些能力它都有,不需要靠框架补足。第三,后续如果要把代码迁移到其他平台,原生逻辑也相对容易理解,不至于被框架的抽象概念绕晕。
当然不是说跨端框架不好,而是说要根据项目体量来。一个单页面为主的工具类小程序,原生是性价比最高的方案。
1.3 页面结构与数据流设计
整个项目我设计了三个页面:天气首页、城市管理页、设置页。首页是核心,城市管理页维护本地城市列表,设置页放一些简单的偏好选项。数据流保持单向,全局变量 + 本地存储搭配使用:
页面操作 -> 更新globalData -> 写入storage -> 触发页面渲染之所以不用复杂的状态管理库,是因为这个项目的数据依赖方向很清晰:城市列表和当前城市是全局共享数据,天气数据是城市级别的局部数据。用官方自带的 storage 和 globalData 就能解决,没必要引入 Redux 这类方案给项目增加复杂度。
2. 天气数据接入与核心细节解析
2.1 天气数据服务从哪来
做天气类小程序,最关键的一步是搞定天气数据来源。我选择的是国内一家主流天气数据开放平台,申请一个开发者 key 就能调用。接入的核心是这几个接口:
- 城市模糊搜索:通过关键词匹配城市,返回城市编码
- 当前天气:返回温度、天气现象、风力风向、湿度、体感温度等
- 未来24小时预报:按小时返回温度、天气现象
- 未来7-15天预报:按天返回白天气温、夜间气温、天气现象
免费额度对个人开发学习完全够用,每天调几百次不会触发限制。但是要特别注意一点:接口的返回字段是 JSON,有些字段是数字编码(比如天气现象用 0-99 表示晴、多云、雨等),有些是字符串枚举(比如风向),这些解析规则平台文档里都有,需要自己在代码里做一个映射表。
2.2 API请求封装:Promise化处理
原生小程序的wx.request是回调风格,如果页面里到处写回调嵌套,代码会非常丑陋。我做的第一件事就是把它 Promise 化,统一封装请求入口。
// utils/request.js const BASE_URL = 'https://api.example.com'; function request(path, params = {}, method = 'GET') { return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${path}`, data: params, method, header: { 'Content-Type': 'application/json' }, timeout: 8000, success: (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else if (res.statusCode === 401) { wx.showToast({ title: '接口授权失败', icon: 'none' }); reject(res); } else { wx.showToast({ title: `请求失败(${res.statusCode})`, icon: 'none' }); reject(res); } }, fail: (err) => { if (err.errMsg.includes('timeout')) { wx.showToast({ title: '请求超时,请重试', icon: 'none' }); } else { wx.showToast({ title: '网络异常', icon: 'none' }); } reject(err); } }); }); } module.exports = { request };这里做了一层拦截,统一错误提示,页面里就不用每个请求都写重复的 toast 了。超时时间我设成 8 秒,天气接口一般 1-2 秒内返回,超时 8 秒基本可以断定网络有问题,不用让用户干等。
2.3 数据缓存策略:少调接口,体验更快
天气数据不是实时刷新的,同一城市在半小时内一般没有明显变化。如果每次进页面都请求接口,不光消耗用户流量,还可能触发平台的频率限制。所以我加了一层本地缓存,核心逻辑如下:
- 缓存 key 以“城市编码 + 日期”组合
- 当前天气缓存 30 分钟
- 24小时预报缓存 30 分钟
- 7天预报缓存 2 小时
// utils/cache.js const CACHE_PREFIX = 'weather_cache_'; function getCache(key) { const cacheKey = CACHE_PREFIX + key; const data = wx.getStorageSync(cacheKey); if (!data) return null; if (Date.now() > data.expireAt) { wx.removeStorageSync(cacheKey); return null; } return data.value; } function setCache(key, value, ttlMinutes) { const cacheKey = CACHE_PREFIX + key; wx.setStorageSync(cacheKey, { value: value, expireAt: Date.now() + ttlMinutes * 60 * 1000 }); } module.exports = { getCache, setCache };这个缓存的思路是“短时复用”,不是永久存储。用户切到另一个城市再切回来,缓存还在,直接秒开。如果用户手动下拉刷新,则强制绕过缓存重新请求。这样首页不管怎么切都很快,也没有牺牲数据新鲜度。
2.4 天气数据解析与字段清洗
天气接口返回的 JSON 字段比较原始,比如温度可能是字符串"23.0"而不是数字 23,天气现象可能是数字编码而不是直接的中文描述。我把解析逻辑单独放到一个weatherParser.js文件里,页面上只接收已经清洗好的结构化数据。
// utils/weatherParser.js function parseNow(rawData) { return { temp: Math.round(Number(rawData.temp)), feelsLike: Math.round(Number(rawData.feelsLike)), text: WEATHER_TEXT_MAP[rawData.condCode] || '未知', windDir: rawData.windDir, windScale: rawData.windScale, humidity: rawData.humidity + '%', // ... }; }这里做了两类清洗:一是类型转换,把字符串数字转成真正的数字,方便前端计算;二是字段映射,把接口的数字编码转成用户能看懂的中文天气文案。有一件事很容易被忽略:接口返回的更新时间可能与本地时区有偏差,最好直接使用后端时间戳,别用手机本地时间做计算,否则会差 8 个小时或者更多。
3. 实操过程与关键模块实现
3.1 项目初始化与目录结构
我用微信开发者工具新建了一个“不使用模板”的原生小程序项目,AppID 先选择测试号,等需要真机预览时再换成自己注册的 AppID。整个项目的目录结构如下,我习惯把工具函数、页面、静态资源分开:
├── app.js ├── app.json ├── app.wxss ├── project.config.json ├── sitemap.json ├── utils/ │ ├── request.js │ ├── cache.js │ ├── weatherParser.js │ └── location.js ├── pages/ │ ├── index/ │ │ ├── index.wxml │ │ ├── index.wxss │ │ ├── index.js │ │ └── index.json │ ├── city/ │ │ ├── city.wxml │ │ ├── city.wxss │ │ ├── city.js │ │ └── city.json │ └── settings/ │ ├── settings.wxml │ ├── settings.wxss │ ├── settings.js │ └── settings.json └── images/ └── icons/app.json里注册页面路由、配置窗口样式、配置 tabBar。天气首页是核心,所以放在第一个 tab。tabBar 需要准备两个尺寸的图标,分别为选中态和非选中态,图标文件大小不能超过 40kb,格式用 png。我在这一步踩过一次坑:图标文件没放在本地而用了网络图标,真机预览时 tabBar 图标有时候加载不出来。所以 tabBar 图标必须本地静态文件,这一点是硬约束。
3.2 首页天气卡片:实时天气展示
首页的实时天气卡片是整个项目的门面,天气信息要一眼能看明白。我把卡片拆成三块:最上方是城市名和更新时间,中间是大温度数字和天气文案,下面是一排气象指标(湿度、风向、风力、空气质量)。
<!-- pages/index/index.wxml --> <view class="page"> <view class="weather-card {{weatherCardBg}}"> <view class="card-header"> <text class="city-name">{{currentCity.name}}</text> <text class="update-time">{{updateTime}}</text> </view> <view class="card-main"> <text class="temp">{{currentWeather.temp}}°</text> <text class="desc">{{currentWeather.text}}</text> </view> <view class="card-metrics"> <view class="metric"> <text class="metric-label">湿度</text> <text class="metric-value">{{currentWeather.humidity}}</text> </view> <view class="metric"> <text class="metric-label">风向</text> <text class="metric-value">{{currentWeather.windDir}}</text> </view> <view class="metric"> <text class="metric-label">风力</text> <text class="metric-value">{{currentWeather.windScale}}级</text> </view> </view> </view> <!-- 后面还有24小时和7天模块 --> </view>天气卡片背景我是用动态 class 切换的,根据天气文案映射成不同的渐变色。晴天用蓝到浅蓝的渐变,雨天用灰到深灰的渐变,雪天用浅灰到白的渐变。这个效果虽然简单,但视觉上很加分,一个卡片就撑起了首页的氛围。
3.3 24小时预报与7天预报模块
24小时预报我用横向滚动的scroll-view实现,每个小时一个纵向排布的单元格:时间、天气图标、温度。横向滚动在 wxml 里注意要给 scroll-view 加scroll-x属性,同时内部的 cell 要设display: inline-block或 flex 布局,否则滚动不会生效。
7天预报用普通列表渲染,每天一行:星期几、天气图标、最高/最低温度。这里温度展示我用了一个温度区间条,左右两端分别是最低温和最高温,中间用渐变色条表示温差范围,比单纯显示两个数字要直观。
// pages/index/index.js 关键片段 function fetchWeather(city, forceRefresh = false) { const cacheKey = `${city.id}_daily`; if (!forceRefresh) { const cached = getCache(cacheKey); if (cached) { renderWeather(cached); return; } } request('/weather/daily', { cityId: city.id }) .then((data) => { const parsed = parseDaily(data); setCache(cacheKey, parsed, 120); renderWeather(parsed); }) .catch((err) => { // 请求失败时如果有缓存就先用缓存展示 const cached = getCache(cacheKey); if (cached) { renderWeather(cached); wx.showToast({ title: '网络异常,显示缓存数据', icon: 'none' }); } }); }这段逻辑里有一个细节:请求失败时降级使用缓存。用户在地下室、电梯里打开小程序,接口大概率是失败的,但之前缓存过天气数据,直接展示缓存并提示网络异常,比让用户看一个空白页要强得多。这是我做这个项目时加的最有价值的一行容错逻辑。
3.4 城市管理:搜索、添加、排序与删除
城市管理页是另一个核心页面,解决的是多城市切换的问题。我用了两个存储变量:一个是cityList,保存用户添加的城市数组;一个是currentCityId,记录当前选中的城市编码。
添加城市的流程:页面打开时展示当前城市列表,点击新增按钮进入城市搜索页。搜索的逻辑是调用天气数据平台的城市搜索接口,用户输入关键字后,对返回结果做筛选,点击某个城市项后写入 storage 并跳回首页刷新数据。
删除和排序我用的是长按触发操作的方式:长按某个城市项弹出 actionSheet,选项包含“设为默认”和“删除城市”。删除时要同步处理两件事:从 cityList 中移除该城市、如果删除的是当前城市则需要把当前城市切换到城市列表的第一项。这个联动逻辑一开始我漏掉了,导致删除默认城市后首页还停留在已删除城市的数据,后来专门补上了这段处理。
3.5 下拉刷新与自动定位
首页配置了enablePullDownRefresh: true,用户下拉时触发onPullDownRefresh,重新请求当前城市的最新数据。注意下拉刷新的生命周期函数必须写在页面的Page()配置里,它不是普通的回调函数。
自动定位这块,我用的是wx.getLocation,拿到经纬度后通过逆地理编码接口转换成城市信息。这里有一个权限问题,iOS 和 Android 对定位权限的处理不太一样,用户如果拒绝了授权,小程序会自动清理相关权限,需要引导用户手动打开设置页重新授权。
// utils/location.js function getLocation() { return new Promise((resolve, reject) => { wx.getLocation({ type: 'gcj02', success: resolve, fail: reject }); }); } function handleLocationAuth() { return getLocation().catch(() => { wx.showModal({ title: '需要定位权限', content: '请在设置中开启定位权限,以便获取当前城市的天气', confirmText: '去设置', success: (res) => { if (res.confirm) { wx.openSetting(); } } }); }); }授权的核心思路是:不直接拒绝用户,而是在失败时弹窗引导。wx.openSetting可以打开小程序的设置页,用户在那里手动开启定位权限后返回,小程序会自动触发 onShow 生命周期,这时候我再重新获取一次定位即可。这个流程虽然多几步,但比直接 toast 提示“定位失败”要友好非常多。
4. 常见问题与排查技巧实录
4.1 真机预览时接口报错:未配置合法域名
开发工具里接口请求一切正常,但手机预览时请求直接 fail,控制台会看到这样的提示:不在以下 request 合法域名列表中。这是小程序平台的安全机制,线上环境要求所有请求域名必须在小程序后台配置白名单。
处理办法:登录小程序管理后台,把天气数据服务的域名添加到“服务器域名 - request合法域名”中。如果还在开发阶段,也可以在开发者工具中勾选“不校验合法域名”来临时绕过,但这个选项只对本地开发有效,真机预览时不一定生效。
另外一个容易踩的坑:域名不能有路径,只能写成https://api.example.com这种根域形式,不能带/v1/weather这类路径后缀。子域名也需要注意,如果接口实际域名是https://api.example.com,配置里必须写这个,写https://example.com是无效的。
4.2 定位权限被拒后的恢复逻辑
定位权限被拒后,很多新手会陷入死循环:拒绝授权 -> 请求失败 -> 弹出授权 -> 再次拒绝。我前面写的handleLocationAuth就是专门处理这个循环的。
这里有一个体验细节:如果用户第一次拒绝,我不建议再次自动拉起wx.authorize,因为小程序会直接返回 fail,频繁弹窗体验很差。更好的方式是引导用户去设置页手动开启,配合一个友好的说明弹窗。用户从设置页返回后,通过onShow生命周期来重新检查并执行定位,而不是依赖用户手动刷新页面。
4.3 温度数据显示异常:零下温度被截断
我遇到过一个比较隐蔽的问题:当温度为 -5 度时,WXML 里直接渲染{{currentWeather.temp}}°,显示出来没问题。但如果我把它处理成带符号的字符串,比如-5°,在某些机型上会被样式裁掉负号。原因很简单:卡片里的温度字号设置很大,文本容器宽度不足,负号被挤出了可视区。
这个问题的排查思路:先看渲染出来的数据是不是正常的,再看样式是不是裁剪了。解决办法有两种,一是给温度容器加padding-left,预留负号的宽度;二是用view而不是text来装温度数字,把宽度计算方式改成box-sizing: border-box。我最后选了前者,因为改动最小,且不会影响正数温度的展示位置。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 解决建议 |
|---|---|---|
| 真机请求失败 | 没有配置合法域名 | 后台添加 request 合法域名 |
| 定位一直失败 | 用户拒绝授权 | 弹窗引导去设置页开启 |
| 下拉刷新不触发 | 页面未配置 enablePullDownRefresh | 在页面 json 中开启 |
| 缓存导致数据旧 | 缓存时间设置太长 | 缩短缓存 TTL 或强制刷新 |
| 城市切换后天气没变 | currentCityId 未同步 | 检查全局状态是否更新 |
| 滚动列表卡顿 | 一次渲染太多列表项 | 用 setData 分批渲染 |
这张表是我自己开发过程中沉淀出来的,不一定覆盖所有问题,但都是高频故障。排查小程序问题有个通用思路:先分前端后端,再看数据链路,最后查缓存。很多看起来诡异的问题,最后都是缓存没更新导致的。
4.5 调试技巧:学会用 vConsole 和本地缓存面板
开发者工具里有个非常好用的功能是调试器面板中的 Storage 查看器,可以直观看到当前wx.setStorageSync存了什么数据。我排查缓存问题时基本离不开它:先看缓存 key 是否存在,再看 expireAt 是否过期,最后确认页面上读取的 key 和写入的 key 一致。曾经遇到过一次 key 拼写不一致导致永远走网络请求的 bug,所有接口都慢了一截,就是靠这个面板发现的。
手机真机调试时,可以在wx.request的 success 回调里临时加一行console.log,然后在真机调试面板的 vConsole 里查看网络返回。排查完记得删掉这些临时日志,避免上线后控制台噪音太大。
5. 未来优化与扩展方向
5.1 体验层优化:骨架屏与过渡动画
现在的首页加载过程中,在拿到接口数据之前是全白一片的。虽然网络正常情况下请求耗时只有几百毫秒,但弱网场景下这个白屏时间会拉长到两秒以上,体验很不好。我计划接下来做两件事:骨架屏和过渡动画。
骨架屏的实现思路:先用假的灰色卡片形状占位,等数据请求完成后再替换成真实内容,这样视觉上不会出现“页面没东西”的尴尬。过渡动画可以用小程序自带的分组动画或者 css 动画过渡,比如温度数字变化时做一个滚动效果,卡片背景切换时做一个透明度渐变。这些优化不改变数据结构,纯粹是体验提升,适合在功能跑通之后再做。
5.2 数据层增强:空气质量与生活指数
目前首页只展示了最基础的天气数据,后续可以加的模块还挺多:空气质量 AQI、紫外线指数、洗车指数、穿衣建议、过敏指数等等。这些数据接口基本都支持一次性返回,不需要额外很多开发量。关键要控制首页信息密度,不要把所有数据都堆在首屏。我目前的规划是在卡片下方增加一个可横向滑动的数据入口,点击某项进入二级页面查看详细数据。这样保持首页干净,又不会让功能藏得太深。
5.3 功能层扩展:订阅消息与天气预警推送
天气预报类小程序做订阅消息很有优势:用户可以订阅“明日降雨提醒”“高温预警通知”这类消息。小程序的消息订阅模板需要在后台申请,一次订阅可以推送一条消息,之后需要用户再次订阅。这个能力非常适合天气类工具:每天定时调用云函数查询用户所在城市的天气,达到阈值就下发订阅消息。目前我还在研究云函数的调度方案,如果后期做完,整个项目就从一个纯展示型工具,变成一个能主动触达用户的服务型小程序了。
5.4 工程化与交付:云开发与CI/CD
如果这个项目要长期维护,我建议把数据层也迁到云开发。小程序云开发自带云函数、云数据库和云存储,天气数据可以先由云函数定时拉取并写入云数据库,前端直接读取云数据库而不是每次请求外部接口。这样有两个好处:一是外部服务的 key 不用暴露在小程序前端代码里,安全系数提高;二是可以多端共用服务端逻辑,以后如果做 App 或者 Web 端,可以直接复用云函数。
上线前还可以结合微信开发者工具的 CI 能力,把上传代码、预览等步骤集成到自动化流程里,减少手动操作带来的版本混乱。不过这一步对个人项目来说优先级不高,等确实需要频繁发版时再上也不迟。
写在最后:我从这个项目里学到的三件事
第一次完整做完一个能上真机跑的小程序天气应用,我的感受是三句话:数据转换问题永远比 UI 问题难排查,缓存做得好不好直接决定用户体验,合不合法域名这种基础配置会浪费你一下午。所以我的建议是,做这类工具小程序时,一定要先找一个稳定的数据源,把网络请求和缓存逻辑理顺,再花时间打磨 UI。
整个项目的完整源码,我是按照这篇文章里的目录结构和代码逻辑组织的,文章里贴出的都是可运行的关键代码片段,其他部分(页面样式、地图选点、城市搜索)你完全可以照着这个思路自己补全。如果你在实现过程中遇到了文章里没提到的问题,欢迎按这篇文章的思路去排查。愿你的小程序能顺利跑起来。