在移动互联网时代,多端统一开发框架成为提升开发效率的关键。本文将深入探讨UniApp的核心技术,通过构建一个完整的企业级电商应用,展示如何实现一套代码多端部署的实战方案。
一、UniApp架构设计与环境搭建
UniApp基于Vue.js框架,通过条件编译实现多端适配,下面从项目初始化开始完整讲解。
1.1 项目初始化与工程配置
// 使用Vue3 + TypeScript初始化项目
vue create -p dcloudio/uni-preset-vue#vite-ts uni-mall
// 项目目录结构
uni-mall/
├── src/
│ ├── pages/ // 页面文件
│ ├── static/ // 静态资源
│ ├── components/ // 自定义组件
│ ├── store/ // 状态管理
│ ├── api/ // 接口管理
│ └── utils/ // 工具函数
├── manifest.json // 应用配置
├── pages.json // 页面路由
└── uni.scss // 样式变量
// manifest.json 基础配置
{
"name": "uni-mall",
"appid": "__UNI__XXXXXX",
"description": "企业级电商应用",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3
},
"h5": {
"publicPath": "/",
"router": {
"mode": "hash"
}
},
"mp-weixin": {
"appid": "wxxxxxxxxxxxxxx",
"setting": {
"urlCheck": false
},
"usingComponents": true
}
}
1.2 状态管理与数据流设计
// store/index.ts - Pinia状态管理
import { createPinia } from 'pinia'
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: uni.getStorageSync('token') || '',
userInfo: uni.getStorageSync('userInfo') || null,
cartCount: 0
}),
actions: {
async login(credentials: LoginParams) {
try {
const res = await uni.request({
url: '/api/auth/login',
method: 'POST',
data: credentials
})
this.token = res.data.token
this.userInfo = res.data.userInfo
uni.setStorageSync('token', this.token)
uni.setStorageSync('userInfo', this.userInfo)
return res.data
} catch (error) {
throw new Error('登录失败')
}
},
updateCartCount(count: number) {
this.cartCount = count
// 同步更新TabBar角标
if (count > 0) {
uni.setTabBarBadge({
index: 2,
text: count.toString()
})
} else {
uni.removeTabBarBadge({ index: 2 })
}
}
}
})
export const useProductStore = defineStore('product', {
state: () => ({
products: [],
categories: [],
searchHistory: uni.getStorageSync('searchHistory') || []
}),
actions: {
async fetchProducts(params: ProductQuery) {
const res = await uni.request({
url: '/api/products',
data: params
})
this.products = res.data.list
return res.data
},
addSearchHistory(keyword: string) {
const index = this.searchHistory.indexOf(keyword)
if (index > -1) {
this.searchHistory.splice(index, 1)
}
this.searchHistory.unshift(keyword)
this.searchHistory = this.searchHistory.slice(0, 10)
uni.setStorageSync('searchHistory', this.searchHistory)
}
}
})
二、核心页面开发实战
2.1 首页设计与实现
// pages/index/index.vue
<template>
<view class="page-container">
<view class="custom-navbar">
<view class="search-bar" @click="goSearch">
<uni-icons type="search" size="16"></uni-icons>
<text class="placeholder">搜索商品名称</text>
</view>
</view>
<swiper class="banner-swiper" :autoplay="true" :interval="3000">
<swiper-item v-for="banner in banners" :key="banner.id">
<image :src="banner.image" mode="aspectFill" @click="onBannerClick(banner)"></image>
</swiper-item>
</swiper>
<scroll-view class="category-scroll" scroll-x>
<view v-for="category in categories" :key="category.id"
class="category-item" @click="onCategoryClick(category)">
<image :src="category.icon"></image>
<text>{{ category.name }}</text>
</view>
</scroll-view>
<waterfall-flow :list="products" @click="onProductClick">
<template v-slot:default="item">
<product-card :product="item"></product-card>
</template>
</waterfall-flow>
<view v-if="loading" class="loading-state">
<uni-load-more status="loading"></uni-load-more>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, onReachBottom } from '@vue/composition-api'
import { useProductStore } from '@/store'
const productStore = useProductStore()
const banners = ref([])
const categories = ref([])
const products = ref([])
const loading = ref(false)
const page = ref(1)
const loadHomeData = async () => {
try {
loading.value = true
const [bannerRes, categoryRes, productRes] = await Promise.all([
uni.request({ url: '/api/banners' }),
uni.request({ url: '/api/categories' }),
productStore.fetchProducts({ page: 1, pageSize: 20 })
])
banners.value = bannerRes.data
categories.value = categoryRes.data
products.value = productRes.list
} catch (error) {
uni.showToast({ title: '数据加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
const goSearch = () => {
uni.navigateTo({ url: '/pages/search/search' })
}
const onProductClick = (product) => {
uni.navigateTo({
url: `/pages/product/detail?id=${product.id}`
})
}
onMounted(() => {
loadHomeData()
})
onReachBottom(async () => {
if (loading.value) return
page.value++
const res = await productStore.fetchProducts({
page: page.value,
pageSize: 20
})
products.value = [...products.value, ...res.list]
})
</script>
2.2 商品详情页与购物车交互
// pages/product/detail.vue
<template>
<view class="product-detail">
<product-gallery :images="product.images"></product-gallery>
<view class="product-info">
<view class="price-section">
<text class="current-price">¥{{ product.price }}</text>
<text class="original-price">¥{{ product.originalPrice }}</text>
<text class="discount">{{ product.discount }}折</text>
</view>
<text class="product-title">{{ product.title }}</text>
<view class="sales-info">
<text>销量 {{ product.sales }}件</text>
<text>库存 {{ product.stock }}件</text>
</view>
</view>
<sku-selector
:product="product"
@change="onSkuChange"
v-model="selectedSku">
</sku-selector>
<view class="action-bar">
<view class="action-icons">
<view class="action-item" @click="toggleFavorite">
<uni-icons :type="isFavorite ? 'heart-filled' : 'heart'"></uni-icons>
<text>收藏</text>
</view>
<view class="action-item" @click="goCart">
<uni-icons type="cart"></uni-icons>
<text>购物车</text>
<view v-if="cartCount > 0" class="cart-badge">{{ cartCount }}</view>
</view>
</view>
<view class="action-buttons">
<button class="btn add-cart" @click="addToCart">加入购物车</button>
<button class="btn buy-now" @click="buyNow">立即购买</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onLoad } from '@vue/composition-api'
import { useProductStore, useUserStore, useCartStore } from '@/store'
const productStore = useProductStore()
const userStore = useUserStore()
const cartStore = useCartStore()
const product = ref({})
const selectedSku = ref(null)
const isFavorite = ref(false)
const cartCount = computed(() => userStore.cartCount)
const onLoad = (options) => {
loadProductDetail(options.id)
}
const loadProductDetail = async (id) => {
try {
const res = await uni.request({
url: `/api/products/${id}`
})
product.value = res.data
} catch (error) {
uni.showToast({ title: '商品加载失败', icon: 'none' })
}
}
const addToCart = async () => {
if (!selectedSku.value) {
uni.showToast({ title: '请选择规格', icon: 'none' })
return
}
if (!userStore.token) {
uni.navigateTo({ url: '/pages/auth/login' })
return
}
try {
await cartStore.addToCart({
productId: product.value.id,
skuId: selectedSku.value.id,
quantity: 1
})
uni.showToast({ title: '添加成功' })
userStore.updateCartCount(cartStore.totalCount)
} catch (error) {
uni.showToast({ title: '添加失败', icon: 'none' })
}
}
const buyNow = async () => {
if (!selectedSku.value) {
uni.showToast({ title: '请选择规格', icon: 'none' })
return
}
const orderItems = [{
productId: product.value.id,
skuId: selectedSku.value.id,
quantity: 1,
price: selectedSku.value.price
}]
uni.navigateTo({
url: `/pages/order/confirm?items=${encodeURIComponent(JSON.stringify(orderItems))}`
})
}
</script>
三、多端适配与性能优化
3.1 条件编译实现平台差异化
// utils/platform.ts - 平台适配工具
export const getPlatform = () => {
// #ifdef MP-WEIXIN
return 'wechat'
// #endif
// #ifdef MP-ALIPAY
return 'alipay'
// #endif
// #ifdef H5
return 'h5'
// #endif
// #ifdef APP-PLUS
return 'app'
// #endif
}
export const adaptNavigationStyle = () => {
const platform = getPlatform()
switch (platform) {
case 'wechat':
return {
navigationBarTitleText: 'uni商城',
navigationBarBackgroundColor: '#ff5000',
navigationBarTextStyle: 'white'
}
case 'app':
return {
titleNView: {
titleText: 'uni商城',
backgroundColor: '#ff5000',
titleColor: '#ffffff'
}
}
default:
return {}
}
}
// 支付功能多端适配
export const unifiedPay = (orderInfo) => {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
wx.requestPayment({
...orderInfo,
success: resolve,
fail: reject
})
// #endif
// #ifdef MP-ALIPAY
my.tradePay({
tradeNO: orderInfo.tradeNo,
success: resolve,
fail: reject
})
// #endif
// #ifdef H5
// H5支付处理
window.location.href = orderInfo.paymentUrl
// #endif
// #ifdef APP-PLUS
uni.requestPayment({
provider: orderInfo.provider,
orderInfo: orderInfo.orderInfo,
success: resolve,
fail: reject
})
// #endif
})
}
3.2 性能优化策略
// 图片懒加载组件
// components/lazy-image.vue
<template>
<image
:src="placeholder || '/static/images/placeholder.png'"
:data-src="src"
:class="['lazy-image', { loaded: isLoaded }]"
:mode="mode"
@load="onImageLoad"
lazy-load>
</image>
</template>
<script setup lang="ts">
import { ref, onMounted } from '@vue/composition-api'
const props = defineProps({
src: String,
placeholder: String,
mode: {
type: String,
default: 'aspectFill'
}
})
const isLoaded = ref(false)
const onImageLoad = () => {
isLoaded.value = true
}
onMounted(() => {
// 监听元素进入可视区域
const observer = uni.createIntersectionObserver(this)
observer.relativeToViewport().observe('.lazy-image', (res) => {
if (res.intersectionRatio > 0 && !isLoaded.value) {
const img = res.target
img.src = img.dataset.src
}
})
})
</script>
// 数据缓存策略
// utils/cache.ts
class CacheManager {
private prefix = 'uni_mall_'
set(key: string, data: any, expire?: number) {
const cacheData = {
data,
expire: expire ? Date.now() + expire * 1000 : null
}
uni.setStorageSync(this.prefix + key, JSON.stringify(cacheData))
}
get(key: string) {
try {
const cached = uni.getStorageSync(this.prefix + key)
if (!cached) return null
const cacheData = JSON.parse(cached)
if (cacheData.expire && Date.now() > cacheData.expire) {
this.remove(key)
return null
}
return cacheData.data
} catch {
return null
}
}
remove(key: string) {
uni.removeStorageSync(this.prefix + key)
}
// 预加载关键数据
async preloadCriticalData() {
const preloadList = [
{ key: 'categories', url: '/api/categories', expire: 3600 },
{ key: 'user_info', url: '/api/user/info', expire: 1800 }
]
for (const item of preloadList) {
const cached = this.get(item.key)
if (!cached) {
try {
const res = await uni.request({ url: item.url })
this.set(item.key, res.data, item.expire)
} catch (error) {
console.error(`预加载 ${item.key} 失败:`, error)
}
}
}
}
}
export default new CacheManager()
四、部署与发布流程
4.1 多端打包配置
// package.json 脚本配置
{
"scripts": {
"dev:h5": "uni -p h5",
"dev:mp-weixin": "uni -p mp-weixin",
"build:h5": "uni build -p h5",
"build:mp-weixin": "uni build -p mp-weixin",
"build:app": "uni build -p app",
"lint": "eslint --ext .js,.ts,.vue src/"
}
}
// GitHub Actions 自动化部署
// .github/workflows/deploy.yml
name: Deploy UniApp
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Build H5
run: npm run build:h5
- name: Build WeChat MiniProgram
run: npm run build:mp-weixin
- name: Deploy H5 to Server
uses: easingthemes/ssh-deploy@v2
with:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SOURCE: "dist/build/h5/"
TARGET: "/var/www/uni-mall"
- name: Upload WeChat Package
uses: actions/upload-artifact@v2
with:
name: wechat-miniprogram
path: dist/build/mp-weixin/
五、项目监控与错误追踪
// utils/monitor.ts - 应用监控
class AppMonitor {
private performanceData = {}
// 性能监控
monitorPerformance() {
// #ifdef H5
const navigationTiming = performance.getEntriesByType('navigation')[0]
this.performanceData = {
dns: navigationTiming.domainLookupEnd - navigationTiming.domainLookupStart,
tcp: navigationTiming.connectEnd - navigationTiming.connectStart,
ttfb: navigationTiming.responseStart - navigationTiming.requestStart,
domReady: navigationTiming.domContentLoadedEventEnd - navigationTiming.fetchStart,
pageLoad: navigationTiming.loadEventEnd - navigationTiming.fetchStart
}
// #endif
// APP端性能监控
// #ifdef APP-PLUS
plus.performance.getEntries(entries => {
this.performanceData = entries
})
// #endif
}
// 错误收集
setupErrorHandler() {
// Vue错误捕获
Vue.config.errorHandler = (err, vm, info) => {
this.reportError({
type: 'vue_error',
error: err.toString(),
info,
stack: err.stack
})
}
// 未处理的Promise拒绝
window.addEventListener('unhandledrejection', (event) => {
this.reportError({
type: 'promise_rejection',
reason: event.reason
})
})
// 全局错误捕获
window.onerror = (message, source, lineno, colno, error) => {
this.reportError({
type: 'global_error',
message,
source,
lineno,
colno,
stack: error?.stack
})
}
}
// 错误上报
private reportError(errorInfo) {
const reportData = {
...errorInfo,
platform: getPlatform(),
version: plus.runtime.version,
timestamp: Date.now(),
userAgent: navigator.userAgent
}
// 开发环境打印,生产环境上报
if (process.env.NODE_ENV === 'development') {
console.error('Error captured:', reportData)
} else {
uni.request({
url: '/api/monitor/error',
method: 'POST',
data: reportData,
header: { 'Content-Type': 'application/json' }
})
}
}
}
export default new AppMonitor()
六、总结与最佳实践
通过本实战教程,我们完整构建了一个企业级的UniApp电商应用,涵盖了:
- UniApp项目架构设计与工程化配置
- 基于Pinia的状态管理方案
- 多端适配的条件编译技巧
- 性能优化与缓存策略
- 自动化部署与监控体系
UniApp开发的关键要点:
- 合理规划项目结构,确保代码可维护性
- 充分利用条件编译处理平台差异
- 实施性能监控和错误追踪机制
- 建立完整的CI/CD流水线
- 关注用户体验和交互细节
通过掌握这些技术,您将能够高效开发出高质量的跨平台应用,实现真正的”一次开发,多端部署”。

