引言:直播电商的技术挑战与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提供更好的开发体验和代码质量
本方案已在实际项目中得到验证,能够稳定支撑高并发场景下的直播电商业务需求。开发者可以根据具体业务需求,在此基础上进行功能扩展和优化。

