前言:小程序文件选择的尴尬
在微信小程序里做「上传文件」功能时,很多人会第一时间想到 wx.chooseMessageFile。用了一段时间才发现:它只能从聊天记录里选文件,根本不会调起系统文件管理器。
这就导致:
- 用户:没法从 iCloud Drive、本地文件夹、最近下载里选文件,只能先把文件发到某个聊天再选,体验很割裂
- 产品诉求:希望支持「从手机文件导入」,和「从聊天导入」并存
- 平台限制:小程序没有提供「调起系统文件选择器」的 API,只能另辟蹊径
所以我们的目标很明确:在不大改现有逻辑的前提下,增加一种「从手机本地文件导入」的方式 。最后采用的方案是:用 <web-view> 打开一个 H5 页面,在 H5 里用 <input type="file"> 调起系统文件选择器,选完上传后再通知小程序刷新列表。
下面就是这条路上的踩坑和实现要点,给有类似需求的同学做个参考。
踩坑经历
坑一:以为 postMessage 能实时通知小程序
H5 里上传成功后,很自然就想用 wx.miniProgram.postMessage({ success: true }) 告诉小程序「上传好了,去刷新列表吧」。
结果发现:postMessage 并不是实时送达的。消息只会在 web-view 的特定时机(例如用户点击返回、分享、页面被销毁)才被小程序收到,不能指望「上传接口返回 200 就立刻让列表刷新」。
所以更稳妥的做法是:上传成功后直接 wx.miniProgram.navigateBack() 回到列表页,在列表页的 onShow 里统一做一次刷新。这样不依赖 postMessage 的时机,逻辑也更简单。
坑二:业务域名和服务器域名搞混
在微信里,业务域名 和服务器域名是两套配置:
- 业务域名 :
<web-view>的src必须是已配置的业务域名下的页面,否则体验版/正式版里 web-view 会白屏或报错 - 服务器域名:request、uploadFile 等接口要请求的 API 域名,在「服务器域名」里配置
我们一开始只在「服务器域名」里配了 API 的域名,结果 web-view 加载的 H5 地址没进「业务域名」,真机上就打不开。记得把 H5 页面的域名(例如 https://your-h5.com)配到「开发管理 → 开发设置 → 业务域名」,并且域名根路径要放好微信的校验文件。
坑三:H5 页面和 API 的域名没分开配
H5 页面可能部署在前端静态资源域名,上传接口在后端 API 域名,两者不一定同域。如果只在 H5 里写死一个 baseURL,以后换环境、换域名就要改代码。
我们的做法是:小程序跳转 web-view 时,把当前环境的 API 根地址通过 URL 参数(如 api_base)传给 H5 ,H5 用这个参数拼上传接口的完整 URL。这样小程序侧用 getEnvBaseUrl() 之类的方法根据环境变量取 API 地址即可,H5 只认参数,方便多环境部署。
坑四:web-view 里该用独立 HTML 还是 Vue 页面
web-view 里跑的是完整浏览器环境,理论上可以塞进去一个 Vue 应用。但考虑到:
- 这个页面只有一个「选文件 → 上传」的简单流程
- 希望加载尽量快、不依赖一堆 JS 库
- 部署要简单,最好和主站一起发版即可
我们最终选的是:在 public/h5/ 下放一个独立的 upload-file.html,不经过 Vue 打包,只引微信 JS-SDK,用原生 JS 做选文件和 XHR 上传。这样无需额外构建、体积小、首屏快,也避免和主项目路由、构建环境耦合。
坑五:token 怎么安全地给到 H5
H5 调用后端上传接口需要带鉴权(如 Authorization: Bearer <token>)。token 在小程序里已有,但 H5 拿不到小程序的 storage。
我们采用的方式是:小程序打开 web-view 时,把 token 放在 URL 的 query 里 (例如 ?token=xxx&user_id=xxx&api_base=xxx)。
风险控制方式:仅用于这一次上传流程、token 有时效、全程 HTTPS。如果你们对 token 暴露在 URL 里特别敏感,也可以让后端为「web-view 上传」单独发一次性临时 token,用一次即废。
方案架构与数据流
整体可以理解为三块:小程序列表页 → web-view 容器页 → H5 上传页,再加大后端上传接口。
- 小程序列表页:列表 + 添加文件入口;点击「从手机文件导入」时跳转到 web-view 页面,并带上 token、user_id、api_base 等参数(通过 web-view 的 URL 传)。
- web-view 页面 :只负责承载一个全屏 web-view,
src指向 H5 上传页的完整 URL(含上述参数)。 - H5 上传页 :一个独立 HTML,内有一个「选择文件」按钮(对应
<input type="file">),选完做前端校验(大小、格式),再用 XHR 以multipart/form-data调后端上传接口,成功后调用wx.miniProgram.navigateBack()回到小程序。 - 列表页 onShow :从 web-view 返回时会再次触发
onShow,在这里调refreshList()拉最新列表即可,无需依赖 postMessage。
这样用户路径就是:添加文件 → 选择「从手机文件导入」→ 进入 H5 → 选文件 → 上传 → 自动返回 → 列表已更新。
关键实现要点
1. 小程序侧:导入方式用 ActionSheet 二选一
在「添加文件」处不直接调 wx.chooseMessageFile,而是先弹出 ActionSheet,再根据用户选择走不同分支:
- 从聊天中导入 :走原来的
wx.chooseMessageFile逻辑 - 从手机文件导入 :
navigateTo到 web-view 页面,并把 H5 地址和参数拼好(H5 根地址用getH5BaseUrl()按环境区分,再拼上路径和 query)。
2. H5 页:input[type=file] + XHR 上传
- 用
<input type="file" accept=".pdf,.doc,.docx">调起系统文件选择器(iOS/Android 都会用系统原生选择器)。 - 在
change里取 file,做前端校验:大小(如 ≤10MB)、格式白名单。 - 用
FormData把 file 塞进去,XHR 的 URL 用 URL 参数里的api_base拼出完整上传地址,请求头里加Authorization: Bearer <token>(token 从 URL 参数里取)。 - 上传成功后调用
wx.miniProgram.navigateBack(),失败则在当前页提示错误,允许重选重传。
3. 环境与域名配置
- 为不同环境(开发/体验/正式)配置两套:H5 页面域名 (业务域名)、API 域名(服务器域名)。
- 小程序里用
getH5BaseUrl()、getEnvBaseUrl()之类方法按环境选 base,再拼到 web-view 的 URL 和api_base参数里。 - 微信后台:业务域名 里配 H5 所在域名;request / uploadFile 合法域名里配后端 API 域名(若 H5 用 XHR 直连后端,则后端域名要在这里配置)。
4. 安全性简要说明
- token 通过 HTTPS 的 URL 传参,仅用于本次上传,并有时效。
- 文件类型、大小在前端做一次校验,后端再做一次(如只允许 PDF/DOC/DOCX、大小上限 10MB),避免滥传。
小结
用 web-view + H5 的 <input type="file"> 来扩展「从手机文件导入」,可以绕过小程序无法调起系统文件选择器的限制,同时保留「从聊天导入」的原有逻辑。实现时注意:
- 不依赖 postMessage 触发刷新 :用
navigateBack+ 列表页onShow刷新列表更稳。 - 业务域名:web-view 的 H5 域名必须配在「业务域名」里,并放好校验文件。
- H5 与 API 域名分离 :通过 URL 参数把
api_base、token 等传给 H5,便于多环境和后续扩展。 - 简单场景用独立 HTML:上传页逻辑简单时,独立 HTML + 微信 JS-SDK 就够用,无需上 Vue 全家桶。
- token 传参:HTTPS + 短期 token 可接受;若有更高安全要求,可改为一次性临时 token。
如果你也在做小程序里的「从手机本地选文件」能力,希望这篇能少让你走一点弯路。