【Vue】H5浏览器实现【微信分享】功能的终极解决方案

需求背景

老板希望在H5手机端浏览器中实现类似于APP的微信分享功能,就像这样:

但是呢,微信官方在 1.40及后续JS-SDK中取消了H5浏览器直接调起微信分享的API,所以这个方案是无法实现的,只能采用替代方案:

在微信内置 webview中:引导用户点击右上角的菜单按钮,并选择分享微信好友;

在非微信内置浏览器中:禁止用户分享微信好友,或者提示用户使用微信打开本链接。

效果预览

对方视角:

 

步骤流程:

1、用户点击分享按钮,弹出分享弹窗;

2、用户点击转发给微信好友图标,引导用户点击右上角菜单(非微信浏览器仅允许用户复制链接);

3、用户选中一个微信好友,确定发送,完成分享。

这时候可能有人会说了,这不是脱裤子放屁吗,本来微信右上角菜单就可以分享好友,只要引导用户点击菜单就行了,为什么还要写一篇博客专门探究这种事?

此言差矣,其实还是有些差别的:

  1. 不采用任何额外措施,直接引导用户点击右上角菜单并进行分享,只能分享链接给微信好友,而无法是卡片形式。(卡片形式可以显示公司Logo、标题和描述,体验更好
  2. 只能分享当前页面的链接,无法自定义分享的链接内容,限制较大,不够灵活

链接分享效果:

卡片分享效果:

如果你的产品经理接受这种方案,期望用户可以在微信浏览器中分享卡片视图给微信好友,那么你可以接着阅读下面的内容;

如果产品经理说不行,用户还要手动点击两下,分享出来的只能是微信卡片,而且不支持非微信浏览器,那么很抱歉,经过我多方面深入研究,H5浏览器基本不可能完成非微信浏览器调起微信分享好友功能,可以到此为止了。

(包括 先跳转到小程序再手动分享 和 各种.js原生分享方案,经实验要么繁琐程度相同,要么无法广泛兼容 安卓和IOS设备,广大前端er如有更好解决方案,欢迎打脸)

代码实现

如果你已经跟产品经理确定,这个方案下用户体验和浏览器限制确实可以接受,那么下面笔者将详细说明如何实现此效果,并带你避过路上所有大坑。

概念说明

  1. 微信官方的JS-SDK是微信公众平台 面向网页开发者提供的基于微信内的网页开发工具包,内置了很多微信API,但基本只能在微信内置浏览器中调用,非微信浏览器不支持。
  2. 我们主要使用微信JS-SDK里面的 updateAppMessageShareData 方法,该方法的功能是允许开发者把右上角的分享样式从链接变为卡片样式,并可以自定义 标题、描述和右侧的icon图标。

流程概述

运营同学:
  1. 微信公众平台中,为微信公众号/服务号添加JS安全域名(如下图)

前端同学:

        1. 引入微信JS-SDK

// npm
npm i weixin-js-sdk

// Vite 
import wx from 'weixin-js-sdk'

        2. 判断用户此时是否处于微信浏览器中(weixin-js-sdk仅支持微信浏览器环境)

// 判断当前web浏览器是否是微信环境 (此函数在微信小程序中返回true,但微信小程序不支持weixin-js-sdk)
export const isWeChatBrowser = () => {
  // 获取 userAgent 信息
  const userAgent = navigator.userAgent.toLowerCase()
  // 检查是否包含 'micromessenger' 字样
  return userAgent.indexOf('micromessenger') !== -1
}

        3. 向后端服务器请求签名等参数信息

// 处理微信分享事件
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' // 无法确定或非移动操作系统
  }
}

        4. 使用 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)
  })
}

        5. 引导遮罩出现,用户手动点击微信浏览器右上角的菜单按钮,进行分享:    

后端同学:
  1. 根据前端给你的url地址,计算出sdk签名,和公众号APPID等信息一同提供给前端即可。
  2. 按照weixin-js-sdk官方文档说法,签名必须在服务端进行计算,具体签名计算方法请参考微信官方文档:

概述 | 微信开放文档微信开发者平台文档https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62

特别注意事项

需要特别注意的是,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签名错误,官方文档推荐按照以下顺序进行检

  1. 确认签名算法正确,可用微信 JS 接口签名校验工具 页面工具进行校验。
  2. 确认config中nonceStr(js中驼峰标准大写S), timestamp与用以签名中的对应noncestr, timestamp一致。
  3. 确认url是页面完整的url(请在当前页面alert(location.href.split('#')[0])确认),包括'http(s)://'部分,以及'?'后面的GET参数部分,但不包括'#'hash后面的部分。
  4. 确认 config 中的 appid 与用来获取 jsapi_ticket 的 appid 一致。
  5. 确保一定缓存access_token和jsapi_ticket。
  6. 确保你获取用来签名的url是动态获取的,动态页面可参见实例代码中php的实现方式。如果是html的静态页面在前端通过ajax将url传到后台签名,前端需要用js获取当前页面除去'#'hash部分的链接(可用location.href.split('#')[0]获取,而且需要encodeURIComponent),因为页面一旦分享,微信客户端会在你的链接末尾加入其它参数,如果不是动态获取当前链接,将导致分享后的页面签名失败。

此外,另外一个需要特别注意的是:从别人微信分享的蓝色链接打开网址的用户,是无法分享出来卡片的!!!只有规避掉蓝色链接才能分享出来卡片!!!

这是微信另一个Bug导致的,你从别人分享的卡片打开网址,然后分享给其他人,采用笔者上述方法是完全可以分享出来卡片的。但是!如果你是从别人分享的蓝色链接打开网址,那么就百分之一亿分享不出来卡片!

从别人发送/分享的蓝色链接打开网址,只能分享出同样的蓝色链接:

从上面的链接进入时,只能分享出链接地址

从分享的卡片打开网址,或扫码打开网址,都可以分享出卡片给微信好友:

从上面的卡片进入时,可以正常分享出卡片

上面两个大坑,花费了我无数根头发才发现的,文档完全没提这事,着实是坑爹啊😅

附录:代码全文

除去上面几个大坑之外,代码实现起来还是很简单的,照官方文档抄过来就行。

从手机底部出现一个弹窗的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>

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值