一、背景:一个 AppID 里住着两类人
我们的多租户小程序 SaaS,每个商户一个独立品牌、独立 AppID。上一篇讲 ext_json 动态路由让 N 个商户共用一套代码,这篇往里看一层:一个小程序里住着两类用户。
消费者扫码进店、下单支付、领券核销,流量占绝对大头,内部口径九成七以上;商户侧是三个角色——老板看数据改价格,店长管订单管店员,核销员在柜台后扫券码。人不多,页面却重:报表、图表、扫码组件个个吃体积。
最早工作台页面全放主包。去年 12 月一个周四晚上,给工作台加经营日报,图表组件一进来,主包从 1.3M 涨到 1.9M;扫码核销 UI 再塞,开发工具直接报红:主包超 2M,禁止上传。同一晚,合作商户的老板在微信上问:"顾客说打开有点慢,你们是不是偷偷塞东西了?"低端机实测冷启动 3.9 秒——消费者每打开一次,都在替用不到的报表付时间。
拆两个小程序看着最干净,我们否了它。业务约束:模板托管翻倍,每商户两次授权、两份 ext_json、双线提审;订单核销要在同一体系对账,跨 AppID 的 openid 对不上。更关键是心智:老板也是消费者,上线前自己会先下一单;店长核销员本是店里的人,下了班也会买。让同一个人装两个小程序,解释成本比开发成本还高。
于是走另一条路:同一个小程序,分包。一个 AppID、一套模板、一次授权;扫码进店的顾客落在主包,柜台后的店员再走一步进分包。
二、切法:按角色切,不按功能切
定原则是同事一句话拍的板:"页面跟人走,不跟功能走。"按功能切——订单一个分包、商品一个分包——两头都要碰订单,页面归谁?路径全交叉。按角色切就干净:页面是谁的高频路径,就放进谁的包。
主包放公共件、登录与"我的",加消费者高频页:首页、服务详情、下单确认、券包;启动只下主包,消费者的流量不花在工作台上。工作台分包放重货:仪表盘、订单管理、扫码核销、店员与价格管理。
分包多出的代价是进入时一次下载,用预下载抹平:app.json 配 preloadRule,登录后确认是商户侧角色,就在"我的"页预拉工作台分包;wifi 全量预拉,蜂窝网络不拉,不费老板的流量。
三、收敛:三角色的路由守卫
权限收敛三档:老板全权限,店长少改价格一项,核销员只有扫码核销和看当日订单。落到实现是双保险。
前端引导:工作台每个页面的 onLoad 过一道守卫——没角色的送去登录,角色不够的送去引导页,告诉他找谁能开权限。守卫的价值是体验,误入者第一时间得到指引。
服务端兜底:前端校验只是导航。角色声明是登录时服务端签进会话的,工作台每个接口再验一遍——改价格必须是老板,核销必须是三角色之一,验不过一律 403。代码包可解包、请求可伪造,边界永远在服务端(1241 个测试方法里,越权是重点)。
四、体积与启动的账
主包瘦身是一张清单:
- 图标:本地 PNG 清出主包,统一 iconfont,运营图走 CDN,不留一张超过 10K 的图;
- 组件:工作台专用的(图表、扫码 UI、表格)下沉分包,共用的(按钮、弹层)留主包;方向单向——分包能引主包,主包引不了分包;
- 工具函数:报表导出、日期序列化这类重工具挪进分包,主包只留请求封装、埋点、鉴权;
- 启动逻辑:app.js 只做会话初始化,其余按需加载。
瘦下来的账,启动时长是定性收益:主包从 1.9M 回到 1.3M 出头,低端机冷启动从"先愣一下再出首页"变成"直接出首页";工作台分包 0.7M,预下载后点进去基本无感。九成七的流量,从此不用为 3% 的人买单。
五、脱敏配置示例与守卫伪代码
app.json(占位,非真实):
{
"pages": ["pages/index/index", "pages/goods/detail", "pages/mine/mine"],
"subpackages": [
{
"root": "packages/workbench",
"name": "workbench",
"pages": ["dashboard/index", "orders/index", "verify/scan"]
}
],
"preloadRule": {
"pages/mine/mine": { "network": "wifi", "packages": ["workbench"] }
}
}
工作台守卫(JavaScript,伪代码):
const ROLE = { customer: 0, verifier: 1, manager: 2, boss: 3 };
function guard(minRole) {
const s = getApp().globalData.session;
if (!s || !s.roles) { // 未登录,先送去登录
wx.reLaunch({ url: '/pages/mine/mine?from=workbench' });
return false;
}
if (s.roles.level < minRole) { // 角色不够,去引导页
wx.redirectTo({ url: '/packages/workbench/pages/deny/deny' });
return false;
}
return true; // 通过不等于放行,接口侧再验
}
// 页面 onLoad 首行:if (!guard(ROLE.manager)) return;
六、踩坑清单
前两个坑最贵。
坑 1:跨分包跳转的路径。 主包跳工作台,navigateTo 写了相对路径,工具正常,真机直接"页面不存在"——分包页面必须以分包 root 为前缀拼绝对路径。修法:跨分包一律绝对路径,常用路径收进常量文件,code review 就能拦住。
坑 2:分包内的静态资源。 切图随手丢进 packages/workbench/images,首页复用一张,本地正常,真机不显示——分包目录里的资源打进该分包,只有本分包能用,主包引用不到。修法:共享图进主包或 CDN;CI 加条检查,主包代码指向分包目录的引用直接红灯。
其余两个一行带过:
| # | 坑 | 现象与修法 |
|---|---|---|
| 3 | tabBar 页放进分包 | tabBar 页必须在主包;工作台做不成底部 tab,改为主包 tab + 分包二级导航 |
| 4 | preloadRule 不生效 | network 只认 all/wifi;触发点为进入所配置页面,登录不在这页就永不预下载 |
七、三句话总结
- 一个 AppID 装两类用户,分包按角色切、不按功能切:主包只背消费者高频页,工作台重货下沉,预下载抹平等待;
- 三角色权限双保险:前端守卫管引导,服务端校验管边界,缺前者体验差,缺后者不安全;
- 跨分包绝对路径、静态资源归属、tabBar 必须在主包,提前写进规范,比出事后排查便宜一个量级。
运营主体:北京位元跃迁科技有限公司。本文同步发布于本号技术专栏,可搬运至 CSDN/掘金。
