ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

uni-app租车小程序源码拆解:Vue跨端开发与微信小程序适配实践

uni-app租车小程序源码拆解:Vue跨端开发与微信小程序适配实践 简介这是一套基于Vue框架与uni-app技术栈开发的微信小程序租车网设计源码面向需要快速搭建租车类小程序或学习跨端开发的前端开发者。源码共543个文件压缩包约7.07MB包含147个JavaScript逻辑文件、79个Vue组件、114个JSON配置与16个微信小程序样式文件另有WXML模板、SCSS样式、PNG/JPG图像及Markdown文档等覆盖页面结构、交互逻辑、资源映射和项目说明。项目以用户体验为核心提供车型浏览、租车预订与订单查看等完整流程代码按组件化方式组织结构清晰易复用适合作为毕业设计、课程项目或商业二次开发的参考模板。目前已有913人学习下载是理解uni-app多端适配与小程序工程化配置的实用样例。1. 一套能直接跑的 uni-app 租车小程序源码先把骨架和边界摸清楚拿到“基于Vue框架的uni-app微信小程序租车网设计源码”后我第一件事是扫描文件类型分布601 个文件里147 个 JavaScript、79 个 Vue 组件、114 个 JSON 配置再配上 15 个 WXML 模板和 14 个 SCSS 样式文件。这个比例说明它不是单页 demo而是把租车浏览、车辆详情、下单支付和订单管理串起来的完整工程并且通过 uni-app 同时编译到微信小程序、H5 和 App。对想用 Vue 语法开发微信小程序的团队来说这套源码的价值不只在某个页面本身更在全局配置、请求层、样式适配、支付回调这些“一跑起来就暴露问题”的地方。下面按一个真实工程从上到下的顺序拆开。2. 工程结构与 pages.json把 601 个文件按逻辑装起来uni-app 项目能不能快速接手先看两个地方目录里每个文件类型承担什么职责以及 pages.json 里页面和 tabBar 配得是否清晰。这套源码的文件类别很多是因为 uni-app 本身会混编业务代码、依赖包和原生小程序组件弄混之后很容易出现样式不生效、路径找不到这类问题。2.1 文件布阵JS、JSON、Vue、WXML、SCSS 各管什么根据源码文件数量整理出来的职责表如下。文件类型数量主要职责JavaScript147业务逻辑、工具函数、API 封装、构建依赖JSON114pages.json、manifest.json、tsconfig、包描述文件Markdown112依赖包文档、项目 README、开发记录Vue 组件79页面和组件化模块基于 Vue 模板语法资源映射33静态资源引用、alias 映射、tsconfig pathsPNG / JPG32 / 10图标、车辆图、背景图微信小程序样式16wxss 编译产物或原生组件样式WXML 模板15原生小程序组件模板SCSS 样式14全局和组件级样式源文件112 个 Markdown 文件并不代表项目文档有 112 篇多数来自 node_modules 里依赖包自带的说明和类型描述。真正要维护的是 README、开发规范和页面备注放在 docs 下即可。微信小程序样式文件和 WXML 模板的存在通常意味着src/wxcomponents或根目录下有原生组件例如地图、支付、图表这类 uni-app 生态暂时覆盖不到的组件必须以原生方式嵌入。2.2 目录组织区分业务代码与原生小程序组件结合源码里出现的.eslintrc、index.html、index.d.cts、axios.cjs来看它的构建链路更接近基于 Vite 的 uni-app 工程。常见目录结构如下├── src │ ├── pages # 页面级 Vue 组件 │ │ ├── home │ │ ├── car │ │ └── order │ ├── components # 可复用业务组件 │ ├── wxcomponents # 原生微信组件含 wxml/wxss/js/json │ ├── static # 图片、字体 │ │ ├── tabbar │ │ └── images │ ├── store # 全局状态 │ ├── api # 接口定义 │ ├── utils # 请求、工具 │ ├── styles # SCSS 全局样式 │ ├── App.vue # 应用入口 │ ├── main.js │ ├── pages.json │ └── manifest.json ├── package.json └── vite.config.jsaxios.cjs出现在文件列表里并不代表小程序端会直接用 axios它更多是 H5 调试或本地工具脚本的依赖。微信小程序环境不支持完整的 Node 模块解析所以业务请求必须走uni.request这一点在第三章会展开。wxcomponents目录里的 WXML 模板会被 uni-app 原样复制到编译产物中不会经过 Vue 编译器因此可以放心放原生插件。注意原生组件的 js 文件如果使用了 CommonJS 导出也不要 import 到 Vue 业务代码里跨模块引用很容易触发“module is not defined”的编译错误。2.3 pages.json 配置与 tabBar先解决页面注册和导航栏微信小程序每个页面都要注册uni-app 同样靠 pages.json 驱动路由和导航栏。下面的配置是租车网首页、车型列表、订单页和 tabBar 的典型写法{ pages: [ { path: pages/home/index, style: { navigationBarTitleText: 租车首页 } }, { path: pages/car/list, style: { navigationBarTitleText: 车型列表, enablePullDownRefresh: true } }, { path: pages/order/confirm, style: { navigationBarTitleText: 确认订单 } } ], globalStyle: { navigationBarTextStyle: black, navigationBarTitleText: 租车网, navigationBarBackgroundColor: #ffffff, backgroundColor: #f5f5f5 }, tabBar: { color: #999999, selectedColor: #007aff, list: [ { pagePath: pages/home/index, text: 首页, iconPath: static/tabbar/home.png, selectedIconPath: static/tabbar/home-active.png }, { pagePath: pages/car/list, text: 车型, iconPath: static/tabbar/car.png, selectedIconPath: static/tabbar/car-active.png } ] } }pages数组的第一个页面是启动首页不能漏tabBar.list最少 2 个、最多 5 个。很多新手把页面写进 vue 文件却忘了在 pages.json 注册结果跳转时报page not found。navigationBarTitleText只影响原生导航栏文字如果页面用了自定义导航栏这里的配置会被组件覆盖。如果后续要跑通微信支付manifest.json里还要正确填写小程序 appid否则真机支付拉起不了。首页onLoad里如果频繁调用登录接口建议先用uni.checkSession判断一下会话是否有效避免每次冷启动都打一次无意义的请求。3. Vue 组件层与请求封装租车列表页是怎么跑通的租车网源码里 79 个 Vue 组件是整个项目最值得看的区域。它们不是把一个页面堆到几千行而是把首页、车型卡片、订单卡片、支付按钮拆成多个可复用块。配合 JS 文件里的 API 模块和请求工具才让页面既能维护又跑得通。3.1 Vue 组件分层页面级、业务级、基础级按通用拆分方式可以把源码组件分成三层页面级组件放在src/pages每个页面一个目录负责路由入口和数据聚合业务组件例如CarCard.vue、OrderStatusTag.vue放在src/components被多个页面复用基础组件例如按钮、空状态、加载中通常用 uni-ui 或自定义实现。租车首页往往同时包含筛选条件、车型列表、热门活动入口页面组件只做组合真正渲染车辆信息的是CarCard。每个CarCard接收一个car对象内部处理图片加载、价格展示、立即预订按钮跳转。这样同样的卡片在首页和车辆列表页都能直接用改动样式只动一处。这样做的好处是状态边界清楚。列表页负责加载列表数据卡片只负责展示和派发事件。如果租车价格有折扣、押金、手续费多个字段卡片内部不适合直接做加减应在 API 层返回格式化好的priceText和depositText否则每个用到卡片的页面都要重复计算后续调价容易漏改。3.2 从 axios.cjs 到 uni.request微信端请求层统一收敛源码里出现axios.cjs会让人误以为小程序端也走 axios。实际上 axios 在微信小程序中不能直接使用除非通过 adapter 适配 wx.request。uni-app 官方推荐的跨端方案是封装uni.request。我会在所有项目里统一放一个request.js这套租车网源码大概率也是类似结构// src/utils/request.js const BASE_URL https://api.example.com; export function request({ url, method GET, data {}, header {} }) { return new Promise((resolve, reject) { uni.request({ url: ${BASE_URL}${url}, method, data, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || , ...header }, success: (res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.statusCode 401) { uni.navigateTo({ url: /pages/login/index }); reject(res.data); } else { uni.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }这个封装把四件事收敛到一起请求地址前缀、统一鉴权 header、状态码判断、错误提示。调用方不需要在每个页面重复处理 401 跳转和 toast。注意BASE_URL要按环境切换一般放在根目录.env文件里用import.meta.env.VITE_API_BASE读取。如果后端返回值里code字段命名不同比如叫status要把判断逻辑同步改掉否则会出现“拿到数据但进不了 resolve”的隐藏问题。在 H5 端如果项目里确实要用 axios 做特殊拦截可以把 axios 实例单独放在src/utils/http-h5.js只在#ifdef H5条件编译里引入小程序端仍然走uni.request。3.3 租车列表页加载数据、下拉刷新、跳转下单有了请求封装之后Vue 页面只需要关注渲染和交互。以车型列表页为例template view classcar-list view v-forcar in cars :keycar.id classcar-card image :srccar.cover modeaspectFill classcar-cover / text classcar-name{{ car.name }}/text text classcar-price¥{{ car.price }}/天/text button sizemini clickgoOrder(car)立即预订/button /view /view /template script setup import { ref } from vue; import { onLoad, onPullDownRefresh, onReachBottom } from dcloudio/uni-app; import { getCarList } from /api/car; const cars ref([]); const page ref(1); const hasMore ref(true); onLoad(async () { const data await getCarList({ page: page.value, pageSize: 10 }); cars.value data.list; hasMore.value data.hasMore; }); onPullDownRefresh(async () { page.value 1; const data await getCarList({ page: 1, pageSize: 10 }); cars.value data.list; hasMore.value data.hasMore; uni.stopPullDownRefresh(); }); onReachBottom(async () { if (!hasMore.value) return; page.value 1; const data await getCarList({ page: page.value, pageSize: 10 }); cars.value cars.value.concat(data.list); hasMore.value data.hasMore; }); function goOrder(car) { uni.navigateTo({ url: /pages/order/confirm?id${car.id} }); } /script style langscss scoped .car-list { padding: 24rpx; } .car-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; } .car-price { color: #ff6600; font-weight: bold; } /styleonLoad、onPullDownRefresh、onReachBottom来自dcloudio/uni-app这是页面级生命周期组件中不能用它们组件加载要用onMounted。/api/car模块返回的是 request 封装后的 Promise页面里不需要再处理错误码。pages.json 对应页面需要开启enablePullDownRefresh: true否则onPullDownRefresh不会被触发。onReachBottom分页时要注意hasMore判断否则用户滑到底部会重复请求同一页数据。setup语法下模板可以直接访问cars不用this如果项目里混合了 Options API要记得data字段按对象返回否则小程序端编译后响应式数据可能丢失。4. WXML 模板与 SCSS 样式的微信端兼容从 rpx 到导航栏安全区uni-app 在实际开发中最容易翻车的不是逻辑而是样式。源码里同时存在 WXML 模板文件和 SCSS 样式文件意味着项目里有一部分原生微信组件另一部分走 Vue 编译链路。搞清楚哪些样式会被编译、哪些会原样透传才能控制页面表现一致。4.1 WXML 模板在 uni-app 工程里的两种存在方式第一种是原生wxcomponents下的小程序组件比如地图、车牌识别、支付 SDK 自带的 wxml、wxss、js、json。uni-app 构建时会把这些目录原样拷贝到编译结果Vue 页面通过usingComponents引用微信开发者工具才能识别。在pages.json的页面 style 中这样声明{ path: pages/car/detail, style: { usingComponents: { van-map: /wxcomponents/van-map/index } } }第二种是从.vue文件编译生成的 wxml开发者日常不会直接写 WXML。如果调试时看到编译产物里wxml结构和预期不同问题一定在.vue模板的标签上。例如view对应viewdiv会被编译成view但div上的一些样式行为在不同端不一致所以 uni-app 文档建议直接用view、text。源码里的 15 个 WXML 文件更可能是第一种原生组件因此不要把它们当成页面模板去改改了构建产物会被覆盖。4.2 SCSS 编译到 wxssrpx、安全区与顶部导航栏高度微信小程序使用 rpx 作为响应式单位750rpx 等于屏幕宽度。设计稿如果是 750 宽1px 等于 1rpx 可以直接换算。SCSS 文件在编译时不会把px自动转成rpxpx保留原样。想要px自动转 rpx需要在构建工具里配置 css 处理器但更稳妥的做法是设计视觉稿直接用 750 宽度代码里写 rpx。项目中的 SCSS 文件通常配合uni.scss定义全局变量。下面这段是处理自定义导航栏高度的常见方式.nav-bar { height: calc(var(--status-bar-height) 44px); padding-top: var(--status-bar-height); background: #ffffff; position: sticky; top: 0; z-index: 100; }--status-bar-height是 uni-app 在页面根节点上注入的 CSS 变量值为微信状态栏高度44px是微信小程序导航栏标准高度。但有刘海屏的机型胶囊按钮位置会浮动仅靠44px不可靠。更严谨的做法是用uni.getMenuButtonBoundingClientRect()拿到胶囊按钮的 top 和 height然后动态计算导航栏高度const menu uni.getMenuButtonBoundingClientRect(); const statusBarHeight uni.getSystemInfoSync().statusBarHeight; const navBarHeight menu.height (menu.top - statusBarHeight) * 2;这个值通常用于自定义导航栏避免页面顶部被刘海遮挡也能准确对齐右侧胶囊按钮。如果发现自定义导航栏在安卓和 iOS 上高度不一致优先检查是px还是rpx混用以及是否在calc里漏了空格导致 SCSS 编译失败。安全区底部建议加padding-bottom: env(safe-area-inset-bottom)否则在 iPhone 底部 home indicator 上按钮会压在横条上很难看。4.3 自定义 tabBar用 uni-icons 替换原生图标的正确路径原生 tabBar 只能使用图片无法直接使用uni-icons这让很多想统一图标风格的团队踩坑。最稳妥的方案是开启自定义 tabBar。在pages.json的 tabBar 里加custom: true保留list后小程序会渲染src/custom-tab-bar下的组件。这个组件可以用uni-icons因为自定义 tabBar 本质是一个页面级别的组件uni-icons 会被编译成小程序组件完全可用。template view classtab-bar view v-for(item, index) in list :keyitem.pagePath :class{ active: current index } classtab-item clickswitchTab(item, index) uni-icons :typecurrent index ? item.selectedIcon : item.icon size24 / text{{ item.text }}/text /view /view /template script setup import { useStore } from /store; const store useStore(); const list [ { pagePath: /pages/home/index, text: 首页, icon: home, selectedIcon: home-filled }, { pagePath: /pages/car/list, text: 车型, icon: car, selectedIcon: car-filled } ]; function switchTab(item, index) { store.tabIndex index; uni.switchTab({ url: item.pagePath }); } /script自定义 tabBar 会覆盖原生逻辑所以必须用uni.switchTab不能用uni.navigateTo。每个 tab 页面在onShow里要更新当前激活索引否则从详情页返回后选中态会丢失。还需要注意自定义 tabBar 组件路径是固定约定不能随意改目录并且编译到微信小程序后组件会被编译成custom-tab-bar/index不是 Vue 页面获取 store 时不能依赖组件实例最好把 current 索引放在持久化 store 中。自定义 tabBar 就绪前页面会短暂闪烁原生 tabBar解决办法是在App.vue的onLaunch里延迟渲染主内容或者在页面根节点加v-ifready。5. 下单、支付与订单状态同步租车业务的闭环实现思路租车不是单纯商品交易它有起止时间、车辆状态、违约判断因此订单状态机比普通电商复杂。分析这套源码时我会重点盯三个东西状态字段是否清楚、支付回调是否可靠、前端错误是否可追踪。5.1 租车订单状态机从待支付到已完成最少需要维护这些状态待支付、已支付/待取车、租用中、待还车、已完成、已取消。用一个常量文件把状态收敛// src/constants/order.js export const ORDER_STATUS { PENDING_PAY: 0, PAID: 1, PENDING_PICKUP: 2, RENTING: 3, PENDING_RETURN: 4, FINISHED: 5, CANCELLED: -1 }; export const ORDER_STATUS_TEXT { [ORDER_STATUS.PENDING_PAY]: 待支付, [ORDER_STATUS.PAID]: 已支付, [ORDER_STATUS.PENDING_PICKUP]: 待取车, [ORDER_STATUS.RENTING]: 租用中, [ORDER_STATUS.PENDING_RETURN]: 待还车, [ORDER_STATUS.FINISHED]: 已完成, [ORDER_STATUS.CANCELLED]: 已取消 };状态迁移最好由后端控制前端只做展示和操作入口。比如“立即支付”按钮只在PENDING_PAY显示“取车”按钮只在PAID状态显示。很多项目为了省事前端根据时间擅自判断订单状态会导致用户端显示“已还车”但后台还在租用中最后被扣费投诉。前端如果只拿到一个status数字可以用上面的映射直接渲染文本也可以根据当前状态控制按钮组。页面里不要写一长串if status 1 ... else if统一封装一个getOrderActions(status)函数返回可用操作列表这样页面代码会清晰很多。5.2 微信支付 v3 对接预支付参数与回调验签微信支付 v3 是小程序端最常见的支付方式。前端只需要两个步骤调后端接口获取支付参数再调用uni.requestPayment。后端负责生成预支付单小程序端不要直接拼接签名否则密钥会暴露。代码流程如下async function payOrder(orderId) { const payment await request({ url: /order/${orderId}/pay, method: POST }); uni.requestPayment({ provider: wxpay, timeStamp: payment.timeStamp, nonceStr: payment.nonceStr, package: payment.package, signType: payment.signType || RSA, paySign: payment.paySign, success: () { uni.redirectTo({ url: /pages/order/result?statussuccessid${orderId} }); }, fail: (err) { if (err.errMsg.includes(cancel)) { uni.showToast({ title: 已取消支付, icon: none }); } else { uni.showToast({ title: 支付失败, icon: none }); } } }); }payment.package通常是一个形如prepay_idxxxx的字符串后端从微信支付 v3 的/v3/pay/transactions/jsapi接口取到。前端signType在 v3 中固定为RSA不是RSA2。微信支付 v3 的通知是异步的前端不能因为uni.requestPayment成功就把订单标记为已成单必须等后端收到回调再改状态。调试时常见问题是真机上requestPayment提示“invalid signature”多半是后端生成paySign时使用了错误的 nonceStr或时间戳单位错误timeStamp必须是秒不是毫秒。在微信开发者工具里可以临时关闭域名校验但真机预览必须把api.example.com换成后台配置好的合法域名否则请求直接 fail。5.3 常见 uni-app 运行时错误undefined 属性是怎么产生的“cannot read properties of undefined (reading...)”是微信小程序和 uni-app 项目里出现频率最高的报错来源通常有三个路由参数缺失、接口数据结构不一致、组件渲染早于数据加载。比如跳转详情页时options.id拿不到后面所有car.id都会炸。健壮的页面开头应该先做参数守卫onLoad(options) { const carId Number(options.id); if (!carId) { uni.showToast({ title: 车辆参数异常, icon: none }); setTimeout(() uni.navigateBack(), 800); return; } this.loadCarDetail(carId); }微信小程序对undefined的处理和浏览器不完全一致模板里访问不存在的字段不会报错渲染为空白但在 JS 逻辑里继续读取就会抛异常。接口返回的data结构没对齐时经常出现res.data.data.list里部分对象缺少price字段这时候用默认值兜底比到处判空更省心const formatCar (car) ({ price: Number(car.price) || 0, name: car.name || 未命名车型, cover: car.cover || /static/images/default-car.png });如果页面使用 Vue 3setup还要注意 ref 自动解包后模板中不应出现car.value否则编译到微信端会变成 undefined。同样reactive对象的属性在onLoad里赋值时如果属性本身一开始没声明模板里第一次渲染也拿不到建议提前声明完整数据结构。6. 微信开发者工具真机调试与热更新排错的几个收尾技巧6.1 编译输出后如何让微信开发者工具稳定读取用 CLI 方式跑 uni-app 时先执行npm run dev:mp-weixin生成dist/dev/mp-weixin目录再用微信开发者工具“导入项目”选中这个目录。注意不要直接打开整个仓库根目录否则工具会提示 project.config.json 缺失。如果改动源码后开发者工具没有自动刷新看一下HBuilderX或命令行终端是否还在 watch 状态以及是否同时运行了多个 dev 进程。一个很实用的做法是在manifest.json的mp-weixin节点里固定appid这样每次编译产物都不会生成一个随机 token真机和模拟器切换时少很多不确定因素。6.2 白屏、导航栏高度与 wgt 热更新排查真机预览白屏时先打开开发者工具的vConsole看输入端有没有红色报错。如果报错指向__uniappview基础库通常是基础库版本过低或者代码有平台不兼容的 API。页面内容瞬间渲染但位置偏高说明顶部安全区没处理需要检查自定义导航栏是否引入了--status-bar-height。wgt 热更新不生效时确认生成的.wgt包没有包含原生插件改动同时检查manifest.json的版本号是否高于当前安装包版本。热更新完成后最好触发一次plus.runtime.restart()否则部分页面还会保留旧代码的缓存。最后一个建议上线前在App.vue的onError里收集err.stack并上报到自己的日志平台这样线上报错不需要用户反复截图也能在下一次发版前定位到具体页面。本文还有配套的精品资源点击获取
返回列表