资讯动态

微信小程序公交查询源码二次开发:从定位到换乘的完整实践

发布时间:2026/9/17 16:59:05 来源:尧图企业网站定制
简介一份微信小程序滴滴公交-查公交完整源码面向初中级小程序开发者用于学习公交查询类工具的实现思路。资源共24个文件包含6个JavaScript逻辑脚本、5个WXSS样式表、4个WXML页面结构、4个JSON配置文件及4个PNG图标压缩包仅18KB代码结构紧凑适合快速阅读与二次开发。项目按小程序标准目录组织涵盖页面注册、数据绑定、事件处理等核心环节能够帮助开发者理解从界面布局到交互响应的完整链路。目前已有953人学习该源码适合作为入门公交类小程序实战参考也可在此基础上扩展实时线路、站点搜索等功能。1. 微信小程序查公交为什么我建议直接拿这套滴滴公交源码做二次开发如果你动手写过微信小程序里的LBS应用大概会遇到一类很别扭的需求查公交。它不像查电影、查天气那样调一个接口就能交差而是要把线路、站点、实时位置、换乘方案揉进同一个页面里还要考虑城市差异和接口限流。这套滴滴公交-查公交的微信小程序源码本质上是把“输入起点终点→解析换乘方案→渲染线路走向→展示到站信息”的完整链路做成了可运行的项目骨架。源码包不大但结构上分得比较清楚有app全局配置、pages页面目录、utils工具层几屏截图就能看出它不是为了交作业拍的是真的能编译通过、能跑到模拟器里的那种工程。适合两类人一是刚接触微信小程序、想找一个带完整地图交互的实战项目来拆二是已经在做出行类应用需要一个能快速替换数据源和 UI 的前端框架。拿它当底座比从零写省掉至少一周的页面和状态管理时间下面我会把每个关键模块的代码逻辑和改造方法拆开讲。2. 工程初始化与页面栈设计先看 app.json 和 app.js 里藏了哪些关键配置拿到源码压缩包后先不要急着看页面一个微信小程序项目最值得先读的文件是根目录下的 app.json 和 app.js。这两个文件决定了小程序启动后第一个渲染什么页面、全局数据放在哪里、以及各个分包或组件如何被加载。滴滴公交这套源码里pages 目录下按功能拆分了多个路由而 app.js 则负责把定位信息、城市 ID、经纬度这类高频数据挂载到全局实例上方便每个页面通过 getApp() 直接读取而不是反复调用 wx.getStorageSync。2.1 路由表和 tabBar 配置决定首屏加载优先级先看 app.json 的典型结构一般会包含 pages 数组、window 配置、以及 networkTimeout 这类网络超时设置。{ pages: [ pages/index/index, pages/line/line, pages/search/search, pages/plan/plan ], window: { navigationBarTitleText: 查公交, navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black, backgroundColor: #f5f5f5 }, networkTimeout: { request: 10000, connectSocket: 10000, uploadFile: 10000, downloadFile: 10000 }, permission: { scope.userLocation: { desc: 你的位置信息将用于查找附近公交线路 } }, requiredPrivateInfos: [getLocation, chooseLocation] }注意这里的 requiredPrivateInfos 字段从微信基础库某个版本开始只要代码里调用 wx.getLocation 或 wx.chooseLocation就必须在 app.json 里提前声明否则接口直接返回 fail。很多二手源码编译报错就是漏了这个配置。pages 数组里第一项是 pages/index/index也就是首页如果你开发时想先调试线路详情页把 pages/line/line 挪到第一位即可但记得发布前改回来。networkTimeout 里 request 设置 10000 毫秒公交接口属于弱网敏感型10 秒超时是合理取值不建议设更短因为换乘规划接口经常要聚合多个上游数据源。2.2 getApp() 全局变量挂载定位与城市配置接着看 app.js核心逻辑一般是这样启动时通过 wx.getLocation 拿经纬度再用逆地址解析换成城市名称和 cityCode统一存到 globalData 里。这样首页和换乘页拿到的是同一份定位数据不会出现首页选 A 城市、结果规划页用 B 城市数据的问题。App({ globalData: { cityCode: , cityName: , longitude: 0, latitude: 0 }, onLaunch() { this.initLocation(); }, initLocation() { wx.getLocation({ type: gcj02, success: (res) { this.globalData.longitude res.longitude; this.globalData.latitude res.latitude; this.reverseGeocode(res.longitude, res.latitude); }, fail: () { this.globalData.cityCode 0755; this.globalData.cityName 深圳; } }); }, reverseGeocode(lng, lat) { // 接入腾讯位置服务或高德逆地址解析 // 解析结果写到 this.globalData } });这套设计的好处在于城市切换只是替换 globalData 里的 cityCode所有页面读取城市 ID 时互相隔离不会出现一个页面改配置影响另一个页面的情况。而且定位失败时回退到默认城市这里假设是深圳保证模拟器上第一次编译运行不会白屏。你接手这套源码后建议把 reverseGeocode 里的请求地址替换成自己在腾讯位置服务控制台申请的 key因为源码里内置的 key 大概率是作者的随时会被限制调用。2.3 utils 目录的请求封装与鉴权头处理源码包的 utils 目录里一般放着 request.js 这类公共网络层它的职责是统一拼接 baseURL、注入 token、拦截 HTTP 错误码并跳转登录。查公交场景下不存在用户登录但往往需要注入一个 appkey 或签名参数因为公交数据源属于商业 API服务端要按 key 计费。代码风格通常是返回 Promise业务页面直接 async/await 调用。const BASE_URL https://api.example.com/bus/v1; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json, X-Requested-With: XMLHttpRequest }, success: (res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: 请求失败, icon: none }); reject(res); } }, fail: (err) reject(err) }); }); } module.exports { request };这套封装把业务数据的判断收敛到一个入口不管后端返回的 code 是 0 还是 200前端只需要处理 data 字段。实际开发公交类应用时后端返回的数据往往是嵌套结构比如 { code: 0, data: { lines: [], stations: [] } }统一在 request 层解包后页面里拿到的就是纯粹的公交数据数组判断逻辑会干净很多。值得注意的一个细节是失败时的 toast 提示涉及具体错误码时建议改成 wx.showModal 并带上 retry 按钮因为公交接口的超时概率比普通接口高用户需要自行重新加载而不是傻等。3. 首页公交路线展示与搜索列表逻辑canvas 绘制路线与状态管理拆分首页是整个小程序的门面滴滴公交源码里这一页承载了附近公交站、途经线路列表、以及线路走向图三个核心模块。先说整体数据流页面 onLoad 时从 getApp() 拿到城市码调用附近站点接口获取站点列表再以站点为中心拉取经过的线路用户点击某一趟车后跳转到线路详情页。源码里对视图层的组织方式值得学习它把附近站点列表和线路列表拆成两个独立组件而不是塞进一个大 wxml 里循环两遍这样后续维护时只改一个组件不会影响另一个区域的渲染性能。3.1 触底加载与分页防重复技巧附近站点列表走的是触底分页模式微信小程序里用 onReachBottom 做触底加载坑在于页面滚动的判定边界和防重复请求。源码里通常使用 isLoading 标志位加 pageNo 字段来防止并发。Page({ data: { stationList: [], pageNo: 1, isLoading: false, hasMore: true }, onReachBottom() { if (this.data.isLoading || !this.data.hasMore) return; this.fetchStations(); }, fetchStations() { this.setData({ isLoading: true }); request(/station/nearby, GET, { page: this.data.pageNo, city: getApp().globalData.cityCode }) .then((res) { const list this.data.stationList.concat(res.list); this.setData({ stationList: list, pageNo: this.data.pageNo 1, hasMore: res.list.length res.pageSize }); }) .finally(() { this.setData({ isLoading: false }); }); } });这里的防重复逻辑有一个容易被忽略的细节finally 里把 isLoading 复位但如果请求失败hasMore 不会被正确更新导致用户下滑时一直触发请求形成无效循环。建议 catch 分支里不改变 hasMore但增加一个错误计数连续失败 3 次就提示切换到手动刷新很多生产环境踩过的坑就是这里。监听 isLoading 再配合 onReachBottom 的方式比单纯用 setTimeout 防抖可靠得多因为用户滚动速度不是匀速的。3.2 搜索框自动补全与防抖输入搜索公交线路通常使用 input 组件的 bindinput 事件这套源码里实现了两个级别的搜索线路号搜索和站点名搜索。难点在于防抖如果用户每输入一个字符就发起一次请求很可能导致搜索接口被限流而且请求返回顺序错乱时会出现旧结果覆盖新结果。源码里可能直接用 setTimeout 实现了简单防抖但更规范的方案是引入一个 timer 控制。let searchTimer null; function onSearchInput(e) { const keyword e.detail.value.trim(); if (searchTimer) clearTimeout(searchTimer); searchTimer setTimeout(() { this.querySearch(keyword); }, 300); }delaying 300 毫秒是相对通用的取值比 200 毫秒更能过滤掉中文拼音输入法的中间态抖动。需要提醒的是微信小程序的 input 组件在部分 Android 机型上会把拼音输入过程也触发 bindinput这种 300ms 的防抖能有效减少中间态请求。搜索结果的跳转也别直接 wx.navigateTo 到详情页而是先判断当前页面栈深度超过 5 层时用 wx.redirectTo 防止页面栈过深否则用户连续搜索几条线路后按返回键会直接退出小程序。3.3 canvas 绘制线路走向的坐标系换算首页的公交线路走向图是这个项目里视觉效果最强的模块源码里用的是 canvas 2d 接口通过经纬度点数组绘制折线。这里最容易踩坑的是坐标系后端返回的经纬度一般是 GCJ-02 火星坐标直接画到 canvas 上会偏出屏幕必须先把经纬度映射成平面坐标再换算到 canvas 尺寸。function drawLineOnCanvas(canvasId, points) { const query wx.createSelectorQuery(); query .select(# canvasId) .fields({ node: true, size: true }) .exec((res) { const canvas res[0].node; const ctx canvas.getContext(2d); const dpr wx.getSystemInfoSync().pixelRatio; canvas.width res[0].width * dpr; canvas.height res[0].height * dpr; ctx.scale(dpr, dpr); // 计算经纬度边界并映射到画布坐标 let minLng Infinity, maxLng -Infinity, minLat Infinity, maxLat -Infinity; points.forEach((p) { if (p.lng minLng) minLng p.lng; if (p.lng maxLng) maxLng p.lng; if (p.lat minLat) minLat p.lat; if (p.lat maxLat) maxLat p.lat; }); const canvasWidth res[0].width; const canvasHeight res[0].height; const scaleX canvasWidth / (maxLng - minLng); const scaleY canvasHeight / (maxLat - minLat); const scale Math.min(scaleX, scaleY) * 0.85; ctx.beginPath(); points.forEach((p, index) { const x (p.lng - minLng) * scale (canvasWidth - (maxLng - minLng) * scale) / 2; const y canvasHeight - (p.lat - minLat) * scale - (canvasHeight - (maxLat - minLat) * scale) / 2; if (index 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.strokeStyle #1AAD19; ctx.lineWidth 3; ctx.stroke(); }); }这段代码里 dpr 的缩放是关键如果忽略 dpr在部分高分辨率机型上画出来的线路会明显发虚。坐标映射时要注意 y 轴方向canvas 的 y 轴向下为正而经纬度的纬度向上递增所以计算 y 时要拿 canvasHeight 减去映射值。公交线路通常不是直线中间可能有十几个拐点绘制前建议对经纬度点做抽稀处理去掉距离小于 5 米的点否则路径锯齿感明显而且影响 canvas 渲染性能。4. 换乘方案规划与站点详情路线卡片的数据结构设计与事件冒泡处理公交应用最核心的体验不只是“看到线路”而是“从当前位置到目的地怎么换乘”。这套源码里的换乘页承担了方案列表、方案排序、途经站数、步行距离等信息的渲染。后端返回的换乘方案通常是一个数组每个元素包含多条腿leg每条腿要么是步行要么是公交线路。前端要做的就是把这种嵌套结构平铺成用户易懂的卡片式列表。4.1 换乘方案的数据扁平化重组假设后端返回的结构是每条路线里包含 steps 数组每个 step 里有 transport 字段标识这是步行还是公交源码里的做法是先做数据重组统一格式后再交给 WXML 渲染。一般会是下面这种结构处理function normalizePlan(rawPlan) { return rawPlan.map((route) { return route.steps.map((step) { if (step.mode walking) { return { type: walk, distance: step.distance, duration: step.duration, instructions: step.instructions }; } return { type: bus, lineName: step.bus.lineName, lineId: step.bus.lineId, startStop: step.bus.startStop, endStop: step.bus.endStop, stopCount: step.bus.stopCount }; }); }); }扁平化后的数组每组长度在 2 到 5 段之间第一段通常是步行到公交站中间是 1 到 3 趟公交最后一段又是步行。在 WXML 里这种结构可以用嵌套 wx:for 渲染但要注意 key 的取值。如果换乘方案里有两个连续的公交步骒它们可能存在相同 lineId如果用 lineId 做 key 就会报警告甚至渲染错乱建议使用 index 拼接 lineId 来保证唯一性。4.2 点击整张卡片跳转详情的事件聚合换乘卡片上一般有“查看详情”按钮同时也希望用户点卡片任意区域都能跳转。这里要注意小程序事件冒泡问题卡片上的多个子元素都有 bindtap 时容易发生多次跳转。源码里通常会这样处理view classplan-card>onPreferenceChange(e) { const value e.detail.value; this.setData({ preference: value, planList: [] }); this.fetchPlans(); } fetchPlans() { request(/plan/route, GET, { fromLng: this.data.fromLng, fromLat: this.data.fromLat, toLng: this.data.toLng, toLat: this.data.toLat, city: getApp().globalData.cityCode, preference: this.data.preference }).then((res) { this.setData({ planList: normalizePlan(res.plans) }); }); }这里有个体验细节切换偏好时先把 planList 清空并显示 loading避免用户在加载过程中看到上一轮的数据产生“页面没反应”的错觉。另外要注意 preference 参数不能依赖后端默认值有的公交数据源默认是“时间优先”而你页面里写的是“少换乘”两者结果不一致会导致用户投诉。所以即使 UI 上只提供默认推荐选项请求里也要显式传递参数。参数命名的坑也需要提一下有些接口用 preference有些用 strategy对接新数据源时先用接口文档确认不要想当然。4.4 定位权限拒绝时的回退方案换乘规划依赖起点和终点的经纬度参数用户如果拒绝了定位授权页面里就需要引导用户手动选择位置。小程序里 wx.getLocation 的 fail 回调里可能返回 errMsg 为 “getLocation:fail auth deny”此时应该跳转到一个手动选择站点的页面而不是直接提示失败。wx.getLocation({ type: gcj02, success: (res) { this.setData({ startLng: res.longitude, startLat: res.latitude }); }, fail: () { wx.showModal({ title: 需要定位权限, content: 请在设置中开启位置权限或手动选择出发站点, confirmText: 去设置, success: (modalRes) { if (modalRes.confirm) { wx.openSetting(); } else { wx.navigateTo({ url: /pages/search/search?typestart }); } } }); } });wx.openSetting 会直接打开小程序的设置页用户手动开启权限后返回小程序但并不会自动回到原来的逻辑而是停留在当前页面。稳妥的做法是在 onShow 里重新调用定位逻辑用 App 的 onShow 生命周期或页面的 onShow 都行。这一步处理不周的话用户在设置页开完权限回来页面没有任何反应很容易直接退出。5. 城市切换、缓存策略与常见报错修正这套源码能顺利跑起来的最后几个关键点查公交应用的特殊性在于城市差异极大不同城市的公交数据源格式不同、线路 ID 规则不同甚至 API 服务商的覆盖范围也不同。因此这套源码在多城市适配上的设计直接影响二次开发的成本。通常的做法是在 app.js 里维护一份城市列表每个城市包含城市名、城市码、公交数据服务商标识页面请求时从全局配置中读取而不是硬编码城市代码。5.1 城市配置与切换时的全局联动源码里的城市列表大概率放在 utils 目录下的 city.js 文件里结构参考如下module.exports [ { name: 深圳, code: 0755, dataSource: shenzhen_bus }, { name: 广州, code: 020, dataSource: guangzhou_bus }, { name: 北京, code: 010, dataSource: beijing_bus } ];城市切换功能通常放在首页的右上角点击后弹出半屏弹窗列出城市列表。切换城市要做的联动包括三个地方更新全局 globalData、清空首页的站点与线路缓存、重新请求目标城市的公交数据。这里有一个易犯的错误是只改了 globalData 而忘了重置页面 data导致老城市数据短暂闪烁后又被新城市数据覆盖。合理的做法是把城市切换逻辑收敛到 app.js 的一个 action 方法里页面通过回调感知变化而不是各个页面各自处理。switchCity(cityInfo) { this.globalData.cityCode cityInfo.code; this.globalData.cityName cityInfo.name; const pages getCurrentPages(); const currentPage pages[pages.length - 1]; if (currentPage currentPage.onCityChange) { currentPage.onCityChange(cityInfo); } }利用 getCurrentPages 拿到当前页面实例直接调用页面实例上的 onCityChange 方法比通过事件总线或者全局状态管理简单得多。前提是每个需要响应城市切换的页面都实现了 onCityChange 方法。这种方案在首页、换乘页、线路详情页之间保持一致不会出现频繁切换城市后页面状态错乱的问题。5.2 storage 缓存策略离线可看的历史线路为了减少接口请求这套源码可以在 utils 层增加一个缓存模块把最近搜索的线路和换乘方案写入本地缓存。公交线路的基本信息变化频率很低缓存有效期设置为 24 小时以内是合理的。换乘方案受实时路况影响较大建议只缓存 5 到 10 分钟。const CACHE_PREFIX bus_cache_; function getCache(key) { const cached wx.getStorageSync(CACHE_PREFIX key); if (!cached) return null; if (Date.now() cached.expireAt) { wx.removeStorageSync(CACHE_PREFIX key); return null; } return cached.data; } function setCache(key, data, ttl) { wx.setStorageSync(CACHE_PREFIX key, { data: data, expireAt: Date.now() ttl * 1000 }); }线路详情页的缓存 key 可以用 cityCode lineId 拼接换乘方案用起终点经纬度拼接。缓存读写的接口统一收敛后网络请求只在缓存失效时发出弱网环境下公交应用可用性会有质的提升。一个容易忽略的问题换乘方案中如果包含实时位置信息这部分数据在缓存恢复后已经过期需要在渲染时做标记提示用户下拉刷新。5.3 常见编译错误的快速修复对照这套源码流传版本较多接手后容易碰到几个固定报错。第一个是 app.json 里缺少 requiredPrivateInfos 声明导致的 getLocation 调用失败具体报错信息是 “getLocation:fail the api need to be declared in the requiredPrivateInfos field”修复方法是把 getLocation 和 chooseLocation 加进声明数组。第二个是 canvas 相关 API 失效原因是基础库版本过低或使用了废弃的 wx.createCanvasContext新版建议统一迁移到 Canvas 2D 接口。第三个是 wx.request 请求的域名不在白名单里开发阶段可以在开发者工具里勾选“不校验合法域名”但真机预览时必须把接口域名配置到小程序管理后台的 request 合法域名列表中。提示如果看到日志里出现 “url not in domain list” 字样说明接口域名没配置或配置后未生效等 5 到 10 分钟再试微信服务端有缓存延迟。5.4 性能优化与体验增强节流滚动监听和骨架屏占位公交列表通常很长滚动流畅度直接决定用户对应用的评价。微信小程序的 setData 操作会同步阻塞视图更新如果页面里一次性 setData 几十条线路数据在低端 Android 机型上会有明显卡顿。针对性优化手段是把列表数据分块渲染每次只设置当前可视区域附近的 10 到 15 条数据。同时监听页面的 onPageScroll 时建议用 throttle 节流到 100 毫秒以上避免滚动回调高频触发 setData。换乘方案的加载过程通常需要 1 秒以上加载期间如果只显示一个普通的 loading 动画用户会感觉应用响应慢。可以在加载态展示骨架屏也就是用灰色色块模拟线路卡片和站点图标的位置让用户知道页面正在构建而不是无响应。骨架屏不一定要引入第三方组件库用 WXSS 的渐变动画加几条固定高度的视图就能实现成本很低但体验提升非常明显。最终这套滴滴公交-查公交源码拿来实战改造时优先做三件事第一替换定位和地图相关的 API key第二重写 request 层对接自己的后端数据源第三在页面生命周期里补充缓存失效和错误重试逻辑。把这几点做完一个可上架的微信小程序查公交应用基本就成型了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价