场景

在微信中点开链接打开网页,在微信浏览器的右上角有“...”,点开后就可以分享链接,但是只能分享本页面的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)
  })
}

后端返回

  1. 根据前端给你的url地址,计算出sdk签名,和公众号APPID等信息一同提供给前端即可。

  2. 按照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签名错误,官方文档推荐按照以下顺序进行检

  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),因为页面一旦分享,微信客户端会在你的链接末尾加入其它参数,如果不是动态获取当前链接,将导致分享后的页面签名失败。

现在H5直接分享不是卡片,而是蓝色的链接。如果想要得到卡片的样式,有以下三种方式:

  1. 在公众号菜单中的链接打开,才能正常分享

  2. 可以通过收藏链接 再打开分享

  3. 把链接换成二维码,扫码后进入分享

附录:代码全文

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

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