前言:短劇小程序的爆發與機遇

在數字娛樂消費升級的背景下,短劇市場正以驚人的速度增長。據統計,2024年短劇市場規模預計將達到1000億元,而小程序作為輕量級入口,已成為短劇分發的重要渠道。本文將從零開始,手把手帶你完成短劇小程序的開發、部署到上架全流程,即使你沒有任何開發經驗,也能跟隨本文一步步實現自己的短劇小程序。

源碼及演示:v.dyedus.top

開發環境搭建

1.1 基礎工具準備

必需軟件清單:

  1. 微信開發者工具 - 小程序開發IDE
  2. Visual Studio Code - 代碼編輯器
  3. Node.js - JavaScript運行環境
  4. Git - 版本管理工具

安裝驗證:

# 檢查Node.js安裝
node --version
# 應顯示 v16.0.0 或更高版本

# 檢查npm安裝
npm --version
# 應顯示 8.0.0 或更高版本

# 檢查Git安裝
git --version
# 應顯示 git version 2.x.x

1.2 小程序賬號註冊

  1. 訪問微信公眾平台
  2. 註冊小程序賬號(選擇"小程序",非公眾號)
  3. 完成主體認證(個人或企業)
  4. 獲取AppID(後續開發需要)

項目初始化與結構解析

1. 創建小程序項目

在微信開發者工具中:

  1. 點擊"新建項目"
  2. 填寫項目信息:
  • 項目名稱:ShortDrama
  • 目錄:選擇項目存放位置
  • AppID:填寫註冊的小程序AppID
  • 後端服務:選擇"不使用雲服務"
  1. 點擊"新建"

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. 真機調試技巧

  1. 開啓調試模式:
// app.js
App({
  onLaunch() {
    // 開發環境開啓調試
    if (__DEV__) {
      wx.setEnableDebug({
        enableDebug: true
      });
    }
  }
});
  1. 性能監控:
// 監控頁面性能
wx.reportPerformance(1101, Date.now() - startTime, 'page_load');
  1. 錯誤監控:
// 錯誤上報
wx.onError((error) => {
  console.error('小程序錯誤:', error);
  // 上報到監控系統
  wx.request({
    url: 'https://your-error-log.com/report',
    method: 'POST',
    data: error
  });
});

上架發佈流程

1. 代碼上傳

  1. 在微信開發者工具中:
  • 點擊"上傳"按鈕
  • 填寫版本號和項目備註
  • 點擊"上傳"
  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. 提交審核

  1. 登錄微信公眾平台
  2. 進入"管理" -> "版本管理"
  3. 在"開發版本"中找到上傳的版本
  4. 點擊"提交審核"
  5. 填寫審核信息:
  • 服務類目:文娛-視頻
  • 標籤:短劇、視頻、娛樂
  • 測試賬號(如有支付功能需提供)

結語:從代碼到舞台的蜕變之旅

通過本文的逐步拆解,我們從零搭建了一個功能完整的短劇小程序。從環境配置、頁面開發到用户系統、支付集成,再到數據埋點和上架發佈,我們完成了一個產品從代碼到上線的完整生命週期。這段旅程不僅展示了技術實現的細節,更呈現瞭如何將創意轉化為實際可用的數字產品。短劇小程序的開發既是技術挑戰,也是理解用户需求的實踐。每一行代碼都在構建用户體驗,每一個功能都在滿足特定場景。在移動互聯網時代,輕量級的小程序正成為內容分發的重要渠道,掌握這項技能意味着抓住了數字娛樂的新入口。

技術的價值在於解決實際問題。無論是為創作者提供展示平台,還是為用户帶來娛樂內容,這個小程序都承載着連接與分享的使命。現在,你的代碼不再只是存儲在計算機中的字符,而是真正服務於千萬用户的數字產品。期待你的短劇小程序在微信生態中綻放光彩,為用户帶來精彩的短劇體驗。