【实战】小程序开发:从零到发布上线的完整手册

完整的小程序开发流程:账号注册、技术选型、核心 API 深度用法、真实项目架构(记账 App)、发布审核细节、常见坑点与优化方案。30 天上线完全可行。

14 分钟阅读
小明

【实战】小程序开发:从零到发布上线的完整手册

小程序开发是前端的"扩展生态":

  • 约束多(没有 DOM、严格的包体积限制)→ 学到架构设计
  • 可商业化(广告、付费功能、小程序商店)
  • 技能迁移(Vue/React 经验完全适用)

这篇是我上线 3 个小程序的完整总结。照着做,30 天能上线一个有真实功能的小程序。


1. 前置准备(2-3 天)

1.1 账号注册

  1. https://mp.weixin.qq.com 注册小程序
  2. 邮箱激活 + 身份认证(企业或个人)
  3. 获取 AppID(开发时必需)
  4. 后台设置:小程序名称、简介、类目、隐私政策链表

⚠️ 坑:隐私政策是强制性的。没有会被驳回。建议用模板,改改就行。

1.2 开发环境

# 下载微信开发者工具
# https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html

# 使用 VSCode + 微信小程序开发工具
# 推荐插件:
# - WXML: usernamehw.wxml
# - WXSS: Qiu.weapp

2. 与 Web 的核心差异

2.1 技术栈对比

特性Web(浏览器)小程序
脚本运行JS 引擎V8(双线程)
DOM APIdocument.querySelector❌无(虚拟树+渲染层)
事件系统addEventListenerbindtapbindchange
样式CSSWXSS(类 CSS,部分特性缺失)
导入模块import / requirerequire 或 ES6
全局对象windowglobalThis + wx 对象

2.2 为什么是双线程?

微信将小程序拆成两部分:

  • 逻辑层:JS 业务代码运行
  • 视图层:WXML + WXSS 渲染

数据通过 setData() 从逻辑层发送到视图层,这是为了安全性和性能隔离

开发影响

  • 不能直接操作 DOM(没有 document
  • 数据更新必须通过 setData()
  • 频繁 setData() 会很卡(跨线程通信开销大)

3. 完整项目架构:记账 App(Day 1-15)

3.1 项目结构规划

mini-expense/
├── app.json              # 全局配置:页面、tabBar、权限
├── app.ts                # 应用生命周期
├── app.wxss              # 全局样式
├── pages/
│   ├── home/             # 首页:账单列表
│   │   ├── home.wxml
│   │   ├── home.ts
│   │   └── home.wxss
│   ├── add/              # 新增账单
│   ├── stats/            # 统计分析
│   └── my/               # 我的:设置、关于
├── components/           # 自定义组件
│   ├── date-picker/
│   ├── category-selector/
│   └── chart/
├── utils/
│   ├── storage.ts        # 存储工具
│   ├── request.ts        # HTTP 请求
│   ├── date.ts           # 日期工具
│   └── calc.ts           # 计算工具
└── assets/               # 图片、图标

3.2 核心配置:app.json

{
  "pages": [
    "pages/home/home",
    "pages/add/add",
    "pages/stats/stats",
    "pages/my/my"
  ],
  "tabBar": {
    "list": [
      {
        "pagePath": "pages/home/home",
        "text": "账单",
        "iconPath": "assets/home.png",
        "selectedIconPath": "assets/home-active.png"
      },
      {
        "pagePath": "pages/stats/stats",
        "text": "统计",
        "iconPath": "assets/stats.png"
      },
      {
        "pagePath": "pages/my/my",
        "text": "我的",
        "iconPath": "assets/my.png"
      }
    ]
  },
  "permission": {
    "scope.userLocation": {
      "desc": "获取位置用于记录账单地点"
    }
  }
}

3.3 存储工具(关键)

小程序没有数据库,本地存储用 wx.getStorageSync() 但容量只有 10MB

// utils/storage.ts
class Storage {
  private prefix = 'expense_'
  
  // 获取所有账单
  getExpenses() {
    const data = wx.getStorageSync(this.prefix + 'list') || []
    return data.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
  }
  
  // 添加账单
  addExpense(expense: {
    type: 'income' | 'expense'
    category: string
    amount: number
    date: string
    remark?: string
    location?: string
  }) {
    const list = this.getExpenses()
    const newExpense = {
      id: Date.now().toString(),
      ...expense,
      createdAt: new Date().toISOString()
    }
    list.unshift(newExpense)
    wx.setStorageSync(this.prefix + 'list', list)
    return newExpense
  }
  
  // 删除账单
  deleteExpense(id: string) {
    const list = this.getExpenses()
    const filtered = list.filter(e => e.id !== id)
    wx.setStorageSync(this.prefix + 'list', filtered)
  }
  
  // 统计:指定月份的收入/支出
  getMonthStats(year: number, month: number) {
    const expenses = this.getExpenses()
    const monthStr = `${year}-${String(month).padStart(2, '0')}`
    
    let income = 0, expense = 0
    expenses.forEach(e => {
      if (e.date.startsWith(monthStr)) {
        if (e.type === 'income') income += e.amount
        else expense += e.amount
      }
    })
    
    return { income, expense, balance: income - expense }
  }
}

export default new Storage()

3.4 网络请求工具(实战)

// utils/request.ts
interface RequestOptions {
  url: string
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  data?: any
  timeout?: number
  showLoading?: boolean
}

class Request {
  private baseUrl = 'https://api.example.com'
  
  request<T>(options: RequestOptions): Promise<T> {
    return new Promise((resolve, reject) => {
      const url = this.baseUrl + options.url
      
      if (options.showLoading) {
        wx.showLoading({ title: '加载中...' })
      }
      
      wx.request({
        url,
        method: options.method || 'GET',
        data: options.data,
        timeout: options.timeout || 10000,
        header: {
          'Content-Type': 'application/json',
          'X-Auth-Token': this.getToken()
        },
        success: (res) => {
          wx.hideLoading()
          
          // 检查状态码
          if (res.statusCode === 401) {
            this.handleUnauthorized()
            reject(new Error('未授权'))
          } else if (res.statusCode >= 200 && res.statusCode < 300) {
            resolve(res.data as T)
          } else {
            reject(new Error(res.statusCode.toString()))
          }
        },
        fail: (err) => {
          wx.hideLoading()
          wx.showToast({
            title: '网络错误',
            icon: 'error'
          })
          reject(err)
        }
      })
    })
  }
  
  private getToken() {
    return wx.getStorageSync('auth_token') || ''
  }
  
  private handleUnauthorized() {
    wx.removeStorageSync('auth_token')
    wx.navigateTo({ url: '/pages/login/login' })
  }
}

export default new Request()

3.5 首页实现:账单列表

// pages/home/home.ts
import Storage from '../../utils/storage'

Page({
  data: {
    expenses: [],
    monthStats: { income: 0, expense: 0, balance: 0 },
    selectedDate: '2026-02',
    categoryIcons: {
      food: '🍔',
      transport: '🚗',
      entertainment: '🎮',
      other: '📌'
    }
  },
  
  onLoad() {
    this.loadData()
  },
  
  onShow() {
    // 每次返回时刷新(新增账单后)
    this.loadData()
  },
  
  loadData() {
    const expenses = Storage.getExpenses()
    const [year, month] = this.data.selectedDate.split('-').map(Number)
    const monthStats = Storage.getMonthStats(year, month)
    
    this.setData({ expenses, monthStats })
  },
  
  onDateChange(e: any) {
    this.setData({ selectedDate: e.detail.value }, () => {
      this.loadData()
    })
  },
  
  onDeleteExpense(e: any) {
    const id = e.currentTarget.dataset.id
    wx.showModal({
      title: '删除',
      content: '确定删除?',
      success: (res) => {
        if (res.confirm) {
          Storage.deleteExpense(id)
          this.loadData()
        }
      }
    })
  }
})
<!-- pages/home/home.wxml -->
<view class="container">
  <!-- 月份选择 -->
  <picker mode="date" value="{{ selectedDate }}" fields="month" 
    bindchange="onDateChange">
    <view class="month-selector">
      {{ selectedDate }}
    </view>
  </picker>
  
  <!-- 月度统计卡片 -->
  <view class="stats-card">
    <view class="stat-item">
      <text class="label">收入</text>
      <text class="amount income">¥{{ monthStats.income }}</text>
    </view>
    <view class="stat-item">
      <text class="label">支出</text>
      <text class="amount expense">¥{{ monthStats.expense }}</text>
    </view>
    <view class="stat-item">
      <text class="label">余额</text>
      <text class="amount" style="{{ monthStats.balance >= 0 ? 'color: #07C160' : 'color: #FA5151' }}">
        ¥{{ monthStats.balance }}
      </text>
    </view>
  </view>
  
  <!-- 账单列表 -->
  <view class="expense-list">
    <view wx:if="{{ expenses.length === 0 }}" class="empty">
      还没有账单,点击"+"添加一笔
    </view>
    
    <view wx:for="{{ expenses }}" wx:key="id" class="expense-item">
      <view class="left">
        <text class="icon">{{ categoryIcons[item.category] || '📌' }}</text>
        <view>
          <text class="category">{{ item.category }}</text>
          <text class="remark">{{ item.remark }}</text>
        </view>
      </view>
      <view class="right">
        <text class="amount" style="{{ item.type === 'income' ? 'color: #07C160' : 'color: #FA5151' }}">
          {{ item.type === 'income' ? '+' : '-' }}¥{{ item.amount }}
        </text>
        <view class="actions">
          <text class="delete" data-id="{{ item.id }}" 
            bindtap="onDeleteExpense">删除</text>
        </view>
      </view>
    </view>
  </view>
  
  <!-- 浮动新增按钮 -->
  <view class="fab">
    <navigator url="../add/add" class="fab-button">+</navigator>
  </view>
</view>

4. 性能和包体积优化(Day 16-20)

4.1 包体积优化

小程序主包限制 2MB,分包 2MB

检查包大小

微信开发者工具 项目 详情 本地代码

优化策略

  1. 删除无用资源(必做)
    # 找出未使用的图片
    find . -name "*.png" -o -name "*.jpg" | xargs -I {} grep -r {} src/
    
  2. 压缩图片
    # 用 TinyPNG 或 ImageOptim
    pngquant --speed 1 image.png
    
  3. 分页面分包
    {
      "subpackages": [
        {
          "root": "pages/advanced",
          "pages": ["pages/advanced/page1"]
        }
      ]
    }
    
  4. 避免重复依赖
    • 不要在多个地方 require() 同样的大文件
    • 用共享 utils 目录

4.2 首屏加载优化

目标:首屏时间 < 2s

  1. 初始化时不要加载全部数据
    onLoad() {
      // ❌ 不要这样:会阻塞
      const allData = Storage.getExpenses()
      
      // ✅ 应该这样:只加载当月
      const monthData = this.getCurrentMonthExpenses()
    }
    
  2. 懒加载长列表
    wx-if 而不是 hidden(隐藏 DOM 仍占用内存)
    <view wx:if="{{ isVisible }}">{{ item }}</view>
    

5. 发布和审核(Day 21-25)

5.1 审核清单

□ 隐私政策:https:// 开头的完整链接
□ 服务类目:选择正确的分类(影响审核标准)
□ 用户协议:如涉及付费必须有
□ 版本号:每次提交要递增(1.0.0 → 1.0.1)
□ 功能描述:清楚说明小程序能做什么
□ 真机测试:iPhone + Android 都要测
□ 代码审查:没有死链、API 调用有错误处理
□ 截图:5 张最能代表功能的截图

5.2 提交步骤

  1. 微信开发者工具 → 上传代码
    • 填版本号、项目备注
    • 包含"需要权限声明"等注释说明
  2. 后台管理系统
  3. 选择类目
    • 根据功能选择 1-2 个类目
    • 记账 App → "生活服务"或"金融服务"
  4. 提交审核
    • 点击"提交审核"
    • 通常 1-3 天有结果

5.3 常见被拒和对策

被拒原因你会看到解决方案
诱导分享"分享得奖励"、"分享解锁功能"改成纯分享,没有奖励
无隐私政策收集手机号/位置没说明加隐私政策链接,说清楚如何用数据
内容违规"涉及政治/暴力"检查是否有用户生成内容(评论等)未审核
功能不完整"核心功能 bug"多测几遍,每个按钮都试试
虚假宣传"宣称官方但非官方"改描述,删除制造"官方"错觉的文案
广告过多"首屏 50% 以上是广告"调整广告位置,体验优先

6. 常见坑点与解决

坑 1:setData 导致卡顿

症状setData() 一个大对象,UI 冻结 1 秒+

原因:跨线程通信开销

解决

// ❌ 不要
this.setData({
  longList: Array(10000).fill({...})
})

// ✅ 可以
// 1. 分次加载
this.setData({
  list: items.slice(0, 20)
})
// 2. 用 virtualList 虚拟滚动组件
// 3. 只更新改变的字段
this.setData({
  'list[0].name': newName
})

坑 2:navigateTo 页面栈溢出

症状:打开太多页面后,navigateBack() 不工作

原因:小程序页面栈最多 10 层

解决

// 如果已经是第 10 层,用 redirectTo 替代 navigateTo
if (this.pageStackSize > 8) {
  wx.redirectTo({ url: '...' })
} else {
  wx.navigateTo({ url: '...' })
}

坑 3:storage 容量满

症状setStorageSync() 中断,数据丢失

原因:10MB 满了

解决

// 定期清理旧数据
function cleanup() {
  const expenses = wx.getStorageSync('expense_list') || []
  const oneYearAgo = new Date()
  oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1)
  
  const filtered = expenses.filter(e =>
    new Date(e.date) > oneYearAgo
  )
  wx.setStorageSync('expense_list', filtered)
}

坑 4:真机和模拟器表现完全不同

常见差异

  • 模拟器网络好,真机慢 → 加 loading 和超时处理
  • 安卓和 iOS 样式不一样 → 两个都要测
  • 浏览器兼容 fetch,小程序没有 → 只能用 wx.request

7. 上线后的运营

  • 数据监控:后台看日活、页面停留时间
  • 用户反馈:小程序客服功能或社群咨询
  • 版本迭代:发现 bug 立即修复 + 重新审核
  • 增长策略:口碑分享比强制分享更有效

总结

阶段时间关键任务
准备2-3 天账号注册、环境搭建
开发14 天核心功能开发(参考上面的记账 App)
测试3-5 天真机多系统测试、性能检查
发布5-7 天审核(1-3 天)+ 修复(若被拒)

关键点:

  1. 优先完成核心功能(不是"全部功能")
  2. 充分测试后再上传(被拒会浪费 3-5 天)
  3. 留有版本号空间(后续迭代用)
  4. 监控上线后的 bug 反馈(快速修复能提升评分)

30 天上线完全可行。