我们有一个 monorepo 结构的项目,需要把里面的 API 服务部署到微信云托管,让小程序切过去。 看起来是很标准的操作,实际踩了 5 个坑,每个都卡了大半天。 这篇文章按踩坑顺序还原全过程。如果你正准备做同样的事,能省掉这些时间。
先说结论
目标很朴素:把 apps/api 发到微信云托管服务上,让小程序切过去,顺便摆脱"每次都要配 request 合法域名"的麻烦。
做完之后有两条结论,第二条是踩了坑才真正明白的:
- API 服务本身可以正常部署到微信云托管;
- "用了云托管就不用配请求域名"是个不完整的说法——只要小程序底层还在用
wx.request,哪怕请求目标是https://*.sh.run.tcloudbase.com,照样受request 合法域名限制。想真正免配,必须改成wx.cloud.callContainer。
下面按我们踩坑的顺序展开。
坑 1:从 monorepo 根目录发版,代码包超限
现象
最自然的做法是站在仓库根目录直接发:
wxcloud run:deploy . -e <envId> -s <service-name>
结果上传阶段就失败了,报错是请求体过大:
ERR_FR_MAX_BODY_LENGTH_EXCEEDED
我们当时的判断
第一反应是"是不是要付费升级套餐"或者"是不是云托管对包大小限制太严"。这两个方向都错了。
根因
仓库是 monorepo,. 意味着把整个仓库根目录都当成部署上下文打包,包括:
- 根目录的
node_modules - 各种临时目录
- 其他 app / package 里与本服务无关的内容
打包体积远超云托管的上传限制。这跟套餐没关系,是打包范围的问题。
解法
不要从仓库根目录上传。我们先跑一个准备脚本,产出一个精简发布目录:
node scripts/cloudrun/prepare-wechat-api.mjs
# 产出:tmp/wechat-cloudrun-api
然后发布这个目录,而不是仓库根目录:
wxcloud run:deploy tmp/wechat-cloudrun-api \
-e <envId> \
-s <service-name> \
--dockerfile Dockerfile \
--containerPort 3001 \
--noConfirm --override
这一点值得记下来
"部署上下文"和"项目根目录"是两回事。 任何容器化部署(云托管、CloudBase、Vercel、Docker build)都会把指定目录整个打进去。monorepo 下这个目录必须是"发布目录",不能是仓库根。
坑 2:镜像构建成功,但健康检查一直失败
现象
换用精简目录之后,镜像构建和上传都成功了,但版本部署失败。日志里能看到健康检查探测失败,探测打的是 80 端口,而应用实际监听的是 3001。
根因
云托管的容器端口配置是 80,服务实际监听 3001,两边对不上。我们的 API 服务读取 PORT 环境变量,默认值是 3001:
apps/api/src/lib/env.ts
apps/api/Dockerfile
解法
发布时显式声明容器端口:
wxcloud run:deploy tmp/wechat-cloudrun-api \
-e <envId> \
-s <service-name> \
--dockerfile Dockerfile \
--containerPort 3001 \
--noConfirm --override
部署完别只看控制台状态,直接打健康检查确认:
curl https://<cloud-hosting-domain>/health
一点经验
"构建成功"和"部署成功"是两个阶段。 构建只验证镜像能不能做出来;部署还要过健康检查。看日志时要分清卡在哪一步,否则容易在错误的方向上排查。
坑 3:/health 返回 200,业务接口全 500
现象
这次部署成功了,/health 返回 200。但一测业务接口:
/home/overview → 500
/announcements → 500
为什么这个现象容易误导人
健康检查过了,直觉会认为"服务是好的,是接口代码有问题"。但健康检查只证明进程活着,不证明依赖可用。
根因
服务本身起来了,但运行时依赖没准备好。业务接口依赖 Supabase,需要三个环境变量:
SUPABASE_URL
SUPABASE_ANON_KEY
SUPABASE_SERVICE_ROLE_KEY
云托管那边的环境变量没有正确注入到运行时,所以初始化客户端时就失败了。
排查方法(这篇里最值得抄走的一招)
先本地带同一套环境变量复现,不要直接猜云端配置。
set -a; source ./.env; set +a
pnpm --filter api start
然后本地打三个接口:
curl http://127.0.0.1:3001/health
curl http://127.0.0.1:3001/home/overview
curl http://127.0.0.1:3001/announcements
本地全绿,就说明三件事:
.env里的配置是真有效的- 业务接口逻辑没有问题
- 问题一定出在"环境变量怎么进到云端运行时"这一步
这三条一确认,排查范围立刻从"整个链路"缩到"一个环节"。
沉淀成 checklist
当 /health 正常但业务接口 500 时,按这个顺序走:
- 先确认服务是不是只是活着(health 只代表这个)
- 再看业务依赖是否可用(数据库、缓存、第三方 API)
- 优先本地带同一套环境变量复现,别凭感觉改云端配置
坑 4:--envParams 同步环境变量不稳定
现象
既然问题出在环境变量,就想着用 --envParams 一次性同步上去:
wxcloud run:deploy ... --envParams=<...>
结果服务参数更新失败,后端报了两类错误:
UnknownParameter
Conf.OperationMode
试了几次,有时成功有时失败,没有稳定规律。
我们的处理策略
这里做了个取舍:不在工具链的坑里死磕。定下的原则是:
- 先试
--envParams - 如果 CLI 或后端更新参数失败,改走临时精简包继续发布,不要在整仓上传或参数同步环节卡死
- 环境变量相关的问题,一律先在本地验证真值,再决定云端怎么注入
为什么值得单独记一笔
踩坑时最容易犯的错是在工具链问题上无限投入。判断标准很简单:
- 如果这个坑阻塞主流程 → 想办法绕过,先让流程跑通
- 如果这个坑只是不够优雅 → 记下来,别现在解决
--envParams 属于前者,所以我们绕过它,而不是花两天研究它为什么不稳定。
坑 5:小程序报 request:fail url not in domain list
这个问题最值得写,因为我们的初始认知就是错的。
现象
API 服务已经在云托管上跑起来了,但小程序首页仍然报:
request:fail url not in domain list
我们当时以为的(错的)
"都用了微信云托管了,应该不需要配域名了吧?"
这个说法不完整,而我们把"不完整"当成了"成立"。
真正的原因
小程序底层的请求方式没有变,仍然是 wx.request(Taro 项目里对应 HTTP 请求)。
只要请求方式本质还是公网 HTTP 请求,不管目标地址是不是 https://*.sh.run.tcloudbase.com,都受 request 合法域名 限制。
云托管提供的是容器调用的通道,不是"自动把你的 HTTP 请求变成免配置的内部调用"。
关键区分
| 请求方式 | 是否受 request 合法域名 限制 |
|---|---|
wx.request → 云托管公网域名 |
受限 |
wx.request → 你自己的域名 |
受限 |
wx.cloud.callContainer |
不受限 |
这个区别,是这次最大的收获。
最终方案:用 wx.cloud.callContainer 取代 wx.request
目标:把小程序底层的 API 调用改成走云开发容器调用,从而真正摆脱域名白名单。
改动一:SDK 请求层优先走 callContainer
在 SDK 的 client 里做能力探测 + 优雅降级:
- 如果同时满足:
wx.cloud.callContainer可用、有云环境 ID、有服务名 → 优先走callContainer - 否则回退到
fetch/wx.request
// packages/api-sdk/src/client.ts(示意)
if (canUseCallContainer()) {
return callContainer({ path, method, data })
}
return fallbackFetch({ path, method, data })
这个设计的价值在于:同一套 SDK 同时服务 H5 和小程序。 H5 走原来的 HTTP,小程序走容器调用,业务层完全无感。
改动二:小程序启动时初始化云开发
// apps/weapp/src/app.tsx(示意)
wx.cloud.init({
env: injectedCloudEnv,
traceUser: true,
})
injectedCloudEnv 不是写死的,而是构建时注入的编译常量。
改动三:云环境和服务名作为编译常量注入
// apps/weapp/config/index.ts(示意)
defineConstants: {
__TARO_APP_CLOUDBASE_ENV__: process.env.TARO_APP_CLOUDBASE_ENV,
__TARO_APP_CLOUDBASE_SERVICE__: process.env.TARO_APP_CLOUDBASE_SERVICE,
}
这样构建产物里能稳定拿到云环境 ID 和服务名,不用在代码里散布硬编码。
测试
SDK 侧补了两个用例,覆盖"优先走 callContainer"和"降级回 wx.request"两条路径:
pnpm exec tsx --test packages/api-sdk/src/__tests__/domain-sdk.test.ts
小程序侧确认构建产物里三样东西都在:云环境 ID、服务名、wx.cloud.init(...):
pnpm --filter weapp typecheck
pnpm --filter weapp build:weapp
为什么要专门测降级路径? 因为 H5 环境没有 wx.cloud,一旦降级逻辑写错,Web 端会直接挂掉——而 Web 端平时没人测。
最后沉淀出的排查方法论
这次最有价值的不是某个具体命令,而是把问题拆开的顺序。
API 服务部署问题 → 拆成三层
1. 打包问题 → 部署上下文是不是对的?(坑 1)
2. 端口问题 → 容器端口和监听端口一致吗?(坑 2)
3. 运行时依赖问题 → 服务活着,但依赖活着吗?(坑 3、4)
三层依次排查,比笼统地"看看日志"快得多。
小程序访问问题 → 先确认底层方式
底层是 HTTP 请求 → 就是要配 request 合法域名
底层是 callContainer → 才真正免配
不要把"云托管公网域名"和"云托管容器调用"混为一谈——这是两个完全不同的通道。
目前我们稳定使用的发布流程
API 服务发布
node scripts/cloudrun/prepare-wechat-api.mjs
wxcloud run:deploy tmp/wechat-cloudrun-api \
-e <env-id> \
-s <service-name> \
--dockerfile Dockerfile \
--containerPort 3001 \
--noConfirm --override
小程序上传
TARO_APP_CLOUDBASE_ENV='<env-id>' \
TARO_APP_CLOUDBASE_SERVICE='<service-name>' \
fnm exec --using 20 -- node scripts/upload-miniprogram.mjs
一句话总结
微信云托管解决的问题是"把服务跑起来",不是"让小程序免配域名"。
这两件事经常被混在一起说,而它们之间差着一个 wx.cloud.callContainer 改造。
作者:苏州畅达软件 小程序团队 | www.szchada.com
