UniApp跨平台开发实战:从零构建企业级电商应用完整指南

2025-11-06 0 364

在移动互联网时代,多端统一开发框架成为提升开发效率的关键。本文将深入探讨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开发的关键要点:

  1. 合理规划项目结构,确保代码可维护性
  2. 充分利用条件编译处理平台差异
  3. 实施性能监控和错误追踪机制
  4. 建立完整的CI/CD流水线
  5. 关注用户体验和交互细节

通过掌握这些技术,您将能够高效开发出高质量的跨平台应用,实现真正的”一次开发,多端部署”。

UniApp跨平台开发实战:从零构建企业级电商应用完整指南
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 uniapp UniApp跨平台开发实战:从零构建企业级电商应用完整指南 https://www.taomawang.com/web/uniapp/1386.html

常见问题

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务