UniApp H5:动态注入微信 JSSDK + utils/weixin.js 二次封装(扫码/选图上传/分享)实战

场景:UniApp 项目同时跑 H5 / 小程序 / App。其中 H5 需要在 微信内置浏览器小程序 WebView 中使用微信能力(扫码、选图上传、分享等)。
本文基于我当前项目的真实代码:App.vue动态注入 JSSDK + utils/weixin.jsJS-SDK 工具类封装


0. 先把两个问题分清楚(本文解决的重点)

很多同学把“微信 H5 能力”混在一起讲,其实在 UniApp H5 里常见的是两个不同问题:

  • 问题 A:小程序 web-view 里如何跳回小程序页面?
    这类问题只需要 wx.miniProgram.navigateBack/navigateTo/...,不等同于完整的公众号 H5 JS-SDK 权限体系。官方文档明确:web-view 网页里可以使用 JSSDK 1.3.2 提供的接口返回小程序页面(含 wx.miniProgram.navigateBack 等),参考:微信开放文档 - web-view

  • 问题 B:为什么 wx.config 总报权限/接口不可用?
    在小程序 web-view 场景下,网页内 仅支持有限的 JS-SDK 接口(比如 scanQRCode / chooseImage / uploadImage / getNetworkType 等)。如果你在 jsApiList 里配置了不支持的能力(例如部分分享相关接口),就会出现“接口不可用/权限失败/调用无响应”等问题。支持列表同样以官方文档为准:微信开放文档 - web-view

下面正文会按这两个问题分别讲清楚:先解决 web-view 回小程序,再解决 web-view 权限接口清单与 wx.config 配置


1. 为什么要“动态注入”微信 JSSDK

在 UniApp 的 H5 里,微信能力来自微信 JS-SDK(wx / jWeixin)。但它有几个典型问题:

  • 不是所有环境都有 wx:普通浏览器打开页面没有微信 JS-SDK
  • 加载时机不确定:脚本没加载完就调用会报错
  • 必须先 wx.config:否则很多 API 不能用
  • 签名 URL 特别容易踩坑:必须去掉 #(hash)部分,但保留 query
  • 多次并发初始化:多个页面同时需要能力,容易重复加载/重复配置

所以最稳妥的策略是:

  1. 进入 H5 时 注入 JSSDK 脚本
  2. 使用一个统一工具类:负责 加载脚本、判断环境、获取签名、config/ready/error、功能封装 + 降级

2. App.vue:H5 环境启动时动态注入微信 JSSDK

项目里在 App.vueonLaunch 中注入脚本(只在 H5 编译条件下执行):

	onLaunch: function(options) {
		this.initApp()
		// #ifdef H5
		// 注入微信 JSSDK(提供 wx / wx.miniProgram 能力)
		this.injectWxJssdk()
		// 获取URL参数并存储到全局
		this.getUrlParams(options)
		// #endif
	},

注入函数本体如下(核心:避免重复注入 + 监听 onload/onerror):

			// 动态注入微信 JSSDK(H5 环境)
			injectWxJssdk() {
				// #ifdef H5
				try {
					if (typeof document === 'undefined') return
					// 避免重复注入
					if (document.getElementById('wx-jssdk')) return

					const script = document.createElement('script')
					script.id = 'wx-jssdk'
					script.src = 'https://res.wx.qq.com/open/js/jweixin-1.3.2.js'
					script.async = true
					script.onload = () => {
						// eslint-disable-next-line no-console
						console.log('wx jssdk loaded')
					}
					script.onerror = (e) => {
						// eslint-disable-next-line no-console
						console.error('wx jssdk load failed:', e)
					}

					;
					(document.head || document.body || document.documentElement).appendChild(script)
				} catch (e) {
					// eslint-disable-next-line no-console
					console.error('injectWxJssdk failed:', e)
				}
				// #endif
			},

这段代码解决了什么?

  • 不在 H5 不执行// #ifdef H5
  • 不会重复注入document.getElementById('wx-jssdk')
  • 能看到加载成功/失败日志:方便排查 CDN、网络、CSP 等问题
  • (关键)小程序 web-view 回小程序:引入 jweixin-1.3.2.js 后,网页内才可以稳定使用 wx.miniProgram.navigateBack/navigateTo/...。对应官方说明见:微信开放文档 - web-view

注意:你的项目里 utils/weixin.js 也带“动态加载脚本”能力(默认 1.6.0)。如果你主要目标是 小程序 web-view 回跳,务必以官方建议的 1.3.2 为准;否则请统一版本并避免重复加载(文末给建议)。


3. utils/weixin.js:为什么要再封装一层

仅仅把脚本插进来还不够,原因是:

  • wx 可能还没 ready
  • 你必须先向后端拿 signaturewx.config
  • 不同环境要做降级(非微信环境用 uni.*

所以项目里做了一个 WeixinSDK 工具类,并导出单例:

// 创建单例实例
const weixinSDK = new WeixinSDK()

export default weixinSDK

4. 环境识别:微信 H5 / 小程序 WebView

4.1 获取 SDK 对象(兼容 jWeixin / wx

	getWxSdk() {
		// #ifdef H5
		if (typeof jWeixin !== 'undefined') {
			return jWeixin
		}
		if (typeof wx !== 'undefined' && typeof wx.config === 'function') {
			return wx
		}
		// #endif
		return null
	}

4.2 判断是否微信环境 / 是否小程序 WebView

	checkIsWeixin() {
		// #ifdef H5
		const ua = navigator.userAgent.toLowerCase()
		// 检测微信环境(包括微信H5和小程序webview)
		// 小程序webview的UserAgent也包含 'micromessenger'
		return ua.indexOf('micromessenger') !== -1
		// #endif
		// #ifndef H5
		return false
		// #endif
	}

	checkIsMiniProgramWebview() {
		// #ifdef H5
		if (!this.checkIsWeixin()) {
			return false
		}
		// 小程序webview的UserAgent通常包含 'miniprogram'
		const ua = navigator.userAgent.toLowerCase()
		return ua.indexOf('miniprogram') !== -1
		// #endif
		// #ifndef H5
		return false
		// #endif
	}

5. 动态加载 JSSDK(更通用的 loader)

utils/weixin.js 里实现了一个“只加载一次”的脚本加载器:已加载直接返回,正在加载复用同一个 Promise。

	loadScript(url = null) {
		// #ifdef H5
		// 如果已经加载,直接返回
		if (this.scriptLoaded) {
			return Promise.resolve(true)
		}

		// 如果正在加载中,返回加载Promise
		if (this.scriptLoadingPromise) {
			return this.scriptLoadingPromise
		}

		// 如果SDK已经存在,标记为已加载
		if (this.getWxSdk()) {
			this.scriptLoaded = true
			return Promise.resolve(true)
		}

		// 创建加载Promise
		this.scriptLoadingPromise = new Promise((resolve) => {
			const scriptUrl = url || this.scriptUrl

			// 检查是否已经存在该script标签
			const existingScript = document.querySelector(`script[src="${scriptUrl}"]`)
			if (existingScript) {
				// 如果script标签已存在,等待加载完成
				const checkLoaded = () => {
					if (this.getWxSdk()) {
						this.scriptLoaded = true
						this.scriptLoadingPromise = null
						resolve(true)
					} else {
						setTimeout(checkLoaded, 50)
					}
				}
				checkLoaded()
				return
			}

			// 创建script标签
			const script = document.createElement('script')
			script.type = 'text/javascript'
			script.async = true
			script.src = scriptUrl

			// 加载成功
			script.onload = () => {
				// 等待一小段时间确保SDK对象可用
				setTimeout(() => {
					if (this.getWxSdk()) {
						this.scriptLoaded = true
						this.scriptLoadingPromise = null
						resolve(true)
					} else {
						console.error('微信JS-SDK脚本加载完成但SDK对象不可用')
						this.scriptLoadingPromise = null
						resolve(false)
					}
				}, 100)
			}

			// 加载失败
			script.onerror = () => {
				console.error('微信JS-SDK脚本加载失败')
				this.scriptLoadingPromise = null
				resolve(false)
			}

			// 添加到页面
			document.head.appendChild(script)
		})

		return this.scriptLoadingPromise
		// #endif

		// #ifndef H5
		return Promise.resolve(false)
		// #endif
	}

6. 初始化 wx.config:签名 URL 处理 + 并发去重

核心逻辑在 initConfig()

  1. 非微信环境直接返回 false
  2. 先确保脚本加载成功
  3. URL 必须 去掉 hashhttps://xxx.com/#/a?b=1 签名用的是 https://xxx.com/(保留 query,去掉 # 之后的所有内容)
  4. 调后端接口拿 appId/timestamp/nonceStr/signature
  5. wx.configjsApiList 只配置当前场景支持的接口(尤其是小程序 web-view
  6. wx.config 后等待 wx.ready / wx.error

代码如下:

	async initConfig(jsApiList = null, debug = false, url = null) {
		// 默认权限:扫码、选择图片、上传图片(标准微信JS-SDK流程)
		const defaultApiList = ['scanQRCode', 'chooseImage', 'uploadImage']
		const finalApiList = jsApiList || defaultApiList
		// #ifdef H5
		// 如果正在配置中,返回配置Promise
		if (this.configPromise) {
			return this.configPromise
		}

		// 检查是否在微信环境(包括微信H5和小程序webview)
		if (!this.checkIsWeixin()) {
			console.warn('当前不在微信浏览器中')
			return Promise.resolve(false)
		}

		// 先加载微信JS-SDK脚本
		const scriptLoaded = await this.loadScript()
		if (!scriptLoaded) {
			console.error('微信JS-SDK脚本加载失败')
			return Promise.resolve(false)
		}

		// 获取SDK对象
		this.wxSdk = this.getWxSdk()
		if (!this.wxSdk) {
			console.error('微信JS-SDK未加载')
			return Promise.resolve(false)
		}

		// 如果已经配置成功,直接返回
		if (this.configReady) {
			return Promise.resolve(true)
		}

		// 创建配置Promise
		this.configPromise = new Promise(async (resolve) => {
			try {
				let currentUrl = url || window.location.href

				// 微信JS-SDK签名需要去掉hash部分,但保留查询参数
				if (currentUrl.indexOf('#') !== -1) {
					currentUrl = currentUrl.split('#')[0]
				}

				// 调用后端接口获取微信配置
				const res = await getWeixinConfig(currentUrl)
				if (res.data) {
					const config = res.data
					this.wxSdk.config({
						debug: debug,
						appId: config.appId,
						timestamp: config.timestamp,
						nonceStr: config.nonceStr,
						signature: config.signature,
						jsApiList: finalApiList
					})

					this.wxSdk.ready(() => {
						console.log('微信JS-SDK配置成功')
						this.configReady = true
						this.configPromise = null
						resolve(true)
					})

					this.wxSdk.error((err) => {
						console.error('微信JS-SDK配置失败:', err)
						this.configReady = false
						this.configPromise = null
						resolve(false)
					})
				} else {
					console.warn('获取微信配置失败:', res.msg || '未知错误')
					this.configReady = false
					this.configPromise = null
					resolve(false)
				}
			} catch (error) {
				console.error('初始化微信配置失败:', error)
				this.configReady = false
				this.configPromise = null
				resolve(false)
			}
		})

		return this.configPromise
		// #endif

		// #ifndef H5
		return Promise.resolve(false)
		// #endif
	}

这里的 getWeixinConfig(currentUrl) 就是后端签名接口:把当前 URL 发给后端,后端用公众号 jsapi_ticket 计算签名并返回。
小程序 web-view 场景下,“网页内可用的 JS-SDK 接口”是受限的,哪些接口能配进 jsApiList 以官方文档为准:微信开放文档 - web-view


7. “统一入口”封装:能用微信就用微信,用不了就降级

这也是本文最值得复用的点:一套 API 兼容多端

7.1 统一扫码:微信用 wx.scanQRCode,其他环境用 uni.scanCode

	async scanCode(options = {}) {
		// #ifdef H5
		if (this.checkIsWeixin()) {
			const ok = await this.initConfig()
			if (!ok) return Promise.reject(new Error('微信配置未完成,请稍后再试'))
			return this.scanQRCode({
				needResult: options.needResult !== undefined ? options.needResult : 1,
				scanType: options.scanType || ['qrCode', 'barCode']
			})
		}
		// #endif

		// 非微信H5 / 小程序 / App:走 uni.scanCode
		return new Promise((resolve, reject) => {
			// eslint-disable-next-line no-undef
			uni.scanCode({
				onlyFromCamera: options.onlyFromCamera !== undefined ? options.onlyFromCamera : true,
				success: (res) => resolve(res.result),
				fail: (err) => reject(new Error(err?.errMsg || '扫码失败'))
			})
		})
	}

7.2 统一选图:微信用 wx.chooseImage 返回 localIds,其他环境用 uni.chooseImage 返回 tempFilePaths

	async chooseImageUnified(options = {}) {
		// #ifdef H5
		// 微信H5环境:使用微信SDK标准流程
		if (this.checkIsWeixin()) {
			try {
				// 确保配置完成(默认配置已包含 chooseImage 和 uploadImage)
				await this.initConfig()

				return this.ready((wxSdk) => {
					return new Promise((resolve, reject) => {
						wxSdk.chooseImage({
							count: options.count || 1,
							sizeType: options.sizeType || ['compressed'],
							sourceType: options.sourceType || ['album', 'camera'],
							success: (res) => {
								const localIds = res.localIds || []
								if (localIds.length === 0) {
									reject(new Error('未选择图片'))
									return
								}

								console.log('微信选择图片成功,localIds:', localIds)
								// 返回 localIds,用于后续调用 wx.uploadImage
								resolve({ localIds })
							},
							fail: (err) => {
								console.error('选择图片失败:', err)
								reject(new Error(err.errMsg || '选择图片失败'))
							}
						})
					})
				})
			} catch (error) {
				// 如果微信SDK失败,降级使用 uni.chooseImage
				console.warn('微信SDK选择图片失败,降级使用 uni.chooseImage:', error)
				return this.chooseImageUnifiedFallback(options)
			}
		}
		// #endif

		// 非微信H5环境:使用 uni.chooseImage
		return this.chooseImageUnifiedFallback(options)
	}

7.3 分享:朋友圈 / 好友

重要说明:小程序 web-view 内网页可用的 JS-SDK 接口是受限的,分享相关接口在 web-view 场景可能不可用。
如果你的 H5 是放在小程序 web-view 里并且需要“分享”,通常应该走 小程序层的分享能力(如页面 onShareAppMessage 获取 webViewUrl 等),而不是强依赖网页里的分享 JS-SDK。相关限制与能力边界请以官方文档为准:微信开放文档 - web-view

	async updateTimelineShareData(shareData) {
		// #ifdef H5
		if (!this.checkIsWeixin()) {
			return Promise.reject(new Error('请在微信中打开此页面'))
		}

		return this.ready((wxSdk) => {
			return new Promise((resolve, reject) => {
				wxSdk.updateTimelineShareData({
					title: shareData.title,
					link: shareData.link,
					imgUrl: shareData.imgUrl,
					success: () => resolve(true),
					fail: (err) => reject(new Error(err.errMsg || '分享失败'))
				})
			})
		})
		// #endif

		// #ifndef H5
		return Promise.reject(new Error('分享功能需要在微信浏览器中使用'))
		// #endif
	}

	async updateAppMessageShareData(shareData) {
		// #ifdef H5
		if (!this.checkIsWeixin()) {
			return Promise.reject(new Error('请在微信中打开此页面'))
		}

		return this.ready((wxSdk) => {
			return new Promise((resolve, reject) => {
				wxSdk.updateAppMessageShareData({
					title: shareData.title,
					desc: shareData.desc,
					link: shareData.link,
					imgUrl: shareData.imgUrl,
					success: () => resolve(true),
					fail: (err) => reject(new Error(err.errMsg || '分享失败'))
				})
			})
		})
		// #endif

		// #ifndef H5
		return Promise.reject(new Error('分享功能需要在微信浏览器中使用'))
		// #endif
	}

8. 页面里怎么用(示例)

在任意页面:

import weixinSDK from '@/utils/weixin'

export default {
  methods: {
    async onScan() {
      try {
        const result = await weixinSDK.scanCode()
        console.log('扫码结果:', result)
      } catch (e) {
        this.$modal?.msgError?.(e.message || String(e))
      }
    },
    async onChooseImage() {
      const res = await weixinSDK.chooseImageUnified({ count: 1 })
      // 微信H5:res.localIds;其他环境:res.tempFilePaths
      console.log(res)
    }
  }
}

9. 我踩过的坑 & 建议

9.1 wx.config 的 URL 一定要去掉 hash

项目里已经做了:

  • window.location.hrefsplit('#')[0]

这点极其关键:签名用的 URL 与后端计算必须完全一致

9.2 动态注入脚本建议“只保留一个入口”

你项目当前同时存在:

  • App.vue 注入 jweixin-1.3.2.js
  • utils/weixin.js loader 默认 jweixin-1.6.0.js

建议二选一(并且先明确你在解决“问题 A”还是“问题 B”):

  • 方案 A:只保留 utils/weixin.jsloadScript(),把 App.vueinjectWxJssdk() 当作可选项移除/注释掉
  • 方案 B:保留 App.vue 注入,但把 utils/weixin.jsscriptUrl 改成同版本,并确保 loadScript() 不会重复加载

如果你主要是在解决 小程序 web-view 回跳(问题 A),请优先遵循官方说明使用 jweixin-1.3.2.js 以获得 wx.miniProgram.* 能力,参考:微信开放文档 - web-view

9.3 并发初始化要“去重”

utils/weixin.jsconfigPromise 做了并发复用,避免多个页面同时 initConfig() 造成重复 wx.config

9.4 非微信环境要优雅降级

scanCode() 这种“统一入口”非常实用:微信用 wx.scanQRCode,否则用 uni.scanCode


10. 自测清单

  • 微信内置浏览器打开 H5
    • 控制台能看到 wx jssdk loaded
    • initConfig() 成功(无 config:invalid signature
    • 扫码/选图/分享可用
  • 普通浏览器打开 H5
    • 不报错(工具类能识别非微信环境并提示/降级)
  • 小程序 WebView 打开 H5
    • UA 识别:micromessenger + miniprogram
    • 验证 wx.miniProgram.navigateBack/navigateTo 可用(解决“web-view 回小程序”)
    • 验证 wx.configjsApiList 不要配置超出 web-view 支持范围的接口(解决“权限接口不可用”)
    • 能力边界以官方文档为准:微信开放文档 - web-view

结语

这套思路的核心就是一句话:“把不稳定(脚本加载、config、环境差异)全部收口到一个工具类里”,页面层只管调用统一 API
后续如果要扩展(如定位、录音、支付、卡券),继续往 utils/weixin.js 里加封装即可。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐