news 2026/8/6 14:06:06

从零构建全栈小程序:基于Node.js+Koa2+MySQL的待办清单实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建全栈小程序:基于Node.js+Koa2+MySQL的待办清单实战

1. 从零到一:一个完整小程序项目的全景图

最近几年,微信小程序已经从一个“新物种”变成了移动端开发的“标配”。无论是电商、工具、内容还是服务,小程序都以其“即用即走”的轻量级体验,成为了连接用户的重要触点。很多开发者,尤其是前端同学,可能已经能熟练地使用 WXML 和 WXSS 搭建出漂亮的界面,但一提到“完整项目”,尤其是涉及到后端、数据库、部署上线,心里就开始打鼓了。这个“完整项目”的鸿沟,恰恰是区分“页面仔”和“全栈工程师”的关键一步。

一个完整的小程序项目,远不止是画几个页面、调几个接口那么简单。它是一套从用户点击小程序图标开始,到数据最终落盘并返回展示的完整技术闭环。这个闭环里,前端(小程序端)负责与用户交互、渲染视图;后端(服务端)负责处理业务逻辑、保障数据安全与持久化;而连接这两端的,是一套设计良好的 API 接口协议。更进一步的,你还需要考虑开发环境、测试流程、代码管理、以及最终的生产部署。听起来很复杂?别怕,这正是我们接下来要一步步拆解并实现的目标。

我将以一个典型的“待办事项清单”(To-Do List)小程序为例,带你走完这个完整的流程。选择这个例子,是因为它的业务逻辑足够清晰(增删改查),但又涵盖了用户认证、数据同步、状态管理等核心问题,非常适合作为全栈实践的入门项目。通过这个项目,你将掌握如何从前端界面开发,到后端 API 设计,再到数据库操作和云服务器部署,最终让一个真正可用的、数据可持久化的小程序跑起来。我们不仅会“做出来”,更会深入探讨每一步“为什么这么做”,以及在实际开发中容易踩的那些“坑”。

2. 谋定而后动:项目架构与技术选型

在动手写第一行代码之前,花时间在架构设计和技术选型上是绝对值得的。一个好的开始能避免后期大量的重构和填坑工作。对于我们的“待办清单”小程序,我们需要明确前后端各自的职责,并选择合适的技术栈。

2.1 前端(小程序端)架构设计

小程序前端我们自然使用微信官方提供的开发框架。它的核心是逻辑层(JavaScript)和视图层(WXML/WXSS)分离的架构。

  • 页面结构:我们将创建几个核心页面:index(首页/清单列表)、detail(待办事项详情/编辑)、profile(个人中心)。使用小程序原生的页面路由进行跳转。
  • 状态管理:对于待办清单这类数据驱动型应用,状态管理至关重要。虽然小程序提供了App()的全局globalData,但对于稍复杂的应用,它容易变得混乱。我个人的经验是,对于本项目,我们可以采用一种“轻量级中心化”模式:在app.js中维护一个核心的todos数组作为唯一数据源,并通过自定义事件或简单的发布订阅模式,在页面间同步状态。这比直接使用globalData到处赋值要清晰得多。如果项目更复杂,可以考虑引入像MobX-miniprogram这样的状态管理库。
  • 网络请求:我们将使用wx.request封装一个统一的 HTTP 客户端。这个封装不是为了炫技,而是为了解决几个实际问题:1) 统一添加请求基地址(Base URL);2) 统一处理请求头(如携带用户 Token);3) 统一拦截错误(如网络异常、服务端错误);4) 统一管理加载状态(显示/隐藏 Loading)。这能极大提升代码的健壮性和可维护性。

2.2 后端(服务端)技术选型

这是很多前端开发者陌生的领域。我们的目标是选择一个能快速上手、生态成熟、适合中小型项目的技术栈。

  • Node.js + Koa2:我选择 Node.js 作为后端语言,主要考虑的是技术栈统一(前后端都用 JavaScript),学习曲线平滑。框架方面,Express 固然经典,但 Koa2 更轻量、更现代,利用 async/await 语法能写出更优雅的异步代码,避免“回调地狱”。对于新手来说,Koa2 的中间件机制(Middleware)概念清晰,易于理解。
  • 数据库:MySQL vs MongoDB:这是一个关键选择。待办事项数据是结构化的(有 id, title, content, status, userId, createTime 等明确字段),且关系简单(主要是用户和其待办项的一对多关系)。因此,我强烈推荐使用关系型数据库 MySQL。很多人觉得 MongoDB 更“简单”,不用设计表结构,但这恰恰是陷阱。在项目初期不定义清晰的结构,后期数据混乱、查询低效的问题会层出不穷。使用 MySQL 配合 Sequelize 这样的 ORM 库,既能享受关系型数据库的数据一致性保障,又能用接近操作对象的方式写代码。
  • 身份认证:JWT (JSON Web Token):小程序调用后端 API 需要识别用户身份。传统的 Session 机制在无状态的 RESTful API 和跨域场景下比较麻烦。JWT 是一种无状态的令牌方案,用户登录后,服务器生成一个包含用户ID等信息的 Token 返回给小程序,小程序后续请求在 HTTP Header 中携带此 Token。服务器验证 Token 签名即可识别用户。它简单、安全、适合分布式部署。

注意:千万不要将敏感信息(如密码)放入 JWT 的 payload(有效载荷)中,因为 payload 仅经过 Base64 编码,是可解码的。JWT 的安全性依赖于签名不被破解。

2.3 接口设计(API Blueprint)

前后端协作的基础是接口契约。我们先定义好 API,前后端就可以并行开发。遵循 RESTful 风格是一个好习惯。

功能请求方法端点 (Endpoint)描述请求体 (Body)成功响应
用户登录POST/api/auth/login微信静默登录后,用 code 换 token{ code }{ token, userInfo }
获取清单GET/api/todos获取当前用户的所有待办事项[ todo1, todo2, ... ]
创建事项POST/api/todos创建一条新的待办事项{ title, content? }{ id, title, ... }
更新事项PUT/api/todos/:id更新指定事项(如标记完成){ title?, content?, status? }更新后的 todo 对象
删除事项DELETE/api/todos/:id删除指定事项空对象或成功消息

这个表格就是前后端的“合作协议书”。注意端点 URL 的复数形式和 HTTP 方法的使用,这是 RESTful 的常见约定。

3. 后端实战:从零搭建 Koa2 服务

让我们从后端开始,因为 API 是数据的源头。确保你的电脑已经安装了 Node.js(建议 LTS 版本)和 MySQL。

3.1 初始化项目与核心依赖安装

首先,创建一个新的目录并初始化项目:

mkdir mini-program-backend && cd mini-program-backend npm init -y

接下来,安装我们需要的核心依赖:

npm install koa koa-router koa-bodyparser koa-json koa-static npm install mysql2 sequelize jsonwebtoken bcryptjs npm install dotenv --save-dev # 用于管理环境变量
  • koa: Web 框架本体。
  • koa-router: 路由中间件,用来组织我们的 API 端点。
  • koa-bodyparser: 解析请求体,这样我们才能在ctx.request.body里拿到 POST 传来的 JSON 数据。
  • koa-json: 美化 JSON 输出。
  • koa-static: 静态文件服务(后续可能用于部署前端页面或上传的文件)。
  • mysql2&sequelize: 数据库驱动和 ORM。
  • jsonwebtoken: 用于生成和验证 JWT。
  • bcryptjs: 用于加密密码(虽然我们小程序用微信登录,但保留用户表时,密码加密是好习惯)。
  • dotenv: 将数据库密码、JWT 密钥等敏感信息从代码中分离,放到.env文件里。

3.2 数据库设计与 Sequelize 模型定义

在 MySQL 中创建一个数据库,例如mini_program_todo。然后,在项目根目录创建config文件夹和config/db.js文件,配置 Sequelize 连接:

// config/db.js const { Sequelize } = require('sequelize'); require('dotenv').config(); // 加载 .env 文件中的变量 const sequelize = new Sequelize( process.env.DB_NAME || 'mini_program_todo', process.env.DB_USER || 'root', process.env.DB_PASSWORD || 'your_password', { host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 3306, dialect: 'mysql', logging: false, // 关闭执行 SQL 语句的日志,需要调试时可设为 console.log timezone: '+08:00', // 设置为东八区时间 } ); module.exports = sequelize;

.env文件中配置你的敏感信息:

DB_NAME=mini_program_todo DB_USER=root DB_PASSWORD=YourActualPassword123 DB_HOST=localhost JWT_SECRET=YourSuperSecretKeyHereAtLeast32Chars

接下来,定义我们的数据模型。创建models文件夹。

首先是用户模型models/user.js

const { DataTypes } = require('sequelize'); const sequelize = require('../config/db'); const User = sequelize.define('User', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, }, openId: { type: DataTypes.STRING(100), unique: true, allowNull: false, comment: '微信用户的唯一标识', }, nickName: { type: DataTypes.STRING(100), comment: '微信昵称', }, avatarUrl: { type: DataTypes.STRING(500), comment: '微信头像', }, }, { tableName: 'users', timestamps: true, // 自动添加 createdAt 和 updatedAt 字段 }); module.exports = User;

然后是待办事项模型models/todo.js

const { DataTypes } = require('sequelize'); const sequelize = require('../config/db'); const Todo = sequelize.define('Todo', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, }, title: { type: DataTypes.STRING(200), allowNull: false, comment: '待办事项标题', }, content: { type: DataTypes.TEXT, comment: '详细内容', }, status: { type: DataTypes.ENUM('pending', 'completed'), defaultValue: 'pending', comment: '状态:pending-待办,completed-完成', }, userId: { type: DataTypes.INTEGER, allowNull: false, comment: '所属用户ID', }, }, { tableName: 'todos', timestamps: true, }); module.exports = Todo;

models/index.js中关联它们,并同步到数据库:

const sequelize = require('../config/db'); const User = require('./user'); const Todo = require('./todo'); // 定义关联关系:一个用户拥有多个待办事项 User.hasMany(Todo, { foreignKey: 'userId', as: 'todos' }); Todo.belongsTo(User, { foreignKey: 'userId', as: 'user' }); const syncDatabase = async () => { try { // `alter: true` 会修改表结构以匹配模型。生产环境慎用,建议用迁移工具。 await sequelize.sync({ alter: true }); console.log('数据库表同步成功!'); } catch (error) { console.error('数据库表同步失败:', error); } }; module.exports = { sequelize, User, Todo, syncDatabase, };

在入口文件app.js中调用syncDatabase()

3.3 实现核心业务逻辑与路由

创建routes文件夹来组织路由。首先是认证路由routes/auth.js

const Router = require('koa-router'); const jwt = require('jsonwebtoken'); const axios = require('axios'); // 需要安装:npm install axios const router = new Router({ prefix: '/api/auth' }); const { User } = require('../models'); // 小程序登录 router.post('/login', async (ctx) => { const { code } = ctx.request.body; if (!code) { ctx.status = 400; ctx.body = { message: '缺少 code 参数' }; return; } try { // 1. 用 code 向微信服务器换取 openid 和 session_key const appid = process.env.WX_APPID; // 你的小程序 AppID const secret = process.env.WX_SECRET; // 你的小程序 AppSecret const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=authorization_code`; const response = await axios.get(url); const { openid, session_key, errcode, errmsg } = response.data; if (errcode) { throw new Error(`微信接口错误: ${errcode} - ${errmsg}`); } // 2. 根据 openid 查找或创建用户 let user = await User.findOne({ where: { openId: openid } }); if (!user) { // 这里通常还需要用 session_key 解密用户信息(如果需要获取头像昵称) // 为简化,我们假设首次登录只记录 openid user = await User.create({ openId: openid }); } // 3. 生成 JWT Token const token = jwt.sign( { userId: user.id, openId: user.openId }, process.env.JWT_SECRET, { expiresIn: '7d' } // Token 有效期7天 ); ctx.body = { token, userInfo: { id: user.id, nickName: user.nickName, avatarUrl: user.avatarUrl, }, }; } catch (error) { console.error('登录失败:', error); ctx.status = 500; ctx.body = { message: '登录失败,请稍后重试' }; } }); module.exports = router;

重要提示WX_APPIDWX_SECRET是极其敏感的信息,必须放在后端服务器的环境变量中(.env文件),绝对不可以写在小程序前端代码里。前端只传code,后端用codesecret去微信服务器换openid

接着是待办事项路由routes/todos.js

const Router = require('koa-router'); const router = new Router({ prefix: '/api/todos' }); const { Todo } = require('../models'); // 引入一个认证中间件 const authMiddleware = require('../middlewares/auth'); // 所有 todo 相关路由都需要认证 router.use(authMiddleware); // 获取当前用户的所有待办事项 router.get('/', async (ctx) => { try { const userId = ctx.state.user.userId; // 从 authMiddleware 中获取 const todos = await Todo.findAll({ where: { userId }, order: [['createdAt', 'DESC']], // 按创建时间倒序 }); ctx.body = todos; } catch (error) { ctx.status = 500; ctx.body = { message: '获取列表失败' }; } }); // 创建新的待办事项 router.post('/', async (ctx) => { try { const userId = ctx.state.user.userId; const { title, content } = ctx.request.body; if (!title || title.trim() === '') { ctx.status = 400; ctx.body = { message: '标题不能为空' }; return; } const todo = await Todo.create({ title: title.trim(), content: content ? content.trim() : null, userId, }); ctx.status = 201; // Created ctx.body = todo; } catch (error) { ctx.status = 500; ctx.body = { message: '创建失败' }; } }); // 更新待办事项(如标记完成) router.put('/:id', async (ctx) => { try { const userId = ctx.state.user.userId; const todoId = ctx.params.id; const updateData = ctx.request.body; const [affectedCount] = await Todo.update(updateData, { where: { id: todoId, userId }, // 确保只能更新自己的事项 }); if (affectedCount === 0) { ctx.status = 404; ctx.body = { message: '未找到该事项或无权操作' }; return; } // 返回更新后的数据 const updatedTodo = await Todo.findByPk(todoId); ctx.body = updatedTodo; } catch (error) { ctx.status = 500; ctx.body = { message: '更新失败' }; } }); // 删除待办事项 router.delete('/:id', async (ctx) => { try { const userId = ctx.state.user.userId; const todoId = ctx.params.id; const deletedCount = await Todo.destroy({ where: { id: todoId, userId }, }); if (deletedCount === 0) { ctx.status = 404; ctx.body = { message: '未找到该事项或无权操作' }; return; } ctx.status = 204; // No Content } catch (error) { ctx.status = 500; ctx.body = { message: '删除失败' }; } }); module.exports = router;

最后,我们需要实现上面用到的认证中间件middlewares/auth.js

const jwt = require('jsonwebtoken'); module.exports = async (ctx, next) => { // 从请求头中获取 token,格式通常是 `Bearer <token>` const authHeader = ctx.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { ctx.status = 401; ctx.body = { message: '未提供认证令牌' }; return; } const token = authHeader.substring(7); // 去掉 'Bearer ' 前缀 try { const decoded = jwt.verify(token, process.env.JWT_SECRET); // 将解码后的用户信息挂载到 ctx.state 上,供后续中间件和路由使用 ctx.state.user = decoded; await next(); // 继续执行下一个中间件或路由 } catch (error) { if (error.name === 'TokenExpiredError') { ctx.status = 401; ctx.body = { message: '令牌已过期,请重新登录' }; } else { ctx.status = 401; ctx.body = { message: '无效的认证令牌' }; } } };

3.4 整合应用与错误处理

现在,在app.js中把所有部分组装起来:

const Koa = require('koa'); const bodyParser = require('koa-bodyparser'); const json = require('koa-json'); const cors = require('@koa/cors'); // 需要安装:npm install @koa/cors const { syncDatabase } = require('./models'); const authRouter = require('./routes/auth'); const todosRouter = require('./routes/todos'); const app = new Koa(); // 同步数据库模型 syncDatabase(); // 中间件 app.use(cors()); // 处理跨域请求,小程序开发工具需要 app.use(bodyParser()); app.use(json()); // 路由 app.use(authRouter.routes()).use(authRouter.allowedMethods()); app.use(todosRouter.routes()).use(todosRouter.allowedMethods()); // 全局错误处理中间件 app.use(async (ctx, next) => { try { await next(); } catch (err) { console.error('Server Error:', err); ctx.status = err.status || 500; ctx.body = { message: err.message || '服务器内部错误', // 生产环境不建议返回详细的堆栈信息 ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), }; } }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`后端服务已启动,监听端口: ${PORT}`); });

至此,一个功能完整的后端 API 服务就搭建完成了。你可以使用 Postman 或 curl 工具,按照之前设计的 API 文档进行测试。先调用/api/auth/login(需要模拟获取 code,测试时可以先写死一个有效的 openid 直接生成 token),然后用返回的 token 测试待办事项的增删改查。

4. 前端实战:构建交互流畅的小程序界面

后端 API 准备就绪后,我们转向小程序前端。打开微信开发者工具,创建一个新的小程序项目。

4.1 项目初始化与网络请求封装

首先,清理默认项目,创建我们需要的页面:index,detail,profile。在app.js中,我们初始化全局状态和封装的网络请求模块。

utils目录下创建request.js,这是前面提到的统一 HTTP 客户端:

// utils/request.js const BASE_URL = 'https://your-backend-domain.com'; // 替换成你的后端服务器地址 const request = (options) => { // 从全局或本地存储获取 token const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '', // 携带 Token ...options.header, }, success: (res) => { const { statusCode, data } = res; if (statusCode >= 200 && statusCode < 300) { // 请求成功 resolve(data); } else { // HTTP 状态码错误(如 400, 401, 500) reject(new Error(`请求失败: ${statusCode}`)); // 可以根据 statusCode 做统一提示,如 401 跳转到登录页 if (statusCode === 401) { wx.showToast({ title: '登录已过期,请重新登录', icon: 'none', }); // 清空 token,跳转到登录页或首页 wx.removeStorageSync('token'); wx.reLaunch({ url: '/pages/index/index' }); } } }, fail: (err) => { // 网络错误等 console.error('网络请求失败:', err); wx.showToast({ title: '网络连接失败,请检查网络', icon: 'none', }); reject(err); }, complete: () => { // 可以在这里统一隐藏 Loading 提示 if (options.hideLoading !== false) { wx.hideLoading(); } } }); }); }; // 提供便捷方法 const http = { get: (url, data, options) => request({ url, method: 'GET', data, ...options }), post: (url, data, options) => request({ url, method: 'POST', data, ...options }), put: (url, data, options) => request({ url, method: 'PUT', data, ...options }), delete: (url, data, options) => request({ url, method: 'DELETE', data, ...options }), }; module.exports = http;

app.js中,我们进行小程序登录并获取后端 Token:

// app.js const http = require('./utils/request'); App({ globalData: { userInfo: null, todos: [], // 全局待办列表 }, onLaunch: function () { // 初始化时尝试静默登录 this.loginAndGetToken(); }, async loginAndGetToken() { try { // 1. 调用 wx.login 获取 code const loginRes = await wx.login(); const code = loginRes.code; // 2. 调用我们自己的后端登录接口,用 code 换 token const res = await http.post('/api/auth/login', { code }); // 3. 存储 token 和用户信息 wx.setStorageSync('token', res.token); this.globalData.userInfo = res.userInfo; // 4. 登录成功后,可以主动拉取一次待办数据 this.fetchTodos(); console.log('登录成功', this.globalData.userInfo); } catch (error) { console.error('登录失败:', error); // 登录失败可以给用户一个友好提示,但不阻断使用(部分功能可能受限) wx.showToast({ title: '登录状态异常,部分功能可能受限', icon: 'none', duration: 2000, }); } }, async fetchTodos() { try { const todos = await http.get('/api/todos'); this.globalData.todos = todos; // 可以在这里触发一个自定义事件,通知页面数据已更新 this.triggerEvent('todosUpdated', todos); } catch (error) { console.error('获取待办列表失败:', error); } }, });

4.2 核心页面开发与数据绑定

首页 (index):展示待办清单列表,并提供添加入口。

index.wxml:

<view class="container"> <!-- 顶部状态/用户信息 --> <view class="header"> <text class="welcome">你好,{{userInfo.nickName || '用户'}}</text> <text class="summary">共有 {{todos.length}} 项待办</text> </view> <!-- 添加按钮 --> <view class="add-btn" bindtap="goToAdd"> <text>+ 添加新事项</text> </view> <!-- 待办列表 --> <scroll-view class="todo-list" scroll-y> <block wx:for="{{todos}}" wx:key="id"> <view class="todo-item">// index.js const app = getApp(); const http = require('../../utils/request'); Page({ data: { userInfo: {}, todos: [], }, onLoad() { // 从全局 App 实例获取数据 this.setData({ userInfo: app.globalData.userInfo, todos: app.globalData.todos, }); // 监听全局数据更新事件(如果 App 中实现了的话) // app.on('todosUpdated', (todos) => { // this.setData({ todos }); // }); }, onShow() { // 每次页面显示时,从服务器同步最新数据 this.fetchTodosFromServer(); }, async fetchTodosFromServer() { wx.showLoading({ title: '加载中...' }); try { const todos = await http.get('/api/todos'); this.setData({ todos }); // 也更新到全局,供其他页面使用 app.globalData.todos = todos; } catch (error) { wx.showToast({ title: '加载失败', icon: 'error' }); } finally { wx.hideLoading(); } }, goToAdd() { wx.navigateTo({ url: '/pages/detail/detail' }); }, goToDetail(e) { const id = e.currentTarget.dataset.id; wx.navigateTo({ url: `/pages/detail/detail?id=${id}` }); }, formatTime(dateStr) { const date = new Date(dateStr); const now = new Date(); const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); if (diffDays === 0) { return '今天'; } else if (diffDays === 1) { return '昨天'; } else if (diffDays < 7) { return `${diffDays}天前`; } else { return `${date.getMonth() + 1}-${date.getDate()}`; } }, });

详情/编辑页 (detail):用于创建新事项或编辑现有事项。

detail.js的核心逻辑:

Page({ data: { id: null, // 编辑时传入的 id title: '', content: '', isCompleted: false, isEditing: false, // 是否处于编辑模式 }, onLoad(options) { if (options.id) { // 编辑模式:根据 id 加载现有数据 this.setData({ id: options.id, isEditing: true }); this.loadTodoDetail(options.id); } // 创建模式:保持数据为空 }, async loadTodoDetail(id) { // 这里可以从全局列表里找,也可以单独请求接口。为了数据最新,建议请求接口。 const todos = getApp().globalData.todos; const todo = todos.find(t => t.id == id); if (todo) { this.setData({ title: todo.title, content: todo.content || '', isCompleted: todo.status === 'completed', }); } }, onTitleInput(e) { this.setData({ title: e.detail.value }); }, onContentInput(e) { this.setData({ content: e.detail.value }); }, onStatusChange(e) { this.setData({ isCompleted: e.detail.value }); }, async handleSubmit() { const { id, title, content, isCompleted, isEditing } = this.data; if (!title.trim()) { wx.showToast({ title: '请输入标题', icon: 'none' }); return; } const todoData = { title: title.trim(), content: content.trim(), status: isCompleted ? 'completed' : 'pending', }; wx.showLoading({ title: '提交中...' }); try { let result; if (isEditing && id) { // 更新 result = await http.put(`/api/todos/${id}`, todoData); wx.showToast({ title: '更新成功' }); } else { // 创建 result = await http.post('/api/todos', todoData); wx.showToast({ title: '创建成功' }); } // 操作成功后,返回上一页并刷新列表 const pages = getCurrentPages(); const prevPage = pages[pages.length - 2]; // 上一个页面(首页) if (prevPage && prevPage.fetchTodosFromServer) { prevPage.fetchTodosFromServer(); } wx.navigateBack(); } catch (error) { wx.showToast({ title: '操作失败', icon: 'error' }); } finally { wx.hideLoading(); } }, async handleDelete() { const { id } = this.data; if (!id) return; wx.showModal({ title: '确认删除', content: '删除后无法恢复,确定删除吗?', success: async (res) => { if (res.confirm) { wx.showLoading({ title: '删除中...' }); try { await http.delete(`/api/todos/${id}`); wx.showToast({ title: '删除成功' }); // 删除成功后,返回首页并刷新 const app = getApp(); app.fetchTodos(); // 调用 App 中的方法更新全局数据 wx.navigateBack(); } catch (error) { wx.showToast({ title: '删除失败', icon: 'error' }); } finally { wx.hideLoading(); } } } }); }, });

4.3 样式优化与交互细节

小程序使用 WXSS,其语法与 CSS 大部分相同。一些关键的样式和交互点:

  • 状态反馈:任何网络请求(加载、提交、删除)都要配合wx.showLoadingwx.hideLoading给用户明确的等待提示。操作成功或失败后,使用wx.showToast给予即时反馈。
  • 下拉刷新:在index.json中配置"enablePullDownRefresh": true,并在index.js中实现onPullDownRefresh方法,调用fetchTodosFromServer,并在完成后调用wx.stopPullDownRefresh()
  • 本地缓存:对于列表数据,可以考虑在请求成功后用wx.setStorageSync缓存一份,在onLoad时先读取缓存展示,再发起网络请求更新,这样可以提升页面打开速度,实现“秒开”体验。
  • 防抖与节流:对于搜索框输入、频繁触发的按钮(如提交),可以考虑使用防抖函数,避免不必要的请求或重复提交。

5. 联调、部署与上线前 checklist

前后端代码都写完,本地也测试得差不多了,接下来就是让它们在网络上“跑起来”,并最终发布。

5.1 本地联调与真机调试

  1. 启动后端服务:在mini-program-backend目录下运行node app.js或使用nodemon热重启。确保服务在http://localhost:3000正常运行。
  2. 配置小程序端:将utils/request.js中的BASE_URL暂时改为你的本地局域网 IP 地址,例如http://192.168.1.100:3000。这样手机和电脑在同一 WiFi 下时,手机上的小程序才能访问到你的本地后端。
  3. 修改小程序配置:在微信开发者工具中,点击右上角“详情”->“本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。这只是为了本地开发调试,上线前必须取消!
  4. 真机预览:在开发者工具中点击“预览”,用手机微信扫描二维码。在手机上操作,观察控制台日志和网络请求,排查问题。

5.2 服务器部署(以 Linux 云服务器为例)

本地联调通过后,我们需要将后端代码部署到公网可访问的服务器上。

  1. 购买与连接服务器:购买一台云服务器(如腾讯云、阿里云的轻量应用服务器),选择 Linux 系统(如 Ubuntu 22.04)。使用 SSH 工具(如 Terminal, Xshell)连接服务器。
  2. 环境准备
    # 更新系统包 sudo apt update && sudo apt upgrade -y # 安装 Node.js (使用 NodeSource 安装较新版本) curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt install -y nodejs # 安装 MySQL sudo apt install -y mysql-server sudo mysql_secure_installation # 运行安全初始化脚本,设置 root 密码等 # 安装 PM2 (进程管理工具) sudo npm install -g pm2
  3. 上传代码与安装依赖:可以使用 Git 克隆你的代码仓库到服务器,或者用 SFTP 工具上传代码。
    cd /path/to/your/project npm install --production # 只安装生产依赖
  4. 配置生产环境变量:在服务器上创建.env文件,填入生产环境的数据库配置、JWT 密钥、微信 AppSecret 等。务必确保文件权限安全,避免泄露
  5. 配置 MySQL:登录 MySQL,创建生产数据库和用户,并授权。
    CREATE DATABASE mini_program_todo_prod CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'prod_user'@'localhost' IDENTIFIED BY 'StrongPassword123!'; GRANT ALL PRIVILEGES ON mini_program_todo_prod.* TO 'prod_user'@'localhost'; FLUSH PRIVILEGES;
    修改后端config/db.js.env文件,使用生产数据库配置。
  6. 使用 PM2 启动服务
    cd /path/to/your/project # 启动应用,并命名为 `mini-backend` pm2 start app.js --name mini-backend # 设置开机自启 pm2 startup pm2 save
    PM2 会管理你的 Node.js 进程,崩溃后自动重启,非常方便。
  7. 配置 Nginx 反向代理(可选但推荐):直接暴露 Node.js 的 3000 端口不太安全,通常用 Nginx 做反向代理,并配置 HTTPS。
    sudo apt install -y nginx
    编辑 Nginx 站点配置/etc/nginx/sites-available/your-domain
    server { listen 80; server_name your-backend-domain.com; # 你的域名 location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
    创建软链接并重启 Nginx:
    sudo ln -s /etc/nginx/sites-available/your-domain /etc/nginx/sites-enabled/ sudo nginx -t # 测试配置 sudo systemctl restart nginx
  8. 申请 SSL 证书(必须):小程序要求后端 API 必须使用 HTTPS。你可以使用 Let‘s Encrypt 免费证书,云服务商也提供一键申请。配置好 HTTPS 后,Nginx 配置会监听 443 端口。

5.3 小程序上线前终极 Checklist

后端部署完成后,更新小程序端request.js中的BASE_URL为你的 HTTPS 域名。

在小程序管理后台(mp.weixin.qq.com)进行以下配置:

  1. 服务器域名配置:在“开发”->“开发管理”->“开发设置”->“服务器域名”中,将你的后端 API 域名(如https://api.yourdomain.com)添加到request 合法域名列表中。注意:不能使用 IP 地址,必须使用已备案的域名,且必须为 HTTPS
  2. 上传代码:在开发者工具中点击“上传”,填写版本号和备注。
  3. 提交审核:在管理后台“版本管理”中,将上传的版本提交审核。审核通过后,即可发布上线。
  4. 数据监控与错误收集:考虑接入微信小程序自带的“监控”功能,或使用第三方服务(如 Fundebug, Sentry)来收集前端错误日志,这对于线上问题排查至关重要。

走到这一步,你的第一个包含前后端的完整微信小程序项目,就已经从本地开发环境,真正跑在了互联网上,可以被任何微信用户访问和使用了。这个过程里,你不仅学会了如何写代码,更掌握了将一个想法变成可上线产品的完整工作流。这其中的每一步,从设计到开发,从调试到部署,都是全栈工程师的必备技能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/6 14:03:14

Notepad--:高效免费的跨平台国产文本编辑器终极指南

Notepad--&#xff1a;高效免费的跨平台国产文本编辑器终极指南 【免费下载链接】notepad-- 一个支持windows/linux/mac的文本编辑器&#xff0c;目标是做中国人自己的编辑器&#xff0c;来自中国。 项目地址: https://gitcode.com/GitHub_Trending/no/notepad-- Notepa…

作者头像 李华
网站建设 2026/8/6 14:00:47

飞书文档转Markdown终极指南:一键转换的完整解决方案

飞书文档转Markdown终极指南&#xff1a;一键转换的完整解决方案 【免费下载链接】cloud-document-converter Convert Lark Doc to Markdown 项目地址: https://gitcode.com/gh_mirrors/cl/cloud-document-converter 还在为如何将飞书文档转换为Markdown格式而烦恼吗&am…

作者头像 李华
网站建设 2026/8/6 14:00:42

3分钟解决Windows运行安卓应用的终极方案:APK安装器完全指南

3分钟解决Windows运行安卓应用的终极方案&#xff1a;APK安装器完全指南 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 想象一下这样的场景&#xff1a;你在电脑前想要…

作者头像 李华
网站建设 2026/8/6 14:00:37

5秒获取百度网盘提取码:免费资源获取的终极解决方案

5秒获取百度网盘提取码&#xff1a;免费资源获取的终极解决方案 【免费下载链接】baidupankey 在线查询网盘提取码&#xff08;维护中 rm repo&#xff09; 项目地址: https://gitcode.com/gh_mirrors/ba/baidupankey 还在为百度网盘加密资源而烦恼吗&#xff1f;当你在…

作者头像 李华
网站建设 2026/8/6 14:00:22

从组态软件到Web可视化:SagooIoT组态工具的工程化设计实践

从组态软件到Web可视化&#xff1a;SagooIoT组态工具的工程化设计实践 一、凌晨三点的电话 上周三凌晨三点&#xff0c;我被一阵急促的电话铃声惊醒。 电话那头是浙江一家注塑工厂的技术主管老周&#xff0c;声音里带着明显的焦躁&#xff1a;“平台又崩了&#xff01;组态软…

作者头像 李华