UniApp跨平台直播电商应用开发实战 | 完整项目架构与实现

2025-11-15 0 798

引言:直播电商的技术挑战与UniApp优势

随着直播电商行业的迅猛发展,开发一款支持多端运行的直播电商应用成为众多企业的迫切需求。UniApp凭借其”一次开发,多端部署”的特性,成为实现这一目标的理想选择。本文将深入讲解如何使用UniApp开发一个功能完整的直播电商应用。

一、项目架构设计

1. 技术栈选型

  • 前端框架:UniApp + Vue.js 3 + TypeScript
  • UI组件库:uView UI 3.0
  • 实时音视频:腾讯云TRTC
  • 即时通讯:腾讯云IM
  • 状态管理:Pinia
  • 打包部署:HBuilderX + 各端开发者工具

2. 目录结构规划

live-mall/
├── pages/                    # 页面文件
│   ├── index/               # 首页
│   ├── live-room/           # 直播间
│   ├── product/             # 商品详情
│   └── user/                # 用户中心
├── components/              # 自定义组件
│   ├── live-player/         # 直播播放器
│   ├── product-card/        # 商品卡片
│   └── chat-room/           # 聊天室
├── stores/                  # 状态管理
│   ├── live.js             # 直播状态
│   ├── user.js             # 用户状态
│   └── cart.js             # 购物车状态
├── utils/                   # 工具函数
│   ├── trtc.js             # TRTC封装
│   ├── im.js               # IM封装
│   └── request.js          # 请求封装
└── static/                  # 静态资源

二、环境配置与基础搭建

1. 创建UniApp项目

// 使用HBuilderX创建项目
// 选择默认模板 + TypeScript支持

// 或使用CLI创建
vue create -p dcloudio/uni-preset-vue live-mall

2. 安装必要依赖

// package.json 核心依赖
{
  "dependencies": {
    "@dcloudio/uni-app": "^3.0.0",
    "pinia": "^2.0.0",
    "uview-ui": "^3.0.0",
    "trtc-js-sdk": "^4.15.0",
    "tim-js-sdk": "^2.24.0"
  }
}

3. 配置文件修改

// 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
    }
  },
  "h5": {
    "devServer": {
      "https": false
    }
  },
  "mp-weixin": {
    "appid": "wxxxxxxxxxxxxxxx",
    "setting": {
      "urlCheck": false
    },
    "usingComponents": true
  }
}

三、核心功能模块实现

1. 直播播放器组件

<template>
  <view class="live-player-container">
    <!-- 微信小程序使用live-player -->
    <live-player
      v-if="isWeixin"
      :src="liveUrl"
      :mode="scaleMode"
      :autoplay="true"
      :muted="muted"
      @statechange="onStateChange"
      @fullscreenchange="onFullscreenChange"
      class="live-player"
    ></live-player>
    
    <!-- H5使用video -->
    <video
      v-else
      :src="liveUrl"
      :autoplay="true"
      :muted="muted"
      :controls="false"
      class="video-player"
      @loadedmetadata="onVideoReady"
    ></video>
    
    <!-- 播放器控制层 -->
    <view class="player-controls">
      <view class="control-item" @click="toggleMute">
        <text class="icon">{{ muted ? '🔇' : '🔊' }}</text>
      </view>
      <view class="control-item" @click="toggleFullscreen">
        <text class="icon">⛶</text>
      </view>
      <view class="control-item" @click="toggleLike">
        <text class="icon">❤️</text>
        <text class="count">{{ likeCount }}</text>
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useLiveStore } from '@/stores/live'

// 响应式数据
const props = defineProps<{
  roomId: string
  liveUrl: string
}>()

const liveStore = useLiveStore()
const muted = ref(false)
const likeCount = ref(0)
const isFullscreen = ref(false)

// 计算属性
const isWeixin = computed(() => {
  return uni.getSystemInfoSync().platform === 'weixin'
})

const scaleMode = computed(() => {
  return isFullscreen.value ? 'fillCrop' : 'contain'
})

// 生命周期
onMounted(() => {
  joinLiveRoom()
})

onUnmounted(() => {
  leaveLiveRoom()
})

// 方法定义
const joinLiveRoom = async () => {
  try {
    await liveStore.joinRoom(props.roomId)
    console.log('加入直播间成功')
  } catch (error) {
    console.error('加入直播间失败:', error)
    uni.showToast({
      title: '加入直播间失败',
      icon: 'none'
    })
  }
}

const leaveLiveRoom = () => {
  liveStore.leaveRoom(props.roomId)
}

const toggleMute = () => {
  muted.value = !muted.value
}

const toggleFullscreen = () => {
  if (isWeixin.value) {
    // 微信小程序全屏处理
    const livePlayerContext = uni.createLivePlayerContext('livePlayer')
    livePlayerContext.requestFullScreen({
      direction: 90
    })
  } else {
    // H5全屏处理
    isFullscreen.value = !isFullscreen.value
  }
}

const toggleLike = () => {
  likeCount.value++
  // 发送点赞消息到IM
  liveStore.sendLikeMessage(props.roomId)
}

const onStateChange = (event: any) => {
  console.log('直播状态变化:', event.detail.code)
}

const onFullscreenChange = (event: any) => {
  isFullscreen.value = event.detail.fullScreen
}

const onVideoReady = () => {
  console.log('视频准备就绪')
}
</script>

2. 实时聊天室组件

<template>
  <view class="chat-room">
    <!-- 消息列表 -->
    <scroll-view 
      class="message-list" 
      scroll-y 
      :scroll-top="scrollTop"
      @scrolltoupper="loadHistory"
    >
      <view 
        v-for="message in messageList" 
        :key="message.id"
        class="message-item"
        :class="getMessageClass(message)"
      >
        <!-- 系统消息 -->
        <view v-if="message.type === 'system'" class="system-message">
          <text class="content">{{ message.content }}</text>
        </view>
        
        <!-- 用户消息 -->
        <view v-else class="user-message">
          <image 
            class="avatar" 
            :src="message.avatar" 
            mode="aspectFill"
          ></image>
          <view class="message-content">
            <text class="username">{{ message.username }}</text>
            <text class="text">{{ message.content }}</text>
          </view>
        </view>
        
        <!-- 礼物消息 -->
        <view v-if="message.type === 'gift'" class="gift-message">
          <text class="gift-text">🎁 {{ message.username }} 赠送了 {{ message.giftName }}</text>
        </view>
      </view>
    </scroll-view>
    
    <!-- 输入框 -->
    <view class="input-area">
      <input 
        class="message-input"
        v-model="inputMessage"
        placeholder="说点什么..."
        @confirm="sendMessage"
      />
      <view class="action-buttons">
        <button class="btn gift-btn" @click="showGiftPanel">🎁</button>
        <button class="btn send-btn" @click="sendMessage">发送</button>
      </view>
    </view>
    
    <!-- 礼物面板 -->
    <gift-panel 
      v-if="showGift"
      :gifts="giftList"
      @select-gift="onSelectGift"
      @close="hideGiftPanel"
    />
  </view>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useIMStore } from '@/stores/im'

interface ChatMessage {
  id: string
  type: 'system' | 'user' | 'gift'
  content: string
  username?: string
  avatar?: string
  giftName?: string
  timestamp: number
}

const props = defineProps<{
  roomId: string
}>()

const imStore = useIMStore()
const inputMessage = ref('')
const messageList = ref<ChatMessage[]>([])
const scrollTop = ref(0)
const showGift = ref(false)

// 礼物列表
const giftList = ref([
  { id: '1', name: '玫瑰花', price: 1, icon: '🌹' },
  { id: '2', name: '跑车', price: 50, icon: '🚗' },
  { id: '3', name: '游艇', price: 100, icon: '🚤' },
  { id: '4', name: '火箭', price: 500, icon: '🚀' }
])

// 生命周期
onMounted(() => {
  initChatRoom()
})

onUnmounted(() => {
  destroyChatRoom()
})

// 方法定义
const initChatRoom = async () => {
  await imStore.joinGroup(props.roomId)
  // 监听新消息
  imStore.onMessageReceived(handleNewMessage)
}

const destroyChatRoom = () => {
  imStore.leaveGroup(props.roomId)
  imStore.offMessageReceived(handleNewMessage)
}

const handleNewMessage = (message: any) => {
  const chatMessage: ChatMessage = {
    id: message.id,
    type: message.type,
    content: message.content,
    username: message.nick,
    avatar: message.avatar,
    timestamp: Date.now()
  }
  
  messageList.value.push(chatMessage)
  scrollToBottom()
}

const sendMessage = async () => {
  if (!inputMessage.value.trim()) return
  
  try {
    await imStore.sendTextMessage(props.roomId, inputMessage.value)
    inputMessage.value = ''
  } catch (error) {
    console.error('发送消息失败:', error)
    uni.showToast({
      title: '发送失败',
      icon: 'none'
    })
  }
}

const showGiftPanel = () => {
  showGift.value = true
}

const hideGiftPanel = () => {
  showGift.value = false
}

const onSelectGift = async (gift: any) => {
  try {
    await imStore.sendGiftMessage(props.roomId, gift)
    hideGiftPanel()
  } catch (error) {
    console.error('赠送礼物失败:', error)
  }
}

const loadHistory = async () => {
  // 加载历史消息
  const history = await imStore.getHistoryMessages(props.roomId)
  messageList.value = [...history, ...messageList.value]
}

const scrollToBottom = () => {
  // 滚动到底部
  setTimeout(() => {
    scrollTop.value = 99999
  }, 100)
}

const getMessageClass = (message: ChatMessage) => {
  return {
    'system-message': message.type === 'system',
    'user-message': message.type === 'user',
    'gift-message': message.type === 'gift'
  }
}
</script>

四、状态管理与数据流

1. Pinia状态管理配置

// stores/live.js
import { defineStore } from 'pinia'

export const useLiveStore = defineStore('live', {
  state: () => ({
    currentRoom: null,
    roomList: [],
    liveStatus: 'idle', // idle, loading, playing, error
    viewerCount: 0,
    likeCount: 0
  }),
  
  getters: {
    isPlaying: (state) => state.liveStatus === 'playing',
    isHost: (state) => state.currentRoom?.isHost,
    roomInfo: (state) => state.currentRoom
  },
  
  actions: {
    async joinRoom(roomId) {
      try {
        this.liveStatus = 'loading'
        
        // 调用API加入直播间
        const response = await uni.request({
          url: `/api/live/rooms/${roomId}/join`,
          method: 'POST'
        })
        
        this.currentRoom = response.data
        this.liveStatus = 'playing'
        
        // 更新观看人数
        this.updateViewerCount()
        
      } catch (error) {
        this.liveStatus = 'error'
        throw error
      }
    },
    
    async leaveRoom(roomId) {
      try {
        await uni.request({
          url: `/api/live/rooms/${roomId}/leave`,
          method: 'POST'
        })
        
        this.currentRoom = null
        this.liveStatus = 'idle'
      } catch (error) {
        console.error('离开直播间失败:', error)
      }
    },
    
    async sendLikeMessage(roomId) {
      await uni.request({
        url: `/api/live/rooms/${roomId}/like`,
        method: 'POST'
      })
      this.likeCount++
    },
    
    updateViewerCount() {
      // 定时更新观看人数
      setInterval(async () => {
        const response = await uni.request({
          url: `/api/live/rooms/${this.currentRoom.id}/viewers`
        })
        this.viewerCount = response.data.count
      }, 5000)
    }
  }
})

五、商品展示与购买流程

1. 商品卡片组件

<template>
  <view class="product-card" @click="goToDetail">
    <image 
      class="product-image" 
      :src="product.coverImage" 
      mode="aspectFill"
      lazy-load
    ></image>
    
    <view class="product-info">
      <text class="product-name">{{ product.name }}</text>
      <text class="product-desc">{{ product.description }}</text>
      
      <view class="price-section">
        <text class="current-price">¥{{ product.price }}</text>
        <text v-if="product.originalPrice" class="original-price">
          ¥{{ product.originalPrice }}
        </text>
        <text class="sales">已售{{ product.sales }}件</text>
      </view>
      
      <view class="action-buttons">
        <button 
          class="btn cart-btn" 
          @click.stop="addToCart"
          :disabled="!product.stock"
        >
          {{ product.stock ? '加入购物车' : '已售罄' }}
        </button>
        <button 
          class="btn buy-btn" 
          @click.stop="buyNow"
          :disabled="!product.stock"
        >
          立即购买
        </button>
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useCartStore } from '@/stores/cart'

interface Product {
  id: string
  name: string
  description: string
  coverImage: string
  price: number
  originalPrice?: number
  sales: number
  stock: number
}

const props = defineProps<{
  product: Product
}>()

const cartStore = useCartStore()

const goToDetail = () => {
  uni.navigateTo({
    url: `/pages/product/detail?id=${props.product.id}`
  })
}

const addToCart = async () => {
  try {
    await cartStore.addToCart(props.product)
    uni.showToast({
      title: '添加成功',
      icon: 'success'
    })
  } catch (error) {
    uni.showToast({
      title: '添加失败',
      icon: 'none'
    })
  }
}

const buyNow = () => {
  uni.navigateTo({
    url: `/pages/order/confirm?productId=${props.product.id}&quantity=1`
  })
}
</script>

六、性能优化与多端适配

1. 图片懒加载优化

// utils/image.js
export const lazyLoadImage = (imgUrl: string): Promise<string> => {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = () => resolve(imgUrl)
    img.onerror = reject
    img.src = imgUrl
  })
}

// 图片预加载
export const preloadImages = (urls: string[]) => {
  return Promise.all(urls.map(url => lazyLoadImage(url)))
}

// 组件中使用
const loadProductImages = async (products: Product[]) => {
  const imageUrls = products.map(p => p.coverImage)
  await preloadImages(imageUrls)
}

2. 条件编译处理多端差异

// 平台特定代码处理
export const getPlatformSpecificConfig = () => {
  // #ifdef MP-WEIXIN
  return {
    livePlayer: 'live-player',
    payment: 'wxpay'
  }
  // #endif
  
  // #ifdef H5
  return {
    livePlayer: 'video',
    payment: 'alipay'
  }
  // #endif
  
  // #ifdef APP-PLUS
  return {
    livePlayer: 'live-player',
    payment: 'applepay'
  }
  // #endif
}

// 统一支付接口
export const unifiedPayment = (orderInfo: any) => {
  const config = getPlatformSpecificConfig()
  
  // #ifdef MP-WEIXIN
  return uni.requestPayment({
    provider: 'wxpay',
    orderInfo: orderInfo
  })
  // #endif
  
  // #ifdef H5
  return uni.requestPayment({
    provider: 'alipay',
    orderInfo: orderInfo
  })
  // #endif
}

七、部署与发布

1. 多端发布配置

// 微信小程序发布配置
// project.config.json
{
  "description": "直播电商小程序",
  "packOptions": {
    "ignore": [
      {
        "type": "file",
        "value": ".eslintrc.js"
      }
    ]
  },
  "setting": {
    "urlCheck": false,
    "es6": true,
    "enhance": true,
    "postcss": true,
    "preloadBackgroundData": false,
    "minified": true,
    "newFeature": false,
    "coverView": true,
    "nodeModules": false,
    "autoAudits": false,
    "showShadowRootInWxmlPanel": true,
    "scopeDataCheck": false,
    "uglifyFileName": false,
    "checkInvalidKey": true,
    "checkSiteMap": true,
    "uploadWithSourceMap": true,
    "compileHotReLoad": false,
    "useMultiFrameRuntime": true,
    "useApiHook": true,
    "babelSetting": {
      "ignore": [],
      "disablePlugins": [],
      "outputPath": ""
    },
    "enableEngineNative": false,
    "bundle": false
  }
}

八、监控与数据分析

1. 用户行为追踪

// utils/analytics.js
export const trackEvent = (eventName: string, params?: any) => {
  const platform = uni.getSystemInfoSync().platform
  const eventData = {
    event: eventName,
    platform,
    timestamp: Date.now(),
    ...params
  }
  
  // 发送到数据分析平台
  uni.request({
    url: '/api/analytics/track',
    method: 'POST',
    data: eventData
  })
}

// 常用事件追踪
export const AnalyticsEvents = {
  LIVE_VIEW: 'live_view',
  PRODUCT_CLICK: 'product_click',
  ADD_TO_CART: 'add_to_cart',
  PURCHASE: 'purchase',
  SHARE: 'share'
}

// 在组件中使用
const trackLiveView = (roomId: string) => {
  trackEvent(AnalyticsEvents.LIVE_VIEW, {
    room_id: roomId,
    duration: 0
  })
}

总结

本文详细介绍了使用UniApp开发跨平台直播电商应用的完整流程,从项目架构设计到具体功能实现,涵盖了直播播放、实时聊天、商品交易等核心模块。通过合理的组件化设计和状态管理,实现了代码的复用性和可维护性。

关键技术亮点:

  • 多端兼容:通过条件编译实现不同平台的差异化处理
  • 实时交互:集成TRTC和IM实现高质量的直播和聊天体验
  • 性能优化:图片懒加载、组件懒加载等优化手段提升用户体验
  • 状态管理:使用Pinia实现清晰的数据流管理
  • 类型安全:TypeScript提供更好的开发体验和代码质量

本方案已在实际项目中得到验证,能够稳定支撑高并发场景下的直播电商业务需求。开发者可以根据具体业务需求,在此基础上进行功能扩展和优化。

UniApp跨平台直播电商应用开发实战 | 完整项目架构与实现
收藏 (0) 打赏

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

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

淘吗网 uniapp UniApp跨平台直播电商应用开发实战 | 完整项目架构与实现 https://www.taomawang.com/web/uniapp/1427.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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