前言
在微信公众号/小程序 webview 中使用 H5 页面时,经常需要调用微信 JSSDK 提供的能力,如:
wx.chooseImage- 选择图片/拍照wx.getLocation- 获取位置wx.openLocation- 打开内置地图wx.scanQRCode- 扫一扫
使用这些方法的前提是成功调用 wx.config 注入配置信息,而签名验证是最容易踩坑的环节。
官方文档:JS-SDK说明文档
坑点 1:测试账号提示签名非法
现象:使用微信公众平台接口测试帐号时提示 invalid signature 或 require subscribe
原因与解决:
① 检查是否已关注测试账号,未关注会提示 require subscribe
② 如果项目使用 hash 模式路由,需要使用 location.href.split('#')[0] 作为签名 URL:
// hash 路由需要去掉 # 后面的部分
const url = encodeURIComponent(window.location.href.split('#')[0])
注意:URL 需要 encode,后端接口需要对应 decode
坑点 2:SPA 应用页面跳转后签名失败
现象:入口页面 JSSDK 正常,跳转到其他页面后调用失败
原因:仅在入口文件(如 App.vue)调用一次 wx.config,后续页面跳转导致 URL 变化,但签名仍是旧 URL 的签名
解决:在每个需要使用 JSSDK 的页面都调用 wx.config,确保签名 URL 与当前页面一致
坑点 3:iOS 真机签名失败(核心问题)
现象:
- Android 真机正常
- 微信开发者工具正常
- iOS 真机提示
invalid signature
开启 debug 模式后可以看到 realAuthUrl 字段,这是微信实际接收到的 URL。
根本原因:
iOS 和 Android 对 SPA 路由的处理机制不同:
| 设备 | 路由跳转后的行为 |
|---|---|
| Android | router.push 会真实改变 location.href |
| iOS | router.push 不会改变微信认可的 URL,微信只认第一次进入时的 URL |
流程示例:
用户扫码 → 进入页面A → router.push跳转到页面B → 在页面B调用wx.config
- Android:签名 URL 用页面 B 的 URL(正确)
- iOS:签名 URL 必须用页面 A 的 URL(入口 URL)(正确),用页面 B 的 URL(错误)
坑点 4:微信开发者工具模拟器的陷阱
现象:修复 iOS 真机问题后,微信开发者工具模拟器反而签名失败了
原因:微信开发者工具的 UserAgent 包含 iPhone,会被误判为 iOS 设备,但其签名机制与 Android 相同
// 微信开发者工具的 UserAgent 示例
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) ... wechatdevtools"
签名 URL 规则汇总:
| 环境 | 签名所需 URL |
|---|---|
| Android 真机 | 当前页面 URL |
| iOS 真机 | 入口 URL(第一次进入 webview 的 URL) |
| 微信开发者工具 | 当前页面 URL |
解决方案
方案一:wx.error 中刷新页面
原理:签名失败时刷新页面,使当前 URL 成为"入口 URL"
优点:实现简单,无需改动其他代码
缺点:用户体验差,会有明显的页面刷新
// 在 wx.config 调用后添加错误处理
wx.error((res) => {
console.log('wx.config error:', res)
// 检测是否为 iOS 设备(排除微信开发者工具)
const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
const isWechatDevTools = /wechatdevtools/i.test(navigator.userAgent)
// iOS 真机签名失败时刷新页面
if (isiOS && !isWechatDevTools) {
// 使用 sessionStorage 标记,防止无限刷新
const hasReloaded = sessionStorage.getItem('wx_config_reloaded')
if (!hasReloaded) {
sessionStorage.setItem('wx_config_reloaded', 'true')
location.reload()
} else {
// 已经刷新过还是失败,清除标记并提示用户
sessionStorage.removeItem('wx_config_reloaded')
console.error('微信授权失败,请重新进入页面')
}
}
})
// 成功时清除刷新标记
wx.ready(() => {
sessionStorage.removeItem('wx_config_reloaded')
console.log('wx.config success')
})
方案二:保存入口 URL(推荐)
原理:在应用初始化时保存入口 URL,iOS 设备使用入口 URL 签名
优点:用户无感知,体验好
缺点:需要在应用入口处添加逻辑
步骤 1:在应用入口保存 URL
// main.js 或 App.vue 的最开始
// 保存 iOS 入口 URL(必须在任何路由跳转之前)
const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
if (isiOS && !sessionStorage.getItem('wxIosEntryUrl')) {
// 只在第一次进入时保存,后续页面跳转不覆盖
sessionStorage.setItem('wxIosEntryUrl', window.location.href.split('#')[0])
}
步骤 2:封装获取签名 URL 的方法
/**
* 获取微信 JSSDK 签名所需的 URL
* @returns {string} 编码后的 URL
*/
export function getWxAuthUrl() {
const isWechatDevTools = /wechatdevtools/i.test(navigator.userAgent)
const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
const iosEntryUrl = sessionStorage.getItem('wxIosEntryUrl')
// iOS 真机(非开发者工具)使用入口 URL
if (isiOS && iosEntryUrl && !isWechatDevTools) {
return encodeURIComponent(iosEntryUrl)
}
// Android / 开发者工具 使用当前页面 URL
return encodeURIComponent(window.location.href.split('#')[0])
}
步骤 3:调用签名接口时使用
// 获取签名并配置 wx.config
async function initWxConfig() {
try {
const url = getWxAuthUrl()
// 调用后端接口获取签名
const res = await fetch(`/api/wx/signature?url=${url}`)
const { appId, timestamp, nonceStr, signature } = await res.json()
wx.config({
debug: false,
appId,
timestamp,
nonceStr,
signature,
jsApiList: ['chooseImage', 'getLocation', 'openLocation']
})
wx.ready(() => {
console.log('wx.config success')
})
wx.error((err) => {
console.error('wx.config error:', err)
})
} catch (error) {
console.error('initWxConfig error:', error)
}
}
方案三:使用 location.href 跳转
原理:放弃前端路由,使用原生跳转,每次跳转都是"真实"的 URL 变化
优点:简单直接,不需要保存入口 URL
缺点:页面会完全刷新,丢失 SPA 的优势(状态保持、过渡动画等)
适用场景:只有个别页面需要使用 JSSDK
封装跳转方法
/**
* 使用 location.href 进行页面跳转(脱离前端路由)
* @param {string} path - 目标路径,如 '/pages/bindPhone'
* @param {Object} query - 查询参数对象
*/
export function locationPush(path, query = {}) {
const origin = window.location.origin
// 构造查询参数字符串
const queryString = Object.entries(query)
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
// 拼接完整 URL 并跳转
const fullUrl = queryString ? `${origin}${path}?${queryString}` : `${origin}${path}`
window.location.href = fullUrl
}
/**
* 使用 location.replace 进行页面替换(无历史记录)
* @param {string} path - 目标路径
* @param {Object} query - 查询参数对象
*/
export function locationReplace(path, query = {}) {
const origin = window.location.origin
const queryString = Object.entries(query)
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
const fullUrl = queryString ? `${origin}${path}?${queryString}` : `${origin}${path}`
window.location.replace(fullUrl)
}
使用示例
// 原本的写法(前端路由,iOS 会有签名问题)
this.$router.push({
path: '/bindPhone',
query: { redirect: '/bindPhone' }
})
// 改为 location 跳转(解决 iOS 签名问题)
locationPush('/bindPhone', { redirect: '/bindPhone' })
方案对比
| 方案 | 实现难度 | 用户体验 | 适用场景 |
|---|---|---|---|
| 方案一:刷新页面 | 简单 | 差 | 临时修复、紧急上线 |
| 方案二:保存入口URL | 中等 | 好 | 推荐,适合大多数项目 |
| 方案三:location跳转 | 简单 | 一般 | 仅个别页面需要 JSSDK |
完整示例(Vue 项目)
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
// 【关键】在应用最开始保存 iOS 入口 URL
const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
if (isiOS && !sessionStorage.getItem('wxIosEntryUrl')) {
sessionStorage.setItem('wxIosEntryUrl', window.location.href.split('#')[0])
}
new Vue({
router,
render: h => h(App)
}).$mount('#app')
wxConfig.js(工具函数)
import wx from 'weixin-js-sdk'
import { getWxSignature } from '@/api/common'
/**
* 获取微信 JSSDK 签名所需的 URL
*/
function getWxAuthUrl() {
const isWechatDevTools = /wechatdevtools/i.test(navigator.userAgent)
const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
const iosEntryUrl = sessionStorage.getItem('wxIosEntryUrl')
if (isiOS && iosEntryUrl && !isWechatDevTools) {
return encodeURIComponent(iosEntryUrl)
}
return encodeURIComponent(window.location.href.split('#')[0])
}
/**
* 初始化微信 JSSDK 配置
* @param {string[]} jsApiList - 需要使用的 JS 接口列表
* @returns {Promise<void>}
*/
export async function initWxConfig(jsApiList = []) {
const url = getWxAuthUrl()
try {
const { appId, timestamp, nonceStr, signature } = await getWxSignature({ url })
return new Promise((resolve, reject) => {
wx.config({
debug: false,
appId,
timestamp: Number(timestamp),
nonceStr,
signature,
jsApiList
})
wx.ready(() => {
console.log('wx.config success')
resolve()
})
wx.error((err) => {
console.error('wx.config error:', err)
reject(err)
})
})
} catch (error) {
console.error('initWxConfig error:', error)
throw error
}
}
页面中使用
<template>
<div>
<button @click="handleChooseImage">选择图片</button>
</div>
</template>
<script>
import wx from 'weixin-js-sdk'
import { initWxConfig } from '@/utils/wxConfig'
export default {
async mounted() {
// 初始化微信配置
await initWxConfig(['chooseImage', 'getLocalImgData'])
},
methods: {
handleChooseImage() {
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
console.log('选择成功', res.localIds)
},
fail: (err) => {
console.error('选择失败', err)
}
})
}
}
}
</script>
调试技巧
-
开启 debug 模式:
wx.config({ debug: true, ... }),会 alert 出详细错误信息 -
查看 realAuthUrl:debug 模式下签名失败会显示微信实际接收到的 URL
-
检查 URL 编码:确保前后端 URL 编码/解码一致
-
使用 vconsole:在移动端查看 console 日志
import VConsole from 'vconsole' new VConsole()
总结
微信 JSSDK 签名的核心原则:传给后端生成签名的 URL 必须与微信认可的 URL 完全一致
- Android 真机:当前页面 URL
- iOS 真机:入口 URL(SPA 路由跳转不改变微信认可的 URL)
- 微信开发者工具:当前页面 URL(UA 虽含 iPhone 但行为同 Android)
推荐使用方案二,在应用初始化时保存入口 URL,兼顾用户体验和代码可维护性。
