Files
Friendship-and-chat-uniapp/src/sheep/platform/h5-shell.js
T
2026-07-31 13:56:18 +08:00

234 lines
7.7 KiB
JavaScript

const H5_STATUS_BAR_QUERY_KEYS = ['statusBarHeight', 'safeTop', 'safeAreaTop', 'appStatusBarHeight'];
const H5_MIN_STATUS_BAR_HEIGHT = 54;
const H5_MAX_STATUS_BAR_HEIGHT = 200;
let currentStatusBarHeight = H5_MIN_STATUS_BAR_HEIGHT;
let statusBarStyleObserver = null;
// H5 产物运行在 5+ WebView 中时优先交给 App 打开,普通浏览器则在当前页跳转。
export function openExternalUrl(url, source = 'unknown') {
const logPrefix = `[openExternalUrl][${source}]`;
if (!url) {
console.error(logPrefix, '跳转失败:URL 为空');
return false;
}
const hasGlobalThis = typeof globalThis !== 'undefined';
const plusObject = hasGlobalThis ? globalThis.plus : null;
const runtime = plusObject?.runtime;
const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
console.log(logPrefix, '开始跳转', {
targetUrl: url,
currentUrl,
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
documentReadyState: typeof document !== 'undefined' ? document.readyState : '',
hasWindow: typeof window !== 'undefined',
hasGlobalThis,
hasPlus: Boolean(plusObject),
hasPlusRuntime: Boolean(runtime),
hasPlusOpenURL: typeof runtime?.openURL === 'function',
});
if (runtime?.openURL) {
try {
console.log(logPrefix, '命中 5+ WebView,准备调用 plus.runtime.openURL');
runtime.openURL(url, (error) => {
console.error(logPrefix, 'plus.runtime.openURL 错误回调', error);
});
console.log(logPrefix, 'plus.runtime.openURL 已调用(该 API 无成功回调)');
return true;
} catch (error) {
console.error(logPrefix, 'plus.runtime.openURL 抛出异常', error);
return false;
}
}
if (typeof window !== 'undefined') {
try {
console.warn(logPrefix, '未检测到 plus.runtime.openURL,回退到 window.location.assign');
window.location.assign(url);
console.log(logPrefix, 'window.location.assign 已调用');
window.setTimeout(() => {
if (window.location.href === currentUrl) {
console.warn(logPrefix, '1.5 秒后页面 URL 未变化,跳转可能被 WebView 拦截');
}
}, 1500);
return true;
} catch (error) {
console.error(logPrefix, 'window.location.assign 抛出异常', error);
return false;
}
}
console.error(logPrefix, '跳转失败:window 和 plus.runtime 均不可用');
return false;
}
// 将 URL 参数、宿主全局变量和 postMessage 消息中的高度统一转换为数字。
function normalizeStatusBarHeight(value) {
if (value === undefined || value === null || value === '') {
return null;
}
const normalizedValue = typeof value === 'string' ? value.replace(/px$/i, '').trim() : value;
const height = Number.parseFloat(normalizedValue);
if (!Number.isFinite(height)) {
return null;
}
return Math.max(0, Math.min(height, H5_MAX_STATUS_BAR_HEIGHT));
}
// 宿主未提供高度或提供的高度过小时,统一使用 H5 最小状态栏高度。
function resolveStatusBarHeight(value) {
const height = normalizeStatusBarHeight(value);
return height === null ? H5_MIN_STATUS_BAR_HEIGHT : Math.max(height, H5_MIN_STATUS_BAR_HEIGHT);
}
function parseMessageData(data) {
if (!data) {
return null;
}
if (typeof data === 'string') {
try {
return JSON.parse(data);
} catch (error) {
return null;
}
}
return data;
}
// URL 参数优先级最高,同时兼容普通查询参数和 hash 查询参数。
function readStatusBarHeightFromQuery() {
const searchParamsList = [];
searchParamsList.push(new URLSearchParams(window.location.search));
const hashQuery = window.location.hash.includes('?') ? window.location.hash.split('?')[1] : '';
if (hashQuery) {
searchParamsList.push(new URLSearchParams(hashQuery));
}
for (const params of searchParamsList) {
for (const key of H5_STATUS_BAR_QUERY_KEYS) {
const height = normalizeStatusBarHeight(params.get(key));
if (height !== null) {
return height;
}
}
}
return null;
}
// 宿主可以在 H5 应用初始化前将状态栏高度注入 window。
function readStatusBarHeightFromWindow() {
const candidates = [
window.__APP_STATUS_BAR_HEIGHT__,
window.__UNI_STATUS_BAR_HEIGHT__,
window.__APP_SHELL_CONFIG__?.statusBarHeight,
window.__UNI_WEBVIEW_CONFIG__?.statusBarHeight,
];
for (const candidate of candidates) {
const height = normalizeStatusBarHeight(candidate);
if (height !== null) {
return height;
}
}
return null;
}
// 读取页面当前实际使用的 --status-bar-height。
export function getH5StatusBarHeight() {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return 0;
}
const statusBarHeight = window
.getComputedStyle(document.documentElement)
.getPropertyValue('--status-bar-height')
.replace(/px$/i, '')
.trim();
return resolveStatusBarHeight(statusBarHeight);
}
// 依次从 URL、宿主全局变量中获取初始高度,均未提供时使用默认最小高度。
export function getH5HostStatusBarHeight() {
if (typeof window === 'undefined') {
return 0;
}
return resolveStatusBarHeight(readStatusBarHeightFromQuery() ?? readStatusBarHeightFromWindow());
}
// 只在 html 上设置一个状态栏变量,body 和其他子元素会自动继承。
export function applyH5HostStatusBarHeight(height) {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return 0;
}
const statusBarHeight = resolveStatusBarHeight(height);
currentStatusBarHeight = statusBarHeight;
document.documentElement.style.setProperty('--status-bar-height', `${statusBarHeight}px`, 'important');
// 通知使用数值高度参与 JavaScript 布局计算的页面重新计算。
window.dispatchEvent(
new CustomEvent('app-shell-status-bar-change', {
detail: { height: statusBarHeight },
}),
);
return statusBarHeight;
}
// uni-app 重写 html 样式后,如果状态栏高度小于 22px,则立即恢复当前有效高度。
function observeStatusBarHeight() {
if (statusBarStyleObserver || typeof MutationObserver === 'undefined') {
return;
}
statusBarStyleObserver = new MutationObserver(() => {
const statusBarHeight = normalizeStatusBarHeight(
document.documentElement.style.getPropertyValue('--status-bar-height'),
);
if (statusBarHeight === null || statusBarHeight < H5_MIN_STATUS_BAR_HEIGHT) {
applyH5HostStatusBarHeight(currentStatusBarHeight);
}
});
statusBarStyleObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style'],
});
}
// 解析宿主运行期间通过 postMessage 发送的状态栏高度。
function resolveMessageStatusBarHeight(message) {
const data = parseMessageData(message?.data);
if (!data) {
return null;
}
return normalizeStatusBarHeight(
data.statusBarHeight ??
data.safeTop ??
data.safeAreaTop ??
data.payload?.statusBarHeight ??
data.payload?.safeTop,
);
}
// 初始化状态栏高度,并支持宿主通过全局方法或 postMessage 动态更新。
export function initH5HostShell() {
if (typeof window === 'undefined') {
return 0;
}
const safeTop = applyH5HostStatusBarHeight(getH5HostStatusBarHeight());
if (window.__FRIENDSHIP_H5_SHELL_READY__) {
return safeTop;
}
window.__FRIENDSHIP_H5_SHELL_READY__ = true;
observeStatusBarHeight();
window.setAppShellStatusBarHeight = (nextHeight) => applyH5HostStatusBarHeight(nextHeight);
window.addEventListener('message', (message) => {
const nextHeight = resolveMessageStatusBarHeight(message);
if (nextHeight !== null) {
applyH5HostStatusBarHeight(nextHeight);
}
});
return safeTop;
}