H5页面在微信浏览器中自定义分享链接
场景
在微信中点开链接打开网页,在微信浏览器的右上角有“...”,点开后就可以分享链接,但是只能分享本页面的url地址。如果我们想指定内容,并且想要让分享的链接变成一个卡片的形式,那么我们就需要配置一下,具体实现步骤如下。
配置步骤
创建微信公众号/服务号
在微信公众平台中,要为微信公众号/服务号添加JS安全域名
前端引入微信JS-SDK
npm i weixin-js-sdk判断用户此时是否处于微信浏览器中(weixin-js-sdk仅支持微信浏览器环境)
// 判断当前web浏览器是否是微信环境 (此函数在微信小程序中返回true,但微信小程序不支持weixin-js-sdk)
export const isWeChatBrowser = () => {
// 获取 userAgent 信息
const userAgent = navigator.userAgent.toLowerCase()
// 检查是否包含 'micromessenger' 字样
return userAgent.indexOf('micromessenger') !== -1
}向后端服务器请求签名等参数信息
// 处理微信分享事件
const handleWechatShare = async () => {
// 1、向后端请求微信配置信息(目的是为了获取签名等信息 来初始化微信sdk的权限验证信息)
let url = encodeURIComponent(location.href.split('#')[0]) // 只用#之前的内容
if (getDeviceOS() == 'IOS') {
// ios手机会把首次进入的url当做wx.config中的realAuthUrl,所以需要在入口文件将该链接存下来 而不能是当前页面的链接
let t = localGet('firstVisitUrl').split('#')[0]
url = encodeURIComponent(t)
}
const res = await getWechatSDKConfig({ url }) // 调用笔者自己的后端接口 获取签名等参数信息
let wxConfig = {
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来
appId: res.data.app_id, // 必填,公众号的唯一标识
timestamp: res.data.timestamp, // 必填,生成签名的时间戳
nonceStr: res.data.nonce_str, // 必填,生成签名的随机串
signature: res.data.signature, // 必填,签名
jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'] // 必填,需要使用的JS接口列表
}
// 2、注入权限验证信息,初始化权限信息(目的是为了能够调用微信的相关api)
wx.config(wxConfig)
// 3、初始化微信自定义分享信息(目的是为了在用户点击右上角分享的时候,把分享的内容从链接 变成 自定义内容的卡片)
initWechatShareData(shareInfo)
}
// 判断设备OS类型
const getDeviceOS = () => {
const userAgent = navigator.userAgent
if (/android/i.test(userAgent)) {
return 'Android'
} else if (/iPad|iPhone|iPod/.test(userAgent)) {
return 'IOS'
} else {
return 'unknown' // 无法确定或非移动操作系统
}
}使用 wx.updateAppMessageShareData 方法 自定义要分享的内容
const popupVisible = ref(false)
const maskVisible = ref(false)
interface ShareInfo {
// 分享链接,该链接域名必须在公众号JS安全域名列表中(仅需配置父域名即可,如:在安全域名中配置https://a.com,在这里可以写入 https://a.com/xxx/xxx )
link: string
title: string // 分享标题
desc: string // 分享描述
imgUrl: string // 分享图标,请使用类似于 https://xxx.png 网络图片地址
}
const shareInfo: ShareInfo = {
title: 'Hello World',
desc: '这里是描述内容这里是描述内容',
link: 'https://a.com/mobile/xxx',
imgUrl: 'https://xxx.png'
}
// 初始化微信自定义分享信息
const initWechatShareData = (shareInfo: ShareInfo) => {
// wx.config没有success回调事件,通过ready接口保证后续代码都是在config执行成功后再执行的;
wx.ready(() => {
// 自定义分享朋友信息
wx.updateAppMessageShareData({
...shareInfo,
success: () => {
// 设置成功,现在用户点击右上角三个点进行分享时,可以分享出来卡片了
popupVisible.value = false // 隐藏popup弹窗
maskVisible.value = true // 引导用户点击右上角三个点菜单按钮
}
})
// 自定义分享朋友圈信息
wx.updateTimelineShareData({
...shareInfo,
success: () => {
// 设置成功,现在用户点击右上角三个点进行分享时,可以分享出来卡片了
}
})
})
// 通过error接口处理config失败的情况
wx.error(function (res) {
popupVisible.value = false // 隐藏popup弹窗
ElMessage.error('分享失败,请稍后再试' + res.errMsg)
})
}后端返回
根据前端给你的url地址,计算出sdk签名,和公众号APPID等信息一同提供给前端即可。
按照weixin-js-sdk官方文档说法,签名必须在服务端进行计算,具体签名计算方法请参考微信官方文档:微信网页开发 / JS-SDK
特别注意事项
需要特别注意的是,iOS系统中,微信SDK的wx.config方法会把用户首次进入的url当做wx.config中的realAuthUrl !
比如:Android手机中,用户在login页面登录,然后跳转到A页面进行微信分享,代码都在A页面,此时后端只要根据当前A页面的路径来进行签名计算即可;
但如果是iOS手机,那么用户在login页面登录,在A页面进行分享,必须用首次进入的login页面路径进行签名计算! 这是微信SDK自身bug,无法避免!
所以如果 在调试过程中iOS设备出现invalid signature签名错误,一定要小心是否是这个bug导致的。
解决方案: 在入口文件中,记录用户进入时的路径,存储在localStorage中,在代码中判断是否是ios设备,如果是,则使用localStorage中的路径,否则使用当前页面的路径。
// 入口文件(Vue是App.vue文件)
localSet('firstVisitUrl', location.href) // 记录首次访问地址
// 分享页面(代码实际运行页面)
let url = encodeURIComponent(location.href.split('#')[0]) // 只用#之前的内容
if (getDeviceOS() == 'IOS') {
// ios手机会把首次进入的url当做wx.config中的realAuthUrl,不能是当前页面的链接
let t = localGet('firstVisitUrl').split('#')[0]
url = encodeURIComponent(t)
}
// 向后端请求微信配置信息(目的是为了获取签名等信息 来初始化微信sdk的权限验证信息)
const res = await getWechatSDKConfig({ url })
wx.config其余逻辑代码......如果并非这个原因出现invalid signature签名错误,官方文档推荐按照以下顺序进行检
确认签名算法正确,可用微信 JS 接口签名校验工具 页面工具进行校验。
确认config中nonceStr(js中驼峰标准大写S), timestamp与用以签名中的对应noncestr, timestamp一致。
确认url是页面完整的url(请在当前页面alert(location.href.split('#')[0])确认),包括
'http(s)://'部分,以及'?'后面的GET参数部分,但不包括'#'hash后面的部分。确认 config 中的 appid 与用来获取 jsapi_ticket 的 appid 一致。
确保一定缓存access_token和jsapi_ticket。
确保你获取用来签名的url是动态获取的,动态页面可参见实例代码中php的实现方式。如果是html的静态页面在前端通过ajax将url传到后台签名,前端需要用js获取当前页面除去'#'hash部分的链接(可用location.href.split('#')[0]获取,而且需要encodeURIComponent),因为页面一旦分享,微信客户端会在你的链接末尾加入其它参数,如果不是动态获取当前链接,将导致分享后的页面签名失败。
现在H5直接分享不是卡片,而是蓝色的链接。如果想要得到卡片的样式,有以下三种方式:
在公众号菜单中的链接打开,才能正常分享
可以通过收藏链接 再打开分享
把链接换成二维码,扫码后进入分享
附录:代码全文
除去上面几个大坑之外,代码实现起来还是很简单的,照官方文档抄过来就行。
从手机底部出现一个弹窗的UI我直接用的是vant的popup组件,UI没有任何功能意义,纯粹只是为了代码整洁简单,自行替换即可:
<!-- 微信浏览器 - 分享弹窗组件 -->
<template>
<div class="wechat-share-popup">
<!-- 圆角弹窗(底部) -->
<van-popup v-model:show="popupVisible" round position="bottom">
<div class="popup-content">
<div class="popup-title">分享到</div>
<div class="share-list">
<div v-if="isWeChatBrowser()" class="share-box" @click="handleShare('weChat')">
<img class="box-icon" src="@/assets/images/mobile/appwx_logo.png" alt="" />
<div class="box-title">转发给微信</div>
</div>
<div class="share-box" @click="handleShare('link')">
<img class="box-icon" src="@/assets/images/mobile/copy-link.png" alt="" />
<div class="box-title">复制链接</div>
</div>
</div>
</div>
</van-popup>
<!-- 引导用户点击微信右上角的分享遮罩层 -->
<div class="wechat-share-mask" v-show="maskVisible" @click="maskVisible = false">
<img class="share-arrow" src="@/assets/images/mobile/roll-line-arrow.png" alt="请点击右上角菜单按钮,并进行分享" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import wx from 'weixin-js-sdk'
import { ElMessage } from 'element-plus'
import { getWechatSDKConfig } from '@/api/modules/Mobile/weChat'
const popupVisible = ref(false)
const maskVisible = ref(false)
interface ShareInfo {
link: string // 分享链接,该链接域名必须在当前页面对应的公众号JS安全域名列表中(仅需配置父域名即可,如:在安全域名中配置https://a.com,在这里可以写入 https://a.com/xxx/xxx )
title: string // 分享标题
desc: string // 分享描述
imgUrl: string // 分享图标,请使用类似于 https://xxx.png 网络图片地址
}
const shareInfo: ShareInfo = {
title: '',
desc: '',
link: '',
imgUrl: ''
}
// 初始化分享信息
const initShareInfo = (info: { title: string; link: string; desc?: string; imgUrl?: string }) => {
shareInfo.title = info.title
shareInfo.desc = info.desc || ''
shareInfo.link = info.link
shareInfo.imgUrl = info.imgUrl || `${location.origin}/logo.png`
popupVisible.value = true
}
defineExpose({ initShareInfo })
// 处理分享事件
const handleShare = (type: 'weChat' | 'link') => {
if (type == 'link') {
copyToClipboard(shareInfo.link, false)
ElMessage.success('复制成功,快去分享吧')
popupVisible.value = false
} else if (type == 'weChat') {
handleWechatShare()
}
}
let wxConfig: any = null
// 微信分享
const handleWechatShare = async () => {
// 1、向后端请求微信配置信息(目的是为了获取签名等信息 来初始化微信sdk的权限验证信息)
let url = encodeURIComponent(location.href.split('#')[0])
if (getDeviceOS() == 'IOS') {
// ios手机会把首次进入的url当做wx.config中的realAuthUrl,所以需要在入口文件将该链接存下来 而不能是当前页面的链接
let t = localGet('firstVisitUrl').split('#')[0]
url = encodeURIComponent(t)
}
const res = await getWechatSDKConfig({ url })
wxConfig = {
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: res.data.app_id, // 必填,公众号的唯一标识
timestamp: res.data.timestamp, // 必填,生成签名的时间戳
nonceStr: res.data.nonce_str, // 必填,生成签名的随机串
signature: res.data.signature, // 必填,签名
jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'] // 必填,需要使用的JS接口列表
}
// 2、注入权限验证信息,初始化权限信息(目的是为了能够调用微信的相关api)
wx.config(wxConfig)
// 3、初始化微信自定义分享信息(目的是为了在用户点击右上角分享的时候,把分享的内容从链接 变成 自定义内容的卡片)
initWechatShareData(shareInfo)
}
const initWechatShareData = (shareInfo: ShareInfo) => {
// wx.config没有success回调事件,通过ready接口保证后续代码都是在config执行成功后再执行的;
wx.ready(() => {
// 自定义分享朋友信息
wx.updateAppMessageShareData({
...shareInfo,
success: () => {
// 设置成功,现在用户点击右上角三个点进行分享时,可以分享出来卡片了
popupVisible.value = false // 隐藏popup弹窗
maskVisible.value = true // 引导用户点击右上角三个点菜单按钮
}
})
// 自定义分享朋友圈信息
wx.updateTimelineShareData({
...shareInfo,
success: () => {
// 设置成功,现在用户点击右上角三个点进行分享时,可以分享出来卡片了
}
})
})
// 通过error接口处理config失败的情况
wx.error(function (res) {
popupVisible.value = false // 隐藏popup弹窗
ElMessage.error('分享失败,请稍后再试' + res.errMsg)
})
}
// ============================= 工具函数 =============================
/**
* @description 复制内容到粘贴板
* */
function copyToClipboard (text: string, isShowSuccessMsg: boolean = true): void {
if (!text.trim()) {
ElMessage.warning('文本不存在')
return
}
// 兼容非安全域,非安全域下不可使用navigator.clipboard.writeText
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard
.writeText(text)
.then(() => {
isShowSuccessMsg &&
ElMessage({
message: '复制成功',
type: 'success',
duration: 1500
})
})
.catch(() => {
// console.error('复制失败:', error)
})
} else {
const textarea = document.createElement('textarea')
textarea.value = text
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
isShowSuccessMsg && ElMessage({ message: '复制成功', type: 'success', duration: 1500 })
}
}
/**
* @description 获取当前设备系统
* @returns string - 设备类型
*/
function getDeviceOS(){
const userAgent = navigator.userAgent
if (/android/i.test(userAgent)) {
return 'Android'
} else if (/iPad|iPhone|iPod/.test(userAgent)) {
return 'IOS'
} else {
return 'unknown' // 无法确定或非移动操作系统
}
}
// 判断当前web浏览器是否是微信环境 (注意:微信小程序返回true,但微信小程序不支持weixin-js-sdk)
function isWeChatBrowser(){
// 获取 userAgent 信息
const userAgent = navigator.userAgent.toLowerCase()
// 检查是否包含 'micromessenger' 字样
return userAgent.indexOf('micromessenger') !== -1
}
/**
* @description 获取localStorage
* @param {String} key Storage名称
* @returns {String}
*/
function localGet(key: string) {
const value = window.localStorage.getItem(key)
try {
return JSON.parse(window.localStorage.getItem(key) as string)
} catch (error) {
return value
}
}
</script>
<style scoped lang="scss">
.wechat-share-popup {
.popup-content {
padding: 20px;
margin-bottom: 50px;
.popup-title {
text-align: center;
font-size: 16px;
color: #333;
margin-bottom: 20px;
}
.share-list {
display: flex;
justify-content: space-around;
align-items: center;
.share-box {
display: flex;
flex-direction: column;
align-items: center;
row-gap: 10px;
cursor: pointer;
&:hover {
.box-title {
color: var(--el-color-primary);
}
}
.box-icon {
width: 60px;
height: 60px;
}
.box-title {
font-size: 14px;
color: #333;
}
}
}
}
}
.wechat-share-mask {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 999;
background: rgba(0, 0, 0, 0.6);
.share-arrow {
position: absolute;
top: 0;
right: 0;
max-width: 100vw;
max-height: 80vh;
}
}
</style>