【实战】小程序开发:从零到发布上线的完整手册
完整的小程序开发流程:账号注册、技术选型、核心 API 深度用法、真实项目架构(记账 App)、发布审核细节、常见坑点与优化方案。30 天上线完全可行。
14 分钟阅读
小明
【实战】小程序开发:从零到发布上线的完整手册
小程序开发是前端的"扩展生态":
- 约束多(没有 DOM、严格的包体积限制)→ 学到架构设计
- 可商业化(广告、付费功能、小程序商店)
- 技能迁移(Vue/React 经验完全适用)
这篇是我上线 3 个小程序的完整总结。照着做,30 天能上线一个有真实功能的小程序。
1. 前置准备(2-3 天)
1.1 账号注册
- https://mp.weixin.qq.com 注册小程序
- 邮箱激活 + 身份认证(企业或个人)
- 获取 AppID(开发时必需)
- 后台设置:小程序名称、简介、类目、隐私政策链表
⚠️ 坑:隐私政策是强制性的。没有会被驳回。建议用模板,改改就行。
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 API | document.querySelector | ❌无(虚拟树+渲染层) |
| 事件系统 | addEventListener | bindtap、bindchange |
| 样式 | CSS | WXSS(类 CSS,部分特性缺失) |
| 导入模块 | import / require | require 或 ES6 |
| 全局对象 | window | globalThis + 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。
检查包大小:
微信开发者工具 → 项目 → 详情 → 本地代码
优化策略:
- 删除无用资源(必做)
# 找出未使用的图片 find . -name "*.png" -o -name "*.jpg" | xargs -I {} grep -r {} src/ - 压缩图片
# 用 TinyPNG 或 ImageOptim pngquant --speed 1 image.png - 分页面分包
{ "subpackages": [ { "root": "pages/advanced", "pages": ["pages/advanced/page1"] } ] } - 避免重复依赖
- 不要在多个地方
require()同样的大文件 - 用共享
utils目录
- 不要在多个地方
4.2 首屏加载优化
目标:首屏时间 < 2s
- 初始化时不要加载全部数据
onLoad() { // ❌ 不要这样:会阻塞 const allData = Storage.getExpenses() // ✅ 应该这样:只加载当月 const monthData = this.getCurrentMonthExpenses() } - 懒加载长列表
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 提交步骤
- 微信开发者工具 → 上传代码
- 填版本号、项目备注
- 包含"需要权限声明"等注释说明
- 后台管理系统
- https://mp.weixin.qq.com → 版本管理
- 选择刚上传的版本
- 填写"版本描述"(用户能看到)
- 选择类目
- 根据功能选择 1-2 个类目
- 记账 App → "生活服务"或"金融服务"
- 提交审核
- 点击"提交审核"
- 通常 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 天)+ 修复(若被拒) |
关键点:
- 优先完成核心功能(不是"全部功能")
- 充分测试后再上传(被拒会浪费 3-5 天)
- 留有版本号空间(后续迭代用)
- 监控上线后的 bug 反馈(快速修复能提升评分)
30 天上线完全可行。