做微信开发这几年,被问得最多的一个问题是:“我后端口已经写在本地了,小程序里怎么调?没有公网 IP 能调试吗?”答案是能,而且官方工具链从一开始就没打算让你先把后端部署到线上再联调。模拟器、真机调试、局域网预览这三板斧用起来,本地开发调试整个流程都不需要公网 IP。这篇文章把我自己项目里的实操流程、开关位置、踩过的坑全盘写出来,给同样被域名校验和签名问题折磨过的朋友做个参考。
1. 先搞明白:本地调试到底在调什么
1.1 微信开发者工具的定位:一台“装在电脑里的微信手机”
很多人对微信开发工具有个误解,以为它只是一个代码编辑器,真正的运行环境在手机端,所以必须先把代码部署到公网服务器上才能联调。实际上,开发者工具内部集成了一个完整的模拟器,这个模拟器本质上是帮你跑起来一台“虚拟微信客户端”。
这台虚拟客户端跑在你自己电脑上,所以你在工具里编译小程序、打开 H5 页面,所有的渲染和逻辑执行都发生在本地。换句话说,你的代码发起wx.request请求时,目标地址写成http://127.0.0.1:3000,这个请求就是从一个本地进程发到另一个本地进程,中间根本不经过公网,自然也就不需要 “公网 IP” 这个概念参与。
理解了这一点,后面所有操作就顺理成章了:开发期内,微信不校验你的请求域名是合法的——因为校验的目的是约束生产环境,不是给开发流程添堵。模拟器就是给你一个“受控环境”,让你在没有公网资源的情况下先把功能调通。
1.2 哪些场景必须上服务器,哪些不用
这是我每到一个新团队都会先讲清楚的一张表。很多争论其实都来源于场景没分清楚:
| 调试场景 | 是否需要公网 IP | 理由 |
|---|---|---|
| 小程序模拟器请求本地接口 | 不需要 | 请求从工具本地发出,直达 localhost |
| 小程序真机调试 | 不需要 | 代码包经微信官方服务器下发,电脑不需要暴露任何端口 |
| 公众号 H5 页面在模拟器调试 | 不需要 | 模拟器是本地环境,直接填本地 URL |
| 手机和电脑同一 Wi-Fi 下的局域网预览 | 不需要 | 手机访问的是你电脑的局域网 IP |
| 生产环境正式访问 | 需要 | 域名、证书、备案都必须在公网生效 |
| 支付回调、第三方平台主动推送 | 需要 | 对方需要能主动找到你的服务 |
最后一类才是真正绕不开公网 IP 的场景,比如微信支付成功后,微信服务器要把回调通知发到你的接口,这时候你的服务必须能被公网访问。但这类问题属于“生产联调”,不是“本地开发调试”,别混为一谈。
2. 小程序本地调试:模拟器、域名开关、真机调试
2.1 模拟器调试与自定义编译模式
新手最容易卡在第一步:代码写了,怎么在工具里跑起来。
打开微信开发者工具,导入小程序项目后,默认是“普通编译”模式,直接生成一个模拟器页面。这个页面里就是你的小程序,点哪个按钮、跳哪个页面、发什么请求,行为和真机没有本质区别。
我习惯一到手先做一件事:配置“自定义编译模式”。点工具顶部的编译下拉框,选“添加编译模式”,可以指定启动页面、启动参数。这在实际开发里太重要了,比如你要调一个订单详情页,页面路径需要带上订单号,如果每次都要从首页一路点进去,时间全浪费在点击上了。自定义编译模式相当于给页面提供了一个“直达通道”,我一般在项目初始化时就把列表页、详情页、登录页各配一个模式,切换着调特别顺手。
接口请求这里,先给一段最基础的本地方案:
// config.js const API_BASE_URL = 'http://127.0.0.1:3000' // login.js wx.request({ url: `${API_BASE_URL}/api/login`, method: 'POST', data: { code: res.code }, success: (res) => { console.log('login result:', res.data) }, fail: (err) => { console.error('login failed:', err) } })127.0.0.1在这里就是工具所在电脑的地址,模拟器里所有请求都会落到本机。后端只要把服务跑起来,监听在 3000 端口,两边就能直接对上。
2.2 关闭域名校验:解决“url not in domain list”
如果你在模拟器里第一次发请求,大概率会看到类似request:fail url not in domain list的报错。这时候别急着怀疑后端写错了,十有八九是微信的域名校验在拦你。
开发工具默认会校验请求域名,要求请求地址必须在“小程序后台的 request 合法域名列表”里配置过。但本地开发连公网 IP 都没有,哪来的正式域名?微信显然也想到了这一点,所以给工具留了一个总开关。
在工具的“详情”面板里,找到“本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。勾上之后,工具的模拟器环境就不再对请求域名做任何校验,http://127.0.0.1:3000这样的本地地址可以随便请求。
这个开关只对当前项目的本地会话生效,不会影响你提交到后台的代码包,更不会改变真机上正式版小程序的校验逻辑。有一点要提醒:不要因为这个开关好用,就顺手把生产环境的域名校验也关了。我之前见过一个同事,为了图省事把正式环境的配置也改了,结果页面线上报错排查了大半天。开发工具里的开关只管本地,该守的合规边界一步都不能越。
2.3 真机调试:那条扫码通道是怎么工作的
模拟器毕竟不是真机,很多能力(比如摄像头、蓝牙、真实的网络环境)模拟不了,所以微信又给了第二个杀器:真机调试。
操作很简单:工具顶栏点“真机调试”,会弹出一个二维码,手机微信扫码,会自动打开一个小程序调试版。这时候你会在手机上看到真实小程序运行,同时在电脑工具的调试器面板里,能看到手机的 console 日志、网络请求列表、AppData 数据,像给手机装了一根数据线。
这里的原理值得说透:工具先把你当前代码打成带“调试标记”的包,通过微信官方服务器下发到手机微信;手机端运行小程序时,产生的日志和调试数据,再通过官方服务器回传到电脑工具。整个过程,你的电脑不需要暴露任何公网端口,手机也不要求和电脑在同一局域网——只要有网就行。
这也是“无需公网 IP”这个标题里最核心的一层含义:微信自己搭建了这条“调试专用高速路”,开发者的本地服务根本不需要出现在公网上。
真机调试有个前置条件:扫你二维码的微信,必须是这个小程序项目的开发者或体验者。如果你在项目成员列表之外,扫码会提示无权限。首次使用建议先在小程序后台把自己的微信号加进去。
3. 公众号 H5 页面的本地调试
3.1 用模拟器直接打开本地页面
小程序之外,公众号 H5 网页也是微信开发的常见形式。这类页面挂在公众号菜单里,用户点开一个链接,在微信内置浏览器里运行。调这种东西,很多人第一反应也是丢到服务器上。但其实开发者工具同样能模拟 H5 环境。
新建项目时选择“公众号网页项目”,在地址栏填http://localhost:8080(或者你本地前端 dev server 跑的端口),工具会把它当作微信内置浏览器打开。页面的 JS 逻辑、样式渲染,以及微信 JSSDK 提供的wx.chooseImage、wx.scanQRCode等接口,都能在模拟器里调用。
注意一点:模拟器里的 JSSDK 调用,和真机有一个细微差别。模拟器内置了一套“仿真 JSSDK”,部分能力是模拟实现的,比如chooseImage会弹出一个本地文件选择框,而不是真的调起手机相机。这对页面逻辑调试完全够用,但如果要验证真机上的原生能力,最终还得落到真机预览。
3.2 解决 JS-SDK 签名的域名匹配问题
公众号 H5 调试里最折磨人的就是wx.config签名。
公众平台要求,使用 JSSDK 接口前,当前页面域名必须在“JS 接口安全域名”里,同时后端要根据页面真实 URL 计算签名。你的本地地址是 localhost,既不在安全域名列表里,签名接口就算计算出来,微信校验域名也对不上。
我的土办法是用 hosts 文件把公网正式域名指到本机。具体分三步:
第一步,在你自己的电脑上改 hosts:
127.0.0.1 dev.example.com第二步,本地前端 dev server 监听 80 端口,或者直接在http://dev.example.com:3000访问。注意这里访问的域名是dev.example.com,和你在公众平台配置的 JS 接口安全域名是同一个域名,只是 IP 被解析到了本机。
第三步,页面里发起wx.config时,确保传给签名接口的 URL 就是window.location.href.split('#')[0]:
const currentUrl = window.location.href.split('#')[0] axios.get('/api/wechat/jssdk-config', { params: { url: currentUrl } }).then((res) => { wx.config({ debug: false, appId: res.data.appId, timestamp: res.data.timestamp, nonceStr: res.data.nonceStr, signature: res.data.signature, jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'] }) })后端签名接口拿到 URL 后,按官方规则用jsapi_ticket参与 SHA-1 签名,再把参数原样返回。这个方案的精髓在于:页面的实际访问域名和正式配置完全一致,微信校验时自然放行,而流量全程只走本地。
这里有一个非常容易踩的坑:签名 URL 必须和当前页面 URL 完全一致,包含路径和 query,但不包含 hash。如果页面里有?from=xxx这种参数,前端传给签名接口的 URL 忘了带,百分百报invalid signature。我自己的习惯是,前端永远动态取window.location.href.split('#')[0],后端也不做任何“规范化处理”,直接拿着这个 URL 去签。
3.3 本地联调时的跨域与会话处理
H5 前端页面在dev.example.com:3000,后端 API 在dev.example.com:5000,这种跨端口访问天然就是跨域。生产环境通常用 Nginx 做反向代理让两者同域,本地调试我没那么大兴致去配代理,直接用 CORS 放行。
后端如果是 Node.js 的 Express,中间件加几行头就行:
app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*') res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With') res.setHeader('Access-Control-Allow-Credentials', 'true') if (req.method === 'OPTIONS') return res.sendStatus(204) next() })这里把Access-Control-Allow-Origin回显成请求的Origin,是为了配合后面withCredentials带 cookie 跨域。公众号 H5 的用户会话,生产环境全靠微信内置浏览器的 cookie 维持,如果本地调试不处理 cookie,登录态就会断。最省心的做法还是前面说的 hosts 方案:前端、后端都在同一个域名下,跨的是端口不是跨域,cookie 的写入和回传逻辑和生产环境表现一致,CORS 那套反而不用碰了。
4. 真机预览:把手机变成你的测试设备
4.1 手机和电脑同 Wi-Fi,教你让手机访问本地服务
除了小程序真机调试,还有一种更“原始”的真机验证方式:局域网预览。适合调试 H5 页面,以及那些不需要微信官方通道参与的场景。
前提很简单,手机和电脑连同一个路由器。然后看电脑的局域网 IP,Windows 用ipconfig,macOS 用ifconfig,找到类似192.168.1.100这样的 IPv4 地址。接着保证你的本地后端服务监听在0.0.0.0,而不是只监听127.0.0.1。很多人第一步就栽在这里——服务起来了,但手机访问不通,其实就是监听地址写死了。
比如 Node 服务:
# 让所有网卡都能访问,手机才能通过局域网 IP 连进来 node server.js --host 0.0.0.0 --port 3000手机微信里直接访问http://192.168.1.100:3000,或者你生成一个二维码扫码打开,就能在真机上看到本地页面。
小程序也类似。开发阶段可以把请求地址从127.0.0.1临时改成电脑的局域网 IP,然后点工具的“预览”生成二维码,手机扫码打开体验版,页面里的所有请求就会打到你的电脑上。这样虽然不像真机调试那样有完整的调试面板,但胜在“所见即所得”,特别适合做流程体验走查。
4.2 局域网调试的五个常见拦路虎
这一小节是我这几年被问得最多的五个问题的汇总,一个个记录下来:
第一,连不上。最常见的原因是路由器开了“AP 隔离”或“访客网络隔离”。这种情况手机和电脑虽然连在同一个 Wi-Fi,但二层网络互相不可达。解决办法是切到主网络的 5G 频段,或者进路由器后台关掉隔离。
第二,防火墙拦截。电脑的防火墙默认会拦掉从局域网进来的陌生端口。Windows 上需要在“允许应用通过防火墙”里把端口加进去,macOS 则会在第一次监听端口时弹窗询问,别手滑点了拒绝。
第三,服务只监听 127.0.0.1。前面已经强调过,确保服务绑定0.0.0.0。可以用lsof -i :3000或者netstat -ano | findstr 3000看当前监听地址。
第四,弄错 IP。电脑可能有多个网卡,虚拟机、蓝牙网络都会生成额外的 IP。别拿虚拟网卡的 IP 给手机访问,要选和你 Wi-Fi 路由器在同一网段的那一个。
第五,HTTP 能力限制。微信内置浏览器对部分敏感能力有 HTTPS 要求,比如getLocation、录音、支付等接口,必须走 HTTPS。局域网内的 HTTP 只适合普通请求和页面展示,涉及这些能力就用真机调试的官方通道,别在局域网里硬磕。
5. 实测踩坑记录:问题速查与经验沉淀
5.1 高频问题排查清单
把上面所有内容浓缩成一张表,遇到问题先翻这里:
| 现象 | 大概率原因 | 处理办法 |
|---|---|---|
工具里request:fail url not in domain list | 域名校验开关没关 | 详情 → 本地设置 → 勾选“不校验合法域名” |
| 模拟器通了,真机调试请求失败 | 真机上的请求目标是 localhost,手机上不存在这个地址 | 把请求地址改成电脑的局域网 IP |
wx.config报 invalid signature | 签名 URL 和页面实际 URL 不一致 | 用window.location.href.split('#')[0]动态取 URL |
| 真机预览打开后白屏 | 本地服务没监听 0.0.0.0,或防火墙拦了端口 | 检查监听地址,放行端口 |
| 手机能上网但连不上电脑服务 | 路由器 AP 隔离 | 换主网络,或关闭隔离 |
H5 页面在工具里wx未定义 | 页面没引入 JSSDK 的官方 JS 文件 | 确认引入http://res.wx.qq.com/open/js/jweixin-1.6.0.js |
| 签名接口一直报错,但域名配置正确 | 后端 signature 计算时参数拼接顺序错了 | 严格按时间戳、随机串、票据、URL 的字典序拼接再 SHA-1 |
5.2 我沉淀下来的几条经验
最后分享几条实践里打磨出来的“土办法”,都不复杂,但能省很多时间。
一是请求地址和 token 管理。本地联调时,前端代码里的 API baseURL 经常在局域网 IP 和 localhost 之间横跳。我习惯在项目里放一个env.js,用注释标注好开发、测试、生产的地址,每次切换只改一行:
// 开发环境:模拟器用 127.0.0.1,真机用局域网 IP const API_BASE_URL = 'http://192.168.1.100:3000'同时把“真机调试时手机和电脑必须能网络互通”这件事写进团队文档,避免新同事每次都要重新问一遍。
二是日志落文件。真机调试时,手机上的console.log会回传到工具面板,这是排查问题的主要途径。但有一些偶发问题,比如网络抖动触发的失败,日志一闪而过根本来不及看。我后来在本地后端加了一个简易请求日志中间件,把每个请求的时间、路径、参数、响应耗时追加到一个本地文件里,手机端报错后直接看文件定位,比在面板里刷屏高效得多。
三是签名接口做一个开关。公众号 H5 联调时,可以给签名接口加一个环境判断:参数带debug=1时,后端从测试用例里取一个固定 URL 来签名,用来快速验证wx.config整条链路通不通。联调完再摘掉开关,回归正常签名逻辑。这个小工具帮我排掉了大量签名问题。
微信本地开发调试这件事,说穿了就是把“官方模拟器 + 官方调试通道 + 局域网直连”这三层组合用对。没有公网 IP 从来不是开发效率的瓶颈,真正浪费时间的,是不清楚哪一层该由哪个工具负责。我现在的固定工作流是:模拟器做页面和业务逻辑调试,真机调试负责验证支付、授权、硬件能力,局域网预览做体验走查,公众号 H5 的 JSSDK 联调则动用 hosts 那一招。这套组合拳打下来,本地开发几乎不需要碰服务器。你如果还卡在域名校验那一步,按第 2 节的开关先勾上再重新编译,多半就能看到本地接口的响应了。