Files
dukang/packages/weixin-sdk/src/jssdk.ts
T

154 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { WechatJssdkConfig } from '@dukang/shared-types';
import { isIosDevice, isWechatBrowser, isWechatDevTools } from './env';
import { DEFAULT_JS_API_LIST } from './types';
const JSSDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
const IOS_ENTRY_URL_KEY = 'dukang_wx_ios_entry_url';
let scriptPromise: Promise<void> | null = null;
let configured = false;
let configuredUrl: string | null = null;
/** 参与 JSSDK 签名的页面 URL(去掉 hash、OAuth 回跳参数) */
export function normalizeJssdkPageUrl(rawUrl: string): string {
try {
const url = new URL(rawUrl);
url.hash = '';
url.searchParams.delete('code');
url.searchParams.delete('state');
const query = url.searchParams.toString();
return `${url.origin}${url.pathname}${query ? `?${query}` : ''}`;
} catch {
return rawUrl.split('#')[0];
}
}
/** iOS 微信内 SPA:签名 URL 须用首次进入页面的入口 URL,而非路由跳转后的当前 URL */
export function captureIosJssdkEntryUrl(): void {
if (typeof window === 'undefined') return;
if (!isIosDevice() || !isWechatBrowser() || isWechatDevTools()) return;
if (sessionStorage.getItem(IOS_ENTRY_URL_KEY)) return;
sessionStorage.setItem(IOS_ENTRY_URL_KEY, normalizeJssdkPageUrl(window.location.href));
}
/** 获取参与 JSSDK 签名的 URL */
export function getJssdkSignUrl(rawUrl?: string): string {
const current = normalizeJssdkPageUrl(rawUrl ?? (typeof window !== 'undefined' ? window.location.href : ''));
if (typeof window !== 'undefined' && isIosDevice() && isWechatBrowser() && !isWechatDevTools()) {
const entry = sessionStorage.getItem(IOS_ENTRY_URL_KEY);
if (entry) return entry;
}
return current;
}
function isJssdkDebugEnabled(): boolean {
if (typeof window === 'undefined') return false;
try {
return new URLSearchParams(window.location.search).get('wxdebug') === '1';
} catch {
return false;
}
}
export function stripOAuthParamsFromLocation(): void {
if (typeof window === 'undefined') return;
const url = new URL(window.location.href);
if (!url.searchParams.has('code') && !url.searchParams.has('state')) return;
url.searchParams.delete('code');
url.searchParams.delete('state');
const query = url.searchParams.toString();
const next = `${url.pathname}${query ? `?${query}` : ''}${url.hash}`;
window.history.replaceState({}, '', next);
}
function loadScript(): Promise<void> {
if (typeof document === 'undefined') return Promise.reject(new Error('非浏览器环境'));
if (window.wx) return Promise.resolve();
if (scriptPromise) return scriptPromise;
scriptPromise = new Promise((resolve, reject) => {
const existing = document.querySelector<HTMLScriptElement>('script[data-dukang-wx-jssdk]');
if (existing) {
existing.addEventListener('load', () => resolve());
existing.addEventListener('error', () => reject(new Error('微信 JSSDK 加载失败')));
return;
}
const script = document.createElement('script');
script.src = JSSDK_URL;
script.async = true;
script.dataset.dukangWxJssdk = '1';
script.onload = () => resolve();
script.onerror = () => reject(new Error('微信 JSSDK 加载失败'));
document.head.appendChild(script);
});
return scriptPromise;
}
async function fetchJssdkConfig(apiBase: string, clientApp: string, url: string, token?: string | null) {
const headers: Record<string, string> = { 'X-Client-App': clientApp };
if (token) headers.Authorization = `Bearer ${token}`;
const qs = new URLSearchParams({ url });
const res = await fetch(`${apiBase}/common/wechat/jssdk-config?${qs}`, { headers });
const json = await res.json();
if (json.code !== 0) throw new Error(json.message || '获取 JSSDK 配置失败');
return json.data as WechatJssdkConfig;
}
/** 初始化微信 JSSDKH5 公众号内) */
export async function initWechatJssdk(options: {
apiBase: string;
clientApp: string;
url?: string;
getAccessToken?: () => string | null;
jsApiList?: string[];
}): Promise<void> {
captureIosJssdkEntryUrl();
const { apiBase, clientApp, getAccessToken } = options;
const pageUrl = options.url ?? getJssdkSignUrl();
await loadScript();
if (!window.wx) throw new Error('微信 JSSDK 不可用');
const config = await fetchJssdkConfig(apiBase, clientApp, pageUrl, getAccessToken?.());
const jsApiList = options.jsApiList ?? [...DEFAULT_JS_API_LIST];
configured = false;
configuredUrl = null;
await new Promise<void>((resolve, reject) => {
window.wx!.config({
...config,
jsApiList,
debug: isJssdkDebugEnabled(),
});
window.wx!.ready(() => {
configured = true;
configuredUrl = pageUrl;
resolve();
});
window.wx!.error((err) => reject(new Error(err.errMsg || 'wx.config 失败')));
});
}
export function isJssdkReady(): boolean {
const signUrl = getJssdkSignUrl();
return configured && !!window.wx && configuredUrl === signUrl;
}
export async function ensureJssdkReady(options: {
apiBase: string;
clientApp: string;
url?: string;
getAccessToken?: () => string | null;
jsApiList?: string[];
}): Promise<void> {
captureIosJssdkEntryUrl();
const pageUrl = options.url ?? getJssdkSignUrl();
if (!isJssdkReady()) {
await initWechatJssdk({ ...options, url: pageUrl });
}
}
if (typeof window !== 'undefined') {
captureIosJssdkEntryUrl();
}