1. 先搞懂机制:第三方平台为什么要下发一份ext.json
1.1 第三方平台模式的运行链路
做微信小程序第三方平台开发的兄弟,对ext.json肯定不陌生。这个东西在普通小程序开发里根本不会出现,但是一旦你开始做“第三方平台、代开发、SaaS多租户”这类业务,几乎每天都要和它打交道。我第一次接触时也困惑了很久:明明小程序有app.json、project.config.json,为什么还要来一个ext.json?
先理清第三方平台的运行链路。微信提供了一套“服务商模式”:服务商开发一套小程序代码模板,上传到微信的模板库;不同客户(比如不同商家)在小程序后台扫码授权后,服务商拿到授权凭证,再调用微信接口,把“模板代码 + 这份客户的个性化配置”一起提交上传。微信会把配置注入到代码包里,每个客户最终得到的小程序,功能逻辑相同,但商家ID、接口域名、页面标题、底部导航这些都可能不一样。
这里的“个性化配置”就是ext.json。它相当于一张装修清单:模板代码是毛坯房,ext.json告诉施工队每个房间要刷什么颜色、摆什么家具。没有它,服务商就得为每个客户复制一份代码,那模板机制就完全失去意义了。
1.2 ext.json与app.json、project.config.json的区别
很多第一次做第三方平台的开发者会把几个JSON文件搞混,我单独说一下区别:
| 文件 | 作用范围 | 生效时机 | 谁在使用 |
|---|---|---|---|
| app.json | 小程序本身的全局配置(页面路径、窗口、tabBar等) | 编译进代码,一直在 | 小程序运行时 |
| project.config.json | 微信开发者工具的工程配置(appid、编译设置等) | 本地开发调试时 | 开发者工具 |
| ext.json | 第三方平台下发的动态配置,覆盖或补充app.json中的部分内容 | 第三方平台接口上传代码时注入 | 小程序运行时,通过 wx.getExtConfigSync() 读取 |
简单说:app.json是“房子自带的设计图”,ext.json是“业主后加的装修要求”。普通小程序开发不会接触ext.json,只有走第三方平台通道上传代码时,微信才会把ext.json的内容注进去。这也是为什么很多在uni-app里开发第三方平台项目的同学,网上搜不到太完整的配置说明。
2. uni-app里ext.json的正确位置与构建产物
2.1 源码放在src目录,不是项目根目录
这是最多人踩的坑。uni-app工程默认的源码目录是src,编译时会把src下的内容映射到dist目录下的微信小程序产物目录(比如dist/dev/mp-weixin)。如果你把ext.json放在项目根目录(和src同级),uni-app编译器根本不会管它,打包产物里自然就没有ext.json。
正确的做法是在src目录下创建一个ext.json文件:
你的uni-app项目/ ├── src/ │ ├── pages/ │ ├── static/ │ ├── App.vue │ ├── main.js │ ├── manifest.json │ ├── pages.json │ └── ext.json ← 必须在这里 ├── dist/ │ └── dev/ │ └── mp-weixin/ │ ├── app.js │ ├── app.json │ └── ext.json ← 编译后会自动生成到这里 ├── package.json └── vite.config.js至于ext.json里写什么内容,第3部分会完整拆解,这里先说位置。
2.2 HBuilderX与CLI命令行开发时的编译产物
如果你用的是HBuilderX,点菜单栏“运行到小程序模拟器→微信开发者工具”,uni-app会先执行编译,把src里的文件处理到dist/dev/mp-weixin目录。这个过程是增量且自动的,只要src/ext.json存在,编译后dist/dev/mp-weixin/ext.json就会一起出现。
如果你用的是CLI工程,命令行执行:
npm run dev:mp-weixin同样会在dist/dev/mp-weixin目录下生成微信小程序产物。记住一个原则:所有需要进微信小程序包的配置,都放src根目录,让uni-app编译器帮你拷贝。我见过有人直接去dist目录里手动改ext.json,结果下一次编译又被覆盖,白改。
2.3 微信开发者工具应该导入哪个目录
打开微信开发者工具,选择“导入项目”,目录指向dist/dev/mp-weixin,AppID填你正在调试的目标小程序(或第三方平台测试号)。如果项目是通过第三方平台模式调试,开发者工具还支持“按第三方平台模式打开”,这种方式下ext.json和wx.getExtConfigSync()才能按线上链路生效。
3. ext.json核心字段逐个拆解与配置示例
3.1 字段总览表
微信官方对ext.json的配置项不算多,但每个字段的边界和用法都有讲究。先放一张总表:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| extEnable | Boolean | 是 | 是否启用ext配置,必须为true,否则ext和extPages都不会生效 |
| extAppid | String | 是 | 第三方平台的AppID,不是小程序AppID |
| extPages | Object | 否 | 覆盖小程序非首页页面的窗口配置 |
| ext | Object | 否 | 自定义业务数据,通过wx.getExtConfigSync()读取 |
| replaceMode | Boolean | 否 | 为true时,extPages配置的字段会覆盖代码中app.json的对应字段 |
| allowAuthorize | Boolean | 否 | 基础库2.2.2起支持,是否允许启动阶段触发授权逻辑 |
| permission | Object | 否 | 隐私接口的授权说明,等价于app.json中的permission |
| tabBar | Object | 否 | 覆盖小程序app.json中的tabBar配置 |
| supportVersion | String | 否 | 指定最低基础库版本,低于该版本无法运行 |
3.2 extEnable、extAppid、ext:最核心的铁三角
extEnable是总开关。官方文档明确要求,要让ext配置生效,extEnable必须为true。如果它是false,哪怕ext、extPages都写了,微信也会当不存在。
extAppid不是目标小程序的AppID,而是第三方平台自身的AppID。这个ID在微信开放平台后台的“第三方平台→基本信息”里能看到,通常以wx开头。有些旧文档示例写成t开头,那是占位符,千万别直接抄。我第一次配置时就把小程序AppID填了进去,结果wx.getExtConfigSync()一直返回空,排查了半天才发现是AppID填错了对象。
ext是业务数据的核心载体,它的结构完全由你自己定义。比如一个连锁店小程序,希望不同门店使用不同API域名、不同门店编号、不同主题色,这些都可以塞进ext:
{ "extEnable": true, "extAppid": "wx1234567890abcdef", "ext": { "apiBase": "https://api.store1001.example.com", "storeId": "1001", "storeName": "示例门店", "themeColor": "#FF6600" } }小程序端读取时,只需要一行代码:
const extConfig = wx.getExtConfigSync ? wx.getExtConfigSync() : {}; console.log('当前门店配置:', extConfig.storeName);需要注意的是,wx.getExtConfigSync()要求基础库不低于1.1.0,现在绝大多数用户微信版本都满足,但做兼容判断总归稳妥。
3.3 extPages与tabBar:给不同商家做差异化页面
extPages用于覆盖小程序非首页页面的窗口配置,比如标题、下拉刷新、导航栏背景色等。它的key是页面路径,value是窗口配置对象:
{ "extEnable": true, "extAppid": "wx1234567890abcdef", "extPages": { "pages/order/detail": { "navigationBarTitleText": "订单详情", "enablePullDownRefresh": true }, "pages/user/center": { "navigationBarTitleText": "会员中心" } } }这里有个容易写错的地方:页面路径不要带前导斜杠,和app.json里的pages写法保持一致。你写成"/pages/order/detail"虽然开发工具不一定报错,但实际覆盖不一定生效。
tabBar在ext.json里可以理解为“对app.json中tabBar的整段覆盖”。如果你的每个商家小程序底部导航都不同,就可以在ext.json里单独配置。它的字段和app.json的tabBar完全一致:color、selectedColor、backgroundColor、borderStyle、list、custom等。第三方平台下发tabBar时,list里的pagePath必须真实存在于代码中,否则会导致页面打不开。
3.4 replaceMode、allowAuthorize、permission、supportVersion的进阶玩法
replaceMode控制的是“覆盖”还是“合并”。官方语义是:当replaceMode为true时,extPages里配置的字段会覆盖小程序代码中app.json里同名字段的配置;如果为false,则以代码内的配置优先。这个字段初看晦涩,但理解了“业主与施工方”的关系就行:replaceMode为true,等于告诉施工方“某些细节听业主的,不要听设计师的”。我建议在需要严格控制页面差异化的场景下开启,其余情况保持false更安全。
allowAuthorize和permission都涉及授权。permission用来声明小程序使用位置、相册等接口的原因说明,它等价于app.json的permission字段。allowAuthorize设置为true后,允许在小程序启动阶段就触发授权弹窗逻辑,适合一些必须在首页立刻用上地理位置或用户信息的业务。
supportVersion用于声明该项目最低支持的基础库版本,比如"2.20.1"。如果某个商家的用户微信基础库版本过低,小程序可能无法正常运行,设置好这个字段至少能在启动阶段给出更友好的提示。
4. 实战场景:一套代码给N个商家下发不同配置
4.1 业务需求拆解
假设有个小程序开发团队,做了一个“连锁门店会员小程序”,现在要卖给100家门店。每家门店有独立的logo、门店编号、API域名、客服电话,但页面结构、业务流程完全一致。如果复制100套代码再分别改,维护成本会爆炸。
用第三方平台+ext.json的方案就清晰了:
- 开发一套通用模板代码;
- 在src/ext.json里配置好每个商家的差异化数据;
- 通过服务端接口,把模板ID和对应商家的ext.json一起上传;
- 商家的小程序运行时自行读取配置,按配置展示不同的门店信息。
4.2 一份可直接抄作业的ext.json
这里给一个完整的配置示例,业务代码可以直接参考:
{ "extEnable": true, "extAppid": "wx1234567890abcdef", "ext": { "apiBase": "https://api.store001.example.com", "storeId": "store001", "storeName": "示例门店", "phone": "400-800-1000", "homeSlogan": "欢迎光临示例门店", "payEnabled": true }, "extPages": { "pages/order/list": { "navigationBarTitleText": "我的订单" }, "pages/coupon/index": { "navigationBarTitleText": "优惠券" } }, "tabBar": { "color": "#999999", "selectedColor": "#FF6600", "backgroundColor": "#ffffff", "borderStyle": "black", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/coupon/index", "text": "优惠" }, { "pagePath": "pages/mine/mine", "text": "我的" } ] }, "permission": { "scope.userLocation": { "desc": "用于查找附近门店" } }, "replaceMode": true, "supportVersion": "2.20.1" }4.3 小程序端读取配置的代码与注意点
在uni-app里,建议在App.vue的onLaunch阶段读取一次,存到全局变量,避免每个页面都重复读:
// App.vue export default { onLaunch() { let extConfig = {}; try { extConfig = wx.getExtConfigSync ? wx.getExtConfigSync() : {}; } catch (e) { console.error('读取ext.json失败', e); } // 挂到globalData,后面所有页面都能用 this.globalData.extConfig = extConfig; this.globalData.apiBase = extConfig.apiBase || 'https://default.example.com'; }, globalData: { extConfig: {}, apiBase: '' } }页面里使用时:
// 某个页面 const extConfig = getApp().globalData.extConfig; this.storeName = extConfig.storeName || '默认门店'; wx.setNavigationBarTitle({ title: extConfig.storeName });注意,extConfig在某些场景下可能是空对象。比如开发者在普通模式下打开项目调试,没有走第三方平台上传链路,这时getExtConfigSync()拿到的就是{}。所以代码里一定要有兜底默认值,不能假设字段一定存在。
4.4 服务端上传代码时如何携带ext_json
服务端把ext.json传给微信,不是传文件路径,而是把ext.json的内容作为JSON字符串放到请求体里。接口地址是:
POST https://api.weixin.qq.com/wxa/upload?access_token=ACCESS_TOKEN关键参数:
| 参数 | 说明 |
|---|---|
| template_id | 第三方平台模板库中的模板ID |
| ext_json | ext.json文件的JSON字符串 |
| user_version | 用户自定义版本号 |
| user_desc | 版本描述 |
Node.js服务端示例:
const axios = require('axios'); async function uploadMiniProgramCode(accessToken, templateId, extJson, userVersion, userDesc) { const url = `https://api.weixin.qq.com/wxa/upload?access_token=${accessToken}`; const res = await axios.post(url, { template_id: templateId, ext_json: JSON.stringify(extJson), user_version: userVersion, user_desc: userDesc }); return res.data; } const extJson = { extEnable: true, extAppid: 'wx1234567890abcdef', ext: { apiBase: 'https://api.store001.example.com', storeId: 'store001' } }; uploadMiniProgramCode( 'ACCESS_TOKEN', 'TPL_123', extJson, '1.0.0', '示例门店第一个版本' ).then(res => console.log(res));我在实际对接时踩过一个坑:ext_json参数忘加JSON.stringify,直接把对象传过去,微信返回的报错是“invalid ext_json”,看起来像字段错误,其实是类型不对。用curl调试时也容易犯这个错:
curl -d '{"template_id":"TPL_ID","ext_json":"{\"extEnable\":true,\"extAppid\":\"wx1234567890abcdef\",\"ext\":{}}","user_version":"1.0.0","user_desc":"test"}' "https://api.weixin.qq.com/wxa/upload?access_token=ACCESS_TOKEN"服务端用的ext_json内容,尽量和开发环境测试的真实配置保持同一结构,避免“开发时能用,线上读不到字段”这种问题。
5. 完整流程:从开放平台注册到审核发布
5.1 开放平台侧的前置准备
做第三方平台开发,先得有一个通过微信认证的开放平台账号。登录后创建第三方平台,拿到四个核心信息:
- 第三方平台AppID
- 第三方平台AppSecret
- 消息校验Token
- 消息加密Key
其中AppID就是ext.json里的extAppid,AppSecret和Token用于后续获取接口调用凭证。创建完成后,还需要配置授权事件接收URL、消息接收URL等服务器地址,这些URL要能处理微信服务器发来的GET和POST请求,完成token校验和消息解密。
5.2 用开发者工具测试授权模式
开发阶段,通常会用“测试号”或者一个真实的普通小程序来做授权测试。给前端同学的建议是:在微信开发者工具里选择“第三方平台”模式,填入第三方平台AppID,再绑定一个已授权的小程序,这样ext.json才能真正按线上链路生效。
如果没有走第三方平台模式,直接用普通AppID打开项目,ext.json虽然躺在代码包里,但wx.getExtConfigSync()拿到的多半是空对象。这一点非常容易让新手误判成“代码写错了”。
5.3 服务端上传、提审、发布的全链路
一个小程序要从模板变成线上正式版,完整链路大概是:
- 商家在H5页面发起授权,微信返回授权跳转链接;
- 商家确认授权后,服务端拿到auth_code;
- 服务端用auth_code换取该商家的authorizer_access_token和authorizer_refresh_token;
- 服务端调用接口设置小程序服务器域名(modify_domain),把ext.json里的apiBase加进白名单;
- 服务端调用wxa/upload上传代码,携带模板ID和该商家的ext_json;
- 代码上传后状态为“开发版本”,可先提交为“体验版本”供内部测试;
- 测试通过后,调用接口提交审核;
- 微信审核通过后,再调用接口“发布”,商家小程序正式上线。
这条链路里最容易出问题的就是第4步和第5步。域名没配好,小程序请求会直接失败;ext_json字符串格式不对,上传就会报错。建议先把第5步单独测试通,再往前推第4步。
6. 常见问题与排查经验
6.1 配置不生效?先按这个顺序排查
每次收到“ext.json不生效”的反馈,我基本按下面这个顺序排查,效率很高:
- 先确认dist/dev/mp-weixin下有没有ext.json。没有,就是源码位置放错了;
- 有文件,打开看extEnable是不是true。false的话一切白搭;
- extEnable没问题,看extAppid是否是第三方平台AppID,而不是小程序AppID;
- AppID正确,看开发者工具是不是用“第三方平台”模式打开的;
- 都不是,直接在小程序启动代码里打日志,打印wx.getExtConfigSync(),看返回什么;
- 如果返回空对象,再确认是不是用的测试号或普通模式打开,必要时让服务端重新走一次上传流程。
这一套流程走下来,至少能解决九成问题。
6.2 常见问题速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 编译产物里没有ext.json | 文件没放在src根目录 | 移动到src/ext.json,重新编译 |
| getExtConfigSync()返回{} | 普通模式调试、extEnable为false、extAppid错误 | 改用第三方平台模式;修正extEnable、extAppid |
| extPages配置的标题不生效 | 页面路径带了斜杠、replaceMode逻辑不对 | 去掉前导斜杠;确认replaceMode是否符合预期 |
| 上传接口报invalid ext_json | ext_json传了对象而不是字符串 | 用JSON.stringify包装 |
| 线上小程序读不到配置 | 没用第三方平台接口上传,走了普通上传 | 必须走wxa/upload接口 |
| 首页没法通过ext.json直接改成别页 | ext.json不控制启动页 | 在首页代码里用wx.reLaunch动态跳转 |
6.3 一个容易忽略的细节:启动页与“加载页”的修改思路
有朋友问我能不能用ext.json直接修改小程序“刚进入时显示的加载页面”。ext.json本身做不了这件事,小程序的启动页永远是代码包里app.json的pages数组第一项。但是你可以做变通:在首页的onLoad里读取ext配置,然后根据配置reLaunch或者redirectTo到真正想展示的页面。
// pages/index/index.vue onLoad() { const extConfig = getApp().globalData.extConfig; if (extConfig && extConfig.homePath) { wx.reLaunch({ url: extConfig.homePath }); } }这样虽然修改的是代码逻辑,但表现效果上,不同商家打开小程序看到的“首屏”可以完全不同。同理,如果你用webview加载H5页面,想把apiBase、storeId这些ext配置传给H5,可以在webview的url后面拼参数,H5通过location.search解析,再用postMessage和uni-app侧做通信,整套方案非常顺。
关于ext.json的配置,其实核心就一句话:代码模板负责“长什么样”,ext.json负责“给谁用、用在哪”。理解了这个,很多字段看一眼官方文档就能上手。我在实际项目中,因为位置放错、AppID填错、字符串格式化不对这3个问题反复折腾过,所以特别写出来,希望大家第一次配置就一次过。