简介这份微信跑步小程序源码面向希望入门微信小程序开发、尤其是健康运动类应用场景的学习者提供从零构建跑步追踪与数据记录功能的完整代码参考。资源包共20个文件以js逻辑脚本、wxml结构文件、wxss样式表和json配置为主另含少量图片与说明文档压缩包约12KB体量轻巧便于快速导入微信开发者工具运行调试。内容覆盖页面结构与路由跳转、data数据模型与setData状态更新、本地存储、点击与滑动事件处理以及wx.getLocation定位、加速度计步、wx.request网络请求、自定义组件封装和权限授权等关键环节并演示动画效果与UI组件的组合使用。目前已有1975人学习适合对照源码理解小程序工程组织方式掌握实际项目中的设计思路与编程技巧遇到疑问还可留言交流。1. 从一份跑不起来的跑步小程序源码说起去年帮一个做体育社群的朋友看项目他甩过来一个压缩包说“微信跑步小程序源码你帮我跑起来看看”。解压之后目录倒是齐全pages、utils、app.js 一个不少可真到开发者工具里一编译地图不显示、定位不刷新、跑步轨迹画出来是条直线。这类源码最典型的坑不是代码写得多烂而是它默认你已经配好了小程序后台的类目权限、地图 key 和合法域名。跑步类小程序属于典型的“位置服务 实时数据 本地存储”三件套核心就三件事拿到用户实时经纬度、把点连成轨迹算距离配速、把每次跑步记录存下来做历史统计。适合谁呢想学小程序地图与定位 API 的初学者、要给社群做个轻量跑步打卡工具的开发者、或者拿它当毕设脚手架的学生。下面我按“这项目长什么样 → 怎么让它跑起来 → 核心代码怎么读 → 怎么改成你自己的”这条线拆一遍中间会重点讲几个我踩过的坑。2. 项目结构与技术栈一个跑步小程序到底由哪些文件撑起来2.1 目录骨架与页面职责划分拿到源码先别急着点编译先把目录扫一遍。一个结构正常的跑步小程序根目录下通常是这样的running-miniprogram/ ├── app.js # 全局逻辑登录态、全局数据、启动参数 ├── app.json # 全局配置页面路由、窗口样式、权限声明 ├── app.wxss # 全局样式 ├── project.config.json # 开发者工具项目配置appid 在这里 ├── sitemap.json # 搜索索引配置 ├── pages/ │ ├── index/ # 首页开始跑步入口、今日数据 │ ├── running/ # 跑步中地图、实时轨迹、计时配速 │ ├── result/ # 结束页本次成绩、保存记录 │ └── history/ # 历史记录列表与详情 ├── components/ │ └── stat-card/ # 数据卡片组件距离/时长/配速 ├── utils/ │ ├── geo.js # 经纬度距离计算、坐标转换 │ ├── format.js # 时间、配速格式化 │ └── storage.js # 本地记录读写封装 └── images/ # 图标与占位图这个划分不是随便定的。pages/running是整项目最重的页面地图组件、定位监听、计时器全压在这里utils/geo.js是纯函数模块负责把一串经纬度算成总距离它不依赖任何页面方便单独测试utils/storage.js把wx.setStorageSync包一层避免每个页面都写一遍 key 字符串。我一般看别人源码先看app.json的pages数组和permission字段前者告诉你有哪些页面后者告诉你它要不要定位权限——跑步小程序必然有scope.userLocation。2.2 技术栈与关键 API 盘点这项目没有花哨的框架就是原生小程序 微信内置能力技术栈可以列成一张表能力用到的 API / 组件在项目里的作用地图展示map组件跑步页底图、轨迹 polyline实时定位wx.startLocationUpdate/wx.onLocationChange持续拿经纬度逆地址/坐标wx.getLocation、qqmap-wx-jssdk常见做法起点定位、坐标转换本地存储wx.setStorageSync/getStorageSync存历史跑步记录计时setIntervalDate.now()跑步时长与配速授权wx.authorize、wx.getSetting定位权限申请与检测这里有个关键认知跑步要的是“持续定位”不是wx.getLocation那种一次性取点。wx.getLocation调一次拿一个坐标就结束了你拿它做跑步轨迹画出来就是断断续续的几个点。正确姿势是wx.startLocationUpdate开启后台定位更新再wx.onLocationChange注册回调系统会按一定频率推坐标给你。这个区别是新手翻车率最高的地方后面避坑章节还会展开。2.3 数据流一次跑步从开始到落库把数据流捋清楚改代码时才知道该动哪。整个链路是这样的用户在index页点“开始跑步”wx.navigateTo跳到running页带上mode参数户外跑/跑步机。running页onLoad里先查授权没授权就wx.authorize申请scope.userLocation用户拒绝则引导去wx.openSetting。授权通过后wx.startLocationUpdate注册wx.onLocationChange每来一个点就 push 进this.data.points同时用utils/geo.js累加距离、刷新地图 polyline。计时器每秒跑一次更新时长和当前配速。用户点“结束”wx.stopLocationUpdate停掉监听跳result页把本次数据组装成一条记录经utils/storage.js写进本地缓存。history页onShow时从缓存读列表渲染。这条链路里points数组是核心数据结构每个元素至少是{latitude, longitude, timestamp}。距离计算、轨迹绘制、配速计算全从它派生。理解这一点后面看代码就不会迷路。3. 跑起来环境准备、appid 配置与定位权限打通3.1 导入项目与 appid 替换源码拿到手第一步不是编译是改project.config.json。别人源码里的appid大概率是占位符或者别人的号你直接用会报“无权限”或者云能力不可用。打开文件找到appid字段{ appid: wx1234567890abcdef, projectname: running-miniprogram, compileType: miniprogram, libVersion: 2.30.0, setting: { urlCheck: false, es6: true, minified: true } }把appid换成你自己在公众平台注册的小程序 appid。没有的话开发者工具里选“测试号”也能跑但测试号不支持部分能力定位和地图在测试号下可能受限建议还是用正式 appid。urlCheck设成false是本地调试时跳过合法域名校验上线前记得改回true否则请求会失败。libVersion是基础库版本跑步用的startLocationUpdate要求基础库 2.8.0 以上源码里写 2.30.0 是安全的。3.2 定位权限与后台定位声明跑步小程序最容易卡住的一步是权限。app.json里必须声明{ permission: { scope.userLocation: { desc: 你的位置信息将用于记录跑步轨迹 } }, requiredBackgroundModes: [location] }permission.scope.userLocation.desc是弹授权框时给用户看的文案不写或者写得太敷衍审核和用户信任都会打折。requiredBackgroundModes里加location是为了息屏后还能继续记录——跑步时用户大概率锁屏揣兜里不加这个一锁屏轨迹就断。注意这个字段在部分基础库版本下需要配合后台定位能力开通个人主体小程序可能拿不到这是现实限制得提前有心理准备。授权流程代码一般写在running页的onLoad里// pages/running/running.js onLoad(options) { this.mode options.mode || outdoor; this.checkLocationAuth(); }, checkLocationAuth() { wx.getSetting({ success: (res) { if (res.authSetting[scope.userLocation]) { this.startTracking(); } else { // 未授权主动申请 wx.authorize({ scope: scope.userLocation, success: () this.startTracking(), fail: () { // 用户拒绝过只能引导去设置页 wx.showModal({ title: 需要定位权限, content: 跑步记录需要获取你的位置请在设置中开启, success: (r) { if (r.confirm) wx.openSetting(); } }); } }); } } }); }这段逻辑的关键在于区分“从没问过”和“拒绝过”。wx.authorize只在用户没做过选择时弹框如果用户之前点过拒绝再调authorize会直接走fail不会弹框。所以fail分支必须引导到wx.openSetting让用户手动开。很多人只写authorize不写openSetting结果用户拒绝一次之后再也开不了定位还以为代码坏了。3.3 地图 key 与合法域名如果项目里用了腾讯位置服务做逆地址解析把经纬度转成“某某路”需要在app.js里初始化 SDK 并填 key// app.js const QQMapWX require(./libs/qqmap-wx-jssdk.min.js); App({ onLaunch() { this.qqmapsdk new QQMapWX({ key: 你的腾讯位置服务key // 去位置服务控制台申请 }); } });这个 key 要去腾讯位置服务控制台申请申请时绑定的域名要和小程序合法域名一致。如果只是画轨迹、算距离其实不需要逆地址utils/geo.js里的 Haversine 公式就够了能省掉一个外部依赖和 key 配置的麻烦。我一般建议先跑通纯本地版本再按需接逆地址。4. 核心模块拆解定位监听、轨迹绘制与距离计算4.1 持续定位与坐标采集跑步页的定位启动逻辑是整个项目的心脏// pages/running/running.js startTracking() { this.points []; this.totalDistance 0; this.startTime Date.now(); wx.startLocationUpdate({ type: gcj02, // 坐标系地图组件用 gcj02 success: () { this.locationListener (res) { const point { latitude: res.latitude, longitude: res.longitude, timestamp: Date.now() }; this.handleNewPoint(point); }; wx.onLocationChange(this.locationListener); this.startTimer(); }, fail: (err) { console.error(定位启动失败, err); wx.showToast({ title: 定位启动失败, icon: none }); } }); }type: gcj02这个参数不能省。小程序map组件用的是 gcj02 坐标系而wx.getLocation默认返回 wgs84两者差几百米直接混用会导致轨迹整体偏移。startLocationUpdate的type指定 gcj02 后回调里的经纬度就能直接喂给地图。onLocationChange注册的回调要存到this.locationListener因为页面卸载时必须wx.offLocationChange(this.locationListener)解绑否则监听会泄漏下次进页面重复注册点会翻倍。4.2 轨迹 polyline 与地图跟随每来一个新点除了累加距离还要更新地图上的线handleNewPoint(point) { const last this.points[this.points.length - 1]; if (last) { // 过滤掉漂移点位移过大的点大概率是定位跳变 const step getDistance(last, point); if (step 50) return; // 单次位移超 50 米视为异常丢弃 this.totalDistance step; } this.points.push(point); const polyline [{ points: this.points, color: #1aad19, width: 6, arrowLine: true }]; this.setData({ polyline, latitude: point.latitude, longitude: point.longitude, distance: (this.totalDistance / 1000).toFixed(2) }); }这里有两个细节值得说。第一是漂移过滤GPS 在楼宇间、树下容易跳点一个点突然偏出去几百米不滤掉的话轨迹会画出一个大尖刺距离也虚高。step 50是个经验阈值跑步场景下两次定位间隔通常一两秒正常人跑不了 50 米超过基本是漂移。第二是setData的频率onLocationChange可能一秒推好几次每次都setData整条 polyline 会卡常见做法是节流比如 1 秒最多更新一次地图中间的点先攒着。源码里如果没做节流跑久了地图会明显发烫卡顿这是要改的点。4.3 距离计算与配速格式化utils/geo.js里的距离函数是纯数学用 Haversine 公式算球面两点距离// utils/geo.js const EARTH_RADIUS 6378137; // 米 function toRad(deg) { return deg * Math.PI / 180; } // 计算两个经纬度点之间的球面距离米 function getDistance(p1, p2) { const lat1 toRad(p1.latitude); const lat2 toRad(p2.latitude); const dLat lat2 - lat1; const dLng toRad(p2.longitude - p1.longitude); const a Math.sin(dLat / 2) ** 2 Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2; return 2 * EARTH_RADIUS * Math.asin(Math.sqrt(a)); } module.exports { getDistance };EARTH_RADIUS用 6378137 米是 WGS84 椭球长半轴做跑步距离够用了误差在千分之几。toRad把角度转弧度Haversine 的核心是a那一行asin(sqrt(a))得到圆心角乘两倍半径就是弧长。配速格式化在utils/format.js// utils/format.js // 配速每公里用时输入秒和公里数输出 530\ function formatPace(seconds, km) { if (!km || km 0.01) return ----\; const paceSec seconds / km; const min Math.floor(paceSec / 60); const sec Math.round(paceSec % 60); return ${min}${sec.toString().padStart(2, 0)}; }km 0.01的兜底很重要刚开始跑距离接近 0直接除会得到巨大配速或者Infinity显示出来就是乱码。padStart(2, 0)保证秒数是两位505而不是55。4.4 本地存储与历史记录记录落库用同步存储就够跑步数据量小没必要上云// utils/storage.js const KEY RUNNING_RECORDS; function saveRecord(record) { const list wx.getStorageSync(KEY) || []; list.unshift(record); // 新记录放最前 wx.setStorageSync(KEY, list.slice(0, 200)); // 最多留 200 条 } function getRecords() { return wx.getStorageSync(KEY) || []; } module.exports { saveRecord, getRecords };unshift让最新记录排最前历史页直接顺序渲染就是倒序。slice(0, 200)是防止缓存无限膨胀小程序单个 key 有 1MB 上限一条记录几百字节200 条远没到顶但留个上限是习惯。每条记录的结构建议固定成{id, date, distance, duration, pace, points}points存不存看需求存了能做轨迹回放不存能省空间。5. 避坑与排查那些让跑步小程序“跑不动”的常见问题5.1 地图一片空白只有网格现象进跑步页map区域是灰白网格没有底图。原因通常有两个一是project.config.json里appid还是占位符地图组件在无效 appid 下不加载底图二是基础库版本太低map的某些属性不认。解决换成真实 appid开发者工具里“详情 → 本地设置 → 调试基础库”调到 2.20.0 以上重新编译。如果还不行检查map标签是不是被父容器overflow: hidden裁掉了高度地图必须有明确宽高。5.2 轨迹画出来是直线或者断成几截现象跑完一看轨迹从起点直接连到终点中间没有弯。原因用了wx.getLocation一次性取点或者onLocationChange注册了但没真正触发——后者常见于没调startLocationUpdate就注册监听。解决确认startLocationUpdate的success回调里才注册onLocationChange并且type设成gcj02。另外检查是不是把points每次setData时覆盖了而不是 push覆盖的话永远只有一个点polyline 自然画不出线。5.3 息屏后轨迹中断现象手机锁屏揣兜里跑回来发现中间一段没记录。原因没在app.json声明requiredBackgroundModes: [location]或者小程序被系统挂起。解决补上后台定位声明同时确认小程序后台的“后台定位”能力已开通。个人主体拿不到这个能力是常态退而求其次的方案是提示用户跑步时保持屏幕常亮用wx.setKeepScreenOn({keepScreenOn: true})跑完再关掉。5.4 距离虚高配速离谱现象实际跑 3 公里显示 5 公里配速快到不像人类。原因GPS 漂移点没过滤每个跳变点都被算进距离。解决在handleNewPoint里加位移阈值过滤前面代码里的step 50还可以加一个速度过滤两点间算出的瞬时速度超过 10m/s 就丢弃。另外确认坐标系没混用wgs84 和 gcj02 混算也会让距离偏大。5.5 页面退出后计时器还在跑现象从跑步页返回首页再进来时长翻倍。原因setInterval的 timer id 没在onUnload里clearInterval旧计时器还在后台跑。解决onUnload里统一清理——clearInterval(this.timer)、wx.offLocationChange(this.locationListener)、wx.stopLocationUpdate()三件套一个不能少。我见过最隐蔽的情况是onHide没停、onShow又启动一次导致双计时器叠加这种只能靠日志打时间戳排查。6. 二次开发把它改成你自己的跑步工具拿到这份源码多数人不是原样用而是改成自己场景的东西。改之前先想清楚动哪层只改 UI 就动wxml/wxss改业务逻辑动pages/running改数据模型动utils/storage和记录结构。我一般按“先跑通、再换皮、最后加功能”的顺序来避免一上来大改导致跑不起来还找不到原因。第一个改造点是记录结构扩展。比如你想加“卡路里”和“步频”就在result页保存时多算两个字段// pages/result/result.js const record { id: Date.now(), date: formatDate(new Date()), distance: this.data.distance, // 公里 duration: this.data.duration, // 秒 pace: this.data.pace, calories: Math.round(this.data.distance * 60), // 粗略估算每公里 60 千卡 points: this.data.points }; saveRecord(record);卡路里按体重算才准distance * 60是给 60kg 成年人的粗估真要准就得让用户在设置里填体重公式换成MET * 体重 * 小时数。步频则需要加速度计数据小程序wx.onAccelerometerChange能拿到三轴加速度做峰值检测估算步数这块复杂度不低建议先不做。第二个改造点是历史页的可视化。原始源码大概率只是列表你可以加一个周统计按日期分组算本周总距离和次数用简单的柱状条渲染。数据从getRecords()里按date过滤就行不需要额外存储。第三个改造点是分享跑步结果页加onShareAppMessage把距离配速拼进分享标题这是跑步类小程序拉新的主要入口。改完怎么验证我的习惯是造三条假数据直接塞缓存绕过真实跑步// 开发者工具 Console 里执行造测试数据 const mock []; for (let i 0; i 3; i) { mock.push({ id: Date.now() - i * 86400000, date: 2024-06-0${i 1}, distance: (3 i).toFixed(2), duration: 1200 i * 300, pace: 600\, points: [] }); } wx.setStorageSync(RUNNING_RECORDS, mock);塞完刷新历史页看列表、统计、空状态是否都正常。空状态尤其要测把缓存清空再进历史页如果页面白屏或者报错说明没做list.length 0的兜底。这套“造假数据 → 看渲染 → 清空再看空态”的流程比真跑几公里高效得多。最后说个血泪教训我早期改这类定位项目总想着先把功能堆上去结果定位监听和计时器在页面间来回切时反复注册内存和电量双双爆炸用户反馈“跑十分钟手机烫得能煎蛋”。从那以后我每次动onLoad/onUnload都强制走一遍“进页面 → 退页面 → 再进”的循环用console.log数监听器注册次数确认进出对称才继续往下写。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?