前言:短劇小程序的爆發與機遇
在數字娛樂消費升級的背景下,短劇市場正以驚人的速度增長。據統計,2024年短劇市場規模預計將達到1000億元,而小程序作為輕量級入口,已成為短劇分發的重要渠道。本文將從零開始,手把手帶你完成短劇小程序的開發、部署到上架全流程,即使你沒有任何開發經驗,也能跟隨本文一步步實現自己的短劇小程序。
源碼及演示:v.dyedus.top
開發環境搭建
1.1 基礎工具準備
必需軟件清單:
- 微信開發者工具 - 小程序開發IDE
- Visual Studio Code - 代碼編輯器
- Node.js - JavaScript運行環境
- Git - 版本管理工具
安裝驗證:
# 檢查Node.js安裝
node --version
# 應顯示 v16.0.0 或更高版本
# 檢查npm安裝
npm --version
# 應顯示 8.0.0 或更高版本
# 檢查Git安裝
git --version
# 應顯示 git version 2.x.x
1.2 小程序賬號註冊
- 訪問微信公眾平台
- 註冊小程序賬號(選擇"小程序",非公眾號)
- 完成主體認證(個人或企業)
- 獲取AppID(後續開發需要)
項目初始化與結構解析
1. 創建小程序項目
在微信開發者工具中:
- 點擊"新建項目"
- 填寫項目信息:
- 項目名稱:ShortDrama
- 目錄:選擇項目存放位置
- AppID:填寫註冊的小程序AppID
- 後端服務:選擇"不使用雲服務"
- 點擊"新建"
2. 項目結構詳解
short-drama-miniprogram/ # 項目根目錄
├── pages/ # 頁面目錄
│ ├── index/ # 首頁
│ │ ├── index.js # 頁面邏輯
│ │ ├── index.json # 頁面配置
│ │ ├── index.wxml # 頁面結構
│ │ └── index.wxss # 頁面樣式
│ ├── detail/ # 詳情頁
│ ├── search/ # 搜索頁
│ └── mine/ # 我的頁面
├── components/ # 自定義組件
│ ├── drama-card/ # 短劇卡片
│ ├── video-player/ # 視頻播放器
│ └── loading/ # 加載組件
├── utils/ # 工具函數
│ ├── request.js # 網絡請求
│ ├── auth.js # 用户認證
│ ├── cache.js # 緩存管理
│ └── util.js # 通用工具
├── assets/ # 靜態資源
│ ├── images/ # 圖片資源
│ └── icons/ # 圖標資源
├── app.js # 小程序邏輯
├── app.json # 全局配置
├── app.wxss # 全局樣式
└── project.config.json # 項目配置
核心代碼實現
1. 全局配置文件
// app.json - 小程序全局配置
{
"pages": [
"pages/index/index",
"pages/detail/detail",
"pages/search/search",
"pages/play/play",
"pages/mine/mine",
"pages/history/history"
],
"window": {
"navigationBarTitleText": "短劇熱播",
"navigationBarBackgroundColor": "#FF2D55",
"navigationBarTextStyle": "white",
"backgroundColor": "#F5F5F5",
"backgroundTextStyle": "dark",
"enablePullDownRefresh": true
},
"tabBar": {
"color": "#999999",
"selectedColor": "#FF2D55",
"backgroundColor": "#FFFFFF",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首頁",
"iconPath": "assets/icons/home.png",
"selectedIconPath": "assets/icons/home-active.png"
},
{
"pagePath": "pages/search/search",
"text": "搜索",
"iconPath": "assets/icons/search.png",
"selectedIconPath": "assets/icons/search-active.png"
},
{
"pagePath": "pages/mine/mine",
"text": "我的",
"iconPath": "assets/icons/mine.png",
"selectedIconPath": "assets/icons/mine-active.png"
}
]
},
"requiredBackgroundModes": ["audio", "location"],
"permission": {
"scope.userLocation": {
"desc": "你的位置信息將用於推薦附近的短劇"
}
},
"networkTimeout": {
"request": 10000,
"connectSocket": 10000,
"uploadFile": 10000,
"downloadFile": 10000
}
}
2. 網絡請求封裝
// utils/request.js
const BASE_URL = 'https://your-api-domain.com/api/v1';
// 請求攔截器
const requestInterceptor = (options) => {
// 添加token
const token = wx.getStorageSync('token');
if (token) {
options.header = {
...options.header,
'Authorization': `Bearer ${token}`
};
}
// 添加時間戳防止緩存
if (options.method === 'GET') {
options.url += `${options.url.includes('?') ? '&' : '?'}_t=${Date.now()}`;
}
return options;
};
// 響應攔截器
const responseInterceptor = (response) => {
const { statusCode, data } = response;
if (statusCode === 401) {
// token過期,跳轉到登錄
wx.showToast({
title: '登錄已過期',
icon: 'none'
});
setTimeout(() => {
wx.navigateTo({
url: '/pages/login/login'
});
}, 1500);
return Promise.reject(new Error('未授權'));
}
if (statusCode === 403) {
wx.showToast({
title: '無權限訪問',
icon: 'none'
});
return Promise.reject(new Error('無權限'));
}
if (statusCode >= 500) {
wx.showToast({
title: '服務器錯誤',
icon: 'none'
});
return Promise.reject(new Error('服務器錯誤'));
}
return data;
};
// 通用請求方法
const request = (options) => {
return new Promise((resolve, reject) => {
const processedOptions = requestInterceptor(options);
wx.request({
url: BASE_URL + processedOptions.url,
method: processedOptions.method || 'GET',
data: processedOptions.data,
header: {
'Content-Type': 'application/json',
...processedOptions.header
},
success: (res) => {
try {
const data = responseInterceptor(res);
resolve(data);
} catch (error) {
reject(error);
}
},
fail: (error) => {
wx.showToast({
title: '網絡連接失敗',
icon: 'none'
});
reject(error);
},
complete: () => {
// 可在此處隱藏加載提示
}
});
});
};
// 快捷方法
const http = {
get: (url, data = {}) => request({ url, method: 'GET', data }),
post: (url, data = {}) => request({ url, method: 'POST', data }),
put: (url, data = {}) => request({ url, method: 'PUT', data }),
delete: (url, data = {}) => request({ url, method: 'DELETE', data }),
upload: (url, filePath, formData = {}) => {
return new Promise((resolve, reject) => {
wx.uploadFile({
url: BASE_URL + url,
filePath,
name: 'file',
formData,
success: resolve,
fail: reject
});
});
}
};
export default http;
3. 首頁實現
// pages/index/index.js
import { getBanners, getDramaList, getCategories } from '../../api/drama';
Page({
data: {
banners: [], // 輪播圖
categories: [], // 分類
dramas: [], // 短劇列表
activeCategory: 0, // 當前選中分類
page: 1, // 當前頁碼
pageSize: 10, // 每頁數量
hasMore: true, // 是否有更多數據
loading: false, // 是否加載中
refreshing: false // 是否下拉刷新中
},
onLoad() {
this.loadHomeData();
},
onPullDownRefresh() {
// 下拉刷新
this.setData({ refreshing: true });
this.refreshData();
},
onReachBottom() {
// 上拉加載更多
if (this.data.hasMore && !this.data.loading) {
this.loadMoreData();
}
},
// 加載首頁數據
async loadHomeData() {
wx.showLoading({ title: '加載中...' });
try {
const [banners, categories, dramas] = await Promise.all([
getBanners(),
getCategories(),
this.loadDramaList(1)
]);
this.setData({
banners,
categories,
dramas: dramas.data,
hasMore: dramas.has_more
});
} catch (error) {
wx.showToast({
title: '加載失敗',
icon: 'error'
});
} finally {
wx.hideLoading();
}
},
// 加載短劇列表
async loadDramaList(page) {
const { activeCategory, pageSize } = this.data;
return getDramaList({
category_id: activeCategory,
page,
page_size: pageSize
});
},
// 刷新數據
async refreshData() {
this.setData({ page: 1 });
try {
const dramas = await this.loadDramaList(1);
this.setData({
dramas: dramas.data,
hasMore: dramas.has_more,
refreshing: false
});
wx.stopPullDownRefresh();
wx.showToast({ title: '刷新成功', icon: 'success' });
} catch (error) {
this.setData({ refreshing: false });
wx.stopPullDownRefresh();
}
},
// 加載更多
async loadMoreData() {
if (!this.data.hasMore || this.data.loading) return;
this.setData({ loading: true });
try {
const nextPage = this.data.page + 1;
const dramas = await this.loadDramaList(nextPage);
this.setData({
dramas: [...this.data.dramas, ...dramas.data],
page: nextPage,
hasMore: dramas.has_more,
loading: false
});
} catch (error) {
this.setData({ loading: false });
wx.showToast({ title: '加載失敗', icon: 'none' });
}
},
// 切換分類
async switchCategory(e) {
const categoryId = e.currentTarget.dataset.id;
if (categoryId === this.data.activeCategory) return;
this.setData({
activeCategory: categoryId,
dramas: [],
page: 1,
hasMore: true
});
wx.showLoading({ title: '切換中...' });
try {
const dramas = await this.loadDramaList(1);
this.setData({
dramas: dramas.data,
hasMore: dramas.has_more
});
} catch (error) {
wx.showToast({ title: '加載失敗', icon: 'none' });
} finally {
wx.hideLoading();
}
},
// 跳轉到詳情頁
goToDetail(e) {
const dramaId = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/detail/detail?id=${dramaId}`
});
}
});
<!-- pages/index/index.wxml -->
<view class="page-container">
<!-- 輪播圖 -->
<swiper
class="banner-swiper"
indicator-dots
autoplay
interval="3000"
duration="500"
>
<block wx:for="{{banners}}" wx:key="id">
<swiper-item>
<image
src="{{item.image_url}}"
mode="aspectFill"
class="banner-image"
bindtap="onBannerTap"
data-url="{{item.jump_url}}"
/>
</swiper-item>
</block>
</swiper>
<!-- 分類標籤 -->
<scroll-view
class="category-scroll"
scroll-x
scroll-with-animation
>
<view
class="category-item {{activeCategory === 0 ? 'active' : ''}}"
bindtap="switchCategory"
data-id="0"
>
全部
</view>
<block wx:for="{{categories}}" wx:key="id">
<view
class="category-item {{activeCategory === item.id ? 'active' : ''}}"
bindtap="switchCategory"
data-id="{{item.id}}"
>
{{item.name}}
</view>
</block>
</scroll-view>
<!-- 短劇列表 -->
<view class="drama-list">
<block wx:for="{{dramas}}" wx:key="id">
<drama-card
drama="{{item}}"
bindtap="goToDetail"
data-id="{{item.id}}"
/>
</block>
</view>
<!-- 加載狀態 -->
<view class="loading-container" wx:if="{{loading}}">
<view class="loading-text">加載中...</view>
</view>
<view class="no-more" wx:if="{{!hasMore && dramas.length > 0}}">
<text>沒有更多了</text>
</view>
<!-- 空狀態 -->
<view class="empty-state" wx:if="{{dramas.length === 0 && !loading}}">
<image src="/assets/images/empty.png" class="empty-image" />
<view class="empty-text">暫無短劇內容</view>
</view>
<!-- 回到頂部 -->
<view
class="back-to-top {{showBackTop ? 'show' : ''}}"
bindtap="scrollToTop"
wx:if="{{scrollTop > 300}}"
>
<image src="/assets/icons/top.png" class="back-top-icon" />
</view>
</view>
4. 視頻播放器組件
// components/video-player/video-player.js
Component({
properties: {
// 視頻源
src: {
type: String,
value: ''
},
// 封面圖
poster: {
type: String,
value: ''
},
// 是否自動播放
autoplay: {
type: Boolean,
value: false
},
// 是否循環播放
loop: {
type: Boolean,
value: false
},
// 初始播放位置(秒)
startTime: {
type: Number,
value: 0
}
},
data: {
isPlaying: false, // 是否播放中
currentTime: 0, // 當前播放時間
duration: 0, // 視頻總時長
showControls: true, // 是否顯示控制欄
controlsTimer: null, // 控制欄隱藏計時器
progress: 0, // 播放進度百分比
isFullscreen: false, // 是否全屏
playbackRate: 1.0 // 播放速度
},
lifetimes: {
ready() {
this.videoContext = wx.createVideoContext('shortVideo', this);
if (this.data.autoplay) {
this.play();
}
// 初始化控制欄隱藏計時器
this.resetControlsTimer();
},
detached() {
this.pause();
if (this.data.controlsTimer) {
clearTimeout(this.data.controlsTimer);
}
}
},
methods: {
// 播放/暫停
togglePlay() {
if (this.data.isPlaying) {
this.pause();
} else {
this.play();
}
},
// 播放視頻
play() {
this.videoContext.play();
this.setData({ isPlaying: true });
this.resetControlsTimer();
},
// 暫停視頻
pause() {
this.videoContext.pause();
this.setData({ isPlaying: false });
},
// 切換全屏
toggleFullscreen() {
if (this.data.isFullscreen) {
this.videoContext.exitFullScreen();
} else {
this.videoContext.requestFullScreen({
direction: 90 // 橫屏
});
}
},
// 跳轉到指定時間
seek(e) {
const { x, width } = e.currentTarget.getBoundingClientRect();
const touchX = e.touches[0].clientX;
const percent = (touchX - x) / width;
const time = percent * this.data.duration;
this.videoContext.seek(time);
this.setData({ currentTime: time });
},
// 切換播放速度
changePlaybackRate() {
const rates = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
const currentIndex = rates.indexOf(this.data.playbackRate);
const nextIndex = (currentIndex + 1) % rates.length;
const nextRate = rates[nextIndex];
this.videoContext.playbackRate(nextRate);
this.setData({ playbackRate: nextRate });
wx.showToast({
title: `播放速度 ${nextRate}x`,
icon: 'none',
duration: 1000
});
},
// 重置控制欄隱藏計時器
resetControlsTimer() {
if (this.data.controlsTimer) {
clearTimeout(this.data.controlsTimer);
}
const timer = setTimeout(() => {
if (this.data.isPlaying) {
this.setData({ showControls: false });
}
}, 3000);
this.setData({ controlsTimer: timer });
},
// 顯示控制欄
showControls() {
this.setData({ showControls: true });
this.resetControlsTimer();
},
// 視頻播放時間更新
onTimeUpdate(e) {
const { currentTime, duration } = e.detail;
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
this.setData({
currentTime,
duration,
progress
});
// 每5秒上報播放進度
if (Math.floor(currentTime) % 5 === 0) {
this.reportPlayProgress(currentTime);
}
},
// 上報播放進度
reportPlayProgress(currentTime) {
const { videoId } = this.data;
if (!videoId) return;
// 調用API上報播放記錄
wx.request({
url: 'https://your-api.com/api/play/progress',
method: 'POST',
data: {
video_id: videoId,
current_time: currentTime
}
});
},
// 視頻播放結束
onVideoEnded() {
this.setData({ isPlaying: false });
this.triggerEvent('ended');
},
// 全屏變化
onFullscreenChange(e) {
this.setData({ isFullscreen: e.detail.fullScreen });
}
}
});
用户系統實現
微信登錄集成
// utils/auth.js
import http from './request';
// 檢查登錄狀態
const checkLogin = () => {
const token = wx.getStorageSync('token');
const userInfo = wx.getStorageSync('userInfo');
return !!(token && userInfo);
};
// 微信登錄
const wechatLogin = () => {
return new Promise((resolve, reject) => {
// 檢查session是否過期
wx.checkSession({
success: () => {
// session_key 未過期
const userInfo = wx.getStorageSync('userInfo');
if (userInfo) {
resolve(userInfo);
} else {
// 需要重新登錄
loginAndGetUserInfo().then(resolve).catch(reject);
}
},
fail: () => {
// session_key 已過期,重新登錄
loginAndGetUserInfo().then(resolve).catch(reject);
}
});
});
};
// 登錄並獲取用户信息
const loginAndGetUserInfo = () => {
return new Promise((resolve, reject) => {
// 1. 獲取code
wx.login({
success: (loginRes) => {
if (loginRes.code) {
// 2. 獲取用户信息
wx.getUserProfile({
desc: '用於完善用户資料',
success: (profileRes) => {
const userInfo = profileRes.userInfo;
// 3. 發送到後端換取token
http.post('/auth/wechat-login', {
code: loginRes.code,
user_info: userInfo
}).then((response) => {
// 保存token和用户信息
wx.setStorageSync('token', response.token);
wx.setStorageSync('userInfo', response.user);
// 更新全局用户信息
getApp().globalData.userInfo = response.user;
getApp().globalData.isLoggedIn = true;
resolve(response.user);
}).catch(reject);
},
fail: reject
});
} else {
reject(new Error('登錄失敗:' + loginRes.errMsg));
}
},
fail: reject
});
});
};
// 退出登錄
const logout = () => {
return new Promise((resolve) => {
// 清除本地存儲
wx.removeStorageSync('token');
wx.removeStorageSync('userInfo');
// 清除全局數據
getApp().globalData.userInfo = null;
getApp().globalData.isLoggedIn = false;
// 調用後端登出接口
http.post('/auth/logout').finally(() => {
resolve();
});
});
};
// 獲取用户信息
const getUserInfo = () => {
return new Promise((resolve, reject) => {
if (checkLogin()) {
const userInfo = wx.getStorageSync('userInfo');
resolve(userInfo);
} else {
wechatLogin().then(resolve).catch(reject);
}
});
};
// 檢查VIP狀態
const checkVIPStatus = () => {
return new Promise((resolve, reject) => {
getUserInfo().then((userInfo) => {
const now = Date.now();
const vipExpire = new Date(userInfo.vip_expire_at).getTime();
resolve({
is_vip: vipExpire > now,
expire_at: userInfo.vip_expire_at,
level: userInfo.vip_level
});
}).catch(reject);
});
};
export default {
checkLogin,
wechatLogin,
logout,
getUserInfo,
checkVIPStatus
};
支付功能集成
微信支付封裝
// utils/payment.js
import http from './request';
// 創建支付訂單
const createPayment = (orderData) => {
return new Promise((resolve, reject) => {
// 1. 調用後端創建支付訂單
http.post('/payment/create', orderData)
.then((orderInfo) => {
// 2. 調用微信支付
wx.requestPayment({
timeStamp: orderInfo.timeStamp,
nonceStr: orderInfo.nonceStr,
package: orderInfo.package,
signType: 'MD5',
paySign: orderInfo.paySign,
success: (res) => {
// 3. 支付成功,驗證訂單
verifyPayment(orderInfo.order_id)
.then(resolve)
.catch(reject);
},
fail: (err) => {
reject(new Error('支付失敗: ' + JSON.stringify(err)));
}
});
})
.catch(reject);
});
};
// 驗證支付結果
const verifyPayment = (orderId) => {
return new Promise((resolve, reject) => {
// 輪詢驗證支付結果
let retryCount = 0;
const maxRetries = 10;
const retryInterval = 2000; // 2秒
const checkPayment = () => {
http.get(`/payment/verify/${orderId}`)
.then((result) => {
if (result.status === 'success') {
resolve(result);
} else if (result.status === 'pending' && retryCount < maxRetries) {
retryCount++;
setTimeout(checkPayment, retryInterval);
} else {
reject(new Error('支付驗證失敗'));
}
})
.catch(reject);
};
checkPayment();
});
};
// 購買VIP
const purchaseVIP = (vipPlanId) => {
return new Promise((resolve, reject) => {
wx.showLoading({ title: '下單中...' });
// 獲取用户信息
const userInfo = wx.getStorageSync('userInfo');
// 創建VIP購買訂單
createPayment({
product_type: 'vip',
product_id: vipPlanId,
user_id: userInfo.id,
amount: 0, // 後端會計算實際金額
description: '購買VIP會員'
})
.then((result) => {
wx.hideLoading();
wx.showToast({
title: '購買成功',
icon: 'success',
duration: 2000
});
resolve(result);
})
.catch((error) => {
wx.hideLoading();
wx.showToast({
title: '購買失敗: ' + error.message,
icon: 'none',
duration: 3000
});
reject(error);
});
});
};
// 解鎖單集
const unlockEpisode = (episodeId) => {
return new Promise((resolve, reject) => {
wx.showModal({
title: '確認解鎖',
content: '此劇集需要付費觀看,是否解鎖?',
success: (res) => {
if (res.confirm) {
wx.showLoading({ title: '解鎖中...' });
const userInfo = wx.getStorageSync('userInfo');
createPayment({
product_type: 'episode',
product_id: episodeId,
user_id: userInfo.id,
amount: 0, // 後端計算
description: '解鎖劇集'
})
.then((result) => {
wx.hideLoading();
wx.showToast({
title: '解鎖成功',
icon: 'success'
});
resolve(result);
})
.catch((error) => {
wx.hideLoading();
reject(error);
});
} else {
reject(new Error('用户取消'));
}
}
});
});
};
export default {
createPayment,
verifyPayment,
purchaseVIP,
unlockEpisode
};
數據統計與埋點
數據埋點實現
// utils/analytics.js
// 事件類型常量
const EVENT_TYPES = {
PAGE_VIEW: 'page_view', // 頁面瀏覽
BUTTON_CLICK: 'button_click', // 按鈕點擊
VIDEO_PLAY: 'video_play', // 視頻播放
VIDEO_PAUSE: 'video_pause', // 視頻暫停
VIDEO_END: 'video_end', // 視頻結束
VIDEO_PROGRESS: 'video_progress', // 視頻進度
SHARE: 'share', // 分享
PAYMENT: 'payment', // 支付
LOGIN: 'login' // 登錄
};
// 埋點數據
let eventQueue = [];
let isSending = false;
const MAX_RETRY = 3;
const BATCH_SIZE = 10;
// 上報事件
const trackEvent = (eventName, properties = {}) => {
const event = {
event_name: eventName,
properties: {
...properties,
timestamp: Date.now(),
platform: 'wechat_miniprogram',
app_version: getApp().globalData.version,
user_id: getApp().globalData.userInfo?.id || 'anonymous',
session_id: getSessionId(),
page_path: getCurrentPagePath()
}
};
// 添加到隊列
eventQueue.push(event);
// 達到批量大小或重要事件立即上報
if (eventQueue.length >= BATCH_SIZE || isImportantEvent(eventName)) {
sendEvents();
}
// 本地存儲
saveEventToStorage(event);
};
// 判斷是否為重要事件
const isImportantEvent = (eventName) => {
const importantEvents = [
EVENT_TYPES.PAYMENT,
EVENT_TYPES.LOGIN,
EVENT_TYPES.SHARE
];
return importantEvents.includes(eventName);
};
// 發送事件到服務器
const sendEvents = (retryCount = 0) => {
if (eventQueue.length === 0 || isSending) return;
isSending = true;
const eventsToSend = [...eventQueue];
eventQueue = [];
wx.request({
url: 'https://your-analytics-api.com/events',
method: 'POST',
data: {
events: eventsToSend,
device_id: getDeviceId()
},
success: () => {
// 發送成功,清除本地存儲
clearEventsFromStorage(eventsToSend);
isSending = false;
// 檢查是否還有事件需要發送
if (eventQueue.length > 0) {
setTimeout(sendEvents, 1000);
}
},
fail: () => {
// 發送失敗,重試
if (retryCount < MAX_RETRY) {
setTimeout(() => sendEvents(retryCount + 1), 3000);
} else {
// 重試次數用完,放回隊列
eventQueue = [...eventsToSend, ...eventQueue];
isSending = false;
}
}
});
};
// 獲取設備ID
const getDeviceId = () => {
let deviceId = wx.getStorageSync('device_id');
if (!deviceId) {
deviceId = 'device_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
wx.setStorageSync('device_id', deviceId);
}
return deviceId;
};
// 獲取會話ID
const getSessionId = () => {
let sessionInfo = wx.getStorageSync('session_info') || {};
const now = Date.now();
// 會話超時(30分鐘)
if (!sessionInfo.id || now - sessionInfo.timestamp > 30 * 60 * 1000) {
sessionInfo = {
id: 'session_' + now + '_' + Math.random().toString(36).substr(2, 9),
timestamp: now
};
wx.setStorageSync('session_info', sessionInfo);
}
return sessionInfo.id;
};
// 獲取當前頁面路徑
const getCurrentPagePath = () => {
const pages = getCurrentPages();
if (pages.length > 0) {
return pages[pages.length - 1].route;
}
return '';
};
// 保存事件到本地存儲
const saveEventToStorage = (event) => {
let storedEvents = wx.getStorageSync('pending_events') || [];
storedEvents.push(event);
// 限制存儲數量
if (storedEvents.length > 100) {
storedEvents = storedEvents.slice(-100);
}
wx.setStorageSync('pending_events', storedEvents);
};
// 從本地存儲清除已發送事件
const clearEventsFromStorage = (sentEvents) => {
const sentIds = sentEvents.map(e => e.properties.timestamp);
let storedEvents = wx.getStorageSync('pending_events') || [];
storedEvents = storedEvents.filter(event =>
!sentIds.includes(event.properties.timestamp)
);
wx.setStorageSync('pending_events', storedEvents);
};
// 發送未上報的事件
const sendPendingEvents = () => {
const pendingEvents = wx.getStorageSync('pending_events') || [];
if (pendingEvents.length > 0) {
eventQueue = [...pendingEvents, ...eventQueue];
sendEvents();
}
};
// 頁面瀏覽事件
const trackPageView = (pageName, properties = {}) => {
trackEvent(EVENT_TYPES.PAGE_VIEW, {
page_name: pageName,
...properties
});
};
// 按鈕點擊事件
const trackButtonClick = (buttonName, properties = {}) => {
trackEvent(EVENT_TYPES.BUTTON_CLICK, {
button_name: buttonName,
...properties
});
};
// 視頻播放事件
const trackVideoPlay = (videoId, properties = {}) => {
trackEvent(EVENT_TYPES.VIDEO_PLAY, {
video_id: videoId,
...properties
});
};
// 初始化
const initAnalytics = () => {
// 定期發送事件
setInterval(sendEvents, 30000); // 30秒發送一次
// 發送未上報的事件
sendPendingEvents();
// 監聽小程序生命週期
wx.onAppHide(() => {
sendEvents(); // 切換到後台時立即發送
});
};
export default {
EVENT_TYPES,
trackEvent,
trackPageView,
trackButtonClick,
trackVideoPlay,
initAnalytics
};
測試與調試
1. 單元測試示例
// tests/utils/request.test.js
const request = require('../../utils/request.js');
describe('Request Utils', () => {
beforeEach(() => {
// 模擬wx.request
global.wx = {
request: jest.fn(),
showToast: jest.fn(),
getStorageSync: jest.fn()
};
});
test('should add token to header when token exists', () => {
wx.getStorageSync.mockReturnValue('test-token');
request.get('/test');
expect(wx.request).toHaveBeenCalledWith(
expect.objectContaining({
header: expect.objectContaining({
Authorization: 'Bearer test-token'
})
})
);
});
test('should handle network error', async () => {
wx.request.mockImplementation(({ fail }) => {
fail && fail({ errMsg: 'request:fail' });
});
await expect(request.get('/test')).rejects.toThrow('網絡連接失敗');
});
});
2. 真機調試技巧
- 開啓調試模式:
// app.js
App({
onLaunch() {
// 開發環境開啓調試
if (__DEV__) {
wx.setEnableDebug({
enableDebug: true
});
}
}
});
- 性能監控:
// 監控頁面性能
wx.reportPerformance(1101, Date.now() - startTime, 'page_load');
- 錯誤監控:
// 錯誤上報
wx.onError((error) => {
console.error('小程序錯誤:', error);
// 上報到監控系統
wx.request({
url: 'https://your-error-log.com/report',
method: 'POST',
data: error
});
});
上架發佈流程
1. 代碼上傳
- 在微信開發者工具中:
- 點擊"上傳"按鈕
- 填寫版本號和項目備註
- 點擊"上傳"
- 命令行上傳(可選):
# 安裝miniprogram-ci
npm install -g miniprogram-ci
# 上傳代碼
miniprogram-ci upload \
--pp ./project-path \
--pkp ./private.key \
--appid wx1234567890 \
--uv 1.0.0 \
--rd "版本描述"
2. 提交審核
- 登錄微信公眾平台
- 進入"管理" -> "版本管理"
- 在"開發版本"中找到上傳的版本
- 點擊"提交審核"
- 填寫審核信息:
- 服務類目:文娛-視頻
- 標籤:短劇、視頻、娛樂
- 測試賬號(如有支付功能需提供)
結語:從代碼到舞台的蜕變之旅
通過本文的逐步拆解,我們從零搭建了一個功能完整的短劇小程序。從環境配置、頁面開發到用户系統、支付集成,再到數據埋點和上架發佈,我們完成了一個產品從代碼到上線的完整生命週期。這段旅程不僅展示了技術實現的細節,更呈現瞭如何將創意轉化為實際可用的數字產品。短劇小程序的開發既是技術挑戰,也是理解用户需求的實踐。每一行代碼都在構建用户體驗,每一個功能都在滿足特定場景。在移動互聯網時代,輕量級的小程序正成為內容分發的重要渠道,掌握這項技能意味着抓住了數字娛樂的新入口。
技術的價值在於解決實際問題。無論是為創作者提供展示平台,還是為用户帶來娛樂內容,這個小程序都承載着連接與分享的使命。現在,你的代碼不再只是存儲在計算機中的字符,而是真正服務於千萬用户的數字產品。期待你的短劇小程序在微信生態中綻放光彩,為用户帶來精彩的短劇體驗。