微信小程序出行信息聚合应用开发实战:架构、网络与缓存
简介一款面向出行信息查询场景的微信小程序设计与实现资料包适合正在学习小程序开发、需要完成毕业设计或项目实战的开发者使用。内容围绕“出行先知”项目展开覆盖小程序开发基础、项目架构、实时天气与交通信息查询、目的地信息展示、用户个性化设置等功能模块并包含数据获取、界面渲染、交互处理与本地缓存等核心技术实现细节。压缩包内主要包含项目源码、说明文档、演示视频等文件整体约65.16MB可帮助读者从源码阅读、文档理解到视频演示全过程掌握项目开发思路与实现方法。说明文档对开发流程、技术细节及问题解决方案做了梳理演示视频直观展示操作流程便于快速上手。目前已有124人学习下载适合需要参考完整小程序项目案例的开发者。1. 出行先知的定位为什么用微信小程序承载实时出行数据出门前查三样东西天气、路况、目的地周边有什么。这三样数据分散在三个 App 里来回切换本身就费时间。出行先知把这三类信息收进一个微信小程序扫码即用不占桌面。交付物是微信小程序源码、说明文档加演示视频覆盖信息聚合类小程序从零到上的完整链路。适合读这篇文章的三类人准备做微信小程序毕业设计的学生想拿现成微信小程序源码改造成自己项目的前端以及需要快速搭内部出行工具、不想从空白工程起步的工程师。后面按「架构拆分 → WXML/WXSS 渲染 → Promise 请求封装 → 缓存策略 → 真机排错」的顺序展开每章代码都能直接抄走。2. 模块化架构与 WXML/WXSS 动态渲染三个业务模块怎么拆出行先知的核心诉求是「一个页面聚合多种数据」所以工程结构不能拍脑袋堆代码。模块化在这里的含义是数据获取、界面展示、用户交互三件事各管各的页面只负责把这三层串起来。2.1 目录结构与模块职责划分从源码工程打开目录是这样分的miniprogram/ ├── pages/ │ ├── index/ # 首页实时天气 关注地点 │ ├── traffic/ # 交通信息查询 │ └── destination/ # 目的地信息与周边 POI ├── components/ │ └── weather-card/ # 天气卡片首页和订阅页复用 ├── utils/ │ ├── request.js # wx.request 的 Promise 封装 │ └── cache.js # 本地缓存带过期时间封装 ├── app.js # 全局逻辑导航栏高度、全局变量 ├── app.json # 页面注册与窗口配置 └── project.config.json # 项目配置appid 在这里填分层的依据是职责边界utils 里的 request.js 只负责发请求和收响应不知道页面长什么样components/weather-card 只负责把 weather 对象渲染成卡片不关心数据从哪来。pages 目录下的每个页面是一个独立文件夹页面私有组件放页面文件夹内部跨页面复用的组件才提升到 components 根目录。app.json 里 pages 数组的第一个元素就是小程序启动后默认打开的页面。出行先知默认首页是 index这个顺序在开发中很容易被忽略。有人改了几次页面后把 traffic 放到了第一个结果一扫码进的是交通查询页以为源码出了问题。2.2 首页天气卡片setData 与 WXML 数据绑定的边界微信小程序的界面渲染走 WXML 模板语言它和常见前端模板很像但限制更多WXML 里不能写复杂表达式三元运算和简单的{{}}插值是极限。首页天气卡片的 WXML 长这样view classweather-card text classtemp{{weather.temp}}°C/text text classdesc{{weather.condition}}/text view classmeta text湿度 {{weather.humidity}}%/text text{{weather.windDir}} {{weather.windLevel}}级/text /view /view这段模板绑定的是this.data.weather这个对象。页面 JS 里通过this.setData({ weather: {...} })更新它视图才会变化。setData 是整个小程序性能的关键点它不只是改内存数据还会把数据从逻辑层传到渲染层。传一个 500KB 的对象和传一个 5KB 的对象渲染耗时能差出一个量级。所以天气 API 的返回结果在进 setData 之前会先裁剪字段这个后续第三章讲。WXSS 是样式语言rpx 是响应式像素单位设计稿 750rpx 对应屏幕宽度。底部安全区适配是 iPhone 全面屏的必修课.page { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); }constant 是 iOS 11.0 的写法env 是 iOS 11.2 之后的写法两个都写低版本不会因为未知属性报错只是忽略后一条。如果不在 app.json 里开启自定义导航栏页面顶部会有默认导航栏这条 padding 只管底部。2.3 交通查询页列表渲染与事件冒泡交通查询页要展示公交线路和地铁线路本质是一个列表。用 wx:for 渲染view wx:for{{lines}} wx:keyid classline-item bindtaponLineTap>onLineTap(e) { const id e.currentTarget.dataset.id; wx.navigateTo({ url: /pages/destination/destination?id${id} }); }这里有一个高频踩坑点事件对象取数据要用e.currentTarget.dataset而不是e.target.dataset。target 是你实际点中的那个元素currentTarget 是绑定事件的元素。当 view 内部还有子节点时点中 text 和点中 view 拿到的是不同的 target但 currentTarget 永远是绑定了 bindtap 的那个节点。data-id 的值会在 WXML 编译时转成 dataset 里的字段多词属性比如>// utils/request.js const request (url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url, method, data, header: { content-type: application/json }, timeout: 10000, success: (res) { // wx.request 只要收到响应就会走 successstatusCode 需要自己判断 if (res.statusCode 200 res.statusCode 300) { resolve(res.data); } else { reject(new Error(HTTP ${res.statusCode}: ${url})); } }, fail: (err) { // 断网、超时、域名未配置都会走到这个分支 reject(err); } }); }); }; module.exports { request };参数说明method 默认 GETdata 在 GET 场景下会被微信自动拼到 query 上POST 场景下作为请求体。timeout 设 10 秒超过之后走 fail 回调。success 回调不等于请求成功——HTTP 400、500 也会进 success所以必须用 res.statusCode 再判断一次。提示真机调试时如果请求全部失败先检查 wx.request 的合法域名配置。开发者工具可以勾选「不校验合法域名」真机必须在小程序后台配置 request 合法域名。为什么不用回调直接写业务因为三个模块的函数需要链式处理先请求、再裁剪字段、再 setData。Promise 的链式调用让流程变成扁平的 then比嵌套回调好维护得多。3.2 天气 API经纬度参数与返回字段裁剪首页天气模块先通过 wx.getLocation 拿经纬度再请求天气接口const getWeather (lat, lng) { return request(/api/weather, GET, { lat, lng, unit: metric }) .then(res { // 接口返回 30 个字段页面只需要 5 个这里做裁剪 return { temp: res.current.temp, humidity: res.current.humidity, windDir: res.current.wind_dir, windLevel: res.current.wind_level, condition: res.current.condition }; }); };天气数据字段映射表接口原始字段裁剪后字段页面展示current.temptemp24°Ccurrent.humidityhumidity湿度 62%current.wind_dirwindDir东南风current.wind_levelwindLevel3 级current.conditioncondition多云字段裁剪的意义在上一章提过setData 的传输量和字段数量成正比。一个天气预报接口原始返回可能包含逐小时预报、空气质量、紫外线指数等一大堆首页只显示当前实况那就只留 5 个字段。后续要做 7 天预报再单独加字段而不是一开始把整个响应对象怼进 data。实际开发中天气接口一般需要先拿城市编码或经纬度。用经纬度更通用因为 wx.getLocation 直接给坐标省去逆地理编码这一步。注意 app.json 里需要声明permission.scope.userLocation的描述文案否则首次请求定位权限时弹窗会显示默认文字。3.3 交通信息与目的地 POI 检索交通查询页对接的是公交/地铁线路查询接口const searchTraffic (keyword) { return request(/api/bus/query, GET, { keyword, city: beijing }) .then(res { // 部分接口返回的是 { data: { lines: [] } } 结构这里做一层解包 return res.data res.data.lines ? res.data.lines : []; }); };目的地信息页用的是 POI兴趣点检索结合地图选点const searchPoi ({ lat, lng, keyword , radius 2000 }) { return request(/api/poi/search, GET, { lat, lng, keyword, radius, offset: 20 }).then(res res.data.pois || []); };两个函数对比能看到参数设计的两个细节。一个是 city 参数交通线路查询强依赖城市上下文出行先知把它做成页面下拉框选择的全局变量存在 app.globalData 里而不是每次请求都传死值。另一个是 radius目的地周边设施检索默认 2000 米这个值放在函数参数默认值而不是调用方拼接字符串理由是调用方只需要关心业务语义距离换算逻辑收口在封装函数内部。offset 是分页参数POI 接口一次返回 20 条滚动到底部再加载下一页避免一次渲染太多地图标注点。3.4 并发请求与异常降级首页要同时展示天气和用户关注地点的交通状态这两个请求没有依赖关系串行请求会白白浪费一个网络往返。出行先知用 Promise.all 并发const loadHomePage () { wx.showLoading({ title: 加载中 }); Promise.all([getWeather(lat, lng), getFavTraffic()]) .then(([weather, trafficList]) { this.setData({ weather, trafficList }); }) .catch(err { // 降级策略优先用缓存兜底缓存没有才提示错误 const cached wx.getStorageSync(last_weather); if (cached) { this.setData({ weather: cached }); wx.showToast({ title: 已显示缓存数据, icon: none }); } else { wx.showToast({ title: 网络异常, icon: none }); } }) .finally(() wx.hideLoading()); };Promise.all 的特点是 fail-fast任何一个请求失败整个 Promise 直接进入 catch。这里的降级逻辑值得展开缓存里有上次成功的天气数据就先展示缓存同时 toast 提示用户数据可能不是最新的缓存也没有才展示通用报错。不要让用户面对一个空白页。finally 用来收敛 loading 状态注意 finally 需要基础库版本 2.0.4 以上低版本要改成在 then 里双写 hideLoading。4. 本地缓存与个性化订阅减少请求次数提速冷启动出行场景里用户高频打开小程序天气和路况是周期性变化的数据。每次都重新请求接口除了浪费流量更重要的是弱网环境下页面会长时间空白。第四章解决的就是这件事。4.1 wx.setStorageSync 缓存策略过期时间戳微信小程序原生缓存 API 是 wx.setStorageSync但它只有存储、读取、删除没有过期概念。出行先知的 utils/cache.js 自己做了一层 TTL 封装// utils/cache.js const TTL_MAP { weather: 10 * 60 * 1000, // 天气 10 分钟 traffic: 5 * 60 * 1000, // 路况 5 分钟 poi: 7 * 24 * 60 * 60 * 1000 // 目的地详情 7 天 }; const setCache (key, value, type) { wx.setStorageSync(key, { value, expireAt: Date.now() TTL_MAP[type] }); }; const getCache (key) { const data wx.getStorageSync(key); if (!data) return null; if (Date.now() data.expireAt) { wx.removeStorageSync(key); return null; } return data.value; }; module.exports { setCache, getCache };过期时间为什么按数据类型区分天气预报接口的数据 10 分钟更新一次缓存 10 分钟基本不会给用户错误信息实时路况变化更快5 分钟是体验和请求量之间的折中目的地 POI 详情地址、电话、简介几乎不变7 天缓存能显著减少重复请求。这个 TTL_MAP 集中管理的设计比在业务代码里散落Date.now() 600000这种魔法数字清晰得多。首页 onLoad 时先读缓存再发请求能做到秒开onLoad() { const cached getCache(last_weather); if (cached) this.setData({ weather: cached }); // 然后异步刷新覆盖旧数据 this.refreshWeather(); }先渲染缓存、后台刷新的模式是冷启动不白屏的关键。这样用户第一眼看到的是上次的数据等新请求返回后页面自动更新感知上要比转圈等待快很多。4.2 个性化关注地点的订阅存储用户关注的「家」「公司」「常去健身房」这类地点是长期数据走独立的存储键const FAV_KEY favorite_places; const addFavorite (poi) { const list wx.getStorageSync(FAV_KEY) || []; if (!list.some(item item.id poi.id)) { list.push(poi); wx.setStorageSync(FAV_KEY, list); return true; } return false; }; const removeFavorite (poiId) { const list wx.getStorageSync(FAV_KEY) || []; const next list.filter(item item.id ! poiId); wx.setStorageSync(FAV_KEY, next); return next; };去重逻辑放在 addFavorite 内部而不是调用方。同一个 POI 被重复添加会污染列表push 之前用 some 做一次 id 匹配是这里的关键。存储容量方面小程序单个 key 上限 1MB整个 Storage 上限 10MB存几十个地点对象完全够用不需要考虑分片。但注意wx.setStorageSync 是同步阻塞 API关注地点这种小数据用同步没问题如果要缓存图片 base64 之类的数据得换 wx.setStorage 异步版本。4.3 缓存穿透与数据不一致的处理缓存键设计直接决定排错难度。出行先知用的缓存键清单缓存键存储内容过期策略失效时机last_weather最近一次天气数据10 分钟 TTL定时 主动刷新favorite_places关注地点数组不过期用户手动增删poi_{poiId}单个目的地详情7 天 TTL时间到期cache_version缓存版本号不过期发版时手动更新这里要单独说 cache_version。小程序发版后旧缓存结构很可能和新代码对不上——比如 v1 版本存的是数组v2 版本改成了对象直接读取旧缓存会崩溃。解法是发版时在 app.js 里做一次版本校验// app.js onLaunch 里执行 const CURRENT_CACHE_VERSION v2; const storedVersion wx.getStorageSync(cache_version); if (storedVersion ! CURRENT_CACHE_VERSION) { wx.clearStorageSync(); wx.setStorageSync(cache_version, CURRENT_CACHE_VERSION); }全清的问题在于会把用户的登录态一起清掉生产环境不建议直接 clearStorageSync。折中方案是只删已知业务键把登录相关的 token 保留。但对出行先知这种以公开数据为主的小程序全清是最省心且不会漏的。缓存穿透的另一个常见坑是getCache 返回 null 时业务代码直接报错正确做法是 null 时回源请求接口、成功后更新缓存这是前面第三章降级逻辑的完整闭环。5. 真机调试三件事导航栏高度、冷启动白屏、缓存脏数据源码在开发者工具里跑通不代表真机没问题。三件高频翻车的事按实操顺序说。5.1 自定义导航栏高度适配出行先知的首页用的是自定义导航栏在 app.json 里配置 navigationStyle: custom好处是沉浸式坏处是顶部高度要自己算。状态栏和胶囊按钮的位置因机型而异最稳妥的方式是运行时动态获取// app.js onLaunch() { const menu wx.getMenuButtonBoundingClientRect(); const sys wx.getSystemInfoSync(); this.globalData.navBar { statusBarHeight: sys.statusBarHeight, navBarHeight: (menu.top - sys.statusBarHeight) * 2 menu.height, menuRight: menu.right }; }navBarHeight 的计算原理胶囊按钮底部到状态栏底部的距离乘以 2再加上胶囊本身高度就是导航栏总高度。不同安卓机型的胶囊位置不一样写死 44px 或 48px 都会在某些机器上错位。页面 WXSS 里用这个 globalData 设置占位 view 的高度比在每个页面重复计算干净。5.2 冷启动白屏onLoad 与 onReady 时序冷启动时首页要经历「小程序初始化 → 页面加载 → 网络请求 → 数据渲染」四个阶段。如果 data 里没有初始值网络回来之前页面就是空白。解决办法是给 data 设置骨架屏默认值data: { weather: { temp: --, condition: 加载中 }, trafficList: [] }这样首帧渲染出来的是带占位符的骨架请求完成后 setData 覆盖。不要试图在 onLoad 里等待请求完成再渲染——onLoad 的异步不会阻碍 onReady首屏该白还是白。想看请求时序直接在 vConsole 的 Network 面板观察比在电脑上抓包更直接。如果首页关键请求发起时间晚于页面 onLoad 超过 200ms多半是前面有同步阻塞代码。5.3 缓存脏数据按版本清缓存的正确姿势上一章的 cache_version 校验调试时还有一个常用技巧临时把 CURRENT_CACHE_VERSION 改成 v99强制所有设备下次启动清缓存这比让用户手动删除小程序重新进入平滑得多。清缓存动作要在任何业务读取缓存之前执行app.js 的 onLaunch 是唯一可靠的位置。如果不想全清可以按前缀精准删除// 只清业务缓存保留登录态 const keysToClear [last_weather, favorite_places, poi_]; Object.keys(wx.getStorageInfoSync().keys).forEach(key { const matched keysToClear.some(prefix key.startsWith(prefix)); if (matched) wx.removeStorageSync(key); });按前缀匹配 key 再逐个删除比 clearStorageSync 精准。poi_ 开头的缓存键用 startsWith 匹配漏不掉新增的键。这个函数的触发时机可以放在版本校验分支里也可以暴露到设置页面的清除缓存按钮上按钮点击时调用即可。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →