useShare.js 8.04 KB
/*
 * @Date: 2022-06-13 17:42:32
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2026-03-30 14:47:11
 * @FilePath: /mlaj/src/composables/useShare.js
 * @Description: 微信分享相关逻辑
 */
import wx from 'weixin-js-sdk'

import { wxInfo } from '@/utils/tools'

const resolveShareMetaSource = route => route?.meta?.share || route?.meta || {}

const resolveShareTitle = (route, documentObj, { preferDocumentTitle = false } = {}) => {
  const meta = route?.meta || {}
  const documentTitle = documentObj?.title || ''

  if (preferDocumentTitle && documentTitle) {
    return documentTitle
  }

  return meta.shareTitle || meta.ogTitle || meta.title || documentTitle || ''
}

const createTitleObserver = (documentObj, onChange) => {
  const titleEl = documentObj?.head?.querySelector('title')
  if (!titleEl || typeof MutationObserver === 'undefined') {
    return () => {}
  }

  const observer = new MutationObserver(() => {
    onChange()
  })

  observer.observe(titleEl, {
    childList: true,
    subtree: true,
    characterData: true,
  })

  return () => {
    observer.disconnect()
  }
}

/**
 * @function normalizeShareImageUrl
 * @description 规范化分享图片地址;若域名为 cdn.ipadbiz.cn,追加压缩参数。
 * @param {string} src 原始图片地址
 * @returns {string} 处理后的图片地址
 */
export function normalizeShareImageUrl(src) {
  if (!src) return ''
  if (src.includes('cdn.ipadbiz.cn')) {
    const compress = 'imageMogr2/thumbnail/200x/strip/quality/70'
    if (src.includes('?')) {
      return src.includes(compress) ? src : `${src}&${compress}`
    }
    return `${src}?${compress}`
  }
  return src
}

const buildShareLink = locationObj => locationObj?.href || ''

/**
 * @function buildWxSharePayload
 * @description 生成微信分享配置,统一补默认值与图片压缩规则。
 * @param {Object} params 分享参数
 * @param {string} params.title 分享标题
 * @param {string} params.desc 分享描述
 * @param {string} params.imgUrl 分享图标地址
 * @param {string} params.link 分享链接
 * @param {Object} options 运行选项
 * @param {Location} options.locationObj location 对象
 * @returns {{title: string, desc: string, link: string, imgUrl: string, success: Function}}
 */
export function buildWxSharePayload(
  { title = '', desc = '', imgUrl = '', link = '' } = {},
  { locationObj = window.location } = {}
) {
  return {
    title,
    desc,
    link: link || buildShareLink(locationObj),
    imgUrl: normalizeShareImageUrl(imgUrl),
    success() {},
  }
}

/**
 * @function applyWxShareData
 * @description 配置微信分享标题、描述与图标;需在 wx.ready 后调用才会生效。
 * @param {Object} params 分享参数
 * @param {Object} options 运行选项
 * @param {any} options.wxApi 微信 SDK 实例
 * @param {Location} options.locationObj location 对象
 * @returns {{title: string, desc: string, link: string, imgUrl: string, success: Function}}
 */
export function applyWxShareData(params = {}, { wxApi = wx, locationObj = window.location } = {}) {
  const shareData = buildWxSharePayload(params, { locationObj })

  if (!wxApi || typeof wxApi.ready !== 'function') {
    console.warn('微信 JSSDK 未就绪:分享配置可能未生效')
    return shareData
  }

  wxApi.ready(() => {
    if (typeof wxApi.updateAppMessageShareData === 'function') {
      wxApi.updateAppMessageShareData(shareData)
    }

    if (typeof wxApi.updateTimelineShareData === 'function') {
      wxApi.updateTimelineShareData(shareData)
    }

    if (typeof wxApi.onMenuShareWeibo === 'function') {
      wxApi.onMenuShareWeibo(shareData)
    }
  })

  return shareData
}

/**
 * @function resolveRouteShareData
 * @description 根据当前路由与文档标题解析分享配置。
 * @param {import('vue-router').RouteLocationNormalizedLoaded | null | undefined} route 当前路由
 * @param {Object} options 运行选项
 * @param {Document} options.documentObj document 对象
 * @param {Location} options.locationObj location 对象
 * @param {boolean} options.preferDocumentTitle 是否优先使用 document.title
 * @returns {{title: string, desc: string, imgUrl: string, link: string}}
 */
export function resolveRouteShareData(
  route,
  { documentObj = document, locationObj = window.location, preferDocumentTitle = false } = {}
) {
  const metaSource = resolveShareMetaSource(route)
  const title = resolveShareTitle(route, documentObj, { preferDocumentTitle })

  return {
    title,
    desc:
      metaSource.desc ||
      metaSource.shareDesc ||
      metaSource.description ||
      metaSource.ogDescription ||
      '',
    imgUrl: normalizeShareImageUrl(
      metaSource.imgUrl || metaSource.shareImage || metaSource.image || metaSource.ogImage || ''
    ),
    link:
      metaSource.link ||
      metaSource.shareLink ||
      metaSource.url ||
      metaSource.ogUrl ||
      buildShareLink(locationObj),
  }
}

const mergeResolvedShareData = (
  baseShareData,
  resolvedShareData,
  documentObj,
  { preferDocumentTitle = false } = {}
) => {
  const mergedShareData = {
    ...baseShareData,
    ...(resolvedShareData || {}),
  }

  if (resolvedShareData && 'description' in resolvedShareData && !('desc' in resolvedShareData)) {
    mergedShareData.desc = resolvedShareData.description
  }

  if (resolvedShareData && 'image' in resolvedShareData && !('imgUrl' in resolvedShareData)) {
    mergedShareData.imgUrl = resolvedShareData.image
  }

  if (resolvedShareData && 'url' in resolvedShareData && !('link' in resolvedShareData)) {
    mergedShareData.link = resolvedShareData.url
  }

  if (mergedShareData.imgUrl) {
    mergedShareData.imgUrl = normalizeShareImageUrl(mergedShareData.imgUrl)
  }

  if (preferDocumentTitle && documentObj?.title) {
    mergedShareData.title = documentObj.title
  }

  return mergedShareData
}

/**
 * @function installWxShareSync
 * @description 把微信分享配置绑定到路由与标题变化上,避免页面里重复手动调用。
 * @param {import('vue-router').Router} router 路由实例
 * @param {Object} options 运行选项
 * @param {any} options.wxApi 微信 SDK 实例
 * @param {Document} options.documentObj document 对象
 * @param {Location} options.locationObj location 对象
 * @param {boolean} options.isEnabled 是否启用同步;默认仅在微信环境且非开发环境启用
 * @returns {() => void} 清理函数
 */
export function installWxShareSync(
  router,
  { wxApi = wx, documentObj = document, locationObj = window.location, isEnabled, resolver } = {}
) {
  const enabled =
    typeof isEnabled === 'boolean' ? isEnabled : !import.meta.env.DEV && wxInfo().isWeiXin
  if (!router || !enabled) {
    return () => {}
  }

  let syncToken = 0

  const buildShareData = async (route, { preferDocumentTitle = false } = {}) => {
    const baseShareData = resolveRouteShareData(route, {
      documentObj,
      locationObj,
      preferDocumentTitle,
    })
    const resolvedShareData =
      typeof resolver === 'function'
        ? await resolver(route, { documentObj, locationObj, preferDocumentTitle })
        : null

    return mergeResolvedShareData(baseShareData, resolvedShareData, documentObj, {
      preferDocumentTitle,
    })
  }

  const syncFromRoute = async (route, options = {}) => {
    const currentToken = ++syncToken
    const shareData = await buildShareData(route, options)
    if (currentToken !== syncToken) {
      return
    }

    applyWxShareData(shareData, {
      wxApi,
      locationObj,
    })
  }

  const syncFromDocumentTitle = () => {
    syncFromRoute(router.currentRoute?.value, {
      preferDocumentTitle: true,
    })
  }

  const removeAfterEach = router.afterEach?.(to => {
    syncFromRoute(to)
  })

  const stopObserveTitle = createTitleObserver(documentObj, syncFromDocumentTitle)

  syncFromDocumentTitle()

  return () => {
    stopObserveTitle()
    removeAfterEach?.()
  }
}

/**
 * @function sharePage
 * @description 兼容旧调用方式的轻量封装。
 * @param {Object} params 分享参数
 * @returns {{title: string, desc: string, link: string, imgUrl: string, success: Function}}
 */
export const sharePage = (params = {}) => applyWxShareData(params)