发布日期:2023年11月
技术栈:UniApp + Vue.js + uniCloud
一、UniApp框架深度解析与项目架构设计
1.1 UniApp核心架构优势
UniApp基于Vue.js生态,采用条件编译实现真正的”一次开发,多端发布”。其核心架构特点:
- 跨端渲染引擎:基于Vue运行时,适配各平台渲染差异
- 原生能力桥接:通过uni对象统一调用各平台原生API
- 条件编译机制:使用#ifdef #endif实现平台差异化代码
- 插件生态系统:丰富的原生插件市场支持
1.2 电商项目架构设计
我们设计一个多端电商应用,支持微信小程序、支付宝小程序、H5和App:
ecommerce-uniapp/
├── pages/ # 页面目录
│ ├── index/ # 首页
│ ├── category/ # 分类页
│ ├── product/ # 商品详情
│ ├── cart/ # 购物车
│ └── user/ # 用户中心
├── components/ # 公共组件
│ ├── product-card/ # 商品卡片
│ ├── search-bar/ # 搜索组件
│ └── tab-bar/ # 自定义标签栏
├── static/ # 静态资源
├── store/ # Vuex状态管理
├── utils/ # 工具函数
├── uni_modules/ # 插件模块
└── manifest.json # 应用配置
二、环境搭建与项目初始化
2.1 开发环境配置
使用HBuilderX作为开发工具,配置多端开发环境:
# 通过CLI创建项目(备选方案)
vue create -p dcloudio/uni-preset-vue ecommerce-app
# 选择模板
? 请选择 uni-app 模板
❯ 默认模板(Vue2)
默认模板(Vue3)
自定义模板
2.2 项目配置文件详解
manifest.json配置多端适配:
{
"name": "电商商城",
"appid": "__UNI__XXXXXX",
"description": "多端电商应用",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
}
},
"mp-weixin": {
"appid": "wx-your-appid",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "获取位置用于配送服务"
}
}
},
"h5": {
"router": {
"mode": "hash",
"base": "./"
},
"template": "template.h5.html"
}
}
三、核心页面开发实战
3.1 首页开发:高性能商品瀑布流
实现支持虚拟列表的高性能首页:
<template>
<view class="home-page">
<!-- 自定义导航栏 -->
<custom-nav-bar title="电商商城" :show-back="false">
<template #right>
<view class="nav-right">
<uni-icons type="search" size="22" @click="goSearch"></uni-icons>
<uni-badge :text="cartCount" type="error">
<uni-icons type="cart" size="22" @click="goCart"></uni-icons>
</uni-badge>
</view>
</template>
</custom-nav-bar>
<!-- 轮播图 -->
<swiper class="banner-swiper"
:autoplay="true"
:interval="3000"
:circular="true"
indicator-dots
indicator-color="rgba(255,255,255,0.6)"
indicator-active-color="#ff6b35">
<swiper-item v-for="(item, index) in banners" :key="index">
<image :src="item.image"
mode="aspectFill"
@click="handleBannerClick(item)"
class="banner-image">
</image>
</swiper-item>
</swiper>
<!-- 分类入口 -->
<scroll-view class="category-scroll" scroll-x>
<view v-for="category in categories"
:key="category.id"
class="category-item"
@click="goCategory(category.id)">
<image :src="category.icon" class="category-icon"></image>
<text class="category-name">{{category.name}}</text>
</view>
</scroll-view>
<!-- 商品瀑布流 -->
<waterfall ref="waterfall"
:column="2"
:column-gap="10"
:data="productList"
:loading="loading"
:finished="finished"
@load="loadMoreProducts">
<template v-slot:item="{ item }">
<product-card :product="item" @click="goProductDetail(item.id)"></product-card>
</template>
</waterfall>
<!-- 返回顶部 -->
<back-to-top :show="showBackTop" @click="scrollToTop"></back-to-top>
</view>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import ProductCard from '@/components/product-card/product-card.vue'
import Waterfall from '@/components/waterfall/waterfall.vue'
export default {
components: {
ProductCard,
Waterfall
},
data() {
return {
banners: [],
categories: [],
productList: [],
page: 1,
pageSize: 10,
loading: false,
finished: false,
showBackTop: false
}
},
computed: {
...mapState(['cartCount'])
},
onLoad() {
this.initHomeData()
this.setupScrollListener()
},
onPullDownRefresh() {
this.refreshHomeData()
},
methods: {
...mapActions(['updateCartCount']),
async initHomeData() {
try {
// 并行请求首页数据
const [bannerRes, categoryRes] = await Promise.all([
this.$api.home.getBanners(),
this.$api.home.getCategories()
])
this.banners = bannerRes.data
this.categories = categoryRes.data
// 加载首屏商品
await this.loadMoreProducts()
} catch (error) {
uni.showToast({
title: '数据加载失败',
icon: 'none'
})
}
},
async loadMoreProducts() {
if (this.loading || this.finished) return
this.loading = true
try {
const res = await this.$api.product.getList({
page: this.page,
pageSize: this.pageSize
})
if (res.data.length === 0) {
this.finished = true
return
}
// 处理图片懒加载
const processedList = res.data.map(item => ({
...item,
image: this.processImageUrl(item.image)
}))
this.productList = [...this.productList, ...processedList]
this.page++
// 预加载下一页数据
if (this.page % 3 === 0) {
this.prefetchNextPage()
}
} catch (error) {
console.error('加载商品失败:', error)
} finally {
this.loading = false
}
},
processImageUrl(url) {
// 根据网络环境返回不同质量的图片
const networkType = uni.getNetworkType()
if (networkType === 'wifi') {
return `${url}?quality=high`
}
return `${url}?quality=medium&width=375`
},
setupScrollListener() {
// 监听滚动显示返回顶部按钮
uni.pageScrollTo({
scrollTop: 0,
duration: 0
})
const query = uni.createSelectorQuery().in(this)
query.selectViewport().scrollOffset(res => {
this.showBackTop = res.scrollTop > 500
}).exec()
},
scrollToTop() {
uni.pageScrollTo({
scrollTop: 0,
duration: 300
})
},
async prefetchNextPage() {
// 预加载逻辑
const nextPage = this.page + 1
this.$api.product.getList({
page: nextPage,
pageSize: this.pageSize
}).then(res => {
// 缓存数据
uni.setStorage({
key: `prefetch_page_${nextPage}`,
data: res.data
})
})
}
}
}
</script>
四、状态管理与数据持久化
4.1 Vuex Store设计
设计支持持久化的全局状态管理:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
Vue.use(Vuex)
const store = new Vuex.Store({
plugins: [
createPersistedState({
key: 'ecommerce-store',
storage: {
getItem: key => uni.getStorageSync(key),
setItem: (key, value) => uni.setStorageSync(key, value),
removeItem: key => uni.removeStorageSync(key)
},
reducer(state) {
return {
user: state.user,
cart: state.cart,
settings: state.settings
}
}
})
],
state: {
user: null,
cart: {
items: [],
total: 0,
count: 0
},
settings: {
theme: 'light',
notification: true
}
},
mutations: {
SET_USER(state, user) {
state.user = user
},
ADD_TO_CART(state, product) {
const existingItem = state.cart.items.find(item =>
item.id === product.id && item.skuId === product.skuId
)
if (existingItem) {
existingItem.quantity += product.quantity
} else {
state.cart.items.push({
...product,
selected: true
})
}
this.commit('UPDATE_CART_TOTAL')
},
UPDATE_CART_TOTAL(state) {
let total = 0
let count = 0
state.cart.items.forEach(item => {
if (item.selected) {
total += item.price * item.quantity
count += item.quantity
}
})
state.cart.total = parseFloat(total.toFixed(2))
state.cart.count = count
}
},
actions: {
async login({ commit }, credentials) {
try {
const res = await uni.request({
url: '/api/auth/login',
method: 'POST',
data: credentials
})
if (res.data.code === 200) {
commit('SET_USER', res.data.data)
uni.setStorageSync('token', res.data.token)
return Promise.resolve(res.data)
}
} catch (error) {
return Promise.reject(error)
}
},
async syncCartToServer({ state }) {
if (!state.user) return
try {
await uni.request({
url: '/api/cart/sync',
method: 'POST',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
data: {
items: state.cart.items
}
})
} catch (error) {
console.error('同步购物车失败:', error)
}
}
},
getters: {
isLoggedIn: state => !!state.user,
cartItemCount: state => state.cart.count,
selectedCartItems: state =>
state.cart.items.filter(item => item.selected)
}
})
export default store
五、原生能力集成与性能优化
5.1 多端原生API封装
统一封装各平台原生能力:
// utils/native.js
class NativeBridge {
// 获取设备信息
static getDeviceInfo() {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
plus.device.getInfo({
success: resolve,
fail: reject
})
// #endif
// #ifdef MP-WEIXIN
wx.getSystemInfo({
success: resolve,
fail: reject
})
// #endif
// #ifdef H5
resolve({
platform: 'h5',
model: navigator.userAgent,
system: navigator.platform
})
// #endif
})
}
// 图片选择与上传
static chooseImage(options = {}) {
return new Promise((resolve, reject) => {
const config = {
count: options.count || 9,
sizeType: options.sizeType || ['original', 'compressed'],
sourceType: options.sourceType || ['album', 'camera']
}
// #ifdef APP-PLUS
plus.gallery.pick({
count: config.count,
filter: 'image',
system: false
}, resolve, reject)
// #endif
// #ifdef MP-WEIXIN
wx.chooseImage({
...config,
success: res => resolve({ files: res.tempFilePaths }),
fail: reject
})
// #endif
// #ifdef H5
const input = document.createElement('input')
input.type = 'file'
input.multiple = config.count > 1
input.accept = 'image/*'
input.onchange = e => {
const files = Array.from(e.target.files)
resolve({ files })
}
input.click()
// #endif
})
}
// 支付统一接口
static async pay(orderInfo) {
const platform = uni.getSystemInfoSync().platform
switch (platform) {
case 'android':
case 'ios':
return this.appPay(orderInfo)
case 'mp-weixin':
return this.wechatPay(orderInfo)
case 'mp-alipay':
return this.alipay(orderInfo)
default:
return this.h5Pay(orderInfo)
}
}
static async appPay(orderInfo) {
// 处理App支付
const provider = orderInfo.provider || 'wxpay'
return new Promise((resolve, reject) => {
uni.requestPayment({
provider,
orderInfo: orderInfo.data,
success: resolve,
fail: reject
})
})
}
}
export default NativeBridge
5.2 性能优化策略
实施关键性能优化措施:
// utils/performance.js
class PerformanceOptimizer {
// 图片懒加载优化
static initImageLazyLoad() {
const io = uni.createIntersectionObserver(this)
// 监听图片进入视口
io.relativeToViewport({ bottom: 100 }).observe('.lazy-image', res => {
if (res.intersectionRatio > 0) {
const img = res.target
const src = img.dataset.src
if (src) {
img.src = src
img.classList.add('loaded')
}
}
})
}
// 请求缓存
static createCachedRequest() {
const cache = new Map()
const MAX_CACHE_SIZE = 50
return async function cachedRequest(url, options = {}) {
const cacheKey = `${url}_${JSON.stringify(options)}`
const now = Date.now()
// 检查缓存
if (cache.has(cacheKey)) {
const { data, timestamp, ttl = 60000 } = cache.get(cacheKey)
if (now - timestamp = MAX_CACHE_SIZE) {
const firstKey = cache.keys().next().value
cache.delete(firstKey)
}
cache.set(cacheKey, {
data: response.data,
timestamp: now
})
return response.data
} catch (error) {
throw error
}
}
}
// 页面预加载
static preloadPages(pageRoutes) {
pageRoutes.forEach(route => {
// #ifdef APP-PLUS
plus.webview.preload({
url: route,
id: route
})
// #endif
// #ifdef H5
// H5使用link预加载
const link = document.createElement('link')
link.rel = 'prefetch'
link.href = route
document.head.appendChild(link)
// #endif
})
}
// 内存优化:清理未使用的资源
static cleanupResources() {
// 清理过期的缓存
const now = Date.now()
const imageCache = uni.getStorageInfoSync()
Object.keys(imageCache).forEach(key => {
if (key.startsWith('image_cache_')) {
const cacheData = uni.getStorageSync(key)
if (now - cacheData.timestamp > 24 * 60 * 60 * 1000) {
uni.removeStorageSync(key)
}
}
})
// 触发垃圾回收(仅App)
// #ifdef APP-PLUS
if (plus.os.name === 'iOS') {
plus.ios.invoke('performSelector:', 'gc', '')
}
// #endif
}
}
export default PerformanceOptimizer
六、多端适配与发布部署
6.1 条件编译实战
处理各平台差异:
// 导航栏适配
const navBarConfig = {
// #ifdef MP-WEIXIN
height: 44,
paddingTop: 0,
// #endif
// #ifdef MP-ALIPAY
height: 48,
paddingTop: 4,
// #endif
// #ifdef APP-PLUS
height: 44,
paddingTop: plus.navigator.getStatusbarHeight(),
// #endif
// #ifdef H5
height: 44,
paddingTop: 0,
// #endif
}
// 分享功能适配
function setupShare() {
// #ifdef MP-WEIXIN
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
})
// #endif
// #ifdef APP-PLUS
plus.share.getServices(services => {
const weixinService = services.find(s => s.id === 'weixin')
if (weixinService) {
weixinService.authorize(() => {
console.log('微信分享授权成功')
})
}
})
// #endif
}
// 支付功能适配
async function handlePayment(order) {
// #ifdef MP-WEIXIN
const paymentResult = await wx.requestPayment({
timeStamp: order.timeStamp,
nonceStr: order.nonceStr,
package: order.package,
signType: 'MD5',
paySign: order.paySign
})
// #endif
// #ifdef MP-ALIPAY
const paymentResult = await my.tradePay({
tradeNO: order.tradeNo
})
// #endif
// #ifdef APP-PLUS
const paymentResult = await uni.requestPayment({
provider: order.provider,
orderInfo: order.orderInfo
})
// #endif
return paymentResult
}
6.2 云打包与发布
配置自动化构建流程:
// package.json 构建脚本
{
"scripts": {
"build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build",
"build:app": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus vue-cli-service uni-build",
"build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build",
"build:all": "npm run build:mp-weixin && npm run build:app && npm run build:h5",
"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-serve",
"dev:app": "cross-env NODE_ENV=development UNI_PLATFORM=app-plus vue-cli-service uni-serve"
}
}
// GitHub Actions 自动化部署
// .github/workflows/deploy.yml
name: Deploy UniApp
on:
push:
branches: [ main ]
pull_request:
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: '14'
- name: Install Dependencies
run: npm ci
- name: Build for WeChat Mini Program
run: npm run build:mp-weixin
env:
UNI_CLOUD_PROVIDER: aliyun
UNI_APP_ID: ${{ secrets.UNI_APP_ID }}
- name: Build for H5
run: npm run build:h5
- name: Deploy H5 to Server
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist/build/h5
七、调试与监控
7.1 多端调试技巧
// utils/debug.js
class DebugHelper {
static init() {
// 开发环境日志
if (process.env.NODE_ENV === 'development') {
// 性能监控
this.monitorPerformance()
// 网络请求拦截
this.interceptRequests()
// 错误收集
this.setupErrorHandler()
}
}
static monitorPerformance() {
// 页面加载时间
const performance = wx.getPerformance ? wx.getPerformance() : null
if (performance) {
const observer = performance.createObserver(entries => {
entries.forEach(entry => {
console.log(`[Performance] ${entry.name}: ${entry.duration}ms`)
})
})
observer.observe({ entryTypes: ['navigation', 'render', 'script'] })
}
// 内存监控
setInterval(() => {
// #ifdef APP-PLUS
const memory = plus.android.invoke('Runtime/getRuntime', 'maxMemory')
console.log(`[Memory] Used: ${memory}`)
// #endif
}, 30000)
}
static interceptRequests() {
const originalRequest = uni.request
uni.request = function(config) {
const startTime = Date.now()
return originalRequest.call(this, {
...config,
success: res => {
const duration = Date.now() - startTime
console.log(`[Request] ${config.url}: ${duration}ms`)
config.success && config.success(res)
},
fail: error => {
console.error(`[Request Failed] ${config.url}:`, error)
config.fail && config.fail(error)
}
})
}
}
static setupErrorHandler() {
// 全局错误捕获
uni.onError(error => {
console.error('[Global Error]:', error)
// 上报错误
this.reportError(error)
})
// Promise错误捕获
window.addEventListener('unhandledrejection', event => {
console.error('[Unhandled Promise]:', event.reason)
this.reportError(event.reason)
})
}
static reportError(error) {
// 错误上报到服务器
const errorInfo = {
time: new Date().toISOString(),
platform: uni.getSystemInfoSync().platform,
version: plus.runtime.version,
error: error.toString(),
stack: error.stack
}
// 使用sendBeacon异步上报
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/error/report', JSON.stringify(errorInfo))
}
}
}
export default DebugHelper
八、总结与最佳实践
8.1 项目总结
通过本实战项目,我们实现了:
- 基于UniApp的跨平台电商应用完整架构
- 高性能的商品瀑布流和图片懒加载方案
- 统一的多端原生能力封装
- 完善的状态管理和数据持久化
- 自动化构建和部署流程
8.2 最佳实践建议
- 代码组织:按功能模块划分目录结构,保持组件单一职责
- 性能优化:图片懒加载、请求缓存、组件按需加载
- 错误处理:全局错误捕获和用户友好提示
- 测试策略:单元测试组件,端到端测试关键流程
- 持续集成:自动化构建、测试和部署
8.3 扩展学习方向
- uniCloud云开发深度集成
- 原生插件开发与封装
- TypeScript在UniApp中的应用
- 微前端架构在跨端应用中的实践
- AR/VR等新技术集成
UniApp作为跨端开发的重要解决方案,在不断演进中提供了更强大的能力。掌握其核心原理和最佳实践,能够帮助开发者高效构建高质量的多端应用。
// 页面交互增强
document.addEventListener(‘DOMContentLoaded’, function() {
// 代码高亮和复制
const codeBlocks = document.querySelectorAll(‘pre code’)
codeBlocks.forEach(block => {
// 添加复制按钮
const copyBtn = document.createElement(‘button’)
copyBtn.textContent = ‘复制’
copyBtn.style.cssText = `
position: absolute;
right: 10px;
top: 10px;
background: #007aff;
color: white;
border: none;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
opacity: 0.8;
transition: opacity 0.3s;
`
copyBtn.onmouseover = () => copyBtn.style.opacity = ‘1’
copyBtn.onmouseout = () => copyBtn.style.opacity = ‘0.8’
copyBtn.onclick = async () => {
try {
await navigator.clipboard.writeText(block.textContent)
copyBtn.textContent = ‘已复制’
setTimeout(() => {
copyBtn.textContent = ‘复制’
}, 2000)
} catch (err) {
console.error(‘复制失败:’, err)
}
}
block.parentNode.style.position = ‘relative’
block.parentNode.appendChild(copyBtn)
// 语法高亮(简化版)
const keywords = [‘import’, ‘export’, ‘default’, ‘function’, ‘class’, ‘const’, ‘let’, ‘var’, ‘if’, ‘else’, ‘return’, ‘async’, ‘await’, ‘try’, ‘catch’, ‘finally’]
const html = block.innerHTML
let highlighted = html
keywords.forEach(keyword => {
const regex = new RegExp(`\b${keyword}\b`, ‘g’)
highlighted = highlighted.replace(regex, `${keyword}`)
})
// 字符串高亮
highlighted = highlighted.replace(/'[^’]*’|”[^”]*”/g, match =>
`${match}`
)
// 注释高亮
highlighted = highlighted.replace(///.*$/gm, match =>
`${match}`
)
block.innerHTML = highlighted
})
// 章节导航
const headings = document.querySelectorAll(‘h2, h3’)
const navContainer = document.createElement(‘div’)
navContainer.style.cssText = `
position: fixed;
right: 20px;
top: 100px;
background: white;
border: 1px solid #eaeaea;
border-radius: 8px;
padding: 15px;
max-width: 250px;
max-height: 70vh;
overflow-y: auto;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 1000;
`
const navTitle = document.createElement(‘h4’)
navTitle.textContent = ‘文章导航’
navTitle.style.marginTop = ‘0’
navContainer.appendChild(navTitle)
headings.forEach(heading => {
const link = document.createElement(‘a’)
link.textContent = heading.textContent
link.href = ‘#’
link.style.cssText = `
display: block;
padding: 5px 0;
color: #333;
text-decoration: none;
font-size: ${heading.tagName === ‘H2′ ? ’14px’ : ’13px’};
margin-left: ${heading.tagName === ‘H3′ ? ’15px’ : ‘0’};
border-left: ${heading.tagName === ‘H3’ ? ‘2px solid #007aff’ : ‘none’};
padding-left: ${heading.tagName === ‘H3′ ? ’10px’ : ‘0’};
`
link.onclick = (e) => {
e.preventDefault()
heading.scrollIntoView({
behavior: ‘smooth’,
block: ‘start’
})
}
navContainer.appendChild(link)
})
document.body.appendChild(navContainer)
// 移动端隐藏导航
if (window.innerWidth < 768) {
navContainer.style.display = 'none'
}
})

