Vue3+TS的H5项目实现微信和企业微信分享卡片样式,并兼容ios签名失败问题

639 阅读7分钟

需求背景

由于项目是H5的,用户主要是通过微信扫码进入,在微信上操作,并且会有分享操作,这里不做处理的话,另一方收到的就是一段链接而已,不好看,所以想实现卡片式的分享。最终想要实现的效果如下:

image.png

技术栈

  • vue:3.5.13
  • weixin-js-sdk-ts:1.6.1
  • @wecom/jssdk:2.3.1

实现步骤

安装weixin-js-sdk-ts

npm install weixin-js-sdk-ts@1.6.1 npm install @wecom/jssdk@2.3.1

sdk初始化

查阅微信网页开发 / JS-SDK说明文档,内部提到:

image.png

于是我封装了初始化微信的 hooks,在页面初始化的时候调用并传递当前页面 url调用后端接口来获取微信初始化所需要的参数:

import wx from "weixin-js-sdk-ts";
import LiveApi from "@/apis/live/index";
import { usePlatformStore } from "@/store/platformStore";
import * as ww from "@wecom/jssdk";
import { WechatTypeEnum } from "@/enum/platformEnum";

type CorpConfig = {
  corpId: string;
  getSignature: () => Promise<{
    timestamp: number;
    nonceStr: string;
    signature: string;
  }>;
};

/**
 * 多平台微信 SDK 初始化管理器
 * @author 鹏北海 <gaoshunpeng76@163.com>
 * @since 2025-05-27
 *
 * @returns {{
 *   initWechat: (platform_url: string) => Promise<boolean>,
 *   ws: typeof wx | typeof ww | null
 * }} 返回包含以下成员的对象:
 *   - initWechat: 跨平台初始化方法(自动识别微信/企业微信环境)
 *   - ws: 动态绑定的 SDK 实例(微信 wx / 企业微信 ww)
 *
 * @remarks
 * ### 核心特性:
 * 1. **双平台适配**:自动根据用户平台选择微信 JS-SDK 或企业微信 JS-SDK
 * 2. **安全初始化**:通过后端接口动态获取签名配置,防止前端暴露敏感信息
 * 3. **调试支持**:开发环境自动开启 SDK 调试模式(需取消注释 debug 配置)
 * 4. **生命周期管理**:提供 configSuccess/configFail 完整生命周期钩子
 *
 * ### 平台差异说明:
 * | 特性                | 微信                   | 企业微信                  |
 * |---------------------|-----------------------|--------------------------|
 * | SDK 实例            | `wx`                 | `ww`                    |
 * | 分享 API            | updateAppMessageShareData | onMenuShareAppMessage |
 * | 初始化方式          | config()             | register()             |
 * | 企业专属参数        | 无                   | corpId                |
 *
 * @example
 * // 基本使用(自动识别平台)
 * const { initWechat, ws } = useWechat();
 * await initWechat(window.location.href);
 *
 * // 企业微信专属用法
 * if (useUserStore().platform === '企业微信') {
 *   ws.agentConfig({ ... });
 * }
 *
 * @see {@link https://developer.work.weixin.qq.com/document/path/90514 企业微信JS-SDK文档}
 * @see {@link https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html 微信JS-SDK文档}
 */
export function useWechat() {
  const initWechat = async (platform_url: string) => {
    switch (usePlatformStore().wechatType) {
      case WechatTypeEnum.WX:
        console.log("当前是微信");
        await initWeixin(platform_url);
        break;
      case WechatTypeEnum.WXWORK:
        console.log("当前是企微");
        await initWeWork(platform_url);
        break;
    }
  };

  // 初始化企业微信
  async function initWeWork(platform_url: string) {
    console.log("%c初始化企业微信SDK配置", "color: green;");
    const { corpId, getSignature } = await fetchCorpConfig(platform_url);

    return new Promise((resolve, reject) => {
      ww.register({
        corpId: corpId, // 必填,当前用户企业所属企业ID
        jsApiList: ["onMenuShareAppMessage", "onMenuShareWechat", "onMenuShareTimeline"], // 必填,需要使用的JSAPI列表
        getConfigSignature: getSignature, // 必填,根据url生成企业签名的回调函数
        onConfigSuccess: () => {
          console.log("%cwwork.configSuccess", "color: green;");
          resolve(true);
        },
        onConfigFail: (err) => {
          console.log("%cwwork.configFail", "color: red;");
          console.log(err);
          reject(err);
        },
        onConfigComplete: () => {
          console.log("%cwwork.configComplete", "color: blue;");
        },
      });
    });
  }

  // 初始化微信
  async function initWeixin(platform_url: string) {
    console.log("platform_url:", platform_url);
    const { data } = await LiveApi.getWeChatKey({
      platform_url: platform_url,
      type: "wechat",
    });
    console.log("%c初始化微信SDK配置", "color: green;");
    const config: WeixinConfig = {
      debug: /* import.meta.env.MODE === "development" */ false, // 开发环境开启调试
      appId: data.appId,
      timestamp: data.timestamp,
      nonceStr: data.nonceStr,
      signature: data.signature,
      jsApiList: ["updateAppMessageShareData", "updateTimelineShareData"],
      openTagList: [],
    };
    console.log("微信初始化参数:", config);

    wx.config(config);
    return new Promise((resolve, reject) => {
      wx.ready(() => {
        console.log("%cwx.ready", "color: green;");
        resolve(true);
      });
      wx.error((error) => {
        console.log("%cwx.error", "color: red;");
        reject(error);
      });
    });
  }

  async function fetchCorpConfig(platform_url: string): Promise<CorpConfig> {
    // 从API获取企业配置
    const { data } = await LiveApi.getWeChatKey({
      platform_url: platform_url,
      type: "weCom",
    });

    return {
      corpId: data.appId,
      getSignature: () =>
        Promise.resolve({
          timestamp: data.timestamp,
          nonceStr: data.nonceStr,
          signature: data.signature,
        }),
    };
  }

  return { initWechat, ww, wx };
}

/**
 * 初始化iOS入口URL记录器(去除URL哈希片段)
 *
 * @description 在iOS环境中存储不含#的初始页面URL,用于微信签名等场景
 * @see {@link signLink} 关联的签名链接生成函数
 * @see {@link https://blog.51cto.com/u_16213575/11346224} 参考链接
 * @author 鹏北海 <gaoshunpeng76@163.com>
 */
export function setIosEntryUrl() {
  console.log("ios 记录刚进入页面时的url:", window.location.href.split("#")[0]);
  // @ts-ignore
  if (typeof window.iosEntryUrl === "undefined" || window.iosEntryUrl === "") {
    // @ts-ignore
    window.iosEntryUrl = window.location.href.split("#")[0];
  }
}

/**
 * 生成跨平台签名链接
 *
 * @description
 * - Android设备使用当前页面URL(自动去除哈希片段)
 * - iOS设备使用预先存储的入口URL(通过setIosEntryUrl初始化)
 *
 * @returns {string} 适配不同平台的标准化URL,用于微信签名等场景
 * @see {@link setIosEntryUrl} 关联的iOS入口URL初始化函数
 * @author 鹏北海 <gaoshunpeng76@163.com>
 */
export function signLink(): string {
  // @ts-ignore
  const url = /(Android)/i.test(navigator.userAgent) ? location.href.split("#")[0] : window.iosEntryUrl;
  console.log("签名链接:", url);
  // @ts-ignore
  return url;
}

使用方法:

<script setup lang="ts">
const { ww, wx, initWechat } = useWechat();

await initWechat(window.location.href);

// 微信分享
wx.updateAppMessageShareData({...})
wx.updateTimelineShareData({...})

// 企微
ww.onMenuShareAppMessage({...})
ww.onMenuShareWechat({...})
ww.onMenuShareTimeline({...})
</script>

响应式配置分享参数

因为实际业务中,调用分享 api 的参数是异步获取的,,所以我又在初始化的基础上封装了一个微信分享的 hooks,以保证微信分享时,能够获得异步获取到的数据来配置参数:

// src/hooks/useWechatShare.ts
import { watch } from "vue";
import { useWechat, signLink } from "./useWechat";
import { usePlatformStore } from "@/store/platformStore";
import { WechatTypeEnum } from "@/enum/platformEnum";

/**
 * 微信、企业微信分享自定义响应式配置钩子
 * @author 鹏北海 <gaoshunpeng76@163.com>
 * @since 2025-05-27
 *
 * @param {Object} options - 动态分享配置选项
 * @param {Function} [options.trigger] - 触发分享配置更新的观察函数(当返回值变化时触发更新)
 * @param {string|function} options.title - 动态分享标题(支持响应式函数或字符串)
 * @param {string|function} [options.desc] - 动态分享描述(支持响应式函数或字符串,未提供时使用默认描述)
 * @param {string|function} [options.imgUrl] - 动态分享图标URL(支持响应式函数或字符串)
 * @param {string|function} options.link - 微信初始化路径(支持响应式函数或字符串)
 * @param {Function} [options.onSuccess] - 分享配置成功回调(配置成功后自动停止监听)
 * @param {Function} [options.onError] - 分享配置失败/取消回调
 * @returns {{ startWxShareWatch: () => Promise<void> }} 返回包含启动监听方法的对象
 *
 * @remarks
 * ### 核心特性:
 * 1. 响应式配置:通过 Vue `watch` 实现动态配置更新,使用 { flush: 'post' } 确保 DOM 更新后获取最新 URL
 * 2. 多平台支持:自动识别微信/企业微信环境,配置对应分享接口(AppMessage/Timeline/Wechat)
 * 3. 智能类型处理:自动解析函数型配置项,支持同步/异步获取
 * 4. 生命周期管理:配置成功后自动停止监听,避免重复触发
 *
 * ### 使用指南:
 * - **异步场景**:当分享参数需要异步获取时,请使用函数形式返回配置项,并配合 `trigger` 观察函数触发更新
 * - **静态场景**:直接使用字符串配置项,无需传递 `trigger` 参数
 * - **错误处理**:建议通过 onError 回调处理微信 SDK 初始化失败等异常情况
 *
 * @example
 * // 基本使用
 * const { startWxShareWatch } = useWechatShare({
 *   title: () => reactiveData.title,
 *   link: window.location.href,
 *   trigger: () => reactiveData.updateFlag
 * });
 *
 * @see {@link https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#10 微信JS-SDK文档}
 */
export function useWechatShare(options: {
  trigger?: () => unknown;
  title: (() => string) | string;
  desc?: (() => string) | string;
  imgUrl?: (() => string) | string;
  link: (() => string) | string;
  onSuccess?: () => void;
  onError?: () => void;
}) {
  const { ww, wx, initWechat } = useWechat();
  // 如果没有提供描述,则使用默认值
  if (!options.desc) {
    console.log(
      "缺少描述,使用默认值: xxxxxxxxxxxxxxxxxxxxxxxxx",
    );
    options.desc =
      "xxxxxxxxxxxxxxxxxxxxxxxxxx";
  }
  const startWxShareWatch = async () => {
    console.log("startWxShareWatch");
    if (!options.trigger) {
      console.log("!trigger");
      await fun();
    } else {
      console.log("trigger");
      const stop = watch(
        options.trigger,
        async (_newValue, _oldValue) => {
          console.log(_newValue, _oldValue);
          await fun(stop);
        },
        {
          // 确保在 DOM 更新后执行,避免获取过时的页面 URL
          flush: "post",
        },
      );
    }

    async function fun(stop?: () => void) {
      console.log("fun");
      try {
        console.log("initWechat");
        console.log("window.location.href:", window.location.href);
        console.log("link:", typeof options.link === "function" ? options.link() : options.link);
        await initWechat(signLink());
        const success = (msg: string) => {
          return () => {
            console.log(`${msg}分享配置更新成功`);
            options.onSuccess?.();
            stop?.(); // 配置成功后停止监听
          };
        };

        const cancel = (msg: string) => {
          return () => {
            console.log(`${msg}分享配置更新取消`);
            options.onError?.();
          };
        };
        console.log("=============  options =============");
        console.log(options);
        const commonOptions = {
          title: typeof options.title === "function" ? options.title() : options.title,
          link: typeof options.link === "function" ? options.link() : options.link,
          imgUrl: options.imgUrl ? (typeof options.imgUrl === "function" ? options.imgUrl() : options.imgUrl) : "",
        };
        if (usePlatformStore().wechatType === WechatTypeEnum.WX) {
          console.log("注册微信分享");
          // @ts-ignore
          wx.updateAppMessageShareData({
            ...commonOptions,
            desc: typeof options.desc === "function" ? options.desc() : options.desc!,
            success: success("QQ、微信"),
            cancel: cancel("QQ、微信"),
            fail: (err) => {
              console.log("QQ、微信失败");
              console.log(err);
            },
            complete: (res) => {
              console.log("QQ、微信complete");
              console.log(res);
            },
          });
          // @ts-ignore
          wx.updateTimelineShareData({
            ...commonOptions,
            success: success("朋友圈、QQ空间"),
            cancel: cancel("朋友圈、QQ空间"),
            fail: (err) => {
              console.log(err);
            },
            complete: (res) => {
              console.log(res);
            },
          });
        } else if (usePlatformStore().wechatType === WechatTypeEnum.WXWORK) {
          console.log("注册企业微信分享");
          // @ts-ignore
          ww.onMenuShareAppMessage({
            ...commonOptions,
            desc: typeof options.desc === "function" ? options.desc() : options.desc!,
            success: success("转发"),
            cancel: cancel("转发"),
          });
          // @ts-ignore
          ww.onMenuShareWechat({
            ...commonOptions,
            desc: typeof options.desc === "function" ? options.desc() : options.desc!,
            success: success("QQ、微信"),
            cancel: cancel("QQ、微信"),
          });
          // @ts-ignore
          ww.onMenuShareTimeline({
            ...commonOptions,
            success: success("朋友圈、QQ空间"),
            cancel: cancel("朋友圈、QQ空间"),
          });
        }
      } catch (error) {
        console.error("微信初始化失败", error);
        options.onError?.();
      }
    }
  };

  return {
    startWxShareWatch,
  };
}

使用方式,在每一个需要分享的页面:

<script setup lang="ts">
import { useWechatShare } from "@/hooks/useWechatShare";
import { onMounted } from "vue";

// 初始化微信分享
const { startWxShareWatch } = useWechatShare({
  title: () => "标题",
  imgUrl: () => "图片url",
  desc: "描述",
  link: window.location.href, // 分享当前页面
});

// 监听微信分享状态变化
startWxShareWatch();
</script>

而如果分享所需要的数据是接口异步返回的:

<script setup lang="ts">
import { useWechatShare } from "@/hooks/useWechatShare";

const liveReactiveData = reactive({
    livePoster: "",
    liveTitle: "",
    liveDesc: ""
})

// 初始化微信分享
const { startWxShareWatch } = useWechatShare({
  trigger: () => liveReactiveData.liveTitle,
  title: () => liveReactiveData.liveTitle,
  desc: () => liveReactiveData.liveDesc
  imgUrl: () => liveReactiveData.livePoster,
  link: window.location.href
});

  


// 监听微信分享状态变化
startWxShareWatch();

onMounted(() => {
    // 模拟异步获取数据
    setTimeout(() => {
        liveReactiveData.liveTitle = "标题";
        liveReactiveData.livePoster = "https://xxxxxxxxx.png";
        liveReactiveData.liveDesc = "描述";
    }, 5000)
})
</script>

解决ios设备微信锁定spa应用url为首次进入时的url,导致ios路由跳转时签名失败问题(巨坑!!!)

问题参考:

Ios平台接入微信分享 ios分享到微信跳转不过去_mob64ca13f63f2c的技术博客_51CTO博客

调用 jssdk 在ios 上一直报invalid signature 的问题解决_vue ios 微信公众号 定位jssdk报错-CSDN博客

【全局分享】在ios下微信内置浏览器内分享出去的地址不正确的问题 · Issue #579 · slimkit/plus-small-screen-client

// App.vue
import { setIosEntryUrl } from "@/hooks/useWechat";

setIosEntryUrl();

大功告成!!!