前置要求:小程序基础、Vue.js基础
学习时长:约3-5天
适用场景:电商小程序、工具小程序、内容小程序
一、实战项目:待办事项小程序
1.1 项目结构
todo-miniapp/
├── pages/
│ ├── index/ # 首页(待办列表)
│ ├── add/ # 添加待办
│ └── detail/ # 待办详情
├── components/
│ ├── todo-item/ # 待办项组件
│ └── empty/ # 空状态组件
├── utils/
│ ├── request.js # 请求封装
│ ├── storage.js # 本地存储
│ └── util.js # 工具函数
├── store/
│ └── index.js # 状态管理
├── app.js
├── app.json
└── app.wxss
1.2 页面实现
<!-- pages/index/index.wxml -->
<view class="container">
<view class="header">
<text class="title">我的待办</text>
<text class="count">{{todos.length}} 项</text>
</view>
<!-- 添加输入框 -->
<view class="input-box">
<input
placeholder="添加新待办..."
value="{{inputValue}}"
bindinput="onInput"
bindconfirm="addTodo"
/>
<button bindtap="addTodo" size="mini">添加</button>
</view>
<!-- 待办列表 -->
<view class="todo-list" wx:if="{{todos.length > 0}}">
<view
wx:for="{{todos}}"
wx:key="id"
class="todo-item {{item.completed ? 'completed' : ''}}"
>
<checkbox
checked="{{item.completed}}"
bindchange="toggleTodo"
data-id="{{item.id}}"
/>
<text
class="todo-text"
bindtap="goDetail"
data-id="{{item.id}}"
>{{item.text}}</text>
<text
class="delete-btn"
bindtap="deleteTodo"
data-id="{{item.id}}"
>×</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty" wx:else>
<text>暂无待办事项</text>
</view>
<!-- 底部统计 -->
<view class="footer" wx:if="{{todos.length > 0}}">
<text>已完成 {{completedCount}} / {{todos.length}}</text>
<text bindtap="clearCompleted" class="clear-btn">清除已完成</text>
</view>
</view>
// pages/index/index.js
Page({
data: {
todos: [],
inputValue: '',
completedCount: 0
},
onLoad() {
this.loadTodos()
},
onShow() {
this.loadTodos()
},
// 加载待办
loadTodos() {
const todos = wx.getStorageSync('todos') || []
const completedCount = todos.filter(t => t.completed).length
this.setData({ todos, completedCount })
},
// 保存待办
saveTodos() {
wx.setStorageSync('todos', this.data.todos)
},
// 输入事件
onInput(e) {
this.setData({ inputValue: e.detail.value })
},
// 添加待办
addTodo() {
const text = this.data.inputValue.trim()
if (!text) {
wx.showToast({ title: '请输入内容', icon: 'error' })
return
}
const todo = {
id: Date.now(),
text,
completed: false,
createdAt: new Date().toISOString()
}
const todos = [todo, ...this.data.todos]
this.setData({
todos,
inputValue: '',
completedCount: todos.filter(t => t.completed).length
})
this.saveTodos()
wx.showToast({ title: '添加成功', icon: 'success' })
},
// 切换完成状态
toggleTodo(e) {
const id = e.currentTarget.dataset.id
const todos = this.data.todos.map(t => {
if (t.id === id) {
return { ...t, completed: !t.completed }
}
return t
})
this.setData({
todos,
completedCount: todos.filter(t => t.completed).length
})
this.saveTodos()
},
// 删除待办
deleteTodo(e) {
const id = e.currentTarget.dataset.id
wx.showModal({
title: '确认删除',
content: '确定删除这个待办吗?',
success: (res) => {
if (res.confirm) {
const todos = this.data.todos.filter(t => t.id !== id)
this.setData({
todos,
completedCount: todos.filter(t => t.completed).length
})
this.saveTodos()
}
}
})
},
// 跳转详情
goDetail(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/detail/index?id=${id}` })
},
// 清除已完成
clearCompleted() {
const todos = this.data.todos.filter(t => !t.completed)
this.setData({ todos, completedCount: 0 })
this.saveTodos()
}
})
/* pages/index/index.wxss */
.container {
padding: 30rpx;
background: #f5f5f5;
min-height: 100vh;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.title {
font-size: 44rpx;
font-weight: bold;
color: #333;
}
.count {
font-size: 28rpx;
color: #999;
}
.input-box {
display: flex;
gap: 20rpx;
margin-bottom: 30rpx;
}
.input-box input {
flex: 1;
background: #fff;
padding: 20rpx 30rpx;
border-radius: 10rpx;
font-size: 28rpx;
}
.todo-list {
background: #fff;
border-radius: 10rpx;
overflow: hidden;
}
.todo-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}
.todo-text {
flex: 1;
margin: 0 20rpx;
font-size: 30rpx;
color: #333;
}
.delete-btn {
font-size: 40rpx;
color: #999;
padding: 0 10rpx;
}
.empty {
text-align: center;
padding: 100rpx 0;
color: #999;
}
.footer {
display: flex;
justify-content: space-between;
margin-top: 30rpx;
font-size: 24rpx;
color: #999;
}
.clear-btn {
color: #ff4d4f;
}
二、常用功能实现
2.1 网络请求封装
// utils/request.js
const BASE_URL = 'https://api.example.com'
const request = (options) => {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token')
wx.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else if (res.statusCode === 401) {
wx.navigateTo({ url: '/pages/login/index' })
reject(new Error('未登录'))
} else {
wx.showToast({ title: res.data.message || '请求失败', icon: 'error' })
reject(res.data)
}
},
fail: (err) => {
wx.showToast({ title: '网络错误', icon: 'error' })
reject(err)
}
})
})
}
// API方法
export const api = {
// 用户
login: (data) => request({ url: '/auth/login', method: 'POST', data }),
getUserInfo: () => request({ url: '/user/info' }),
// 待办
getTodos: () => request({ url: '/todos' }),
addTodo: (data) => request({ url: '/todos', method: 'POST', data }),
updateTodo: (id, data) => request({ url: `/todos/${id}`, method: 'PUT', data }),
deleteTodo: (id) => request({ url: `/todos/${id}`, method: 'DELETE' })
}
export default request
2.2 登录授权
// 微信登录
const login = async () => {
try {
// 1. 获取code
const { code } = await wx.login()
// 2. 发送code到后端
const res = await api.login({ code })
// 3. 保存token
wx.setStorageSync('token', res.token)
wx.setStorageSync('userInfo', res.user)
// 4. 跳转首页
wx.switchTab({ url: '/pages/index/index' })
} catch (err) {
console.error('登录失败:', err)
}
}
// 获取用户信息(需要用户授权)
const getUserProfile = () => {
wx.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
const userInfo = res.userInfo
// 发送userInfo到后端
}
})
}
2.3 分享功能
// 页面分享
Page({
onShareAppMessage() {
return {
title: '我的待办事项',
path: '/pages/index/index',
imageUrl: '/images/share.png'
}
},
onShareTimeline() {
return {
title: '我的待办事项',
imageUrl: '/images/share.png'
}
}
})
2.4 图片上传
const chooseAndUpload = () => {
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const filePath = res.tempFilePaths[0]
wx.uploadFile({
url: 'https://api.example.com/upload',
filePath,
name: 'file',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
success: (res) => {
const data = JSON.parse(res.data)
console.log('上传成功:', data.url)
}
})
}
})
}
三、自定义组件
3.1 待办项组件
// components/todo-item/index.json
{
"component": true
}
<!-- components/todo-item/index.wxml -->
<view class="todo-item {{completed ? 'completed' : ''}}" bindtap="onTap">
<checkbox checked="{{completed}}" bindchange="onToggle" />
<text class="text">{{text}}</text>
<text class="time">{{createTime}}</text>
</view>
// components/todo-item/index.js
Component({
properties: {
id: Number,
text: String,
completed: Boolean,
createdAt: String
},
computed: {
createTime() {
// 格式化时间
return this.properties.createdAt?.slice(0, 10)
}
},
methods: {
onTap() {
this.triggerEvent('tap', { id: this.properties.id })
},
onToggle(e) {
this.triggerEvent('toggle', {
id: this.properties.id,
completed: !this.properties.completed
})
}
}
})
/* components/todo-item/index.wxss */
.todo-item {
display: flex;
align-items: center;
padding: 30rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.todo-item.completed .text {
text-decoration: line-through;
color: #999;
}
.text {
flex: 1;
margin: 0 20rpx;
font-size: 30rpx;
}
.time {
font-size: 24rpx;
color: #999;
}
四、云开发(可选)
// 云函数示例
// cloudfunctions/getTodos/index.js
const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database()
exports.main = async (event, context) => {
const { OPENID } = cloud.getWXContext()
const result = await db.collection('todos')
.where({ openid: OPENID })
.orderBy('createdAt', 'desc')
.get()
return {
code: 200,
data: result.data
}
}
// 前端调用
const res = await wx.cloud.callFunction({
name: 'getTodos'
})
console.log(res.result.data)
五、性能优化
✅ 使用分包加载
✅ 图片压缩和懒加载
✅ 长列表使用虚拟列表
✅ 减少setData的数据量
✅ 使用WXS处理视图层逻辑
✅ 避免频繁的setData调用
✅ 使用缓存减少请求
✅ 预加载下一页数据
学习建议
- 先做一个完整的小项目,如待办事项
- 掌握网络请求封装,这是前后端交互的基础
- 学习自定义组件,提高代码复用性
- 了解云开发,简化后端开发
- 关注性能优化,提升用户体验
下一步学习