需求背景
由于项目是H5的,用户主要是通过微信扫码进入,在微信上操作,并且会有分享操作,这里不做处理的话,另一方收到的就是一段链接而已,不好看,所以想实现卡片式的分享。最终想要实现的效果如下:
技术栈
- 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说明文档,内部提到:
于是我封装了初始化微信的 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();
大功告成!!!