ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Vue的uni-app微信小程序租车网开发实战

基于Vue的uni-app微信小程序租车网开发实战 简介一份基于Vue框架的uni-app微信小程序租车网设计源码适合前端开发者、小程序爱好者以及需要搭建租车类线上服务的团队参考学习。资源共543个文件压缩包约7.07MB涵盖147个JS逻辑文件、115个JSON配置、79个Vue组件、52个Markdown文档以及WXML/WXSS/SCSS等样式模板完整覆盖从功能逻辑、页面结构到视觉样式的开发链路。项目采用组件化方式组织代码清晰展示租车场景下的车型展示、预约下单、订单管理等典型功能模块同时借助uni-app跨端能力便于后续扩展至多平台。源码内附项目说明与目录文档可帮助开发者快速理解整体架构并二次开发。目前已有913人学习下载是掌握uni-app实战、熟悉小程序服务预约类项目开发的良好参考。1. 基于Vue的uni-app微信小程序租车网是前端工程化的一条捷径在微信小程序生态里租车类应用的核心矛盾从来不在业务逻辑——车辆列表、下单结算、订单查询这些流程在网页端已经被验证过无数次——而在于同一套业务要在小程序、H5、App多端重复开发。uni-app把Vue的组件化思维延伸到微信小程序让租车网的前端代码只写一遍编译到微信小程序时自动完成模板映射和数据绑定适配。这篇文章要解决的正是用Vue框架的写法在uni-app里把租车网从零搭出来这一具体问题项目结构怎么摆、车辆和订单的数据模型怎么定义、uni.request和微信原生wx.request差在哪、真机调试和wgt热更新有哪些高频坑。适合已经写过Vue但第一次碰uni-app的前端工程师也适合需要快速交付小程序版租车业务的技术负责人。2. 租车网的项目结构和数据模型Vue组件与微信小程序能力的交汇点2.1 用CLI还是HBuilderX创建uni-app租车项目创建uni-app项目有两条主流路径HBuilderX可视化创建和vue-cli命令行创建。对于租车网这种需要团队协作、代码要进Git仓库的项目我一般推荐CLI方式原因是依赖版本可控、CI/CD好接入而且prettier、eslint这些工具链可以直接复用Vue生态的配置代码规范能跟着前端团队已有的约定走不需要额外适配。# 使用vue-cli创建uni-app项目 vue create -p dcloudio/uni-preset-vue rent-car-miniapp # 进入项目并安装依赖 cd rent-car-miniapp npm install # 安装状态管理依赖 npm install vuex3 --save这里有一个关键的Vue版本选择uni-app的Vue2版本依赖vuex3而基于Vite的Vue3模板对应pinia或vuex4。如果团队熟悉Options API选Vue2模板能降低上手成本如果项目从零开始且团队已经转到Composition API选Vue3模板-p dcloudio/uni-preset-vue#vite更合理。租车网的业务量级不大车辆列表和订单状态都不算复杂两种方案在性能上没有明显差异选型的决定性因素应该是团队现有代码的惯性——比如团队已经有Vue2后台管理系统那小程序端保持一致可以降低维护者切换上下文的成本。创建完成后项目根目录会生成src/pages.json和src/manifest.json两个配置文件。pages.json对应微信小程序的app.json用来声明页面路由、tabBar和窗口样式manifest.json则管理应用标识、小程序AppID和权限声明。这两个文件是uni-app和原生小程序配置的桥接层熟悉原生开发的开发者可以直接把微信小程序的配置项平移到pages.json里字段名基本一致。2.2 租车业务的核心数据结构车辆、订单和用户状态租车网的数据模型围绕三个核心实体展开车辆car、订单order和用户user。车辆实体需要覆盖品牌型号、日租金、押金、车辆状态、图片列表和取还车门店等字段其中门店信息在后续做地图选点和距离排序时会用到建议在建模阶段就单独拆出一个store字段不要塞进一个笼统的address字符串里。// src/store/modules/car.js export default { namespaced: true, state: { // 当前筛选条件下的车辆列表 carList: [], // 车辆详情缓存避免重复请求 carDetail: {}, // 筛选条件城市、日期、车型分类 filter: { city: 北京, startDate: , endDate: } }, mutations: { SET_CAR_LIST(state, list) { state.carList list }, SET_FILTER(state, payload) { state.filter Object.assign({}, state.filter, payload) } } }订单数据的核心不只是订单号、金额这些常规字段还包括租车的起止时间。在小程序端日期选择器返回的值是2025-01-15这样的字符串提交给后端前必须转成时间戳或ISO格式否则后端在计算租车天数时容易因时区差异产生一天的偏差看起来只是小问题实际会直接导致押金和租金算错。// 提交订单前处理租期 function buildOrderPayload(selectedCar, filter) { const start new Date(filter.startDate).getTime() const end new Date(filter.endDate).getTime() const days Math.ceil((end - start) / 86400000) return { carId: selectedCar.id, startDate: filter.startDate, endDate: filter.endDate, totalAmount: (selectedCar.dailyRent * days).toFixed(2), status: pending } }用户状态包括微信登录后的openid、手机号和常用取还车城市。openid的获取必须通过uni.login拿到临时code再交给后端换取前端不能直接调用获取openid的接口。这是微信小程序平台的安全限制也是租车网这类需要实名租车业务的合规前提。后续接口需要用户身份时统一从请求头里带token不要在小程序端存储openid明文。2.3 tabBar和顶部导航栏的配置细节租车网的tabBar通常包含首页、订单和个人中心三个入口。tabBar的图标路径必须是本地静态文件不支持网络图片和svg格式。如果设计稿用的是iconfont字体图标需要额外转换成png文件放入static目录否则真机上tabBar会出现空白。{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 租车首页, enablePullDownRefresh: true } }, { path: pages/order/order, style: { navigationBarTitleText: 我的订单 } }, { path: pages/user/user, style: { navigationBarTitleText: 个人中心 } } ], tabBar: { color: #999999, selectedColor: #2F7CF6, list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/order/order, text: 订单 }, { pagePath: pages/user/user, text: 我的 } ] } }顶部导航栏高度在不同机型上有差异尤其是刘海屏和灵动岛机型。uni-app提供了uni.getSystemInfoSync()可以拿到状态栏高度自定义导航栏时需要用statusBarHeight 44这个公式来计算标题栏位置。如果直接用原生导航栏pages.json里的navigationBarTitleText就能解决没必要自己造轮子。配置项作用租车场景说明navigationBarTitleText导航栏标题首页显示租车首页enablePullDownRefresh是否开启下拉刷新列表页开启onReachBottomDistance触底距离设为50px触发分页加载3. 租车首页和车辆列表Vue语法在uni-app里的关键差异3.1 首页轮播和九宫格入口的写法租车首页的经典布局是顶部轮播图加功能导航区再往下是热门车型推荐。uni-app内置了swiper组件写法上几乎和微信原生一致但数据绑定用的是Vue的指令语法事件绑定用而不是bind。这个差异对从原生小程序转过来的开发者最需要适应。template view classpage-container swiper classbanner indicator-dots autoplay circular interval4000 duration500 swiper-item v-for(item, index) in banners :keyindex clickgoToActivity(item.link) image classbanner-image :srcitem.imageUrl modeaspectFill lazy-load / /swiper-item /swiper !-- 功能导航区 -- view classnav-grid view classnav-item v-fornav in navMenus :keynav.id clicknavigateTo(nav.url) image classnav-icon :srcnav.icon / text classnav-text{{ nav.name }}/text /view /view /view /template这里的banners数据从后端活动接口获取imageUrl字段需要确保是HTTPS地址。微信小程序从基础库2.20.0开始强制网络图片必须走HTTPSHTTP图片在真机上会直接加载失败但在开发者工具里往往能正常显示——因为工具默认不拦截非安全域名真机才会校验downloadFile和request的合法域名。这是最容易踩的坑排查思路是开发者工具显示正常、真机白图先看域名协议再看小程序后台的downloadFile合法域名配置。和原生小程序的swiper写法对比uni-app把current、indicator-dots这些属性保留为kebab-case事件用change而不是bindchange。本质上uni-app的编译器会把模板语法翻译回原生小程序代码但在源码里你写的一定是Vue风格。3.2 车辆列表的分页加载和筛选交互车辆列表页承载了筛选、排序和分页加载三个功能。uni-app的页面滚动事件通过onReachBottom生命周期捕获下拉刷新通过onPullDownRefresh触发这两个是uni-app的页面级生命周期不依赖scroll-view组件。用scroll-view实现滚动加载在小程序里的性能表现不如页面级滚动能用页面的就用页面。script export default { data() { return { carList: [], page: 1, pageSize: 10, hasMore: true, loading: false } }, async onLoad() { await this.fetchCarList() }, onPullDownRefresh() { this.page 1 this.hasMore true this.fetchCarList().then(() uni.stopPullDownRefresh()) }, onReachBottom() { if (this.hasMore !this.loading) { this.page 1 this.fetchCarList() } }, methods: { async fetchCarList() { this.loading true try { const res await api.getCarList({ page: this.page, pageSize: this.pageSize, city: this.$store.state.car.filter.city }) this.carList this.page 1 ? res.list : this.carList.concat(res.list) this.hasMore res.list.length this.pageSize } finally { this.loading false } } } } /script分页的核心逻辑不止在于请求本身更在于数据拼接策略下拉刷新时重置页码并替换列表触底加载时追加数据。hasMore的判断依赖接口返回的数据条数——当返回条数等于pageSize时继续加载否则停止。这里有三个需要留意的细节第一loading标志位必须放在请求前设置并在finally里清除否则快速滚动会发起大量重复请求这本是后端接口加锁的活但前端也要守住第一道门。第二下拉刷新时如果接口报错uni.stopPullDownRefresh()不会执行用户会看到刷新动画永远停在那里所以最好把stop放在finally里。第三concat不改变原数组需要用this.carList.concat(res.list)的返回值重新赋值这个如果不注意会造成列表不追加的假bug。筛选区域通常用弹出面板实现uni.showActionSheet是最轻量的方案但选项超过6个时在真机上显示会超出屏幕高度。租车场景的筛选条件通常包括车型分类、座位数、排量、租金区间整体超过6个选项需要自定义底部弹出层。3.3 车辆卡片组件的props设计车辆卡片会出现在首页推荐位、列表页和搜索结果页三个场景必须抽成公共组件。组件的边界在于只负责展示和抛事件不负责请求数据。下面这个CarCard组件在租车网里被三个页面复用了十二次后续改动日租金展示样式只需要改一处。template view classcar-card click$emit(select, car) image classcar-thumb :srccar.coverImage modeaspectFill lazy-load / view classcar-info view classcar-name{{ car.name }}/view view classcar-meta text{{ car.gearType 1 ? 自动 : 手动 }}/text text{{ car.seats }}座/text text{{ car.fuelType 1 ? 汽油 : 新能源 }}/text /view view classcar-price text classprice-num¥{{ car.dailyRent }}/text text classprice-unit/天/text /view /view /view /template script export default { name: CarCard, props: { car: { type: Object, required: true } } } /script组件的props里直接传整个car对象在数据层级较深时可以减少模板里的绑定数量。但要注意Vue2的响应式系统对对象新增属性不敏感——如果后端返回的car对象里某个字段是后续动态添加的视图不会自动更新需要使用this.$set或者在初始化data时就声明完整字段结构。事件命名也有讲究。$emit(select, car)中的事件名统一用小写组件在父级使用时是selecthandleSelect。Vue2里事件名不会被编译器做大小写归一化处理select和Select是两个不同的事件项目里事件名统一小写避免混用造成的事件不触发问题。4. 请求封装和用户登录跨端代码的边界在哪里4.1 uni.request的Promise封装与微信端差异uni-app提供了统一的请求API但在微信小程序平台上uni.request最终映射到wx.request开发者工具和真机的行为有差异最典型的就是合法域名校验。开发者工具可以勾选不校验合法域名绕过限制但真机上必须在小程序管理后台配置request合法域名否则请求直接失败。// src/utils/request.js const BASE_URL https://api.rentcar.example.com export function request(options) { return new Promise((resolve, reject) { uni.request({ url: ${BASE_URL}${options.url}, method: options.method || GET, data: options.data || {}, timeout: 10000, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || }, success: (res) { if (res.statusCode 200) { resolve(res.data) } else if (res.statusCode 401) { uni.navigateTo({ url: /pages/login/login }) reject(res) } else { uni.showToast({ title: 请求失败, icon: none }) reject(res) } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) }这里的Promise封装解决了回调嵌套的问题但和axios不同uni.request不支持拦截器所有通用逻辑——token过期统一跳登录、错误码统一toast——都需要在success回调里手动处理。如果项目里每个页面都独立发请求这些重复代码会散落在各处所以统一封装是必须做的第一步。timeout参数在微信端的默认值是60秒对租车网的接口来说太长。建议显式传入10秒或15秒用户在地下车库等弱网环境时能更快得到反馈不至于以为应用卡死了。超时后触发fail回调错误信息可以再细分一下err.errMsg里包含timeout字符串时提示请求超时否则提示网络异常。4.2 微信登录code换取openid的链路租车网需要用户登录后才能下单微信小程序的登录机制核心是code。整个链路由前端发起、后端完成核心验证uni-app前端能拿到的只有临时code。code的有效期只有5分钟且只能使用一次任何缓存code的行为都会导致二次登录失败。// src/utils/auth.js import { request } from ./request.js export function login() { return new Promise((resolve, reject) { uni.login({ provider: weixin, success: async (res) { if (!res.code) { reject(new Error(登录失败)) return } try { // code交给后端由后端调用微信接口换取openid const token await request({ url: /auth/login, method: POST, data: { code: res.code } }) uni.setStorageSync(token, token) resolve(token) } catch (e) { reject(e) } } }) }) }登录时机选择上有两种做法一种是在App.vue的onLaunch里全局登录用户打开小程序就静默登录另一种是在用户点击立即预订时才触发登录。租车网的实践建议是后者提前弹登录框会流失未决定下单的用户。静默登录的问题在于token过期后接口会返回401需要请求封装里统一处理重新登录再重放请求逻辑复杂度会上升一个级别。uni.login还有一种场景要注意手机上可能同时安装了多个微信账号用户在微信切换账号后之前登录态的token对应的openid已经变了此时需要后端在鉴权中间件里做token和openid的绑定关系校验前端能做的就是捕获401后清空token并重新引导登录。4.3 支付流程中的业务判断租车订单的支付流程在2024年后全部切换到了微信支付v3协议。v3协议下前端拿到的支付参数由后端调用微信支付下单API生成前端只负责调起支付收银台。// 发起租车订单支付 export function payOrder(orderId) { return new Promise((resolve, reject) { request({ url: /order/pay, method: POST, data: { orderId } }).then((payParams) { uni.requestPayment({ provider: wxpay, timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: RSA, paySign: payParams.paySign, success: () resolve(paid), fail: (err) { // 用户取消支付和支付失败需要区别对待 if (err.errMsg.includes(cancel)) { resolve(cancelled) } else { reject(err) } } }) }) }) }支付结果以后端回调为准前端不能把success回调当作最终支付成功的判断依据。因为微信支付回跳到小程序时success回调只能说明支付收银台已关闭不能确认资金到账。租车业务中还存在一种极端情况用户支付成功但后端没收到回调订单卡在pending状态。解决思路是用户点击我的订单时做一次主动查询——调用后端的订单状态查询接口以订单状态为准来展示结果。另外注意signType必须和后端统一下单时使用的签名类型一致否则会在调起支付时报签名错误。v2用的是MD5v3则迁移到了RSA如果后端没有同步升级前端这边的signType也不要自己改。5. 编译发布、wgt热更新和三个高频坑5.1 微信开发者工具关联编译产物uni-app开发完后的发布路径是先将源码编译为微信原生小程序代码输出到dist目录然后用微信开发者工具打开这个目录进行调试、预览和上传。# CLI方式编译到微信小程序平台 npm run dev:mp-weixin # 执行生产构建 npm run build:mp-weixin编译产物通常输出到dist/dev/mp-weixin开发模式或dist/build/mp-weixin生产模式。微信开发者工具导入项目时要留意project.config.json是否携带正确的appid——如果用测试号真机预览时支付等能力不可用如果用正式appid需要在管理员后台添加开发者微信号才能预览。日常迭代时可以开着dev:mp-weixin源码保存后会自动重新编译到dist目录。微信开发者工具会自动感知目录变化并刷新模拟器这里就不需要手动点编译了。项目里有时会看到dist目录被gitignore但project.config.json在根目录还是在dist里可能会造成同事之间拉代码后打开空目录的困惑建议CLI项目把dist里的project.config.json复制到src外层提交到Git让开发者可以直接导入根目录。5.2 wgt热更新不生效的排查路径uni-app的App端支持wgt资源包热更新微信小程序其实不依赖wgt机制——小程序的更新由微信客户端在冷启动时检查和拉取新版本。在开发者工具和真机调试环境中经常遇到改完代码后真机还是旧版本这是微信小程序的缓存机制在起作用。排查路径有三个检查开发者工具是否开启了真机调试模式该模式加载的是工具推送到手机的临时包需要重新预览才能拿到新代码检查小程序后台是否设置了低版本兼容或灰度发布比例最后可以尝试在小程序内调用uni.getUpdateManager()主动监听版本更新但微信端的版本更新检查频率由微信客户端控制开发者没有绝对控制权。5.3 三个微信端特有的高频问题第一个是图片跨域问题。开发者工具加载HTTPS图片正常真机显示空白先看图片域名是否加入了小程序后台的downloadFile合法域名。如果图片来自第三方CDN且不方便在后台配置可以考虑在服务端做图片代理转发或把图片保存到自己的对象存储再引用。第二个是iOS键盘顶起页面问题。在租车下单页有手机号输入框时iOS上键盘弹起会顶起整个webview导致布局错乱。处理方案是给输入框外层footer区域加上adjust-position适配或者在页面onShow里记录键盘高度做补偿位移。iOS 17之后微信键盘行为有变化实测用uni.onKeyboardHeightChange监听键盘高度再动态设置底部按钮的bottom值最稳定。第三个是时间格式化兼容性问题。iOS的JavaScript引擎不支持new Date(2025-01-15 10:30:00)中间带空格的时间字符串必须替换为new Date(2025-01-15T10:30:00)或用Date.parse做兼容处理。Android上正常的代码到了iPhone上就返回Invalid Date排查时先看时间串格式。日常开发顺手做一个通用函数处理这个问题// utils/date.js export function safeParseDate(dateStr) { // iOS不支持空格分隔的日期字符串统一替换为T const normalized dateStr.replace( , T) const timestamp new Date(normalized).getTime() return isNaN(timestamp) ? null : timestamp }租车网里所有订单时间的计算都走这个函数能少踩很多关于时间格式不一致的坑。项目做到后期可以在这个统一封装里加单元测试断言覆盖Android、iOS和开发者工具三端的时间解析行为。本文还有配套的精品资源点击获取
返回列表