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

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

这个项目虽然使用 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,我还是会提醒自己:这不是一个普通按钮,它背后站着微信这位非常讲规矩的门卫。证件、时机和用途少一个,门都不会开。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
小程序开发日记⑨:微信开放接口与原生组件调用
https://firefly.cuteleaf.cn/posts/mmp-dev-diary-9/
作者
Yore
发布于
2026-08-23
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Yore
Hello, I'm Yore.
公告
欢迎来到Yore的博客!本站基于原创作者夏叶的博客网站模版创作。
分类
标签
最新动态
站点统计
文章
51
分类
2
标签
124
总字数
94,115
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
EdgeOne Pages
博客版本
Firefly v6.15.0
文章许可
CC BY-NC-SA 4.0