uni-app三端统一商城架构:H5+小程序+App生产级实践
2026/9/15 14:10:28 网站建设 项目流程

简介:这是一套面向前端开发者与uni-app初学者的多端商城实战代码,聚焦小程序与跨端App开发场景,解决从零构建生产级电商应用的工程化难题。资源包含246个文件,主体为153个Vue组件(覆盖首页、商品详情、购物车、订单等核心页面)、56个JS工具与路由逻辑文件、12个SCSS样式文件,辅以PNG图标、JSON配置及字体资源,整体包仅622KB,轻量易上手。已有1407人学习下载,适合希望快速掌握uni-app项目结构、组件化开发规范与多端适配技巧的开发者。代码结构清晰:src目录下components封装轮播图/商品列表等可复用组件,pages组织完整业务流程,utils提供网络请求与数据处理工具,config与manifest.json支持多端差异化配置,结合colorui等UI库实现开箱即用的视觉效果。

1. 这不是“又一个uni-app商城模板”,而是一套能直接进CI/CD、跑通支付闭环、适配微信公众号H5+小程序+App三端的生产级前端架构

你搜“uniapp 商城 开源”时,刷出来的大多是带后台的全栈项目、教学Demo或缺支付/登录/订单状态同步的半成品。但真实业务里,一个能落地生产的uni-app商城前端,核心不在UI多炫,而在三端行为一致性、原生能力调用可靠性、构建产物可审计、错误监控可追溯——比如微信公众号H5里调用uni.getLocation必须处理iOS Safari定位权限降级、小程序里web-view与H5通信需绕过postMessage跨域限制、App端iOS蓝牙连接失败时要区分是系统权限未开还是设备未广播。本项目正是为解决这类问题设计:它不依赖特定后端,但预置了符合OpenAPI 3.0规范的Mock服务接口;不强制使用某套UI库,但通过@/composables/useApi统一封装请求拦截与重试逻辑;所有平台差异代码(如iOS蓝牙DeviceID连接、H5嵌入公众号的JS-SDK注入)都收敛在platform/目录下,且每个模块附带单元测试用例。适合已有后端团队、需要快速交付多端商城前端的中小厂技术负责人,或正在准备前端面试、想深入理解uni-app工程化边界的开发者。

2. 用uni-app 3.9.11 + Vue 3 Composition API 搭建可维护的多端基础架构

2.1 为什么选Vue 3而非Vue 2?关键在响应式穿透与编译优化边界

uni-app自3.7.0起全面支持Vue 3 Composition API,但很多开源商城仍用Vue 2写法,导致两个硬伤:一是ref嵌套对象时,深层属性变更无法触发视图更新(如购物车商品数量修改后UI不刷新);二是<script setup>defineProps类型校验缺失,使H5端传入的location参数在小程序里被忽略。本项目强制使用Vue 3,并在tsconfig.json中启用"skipLibCheck": true跳过@dcloudio/uni-app类型检查,避免因官方TS定义滞后导致构建失败。同时,所有组件均采用<script setup lang="ts">语法,配合PropType显式声明props类型:

// @/components/AddressItem.vue const props = defineProps({ address: { type: Object as PropType<{ id: string; name: string; phone: string; province: string; city: string; area: string; detail: string; isDefault: boolean; }>, required: true } })

提示:若项目需兼容旧版uni-app(<3.7.0),必须将vue依赖锁定在^2.6.14,并改用export default { props: [...] }写法,但会失去useSlots等Composition API能力。

2.2 多端条件编译的最小安全实践:从#ifdefuni.getSystemInfoSync().platform

uni-app的#ifdef编译指令虽方便,但过度使用会导致代码分支爆炸。本项目仅在平台特有API调用处使用编译指令,其余逻辑全部走运行时判断:

// @/utils/platform.ts export const getPlatform = () => { const systemInfo = uni.getSystemInfoSync() // 微信小程序环境:微信内置浏览器内核,无window对象 if (typeof wx !== 'undefined' && !systemInfo?.webView) { return 'mp-weixin' } // H5环境:需区分是否在微信公众号内 if (typeof window !== 'undefined') { const ua = window.navigator.userAgent.toLowerCase() if (ua.includes('micromessenger')) { return 'h5-wechat' } return 'h5' } // App环境:iOS/Android由systemInfo.platform标识 return systemInfo?.platform?.toLowerCase() || 'unknown' }

该函数返回值用于动态加载平台专属模块,例如蓝牙连接:

// @/api/bluetooth.ts import { getPlatform } from '@/utils/platform' const bluetoothModule = getPlatform() === 'ios' ? import('@/platform/ios/bluetooth') : getPlatform() === 'mp-weixin' ? import('@/platform/mp-weixin/bluetooth') : Promise.resolve({ connectToDevice: () => Promise.reject('Not supported') }) export const connectToDevice = async (deviceId: string) => { const module = await bluetoothModule return module.connectToDevice(deviceId) }

注意:#ifdef MP-WEIXIN不能用于包裹整个<template>,否则H5端构建时会丢失DOM结构。所有条件渲染必须用v-if配合getPlatform()判断。

2.3 manifest.json与vue.config.js协同配置:解决H5嵌入公众号的定位白屏问题

微信公众号H5调用uni.getLocation时,若页面未声明HTTPS且未在公众号JS-SDK中配置getLocation权限,会直接白屏。本项目通过双层配置规避:

  1. manifest.json中设置"h5": { "useCustomRouter": true, "devServer": { "https": true } },强制开发环境启用HTTPS;
  2. vue.config.js中注入configureWebpack,为H5端添加<meta name="referrer" content="no-referrer">防止跨域丢失Referer:
// vue.config.js module.exports = { configureWebpack: (config) => { if (process.env.NODE_ENV === 'production' && process.env.UNI_PLATFORM === 'h5') { config.plugins.push(new HtmlWebpackPlugin({ templateParameters: { meta: '<meta name="referrer" content="no-referrer">' } })) } } }

同时,在main.js入口文件中注入JS-SDK初始化逻辑:

// main.js if (uni.getSystemInfoSync().platform === 'h5') { const script = document.createElement('script') script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js' script.onload = () => { wx.config({ debug: false, appId: 'wx1234567890abcdef', timestamp: Date.now(), nonceStr: 'noncestr', signature: 'signature', jsApiList: ['getLocation', 'openLocation'] }) } document.head.appendChild(script) }

3. 实现微信公众号H5+小程序+App三端一致的订单状态同步机制

3.1 基于WebSocket的实时订单状态推送:避免轮询导致的H5内存泄漏

小程序和App端可直接使用uni.connectSocket建立长连接,但H5端在微信公众号内受限于wx.openSocket的域名白名单。本项目采用降级策略:优先尝试WebSocket,失败则回退到Server-Sent Events(SSE),最后才用HTTP轮询:

// @/composables/useOrderSync.ts import { ref, onUnmounted } from 'vue' import { getPlatform } from '@/utils/platform' export const useOrderSync = (orderId: string) => { const status = ref<'pending' | 'paid' | 'shipped' | 'delivered'>('pending') let socket: WebSocket | null = null let eventSource: EventSource | null = null const connect = () => { const platform = getPlatform() if (platform === 'mp-weixin' || platform === 'app') { socket = uni.connectSocket({ url: `wss://api.example.com/order/${orderId}/status`, success: () => { uni.onSocketMessage((res) => { status.value = JSON.parse(res.data).status }) } }) } else if (platform === 'h5-wechat') { // 公众号H5使用SSE eventSource = new EventSource(`https://api.example.com/order/${orderId}/sse`) eventSource.onmessage = (e) => { status.value = JSON.parse(e.data).status } } else { // 普通H5轮询 const timer = setInterval(() => { uni.request({ url: `/api/order/${orderId}/status`, success: (res) => { status.value = res.data.status } }) }, 5000) onUnmounted(() => clearInterval(timer)) } } onUnmounted(() => { socket?.close() eventSource?.close() }) return { status, connect } }

提示:SSE在iOS Safari中存在连接数限制(最多6个),因此必须在组件卸载时调用eventSource.close(),否则页面切换后连接持续占用导致新页面无法建立SSE。

3.2 小程序Webview与H5通信:绕过postMessage跨域限制的双向通道

当小程序使用<web-view>嵌入H5商城时,需实现“H5下单→小程序支付→H5更新订单状态”的闭环。但web-viewpostMessage在iOS上存在跨域限制,本项目采用URL Scheme + localStorage桥接方案:

  1. H5端下单成功后,将订单ID存入localStorage并跳转至小程序支付页:
// H5端 localStorage.setItem('pendingOrder', JSON.stringify({ id: 'ORD123', amount: 99.9 })) uni.navigateToMiniProgram({ appId: 'wx1234567890abcdef', path: '/pages/pay/index?order_id=ORD123' })
  1. 小程序支付成功后,通过wx.miniProgram.navigateBack携带参数返回,并在onShow中读取:
// 小程序端 pages/pay/index.js wx.requestPayment({ // ...支付参数 success: () => { wx.setStorageSync('payResult', { orderId: 'ORD123', status: 'paid' }) wx.navigateBack() } }) // 小程序端 app.js onShow() { const result = wx.getStorageSync('payResult') if (result) { // 通过URL参数通知H5 const webview = wx.getMenuButtonBoundingClientRect() wx.navigateTo({ url: `/pages/webview/index?url=https://h5.example.com/order?status=${result.status}&id=${result.orderId}` }) } }
  1. H5端/order页面监听URL参数变化,触发状态更新:
// H5端 pages/order/index.vue const route = useRoute() watch(() => route.query, (newVal) => { if (newVal.status && newVal.id) { updateOrderStatus(newVal.id, newVal.status as 'paid' | 'failed') } })

3.3 App端iOS蓝牙设备连接:基于DeviceID的稳定连接流程

uni-app官方文档未明确说明如何通过DeviceID建立iOS蓝牙连接,实际需分三步:先扫描获取设备,再连接,最后启用通知。本项目在@/platform/ios/bluetooth.ts中封装:

// @/platform/ios/bluetooth.ts export const connectToDevice = async (deviceId: string) => { try { // 1. 初始化蓝牙适配器 await uni.openBluetoothAdapter() // 2. 扫描设备(iOS需指定serviceUUID) await uni.startBluetoothDevicesDiscovery({ services: ['0000180F-0000-1000-8000-00805F9B34FB'] // 示例服务UUID }) // 3. 根据deviceId连接(注意:iOS deviceId是MAC地址格式) const device = await new Promise<UniApp.BluetoothDeviceInfo>((resolve, reject) => { uni.onBluetoothDeviceFound((devices) => { const target = devices.find(d => d.deviceId === deviceId) if (target) resolve(target) }) setTimeout(() => reject(new Error('Device not found')), 5000) }) // 4. 连接设备 await uni.createBLEConnection({ deviceId: device.deviceId }) // 5. 启用特征值通知 await uni.notifyBLECharacteristicValueChange({ state: true, deviceId: device.deviceId, serviceId: '0000180F-0000-1000-8000-00805F9B34FB', characteristicId: '00002A19-0000-1000-8000-00805F9B34FB' }) return { connected: true } } catch (err) { throw new Error(`Bluetooth connection failed: ${err.message}`) } }

注意:iOS要求info.plist中添加NSBluetoothAlwaysUsageDescription权限描述,且必须在manifest.json"ios": { "usingComponents": true }开启原生组件支持。

4. 构建与发布:uni-app打包配置与安卓/iOS上架关键参数表

4.1 H5端构建产物优化:分离第三方库与路由懒加载

H5端首屏加载时间直接影响转化率。本项目通过vue.config.js配置Webpack分包:

// vue.config.js module.exports = { configureWebpack: { optimization: { splitChunks: { chunks: 'all', cacheGroups: { // 将uni-app核心库单独打包 uni: { name: 'chunk-uni', test: /[\\/]node_modules[\\/](@dcloudio\/uni-app|@dcloudio\/uni-h5)[\\/]/, priority: 20, chunks: 'initial' }, // 将Vue相关库打包 vue: { name: 'chunk-vue', test: /[\\/]node_modules[\\/](vue|vue-router|vuex)[\\/]/, priority: 15, chunks: 'initial' } } } } } }

同时,路由配置启用异步组件:

// router/index.ts const routes: RouteRecordRaw[] = [ { path: '/order', component: () => import('@/pages/order/index.vue') // 自动分割为独立chunk } ]

构建后生成chunk-uni.[hash].jschunk-vue.[hash].js等独立文件,配合CDN缓存可提升复访速度。

4.2 小程序端体积压缩:移除未使用的API与组件

uni-app默认引入所有API,但小程序实际只用到getLocationrequestPayment等少数接口。本项目在manifest.json中配置"mp-weixin": { "nvueStyleCompiler": "uni-app", "usingComponents": true },并通过uni-appunpackage目录手动剔除未用模块:

// manifest.json { "name": "Multi-Platform Mall", "appid": "", "description": "", "versionName": "1.0.0", "versionCode": "100", "transformPx": false, "app-plus": { /* App配置 */ }, "mp-weixin": { "usingComponents": true, "permission": { "scope.userLocation": { "desc": "用于获取您的位置信息" } } }, "h5": { /* H5配置 */ } }

关键点:"usingComponents": true启用自定义组件模式,使<uni-button>等组件按需加载;permission字段声明必要权限,避免审核被拒。

4.3 安卓与iOS上架必备参数对照表

参数项安卓(android目录)iOS(ios目录)说明
应用图标res/icons/xxx.png(144x144)icons/xxx.png(1024x1024)iOS要求1024x1024 PNG,安卓需提供mdpi/hdpi/xhdpi/xxhdpi四套
启动图res/splash/xxx.pngsplash/xxx.pngiOS启动图必须为PNG,安卓支持PNG/JPEG
包名/Bundle IDandroid/app/src/main/AndroidManifest.xmlpackage="com.example.mall"ios/Info.plistCFBundleIdentifier="com.example.mall"必须与应用市场注册一致
签名证书android/app/build.gradlesigningConfigs配置keystore路径Xcode中选择Provisioning Profile安卓需.jks文件,iOS需.p12证书+.mobileprovision配置文件
隐私政策链接android/app/src/main/assets/privacy.htmlios/PrivacyPolicy.html上架必填,需在manifest.json"android": { "privacyUrl": "https://example.com/privacy" }声明

提示:iOS上架前必须在Xcode中勾选Background Modes → Uses Bluetooth LE accessories,否则蓝牙功能会被App Store审核拒绝。

5. 调试与排错:三端常见报错定位与修复方案

5.1Uncaught TypeError: Cannot read properties of undefined (reading 'xxx')的根因分析

该错误在uni-app中高频出现,本质是平台API未就绪即调用。例如H5端未等待wx.config完成就执行wx.getLocation

// ❌ 错误写法:未等待JS-SDK初始化 wx.getLocation({ success: console.log }) // ✅ 正确写法:封装Promise等待 const initWxSDK = () => { return new Promise<void>((resolve, reject) => { wx.config({ // ...配置 success: () => resolve(), fail: (err) => reject(err) }) }) } // 使用 initWxSDK().then(() => { wx.getLocation({ success: console.log }) })

同理,App端调用蓝牙API前必须确保uni.openBluetoothAdapter()成功:

try { await uni.openBluetoothAdapter() // 此时才能调用startBluetoothDevicesDiscovery } catch (err) { // 检查err.errCode是否为10000(未开启蓝牙) if (err.errCode === 10000) { uni.showModal({ title: '提示', content: '请开启手机蓝牙' }) } }

5.2 小程序web-view白屏:检查URL Scheme与HTTPS强制跳转

<web-view src="http://h5.example.com">白屏时,90%原因是HTTP协议被微信拦截。解决方案:

  1. 确保H5域名已配置在公众号JS-SDK安全域名列表;
  2. manifest.json中设置"h5": { "domain": "https://h5.example.com" }
  3. H5服务器配置HTTP 301跳转至HTTPS:
# Nginx配置 server { listen 80; server_name h5.example.com; return 301 https://$server_name$request_uri; }

5.3 iOS App蓝牙连接失败:DeviceID格式与服务UUID匹配验证

iOS要求DeviceID必须为MAC地址格式(如AA:BB:CC:DD:EE:FF),且服务UUID需与设备广播的一致。调试步骤:

  1. 使用LightBlue等工具扫描设备,确认广播的服务UUID;
  2. 在代码中打印扫描到的设备列表:
uni.onBluetoothDeviceFound((devices) => { console.log('Found devices:', devices.map(d => d.deviceId)) })
  1. 检查startBluetoothDevicesDiscoveryservices参数是否包含设备广播的UUID。

若仍失败,在Xcode控制台查看CoreBluetooth日志,关键词CBPeripheralManager可定位权限或硬件问题。

5.4 H5嵌入公众号定位失败:UserAgent检测与权限引导

iOS Safari在微信内核中禁用navigator.geolocation,必须走微信JS-SDK。本项目通过UserAgent检测自动降级:

// @/utils/location.ts export const getLocation = async () => { const ua = navigator.userAgent if (ua.includes('MicroMessenger') && /iPhone|iPad|iPod/.test(ua)) { // 微信iOS端走JS-SDK return new Promise((resolve, reject) => { wx.getLocation({ type: 'gcj02', success: resolve, fail: reject }) }) } else { // 其他环境走标准API return navigator.geolocation.getCurrentPosition() } }

并在失败时引导用户手动开启权限:

try { const pos = await getLocation() } catch (err) { uni.showModal({ title: '定位失败', content: '请在微信设置中开启位置权限', confirmText: '去设置', success: (res) => { if (res.confirm) { uni.openSetting() // 跳转至微信设置页 } } }) }

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

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

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

立即咨询