小程序开发日记⑨:微信开放接口与原生组件调用

这个项目虽然使用 uni-app 和 Vue 3 开发,但登录、手机号、订阅消息、地图、媒体和客服等能力最终仍来自微信小程序运行环境。框架解决跨端写法,开放能力的授权时机、事件结构和平台限制却不能被忽略。
本篇不打算做一份微信 API 百科,而是沿着项目里的真实使用场景,记录哪些能力通过函数调用、哪些必须依赖原生组件,以及封装时踩过的几个关键坑。
1. 项目中使用的微信能力
| 能力 | API 或组件 | 项目场景 |
|---|---|---|
| 微信登录 | wx.login、wx.checkSession | 建立和恢复登录态 |
| 手机号授权 | button open-type="getPhoneNumber" | 手机号快捷登录 |
| 隐私授权 | wx.getPrivacySetting、wx.requirePrivacyAuthorize | 登录前隐私合规确认 |
| 订阅消息 | wx.requestSubscribeMessage | 订单完成和状态提醒 |
| 头像昵称 | chooseAvatar、type="nickname" | 维护个人资料 |
| 图片选择 | wx.chooseMedia | 订单凭证和异常证据 |
| 文件上传 | uni.uploadFile | 上传图片到业务服务器 |
| 定位地图 | uni.getLocation、wx.createMapContext、map | 服务范围、选点和轨迹 |
| 图片预览 | uni.previewImage | 查看凭证和签收图 |
| 客服 | button open-type="contact" | 订单与异常咨询 |
| 分享 | wx.showShareMenu、onShareAppMessage | 分享订单信息 |
| 剪贴板 | uni.getClipboardData、setClipboardData | 粘贴、复制订单号 |
2. 为什么建立 utils/wx 适配层
项目没有在每个页面直接编写大量 wx.xxx 回调,而是把平台能力集中在 utils/wx 下:
utils/wx ├─ login.js 微信登录与会话检查 ├─ phone.js 手机号授权事件解析 ├─ privacy.js 隐私授权 ├─ subscribeMsg.js 订阅消息 ├─ media.js 图片选择与上传 ├─ map.js 定位与地图上下文 ├─ share.js 分享菜单 ├─ clipboard.js 剪贴板 └─ authImage.js 鉴权图片下载和本地缓存封装后的页面可以使用 Promise 和 async/await,平台错误也能统一转换成面向用户的消息。
export function login() { return new Promise((resolve, reject) => { wx.login({ success: result => result.code ? resolve(result.code) : reject(new Error('微信登录未获取到 code')), fail: error => reject(new Error(error?.errMsg || '微信登录失败')) }) })}适配层不是为了把 wx 三个字藏起来,而是隔离回调、错误格式和运行环境差异。
3. wx.login 与会话恢复
登录流程首先检查本地 token。若微信会话仍有效,则使用原 token 获取最新用户资料;会话失效或资料接口拒绝后,再重新调用 wx.login 获取一次性 code。
if (existingToken && await checkSession()) { const response = await UserApi.getProfile() return { token: existingToken, userInfo: response.data }}
const code = await wxLogin()const response = await UserApi.login({ code })微信 code 只能使用一次,因此 wxLoginFlow 使用共享 Promise 防止多个页面同时发起登录:
let loginFlowPromise = null
export async function wxLoginFlow(options = {}) { if (loginFlowPromise) return loginFlowPromise loginFlowPromise = runLoginFlow(options) try { return await loginFlowPromise } finally { loginFlowPromise = null }}这比每个请求发现未登录后各自调用 wx.login 稳定得多。
4. 手机号能力必须由原生按钮触发
手机号授权不是普通函数调用,需要使用微信原生按钮:
<button open-type="getPhoneNumber" @click="preparePrivacy" @getphonenumber="onGetPhoneNumber"> 允许并登录</button>授权结果在事件的 detail.code 中。工具函数只解析 code,并区分拒绝与异常:
export function getPhoneCodeFromEvent(event) { if (event?.detail?.code) return event.detail.code const message = String(event?.detail?.errMsg || '') if (message.includes('deny') || message.includes('cancel')) { throw new Error('您已拒绝手机号授权') } throw new Error('未获取到手机号授权凭证')}随后同时提交 wx.login code 与 phoneCode,由后端向微信服务换取身份和手机号。phoneCode 不是手机号本身,前端不解析敏感数据。
5. 隐私授权与用户手势
登录弹窗在用户点击允许按钮时预先调用 ensurePrivacyAuthorized,手机号事件返回后等待同一个 Promise:
const preparePrivacy = () => { privacyPromise = ensurePrivacyAuthorized()}
const onGetPhoneNumber = async event => { const phoneCode = getPhoneCodeFromEvent(event) await (privacyPromise || ensurePrivacyAuthorized()) await auth.phoneLogin({ phoneCode })}隐私相关 API 先判断运行环境是否支持。旧基础库缺少方法时安全放行,支持时按微信流程请求授权。
这里的关键是授权动作要紧邻用户手势。不能在页面加载后无理由弹出手机号、订阅消息或隐私授权。
6. 订阅消息调用时机
订单支付前,页面同步创建订阅请求,再继续支付流程:
const subscribeRequest = requestSubscribeMessage([ ORDER_STATUS_TEMPLATE_ID])
await OrderApi.payOrder(orderId)await requestOrderCompletionSubscribe(subscribeRequest)wx.requestSubscribeMessage 对调用时机较敏感,封装中在 Promise 构造阶段立即执行原生 API,调用前不能先插入无关的 await。
授权结果不是简单的布尔值,而是以模板 ID 为键:
const accepted = result?.[ORDER_STATUS_TEMPLATE_ID] === 'accept'用户接受后,前端向业务后端上报订阅次数。上报失败只记录警告,不阻断支付,因为订阅提醒是附加能力,不能反过来让核心交易失败。
7. chooseAvatar 与昵称输入
个人中心使用原生头像选择按钮:
<button open-type="chooseAvatar" @chooseavatar="onChooseAvatar"> <image :src="auth.avatarDisplayUrl" /></button>事件返回本地临时文件路径,页面调用头像上传接口,成功后重新获取鉴权图片。
昵称使用:
<input type="nickname" name="nickname" maxlength="30" />即使使用微信昵称能力,提交前仍要做空值、长度与内容校验,最终资料以业务后端保存结果为准。
8. 图片选择、上传与预览
订单和异常反馈通过 wx.chooseMedia 选择相册或相机图片:
wx.chooseMedia({ count, mediaType: ['image'], sourceType: ['album', 'camera'], sizeType: ['compressed'], camera: 'back'})返回的 tempFilePath 是临时本地路径。在开发者工具中它可能表现为 http://tmp/...,仍应作为本地文件交给 uni.uploadFile,不能因为长得像 URL 就跳过。
上传方法手动加入 Authorization,并解析字符串形式的响应体:
uni.uploadFile({ url: apiUrl, filePath, name: 'file', formData, header: { Authorization: `Bearer ${token}` }})订单详情使用 uni.previewImage 打开凭证大图。预览数组使用已经下载好的本地鉴权图片路径,避免原始受保护 URL 无法携带 token。
9. 鉴权图片与文件系统
头像和订单凭证并非公开静态资源。项目使用 uni.request 以 arraybuffer 获取图片,再通过 wx.getFileSystemManager 写入 USER_DATA_PATH:
fileSystem.writeFile({ filePath: `${wx.env.USER_DATA_PATH}/auth-image-${key}.jpg`, data: response.data, success: () => resolve(filePath)})返回的本地路径才能稳定交给 <image>。缓存强制刷新时删除旧文件;退出登录时清空全部鉴权图片,防止不同账号之间复用缓存。
响应还必须检查 Content-Type。有些业务错误使用 HTTP 200 返回 JSON,如果不识别,错误 JSON 会被当成 jpg 写进文件系统,最后页面只会留下一个让人摸不着头脑的破图标。
10. 地图组件和定位接口
服务范围、位置选择和订单轨迹都使用原生 <map>:
<map id="tracking-map" :latitude="latitude" :longitude="longitude" :markers="markers" :polyline="polylines" :scale="mapScale" show-location/>页面通过 wx.createMapContext 获取上下文,执行 includePoints、moveToLocation 等命令:
const mapContext = wx.createMapContext('tracking-map')mapContext.includePoints({ points, padding: [72, 48, 180, 48] })用户定位调用 uni.getLocation({ type: 'gcj02' })。权限被拒绝时可以使用 uni.getSetting 检查状态,并由用户主动打开 uni.openSetting;页面仍保留搜索和拖图选点作为降级入口。
marker、polyline 和 circle 应使用 map 原生属性。把普通 <image> 绝对定位在地图上表示无人机,会在地图缩放后与航线发生偏移。
11. 微信客服组件
首页、订单列表、订单详情和轨迹页都使用:
<button open-type="contact">在线客服</button>客服同样依赖原生 button,普通点击方法不能完整替代。业务页面只需决定何时展示入口,具体会话由微信客服能力承接。
在订单详情中,客服入口与支付、取消等业务按钮分开。客服联系不上不应影响订单状态操作,订单接口失败也不应让客服按钮消失。
12. 分享能力
订单详情使用 uni-app 生命周期定义分享内容:
onShareAppMessage(() => ({ title: `订单 ${detail.value.orderNo}`, path: `/pages/order/detail?orderId=${orderId.value}`}))工具层通过 wx.showShareMenu 和 wx.updateShareMenu 开启好友与朋友圈分享。分享路径携带业务参数,但详情页仍需重新校验登录和订单权限,不能因为别人拿到了链接就直接展示受保护数据。
13. 剪贴板能力
快递单号和外卖订单号支持从剪贴板粘贴,详情中的订单号也可以复制:
const value = await readText()order.courierNo = String(value).trim()
await copyText(detail.value.orderNo)读取剪贴板后仍执行格式和长度校验,不能把整段聊天记录原样提交。剪贴板只是输入捷径,不是可信数据源。
14. 调用微信能力时总结出的规则
14.1 区分函数 API 和组件开放能力
wx.login、chooseMedia 可以封装成函数;getPhoneNumber、chooseAvatar、contact 必须由带 open-type 的原生按钮触发。试图把后一类完全搬进普通 JavaScript 方法,往往会失去微信所需的用户手势上下文。
14.2 授权失败不等于业务失败
用户拒绝定位后仍可手动选点,拒绝订阅后仍可支付,客服不可用也不影响查看订单。只有手机号登录这种业务身份前提,才会阻止进入受保护页面。
14.3 平台返回值必须再做业务适配
微信返回 code、临时路径、授权字典和地图上下文;页面真正需要的是 token、凭证记录、是否订阅和业务坐标。适配层负责把平台结果转换成项目语言。
14.4 注意生命周期与一次性凭证
登录 code、手机号 code、临时文件和订阅请求都有时间或次数限制,不能缓存后无限复用。地图上下文和临时图片也要随页面、账号生命周期正确清理。
15. 本篇小结
在 uni-app 里写微信小程序,最容易产生的错觉是“只要都写成 Promise,它们就是普通接口了”。实际上,授权能力依然受用户手势、原生组件、基础库版本和隐私规则约束。
把微信能力集中封装以后,页面代码确实清爽了很多;但每次看到 open-type,我还是会提醒自己:这不是一个普通按钮,它背后站着微信这位非常讲规矩的门卫。证件、时机和用途少一个,门都不会开。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





























