恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
从零构建全栈小程序:基于Node.js+Koa2+MySQL的待办清单实战
首页
资讯中心
/
从零构建全栈小程序:基于Node.js+Koa2+MySQL的待办清单实战
从零构建全栈小程序:基于Node.js+Koa2+MySQL的待办清单实战
发布时间:2026/8/6 14:06:12
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 URL2) 统一处理请求头如携带用户 Token3) 统一拦截错误如网络异常、服务端错误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: 静态文件服务后续可能用于部署前端页面或上传的文件。mysql2sequelize: 数据库驱动和 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_NAMEmini_program_todo DB_USERroot DB_PASSWORDYourActualPassword123 DB_HOSTlocalhost JWT_SECRETYourSuperSecretKeyHereAtLeast32Chars接下来定义我们的数据模型。创建models文件夹。首先是用户模型models/user.jsconst { 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.jsconst { 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.jsconst 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_typeauthorization_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_APPID和WX_SECRET是极其敏感的信息必须放在后端服务器的环境变量中.env文件绝对不可以写在小程序前端代码里。前端只传code后端用code和secret去微信服务器换openid。接着是待办事项路由routes/todos.jsconst 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.jsconst 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 classcontainer !-- 顶部状态/用户信息 -- view classheader text classwelcome你好{{userInfo.nickName || 用户}}/text text classsummary共有 {{todos.length}} 项待办/text /view !-- 添加按钮 -- view classadd-btn bindtapgoToAdd text 添加新事项/text /view !-- 待办列表 -- scroll-view classtodo-list scroll-y block wx:for{{todos}} wx:keyid view classtodo-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.showLoading和wx.hideLoading给用户明确的等待提示。操作成功或失败后使用wx.showToast给予即时反馈。下拉刷新在index.json中配置enablePullDownRefresh: true并在index.js中实现onPullDownRefresh方法调用fetchTodosFromServer并在完成后调用wx.stopPullDownRefresh()。本地缓存对于列表数据可以考虑在请求成功后用wx.setStorageSync缓存一份在onLoad时先读取缓存展示再发起网络请求更新这样可以提升页面打开速度实现“秒开”体验。防抖与节流对于搜索框输入、频繁触发的按钮如提交可以考虑使用防抖函数避免不必要的请求或重复提交。5. 联调、部署与上线前 checklist前后端代码都写完本地也测试得差不多了接下来就是让它们在网络上“跑起来”并最终发布。5.1 本地联调与真机调试启动后端服务在mini-program-backend目录下运行node app.js或使用nodemon热重启。确保服务在http://localhost:3000正常运行。配置小程序端将utils/request.js中的BASE_URL暂时改为你的本地局域网 IP 地址例如http://192.168.1.100:3000。这样手机和电脑在同一 WiFi 下时手机上的小程序才能访问到你的本地后端。修改小程序配置在微信开发者工具中点击右上角“详情”-“本地设置”勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。这只是为了本地开发调试上线前必须取消真机预览在开发者工具中点击“预览”用手机微信扫描二维码。在手机上操作观察控制台日志和网络请求排查问题。5.2 服务器部署以 Linux 云服务器为例本地联调通过后我们需要将后端代码部署到公网可访问的服务器上。购买与连接服务器购买一台云服务器如腾讯云、阿里云的轻量应用服务器选择 Linux 系统如 Ubuntu 22.04。使用 SSH 工具如 Terminal, Xshell连接服务器。环境准备# 更新系统包 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上传代码与安装依赖可以使用 Git 克隆你的代码仓库到服务器或者用 SFTP 工具上传代码。cd /path/to/your/project npm install --production # 只安装生产依赖配置生产环境变量在服务器上创建.env文件填入生产环境的数据库配置、JWT 密钥、微信 AppSecret 等。务必确保文件权限安全避免泄露。配置 MySQL登录 MySQL创建生产数据库和用户并授权。CREATE DATABASE mini_program_todo_prod CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER prod_userlocalhost IDENTIFIED BY StrongPassword123!; GRANT ALL PRIVILEGES ON mini_program_todo_prod.* TO prod_userlocalhost; FLUSH PRIVILEGES;修改后端config/db.js或.env文件使用生产数据库配置。使用 PM2 启动服务cd /path/to/your/project # 启动应用并命名为 mini-backend pm2 start app.js --name mini-backend # 设置开机自启 pm2 startup pm2 savePM2 会管理你的 Node.js 进程崩溃后自动重启非常方便。配置 Nginx 反向代理可选但推荐直接暴露 Node.js 的 3000 端口不太安全通常用 Nginx 做反向代理并配置 HTTPS。sudo apt install -y nginx编辑 Nginx 站点配置/etc/nginx/sites-available/your-domainserver { 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; } }创建软链接并重启 Nginxsudo ln -s /etc/nginx/sites-available/your-domain /etc/nginx/sites-enabled/ sudo nginx -t # 测试配置 sudo systemctl restart nginx申请 SSL 证书必须小程序要求后端 API 必须使用 HTTPS。你可以使用 Let‘s Encrypt 免费证书云服务商也提供一键申请。配置好 HTTPS 后Nginx 配置会监听 443 端口。5.3 小程序上线前终极 Checklist后端部署完成后更新小程序端request.js中的BASE_URL为你的 HTTPS 域名。在小程序管理后台mp.weixin.qq.com进行以下配置服务器域名配置在“开发”-“开发管理”-“开发设置”-“服务器域名”中将你的后端 API 域名如https://api.yourdomain.com添加到request 合法域名列表中。注意不能使用 IP 地址必须使用已备案的域名且必须为 HTTPS。上传代码在开发者工具中点击“上传”填写版本号和备注。提交审核在管理后台“版本管理”中将上传的版本提交审核。审核通过后即可发布上线。数据监控与错误收集考虑接入微信小程序自带的“监控”功能或使用第三方服务如 Fundebug, Sentry来收集前端错误日志这对于线上问题排查至关重要。走到这一步你的第一个包含前后端的完整微信小程序项目就已经从本地开发环境真正跑在了互联网上可以被任何微信用户访问和使用了。这个过程里你不仅学会了如何写代码更掌握了将一个想法变成可上线产品的完整工作流。这其中的每一步从设计到开发从调试到部署都是全栈工程师的必备技能。