简介:适合微信小程序初学者与服务端开发者,这是一份可直接运行的服务端开发示例,演示了后端接口的基础写法与静态资源托管逻辑。资源包共11个文件,以6个JavaScript源码文件为主,另含依赖清单、转译配置、说明文档及测试文件,压缩后仅8KB,结构精简,便于对照阅读。项目目录展示了从入口文件、核心配置到测试文本的完整项目骨架,可帮助理解小程序服务端从接收请求到返回响应的基本流程。对于刚接触小程序后端开发的读者,该示例提供了最小可运行版本,能够帮助快速打通小程序前端与服务器之间的数据通道,通过阅读源码还可以学习到如何创建网络服务、处理不同请求、设置响应头以及完成基本的参数解析,这些都是在真实项目中频繁使用的技能。该资源已有1583人学习下载,适合正在学习Node.js后端开发或进行小程序前后端联调的读者参考。
1. 微信小程序服务端开发demo到底在解决什么问题
经常有人拿到“微信小程序服务端开发demo(源代码+截图)”以后,在微信开发者工具里打开前端工程,点击登录直接报错,然后怀疑是源码有问题。实际绝大多数情况与代码无关,而是只看到了半条链路——小程序前端负责展示和交互,真正处理登录、数据校验、业务逻辑的是它背后的服务端。这个demo提供的就是这样一套最小可运行的HTTP服务,把登录态交换、鉴权、数据读写串成闭环,再用注释和截图把每个环节的输入输出标清楚。它适合第一次碰前后端联调的学生,适合用uniapp做好前端却卡在服务端接口测试的人,也适合想在微信小程序项目实例里摸清客户端和服务端如何通信的开发者。
2. 服务端demo先立认知:登录态链路与技术栈选型
2.1 为什么小程序必须配一个独立服务端,而不是直接请求微信接口
wx.request可以请求普通的HTTPS接口,但真实项目里没人会让小程序端直接去请求微信的jscode2session接口。原因很直接:小程序前端代码打包下发之后是公开的,任何人用微信开发者工具打开都能看到全部逻辑。如果把AppSecret写在小程序端的page或者utils目录里,等于把账号凭证公开放在路边。因此,所有涉及密钥的操作都必须收口到独立服务端,小程序端只负责把wx.login产生的临时code传给自己的后端。
这条链路的完整时序如下:
小程序端 wx.login() → 得到临时 code 小程序端 wx.request() → 把 code 发到你的服务端 POST /api/login 你的服务端 → 用 appid + secret + code 请求 https://api.weixin.qq.com/sns/jscode2session 微信接口 → 返回 openid(用户唯一标识)和 session_key 你的服务端 → 用 openid 查库或建用户,签发自己的 token 你的服务端 → 把 token 返回给小程序端 小程序端 → 后续请求在 header 里带 Authorization: Bearer <token>这个时序里有三个关键约束。第一,code只能用一次,5分钟过期,重复使用微信会返回40163;第二,openid是小程序与用户两个维度组合出来的唯一ID,同一个用户在不同小程序里的openid不同;第三,session_key不能直接返回给前端,它只用于服务端解密手机号这类敏感数据。把这三条记清楚,调试接口时能少走一半弯路。
2.2 开发demo选Node.js + Express + SQLite,依据是什么
服务端技术栈常见的有Node.js的Express/Koa、Java的Spring Boot、Python的FastAPI/Flask。对于“微信小程序服务端开发demo”这个交付形态,我一般优先选Node.js + Express + SQLite,理由有三个:微信开发者工具和官方文档的生态偏向JavaScript,前端接手时不用切换语言;Express写一个带鉴权的REST接口只需几十行代码,路由和中间件的概念在演示时也容易讲清楚;SQLite是文件型数据库,解压源码包就能跑,不需要额外安装数据库服务端进程,这正好契合“源代码+截图”的交付体验——对方拿到手就能启动。
| 对比项 | Node.js + Express | Java + Spring Boot | Python + FastAPI |
|---|---|---|---|
| 上手成本 | 低,前端可无缝接 | 中高,需要理解注解与容器 | 低,语法简洁 |
| 启动速度 | 秒级 | 秒到十秒级 | 秒级 |
| 内存占用 | 最低 | 最高 | 中等 |
| 生态匹配度 | 微信官方示例多为JS | 企业级规范成熟 | 数据处理方便 |
如果团队主栈是Java,用Spring Boot配H2内存库也是同类思路,接口契约比实现语言更重要。demo阶段不建议引入Redis做token存储、消息队列做异步任务,它们属于规模扩大后再考虑的演进方向,过早引入会让初学者分不清主次。一个能跑通、能截图、能讲明白的最小闭环,比一个依赖复杂但看起来很“企业级”的半成品要有价值得多。
2.3 package.json里的四个依赖,刚好对应demo的四个职责
package.json把依赖固定成下面这样,每一条都对应服务端demo的一个明确职责。
{ "name": "weapp-server-demo", "version": "1.0.0", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "express": "^4.19.2", "axios": "^1.7.0", "jsonwebtoken": "^9.0.2", "better-sqlite3": "^11.3.0" } }express负责HTTP路由与中间件;axios负责服务端主动请求微信的jscode2session接口;jsonwebtoken负责签发和校验登录token;better-sqlite3负责操作本地SQLite数据库文件。四个库没有多余的,演示的时候可以顺着这张清单讲清楚整个服务端demo的骨架。版本号都使用主版本内的最新兼容版本,npm install时会自动解析。
3. 落地一个能跑的最小服务端:建表、登录接口与curl自测
3.1 极简目录结构与数据库初始化
服务端代码遵循极简分层就够,不需要上MVC。一个入口文件负责路由和中间件,一个数据库初始化文件负责建表,运行时自动生成SQLite文件,整个服务端demo的目录就是:
demo-server/ ├── package.json ├── server.js # 入口:路由 + 鉴权中间件 + 接口实现 ├── db.js # 初始化 SQLite 连接和表结构 └── data.sqlite # 运行后自动生成,不需要手动创建db.js的初始化代码:
// db.js - 初始化 SQLite,建表并导出实例 const Database = require('better-sqlite3'); const path = require('path'); const db = new Database(path.join(__dirname, 'data.sqlite')); db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, openid TEXT NOT NULL UNIQUE, nickname TEXT DEFAULT '微信用户', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS access_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, openid TEXT, action TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); module.exports = db;better-sqlite3是同步API,在demo的请求量级下完全够用,代码写起来也比异步数据库驱动更直白。users表里openid必须加UNIQUE约束,保证同一微信用户重复登录时不会产生脏数据;created_at交给数据库默认值,应用层少写一行代码。access_logs这张表不是给业务功能用的,而是为了让截图演示时有据可查——每次接口调用插一条日志,终端和数据库都能看到记录,截图自然更有说服力。
3.2 登录接口:code换token的完整实现与关键参数
server.js是服务端demo的核心,代码分为配置区、登录接口、鉴权中间件、业务接口四块,这里先看登录接口:
// server.js - 微信小程序服务端入口 const express = require('express'); const axios = require('axios'); const jwt = require('jsonwebtoken'); const db = require('./db'); const app = express(); app.use(express.json()); // 以下配置替换成你自己小程序的信息,SECRET 绝不能出现在小程序端代码里 const APPID = 'wx你的appid'; const SECRET = '你的AppSecret'; const JWT_KEY = '自定义随机字符串,至少32位'; // 登录:小程序端把 wx.login 拿到的 code 传过来 app.post('/api/login', async (req, res) => { const { code } = req.body; if (!code) { return res.status(400).json({ code: 400, msg: '缺少 code 参数' }); } // 用 code 向微信服务器换 openid 和 session_key const url = 'https://api.weixin.qq.com/sns/jscode2session' + `?appid=${APPID}&secret=${SECRET}&js_code=${code}&grant_type=authorization_code`; let wxResp; try { wxResp = await axios.get(url); } catch (err) { return res.status(502).json({ code: 502, msg: '微信接口不可达' }); } // 微信返回 errcode 说明换 token 失败,常见 40029 是 code 无效或已使用 if (wxResp.data.errcode) { return res.status(401).json({ code: wxResp.data.errcode, msg: wxResp.data.errmsg }); } const { openid } = wxResp.data; // openid 已存在则忽略,不存在则插入新用户 db.prepare('INSERT OR IGNORE INTO users (openid) VALUES (?)').run(openid); const user = db.prepare('SELECT * FROM users WHERE openid = ?').get(openid); // 用 JWT 签发自定义 token,有效期 7 天,后续请求凭它识别用户 const token = jwt.sign({ openid: user.openid }, JWT_KEY, { expiresIn: '7d' }); res.json({ code: 0, data: { token, user } }); }); // 启动监听 0.0.0.0,真机调试时手机才能通过局域网 IP 访问 app.listen(3000, '0.0.0.0', () => { console.log('server running at http://0.0.0.0:3000'); });逻辑说明:code参数必填,来自小程序端wx.login的返回值,不能用假code做纯接口测试。INSERT OR IGNORE是SQLite的幂等写法,避免了先SELECT判断再INSERT的竞态问题——两个请求同时带着新openid进来时不会重复建用户。jwt.sign默认使用HS256算法,payload里只放openid,不放手机号这类敏感字段。expiresIn设置7天是演示值,正式上线建议调整为2小时短期token加refresh_token刷新机制。app.listen监听0.0.0.0而不是127.0.0.1,是为了让真机调试时手机可以通过局域网IP访问到电脑上的Node进程。
| 参数 | 来源 | 作用 | 注意事项 |
|---|---|---|---|
| APPID | 公众平台→开发管理→开发设置 | 标识你的小程序 | 小程序端也会出现,不算机密 |
| SECRET | 同一页面 | 调用微信接口的凭证 | 只能留在服务端,泄露可被冒用 |
| JWT_KEY | 自己生成 | 签名token的密钥 | demo写死,生产放环境变量 |
| expiresIn | 自己定 | token有效期 | 太短频繁重登,太长增加盗用风险 |
3.3 鉴权中间件与第二个业务接口
只有一个登录接口的demo说服力不够,加一个需要登录才能访问的用户信息接口,顺便演示Express中间件最经典的用法:
// 鉴权中间件:解析 Authorization 头里的 token,失败直接返回 401 function auth(req, res, next) { const token = req.headers.authorization && req.headers.authorization.replace('Bearer ', ''); if (!token) { return res.status(401).json({ code: 401, msg: '未登录' }); } try { req.user = jwt.verify(token, JWT_KEY); next(); } catch (e) { return res.status(401).json({ code: 401, msg: 'token 过期或无效' }); } } // 业务接口:只有携带合法 token 才能拿到用户资料 app.get('/api/profile', auth, (req, res) => { const user = db.prepare('SELECT * FROM users WHERE openid = ?').get(req.user.openid); res.json({ code: 0, data: user }); });逻辑说明:auth函数放在路由路径之后、处理函数之前,Express会先执行它再进入业务逻辑。jwt.verify抛异常说明token被篡改或已过期,统一返回401而不区分具体原因,避免向调用方泄露过多内部信息。解析出来的openid挂在req.user上,处理函数直接取用,不用二次解析token。学会这一种中间件模式,后面加管理员接口、加统计接口都能复用同一套逻辑。
3.4 本地启动与服务端接口测试
依赖安装完成后启动,看到进程监听日志说明服务端就绪:
npm install node server.js # 看到 server running at http://0.0.0.0:3000 说明进程正常用curl做一次服务端接口测试:
curl -X POST http://127.0.0.1:3000/api/login \ -H "Content-Type: application/json" \ -d '{"code":"临时code"}'这里要提醒:code无法凭空伪造,必须是微信开发者工具里wx.login生成的真实code。推荐的做法是先在小程序端临时加一行console.log(code),把打印出来的值复制进curl,或者直接在小程序端触发登录,再看服务端终端打印的日志。返回40029说明code已经过期、被用过,或者是从错误位置复制的。
4. 小程序端联调、截图留档与高频报错排查
4.1 request封装与baseURL的三种配置位置
小程序端不能每个页面都直接写wx.request,那样token注入和错误处理会重复十几遍。封装一个Promise版本的request函数,是所有微信小程序项目实例的标准做法:
// utils/request.js - 在小程序端统一管理请求 const BASE_URL = 'http://127.0.0.1:3000'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (wx.getStorageSync('token') || '') }, success: (res) => { if (res.data.code === 0) { resolve(res.data.data); } else if (res.data.code === 401) { // token 过期,清掉本地登录态并提示重新登录 wx.removeStorageSync('token'); wx.showToast({ title: '登录已过期', icon: 'none' }); reject(new Error(res.data.msg || '登录失效')); } else { reject(new Error(res.data.msg || '请求失败')); } }, fail: reject }); }); } module.exports = { request, BASE_URL };逻辑说明:统一判断服务端返回的code字段,0视为成功;401单独处理,避免用户带着失效token在页面里反复点击都得到同一个异常。BASE_URL有三种配置位置——直接写在utils/request.js里、放在app.js的globalData里、或者独立一个config.js文件维护。demo用常量没有问题,但要注意开发者工具里可以访问127.0.0.1,真机不行,真机必须改成电脑的局域网IP。如果前端是基于uni-app构建的微信小程序,把wx.request换成uni.request,其余封装思路完全一致。
4.2 本地开发阶段的域名校验与豁免
微信小程序生产环境要求request的域名必须是HTTPS,并且要在公众平台后台配置合法域名。但本地开发调试时存在豁免:开发者工具对http://127.0.0.1和http://localhost有默认放行,可以直接请求;如果请求的是局域网IP,比如http://192.168.1.5:3000,就需要在开发者工具右上角「详情」→「本地设置」里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”。这个选项只影响开发者工具,真机预览时仍会受到域名白名单限制,需要走第4.3节的真机调试方案。
提示:勾选绕过域名校验只是开发提速手段,线上版本必须配置HTTPS证书和合法请求域名,否则正式版小程序会直接在wx.request阶段报错。
4.3 真机调试必须处理的三件事
真机预览时所有请求都失败,通常不是代码问题而是网络链路问题,按顺序检查三件事。第一,电脑防火墙是否放行了3000端口,macOS和Windows默认都会拦截来自局域网的新入连接,放行Node进程或3000/tcp端口后重试。第二,手机和电脑必须处于同一局域网,公司办公网经常开启AP隔离,即使连同一个Wi-Fi也无法互通。第三,手机端访问的IP必须是电脑的实际局域网IP,查看方法macOS用ifconfig、Windows用ipconfig,找到类似192.168.1.5的地址,把BASE_URL改成http://192.168.1.5:3000后重新编译。
服务端启动时监听0.0.0.0的意义在这里体现:只监听127.0.0.1时,局域网内的手机连不上电脑上的Node进程。如果不想改IP,也可以用微信开发者工具自带的“真机调试”功能,它会建立一条调试通道,手机端请求映射到开发者工具所在机器,适合快速验证页面逻辑;但要验证真实网络链路,直接改局域网IP更接近上线后的行为。
4.4 “源代码+截图”演示时截图应该怎么截
标题里的“截图”不是随便截两张小程序页面就完事,截图的意义是证明这个demo真正跑通了。合格的演示截图要覆盖三端:小程序端登录成功后的页面、服务端终端的请求日志、数据库里users表和access_logs表的新增记录,三张图拼在一起能还原完整链路。
服务端日志建议在登录接口和业务接口里加console.log,打印请求路径、openid和耗时,这些日志本身就能截图。数据库侧把查询结果显示出来再截,不要只截命令行里CREATE TABLE的输出。小程序端打开开发者工具的Network面板,过滤XHR/Fetch请求之后再截图,请求URL、状态码、响应时间和响应体都出现在同一画面里,这一张图的信息量比任何文字描述都大。
4.5 六个高频报错的定位对照表
demo阶段遇到的报错基本是配置问题,对照下表直接定位:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| request:fail 错误 | BASE_URL写错或服务端没有启动 | 先用电脑浏览器访问该地址确认可通 |
| 401 未登录 | token没写进header或已过期 | 检查Authorization拼写,重新登录 |
| 40029 code无效 | code过期或已被使用 | 每次登录重新调用wx.login |
| 40163 code已被使用 | 同一code重复请求 | code是一次性的,不可能重放 |
| 真机访问超时 | 防火墙拦截或IP不在同一网段 | 放行端口并确认同一局域网 |
| errcode 40013 | APPID格式不完整 | 回到公众平台复制完整的appid |
排查时先看服务端终端有没有收到请求:没收到是网络层问题,收到了再看返回的errcode。接口响应里的code是业务码,HTTP状态码是传输层状态,两者都对了才算链路正常。
5. 把demo打磨到能验收的几个关键细节
5.1 统一业务错误码,让前端分支更简单
demo里常见的写法是每个接口随意返回错误信息,但一个能过审的工程需要统一错误码约定:0成功,400参数错误,401未认证,403无权限,404接口不存在,500服务端异常。前端request函数只要针对401做一次单独处理,其余错误统一走兜底文案。错误码和HTTP状态码可以保持语义一致,但业务判断只认code字段,这样即使以后Nginx层返回502,前端也能区分是业务错误还是基础设施错误,不会被五花八门的响应结构搞乱。
5.2 用一张统计接口让演示有数据
在access_logs表基础上加一个汇总接口,统计最近7天每天请求量,返回形如[{date: '2025-01-13', count: 42}]的结构。实现不超过15行:按日期分组查询access_logs,再用Array.map把SQLite的行转成前端友好的字段名。这个接口让demo从“能登录”升级为“有数据可看”。演示时先后端操作几次,再打开统计页面刷新,数据变化直观可见,比对着空表讲解更有说服力。记录日志时把请求路径一并存入action字段,还能顺手统计哪个接口被调用最频繁。
5.3 上线部署与验收核对清单
验收不能凭感觉,按下面的清单逐项过一遍:
| 核对项 | 验证方式 | 通过标准 |
|---|---|---|
| 本地启动 | npm install后node server.js | 一条命令启动无报错 |
| 功能链路 | 前端登录→拿token→访问profile | 全流程无故障 |
| 异常链路 | 清空token后访问受保护接口 | 前端提示登录已过期 |
| 数据落库 | 重启服务端后登录老用户 | openid不重复创建 |
| 演示截图 | 界面、服务端日志、数据库记录 | 三端截图齐全 |
| 线上配置 | 部署云服务器并配置HTTPS证书 | 公众平台后台已添加合法请求域名 |
最后一项涉及的具体操作是把Node服务部署到带公网IP的服务器上,用Nginx做HTTPS终止,证书可以通过免费证书服务签发,然后把HTTPS域名添加到公众平台后台的request合法域名列表。做完这一步,这个微信小程序服务端开发demo就不再是hello world级别的演示,而是一套可以被复用和继续迭代的最小工程骨架——下次在其上扩展的商品列表、订单模块,都沿用同一套登录、鉴权、日志和错误码约定。
本文还有配套的精品资源,点击获取