小程序前端只是冰山一角,用户看到的每一个页面背后,都依赖一套为移动端场景设计的后端体系。小程序的网络能力有限(并发连接少、弱网频繁),对后端的接口粒度、响应速度、安全边界要求比传统 Web 更高。而 BFF(Backend For Frontend)层正是为解决「前端要什么、后端有什么」的鸿沟而生。本文从小程序后端架构分层、BFF 聚合设计、接口规范、鉴权体系到云开发与自建选型,给出完整的架构思路。
一、小程序后端架构全景
1.1 三层架构
客户端(小程序)
↓ HTTPS / wss
接入层(网关:限流、鉴权、路由)
↓
BFF 层(聚合、裁剪、编排)
↓
领域服务层(业务逻辑、数据访问)
↓
存储层(数据库、缓存、对象存储)
小程序后端比传统 Web 后端多了一层「为端而生的 BFF」。它不是简单的转发,而是把多个后端服务的结果聚合成一个页面需要的数据结构,并裁剪掉端上不用的字段。
1.2 为什么要 BFF
| 问题 | 无 BFF 的现状 | 引入 BFF 后 |
|---|---|---|
| 接口粒度 | 端上要发 N 个请求拼数据 | 一个聚合接口搞定 |
| 字段冗余 | 返回大量无用字段,浪费流量 | 按端裁剪字段 |
| 协议差异 | 各后端协议不统一 | BFF 统一对外契约 |
| 端适配 | 逻辑散落在各端 | 端的适配逻辑集中在一层 |
一句话:BFF 的职责是「让客户端代码简单」,把为页面服务的数据组装工作从客户端挪到服务端。
二、BFF 层聚合设计
2.1 聚合模式
BFF 最典型的场景是页面级聚合:首页需要「用户信息 + 推荐列表 + 公告 + 优惠券」,这四个数据来自四个后端服务。BFF 把它们并行拉取后组装成一个响应:
// BFF 层:并行聚合首页数据
async function composeHomeData(ctx) {
const [profile, feed, banner, coupon] = await Promise.all([
userService.getProfile(ctx.openid),
feedService.getRecommend(ctx.openid),
contentService.getBanner(),
couponService.getAvailable(ctx.openid)
]);
return {
profile: trim(profile, ['openid', 'phone']),
feed: feed.items.slice(0, 20),
banner,
coupon: coupon.available ? coupon.info : null
};
}
2.2 裁剪与字段控制
小程序对流量敏感,BFF 返回的字段应只包含页面渲染需要的数据,敏感字段(openid、内部 ID、手机号)一律在 BFF 层剔除:
// BFF 层的字段白名单裁剪
const USER_WHITELIST = ['nickname', 'avatar', 'level', 'vip'];
function trimUser(user) {
const out = {};
USER_WHITELIST.forEach((key) => {
if (user[key] !== undefined) out[key] = user[key];
});
return out;
}
2.3 超时与降级
聚合多个服务时,任何一个服务慢都会拖垮整体。BFF 必须给每个下游调用设置独立超时与降级策略:
// 下游调用超时控制
async function withTimeout(promise, ms, fallback) {
return new Promise((resolve) => {
const timer = setTimeout(() => resolve(fallback), ms);
promise.then((v) => { clearTimeout(timer); resolve(v); });
});
}
// 推荐服务挂了也返回空列表,不阻塞首页
const feed = await withTimeout(
feedService.getRecommend(openid),
800,
{ items: [] }
);
三、接口设计规范
3.1 统一响应结构
小程序接口应使用统一的响应外壳,客户端解析逻辑只需写一次:
{
"code": 0,
"message": "success",
"data": {
"list": [],
"has_more": false
}
}
| 字段 | 含义 | 约定 |
|---|---|---|
| code | 业务状态码 | 0 成功,非 0 失败 |
| message | 提示信息 | 可直接展示给用户 |
| data | 业务数据 | 结构稳定、字段收敛 |
3.2 错误码规范
| 错误码段 | 含义 | 例子 |
|---|---|---|
| 0 | 成功 | — |
| 10001-10999 | 鉴权类 | 10001 未登录、10002 令牌过期 |
| 20001-20999 | 参数类 | 20001 参数缺失、20002 格式错误 |
| 30001-30999 | 业务类 | 30001 库存不足、30002 重复操作 |
| 50000+ | 服务端错误 | 50001 下游依赖不可用 |
3.3 分页与缓存约定
// 游标分页(避免深分页性能问题)
{
"code": 0,
"data": {
"items": [{ "id": "a1", "title": "..." }],
"next_cursor": "a20",
"has_more": true
}
}
接口层面的缓存策略:
| 数据类型 | 缓存策略 | 生效位置 |
|---|---|---|
| 热点配置 | 30s TTL 缓存 | CDN / BFF 内存 |
| 用户资料 | 写后失效 | Redis |
| 商品/内容 | 小时级缓存 | CDN |
| 订单/金额 | 不缓存 | — |
四、鉴权与安全
4.1 code2session 换取身份
小程序登录的核心是 wx.login 换取 code,再由服务端用 code 换取 openid 与 session_key:
// 服务端:code2session 换取 openid
async function code2session(code) {
const url =
'https://api.weixin.qq.com/sns/jscode2session' +
'?appid=' + APPID +
'&secret=' + APPSECRET +
'&js_code=' + code +
'&grant_type=authorization_code';
const res = await fetch(url).then((r) => r.json());
if (res.errcode) throw new Error('code2session failed: ' + res.errcode);
return { openid: res.openid, sessionKey: res.session_key };
}
4.2 令牌体系
服务端拿到 openid 后签发自有令牌(access_token + refresh_token),客户端所有请求携带令牌:
客户端 → BFF:Authorization: Bearer <access_token>
BFF 校验令牌 → 解析出 openid → 向下游透传可信身份
令牌过期 → 返回 10002 → 客户端用 refresh_token 换新
// BFF 网关:令牌校验中间件
function authMiddleware(ctx, next) {
const header = ctx.req.headers.authorization || '';
const token = header.replace(/^Bearer\s+/i, '');
try {
const payload = verifyJwt(token);
ctx.openid = payload.openid;
return next();
} catch (err) {
ctx.status = 401;
ctx.body = { code: 10002, message: '令牌无效或已过期' };
}
}
一句话:openid 是信任根,服务端所有业务都以「自己解析出的 openid」为准,绝不信任客户端自报身份。
4.3 数据越权防护
多租户小程序必须做垂直越权与水平越权防护:接口层校验「数据归属 openid = 当前 openid」:
// 防止水平越权:只能查询自己的订单
async function getOrder(openid, orderId) {
const order = await orderRepo.find(orderId);
if (!order || order.openid !== openid) {
throw bizError(30003, '无权访问该订单');
}
return order;
}
五、云开发 vs 自建后端
5.1 选型对比
| 维度 | 微信云开发 | 自建后端 |
|---|---|---|
| 上手成本 | 低,无需运维 | 高,需部署与运维 |
| 弹性扩容 | 平台托管 | 自管,需自建 |
| 数据管控 | 云端数据库,可控性中 | 完全自主可控 |
| 复杂业务 | 适合中小规模 | 适合复杂领域模型 |
| 成本 | 按量付费 | 固定成本 + 人力 |
| 与微信打通 | 天然集成云调用 | 走 openapi |
5.2 云开发的适用场景
// 云开发:云函数 + 云数据库
const cloud = require('wx-server-sdk');
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
exports.main = async (event) => {
const db = cloud.database();
const { OPENID } = cloud.getWXContext();
// 云函数天然拿到 openid,无需自建鉴权
const res = await db.collection('orders')
.where({ openid: OPENID })
.orderBy('createdAt', 'desc')
.limit(20)
.get();
return { code: 0, data: res.data };
};
5.3 自建后端的适用场景
当业务出现以下特征时,更应选择自建后端:
- 需要与既有企业系统(ERP、CRM)深度集成
- 对数据安全、合规审计有强要求
- 需要复杂事务、分布式任务、消息队列
- 已有成熟的微服务体系,需要复用
一句话:云开发赢在「快」,自建后端赢在「控」,很多团队用「云开发起步 + 逐步下沉自建」的演进路径。
六、稳定性与扩展保障
6.1 限流与熔断
小程序并发高峰(秒杀、抢购)需要接入层限流,BFF 层对下游做熔断:
| 机制 | 作用 | 参数建议 |
|---|---|---|
| 接入层限流 | 单用户/IP QPS 限制 | 单用户 20 QPS |
| BFF 熔断 | 下游连续失败快速降级 | 失败率 > 50% 熔断 30s |
| 重试 | 幂等接口的重试 | 最多 2 次,指数退避 |
| 兜底 | 缓存空值/降级响应 | 短 TTL 防击穿 |
6.2 幂等设计
支付回调、下单等关键接口必须幂等,用业务幂等键去重:
// 下单接口:同一 order_no 只生效一次
async function createOrder(openid, body) {
const key = `order:${body.order_no}`;
const existed = await redis.get(key);
if (existed) return { code: 0, data: { duplicated: true } };
const order = await orderRepo.create({ ...body, openid });
await redis.set(key, order.id, 'EX', 86400);
return { code: 0, data: order };
}
6.3 日志与可观测
后端日志应带上 openid、请求 ID、耗时,与小程序监控告警打通,实现端到端追踪:
{
"requestId": "req_8f3a",
"openid": "oXXXX",
"path": "/api/home",
"costMs": 45,
"code": 0,
"upstream": {
"feedService": 18,
"userService": 22
}
}
七、总结
小程序后端架构的核心是「为端设计」:BFF 层负责把多服务结果聚合、裁剪成页面直接可用的数据,接口规范追求稳定与统一,鉴权体系以 openid 为信任根并严格防越权,稳定性靠超时、降级、限流、幂等层层保障。云开发与自建后端没有绝对优劣,关键看业务复杂度与团队运维能力,常见路径是云开发快速起步、复杂业务逐步下沉自建。后端稳定,前端才能简单。这套后端体系可与小程序登录鉴权衔接身份链路,配合云开发实战落地云函数方案,再以监控告警守护线上质量,形成完整的小程序服务端能力闭环。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。