☰
微信跑步小程序源码解析:从跑不起来到二次开发实战
2026/11/5 2:51:27 网站建设 项目流程

简介:这份微信跑步小程序源码面向希望入门微信小程序开发、尤其是健康运动类应用场景的学习者,提供从零构建跑步追踪与数据记录功能的完整代码参考。资源包共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存历史跑步记录
计时setInterval+Date.now()跑步时长与配速
授权wx.authorize、wx.getSetting定位权限申请与检测

这里有个关键认知:跑步要的是“持续定位”,不是wx.getLocation那种一次性取点。wx.getLocation调一次拿一个坐标就结束了,你拿它做跑步轨迹,画出来就是断断续续的几个点。正确姿势是wx.startLocationUpdate开启后台定位更新,再wx.onLocationChange注册回调,系统会按一定频率推坐标给你。这个区别是新手翻车率最高的地方,后面避坑章节还会展开。

2.3 数据流:一次跑步从开始到落库

把数据流捋清楚,改代码时才知道该动哪。整个链路是这样的:

  1. 用户在index页点“开始跑步”,wx.navigateTo跳到running页,带上mode参数(户外跑/跑步机)。
  2. running页onLoad里先查授权,没授权就wx.authorize申请scope.userLocation,用户拒绝则引导去wx.openSetting。
  3. 授权通过后wx.startLocationUpdate,注册wx.onLocationChange,每来一个点就 push 进this.data.points,同时用utils/geo.js累加距离、刷新地图 polyline。
  4. 计时器每秒跑一次,更新时长和当前配速。
  5. 用户点“结束”,wx.stopLocationUpdate停掉监听,跳result页,把本次数据组装成一条记录,经utils/storage.js写进本地缓存。
  6. 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 // 配速:每公里用时,输入秒和公里数,输出 "5'30\"" 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')保证秒数是两位,5'05"而不是5'5"。

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: "6'00\"", points: [] }); } wx.setStorageSync('RUNNING_RECORDS', mock);

塞完刷新历史页,看列表、统计、空状态是否都正常。空状态尤其要测,把缓存清空再进历史页,如果页面白屏或者报错,说明没做list.length === 0的兜底。这套“造假数据 → 看渲染 → 清空再看空态”的流程,比真跑几公里高效得多。

最后说个血泪教训:我早期改这类定位项目,总想着先把功能堆上去,结果定位监听和计时器在页面间来回切时反复注册,内存和电量双双爆炸,用户反馈“跑十分钟手机烫得能煎蛋”。从那以后我每次动onLoad/onUnload都强制走一遍“进页面 → 退页面 → 再进”的循环,用console.log数监听器注册次数,确认进出对称才继续往下写。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询