您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
零延迟返回按钮,自动记录和恢复滚动位置,支持无痕模式
// ==UserScript== // @name Universal Android-Back Button – 滚动记忆版 // @namespace https://viayoo.com/universal-back // @version 2.0.0 // @description 零延迟返回按钮,自动记录和恢复滚动位置,支持无痕模式 // @author // @run-at document-end // @match *://*/* // @grant GM_xmlhttpRequest // ==/UserScript== (function () { 'use strict'; /* ---------- 共用工具 ---------- */ const CACHE_EXPIRY = 5 * 60 * 1000; // 5分钟缓存 const pageCache = new Map(); function isPseudoTurn(prev, next) { try { const u1 = new URL(prev); const u2 = new URL(next); // 1. 仅 hash 变化 if (u1.origin === u2.origin && u1.pathname === u2.pathname && u1.search === u2.search) return true; // 2. 仅典型分页 query 变化 const PAGE_KEYS = ['page', 'p', 'start', 'offset']; if (u1.origin === u2.origin && u1.pathname === u2.pathname) { const s1 = new URLSearchParams(u1.search); const s2 = new URLSearchParams(u2.search); for (const k of PAGE_KEYS) { s1.delete(k); s2.delete(k); } return s1.toString() === s2.toString(); } return false; } catch (e) { return false; } } /* ---------- 1. 状态管理 ---------- */ const TAB_ID_KEY = '_ub_tab_id'; if (!sessionStorage.getItem(TAB_ID_KEY)) { sessionStorage.setItem(TAB_ID_KEY, Math.random().toString(36).slice(2)); } const TAB_ID = sessionStorage.getItem(TAB_ID_KEY); 0 const STACK_KEY = `_ub_stack_${TAB_ID}`; const CACHE_KEY = `_ub_cache_${TAB_ID}`; const BACK_FLAG = `_ub_is_back_${TAB_ID}`; const SCROLL_KEY = `_ub_scroll_${TAB_ID}`; let stack = JSON.parse(sessionStorage.getItem(STACK_KEY) || '[]'); let cache = JSON.parse(sessionStorage.getItem(CACHE_KEY) || '{}'); let scrollPositions = JSON.parse(sessionStorage.getItem(SCROLL_KEY) || '{}'); function saveState() { sessionStorage.setItem(STACK_KEY, JSON.stringify(stack)); sessionStorage.setItem(CACHE_KEY, JSON.stringify(cache)); sessionStorage.setItem(SCROLL_KEY, JSON.stringify(scrollPositions)); } // 清理过期缓存和滚动记录 function cleanCache() { const now = Date.now(); for (const url in cache) { if (cache[url].expiry < now) { delete cache[url]; } } // 清理超过24小时的滚动记录 for (const url in scrollPositions) { if (scrollPositions[url].timestamp < now - 24 * 60 * 60 * 1000) { delete scrollPositions[url]; } } } /* ---------- 2. 滚动位置监控 ---------- */ let scrollTimer = null; let lastScrollY = window.scrollY; let lastScrollX = window.scrollX; // 实时保存滚动位置 function saveScrollPosition() { scrollPositions[location.href] = { x: window.scrollX, y: window.scrollY, timestamp: Date.now(), height: document.documentElement.scrollHeight }; saveState(); } // 监听滚动事件 window.addEventListener('scroll', () => { clearTimeout(scrollTimer); scrollTimer = setTimeout(() => { // 只有滚动距离超过50像素才保存 if (Math.abs(window.scrollY - lastScrollY) > 50 || Math.abs(window.scrollX - lastScrollX) > 50) { saveScrollPosition(); lastScrollY = window.scrollY; lastScrollX = window.scrollX; } }, 300); // 300ms延迟,避免频繁保存 }); // 页面即将离开时保存滚动位置 window.addEventListener('beforeunload', saveScrollPosition); document.addEventListener('visibilitychange', () => { if (document.hidden) { saveScrollPosition(); } }); /* ---------- 3. 页面加载记录 ---------- */ (function recordPage() { const isBack = sessionStorage.getItem(BACK_FLAG) === '1'; sessionStorage.removeItem(BACK_FLAG); const ref = document.referrer; if (!isBack && ref && ref !== location.href && !isPseudoTurn(ref, location.href)) { // 保存上一页的滚动位置 if (ref) { // 这里不需要额外保存,因为上一页离开时已经保存了 } stack.push({ url: ref, title: document.title, timestamp: Date.now() }); if (stack.length > 50) stack.shift(); saveState(); // 预加载上一页 if (!cache[ref] || cache[ref].expiry < Date.now()) { prefetchPage(ref); } } // 恢复滚动位置 if (scrollPositions[location.href]) { const { x, y, height } = scrollPositions[location.href]; // 等待页面完全加载 const restoreScroll = () => { // 检查页面高度是否足够 if (document.documentElement.scrollHeight >= height * 0.8) { window.scrollTo(x, y); // 显示提示 showScrollRestored(y); } else { // 如果页面还没加载完,稍后再试 setTimeout(restoreScroll, 100); } }; // 延迟执行,确保页面渲染完成 setTimeout(restoreScroll, 100); } })(); /* ---------- 4. SPA 导航优化 ---------- */ (function hijackSPA() { const rawPush = history.pushState; const rawReplace = history.replaceState; function wrapper(rawFn) { return function () { const prev = location.href; // 离开前保存滚动位置 saveScrollPosition(); rawFn.apply(this, arguments); const next = location.href; if (prev !== next && !isPseudoTurn(prev, next)) { stack.push({ url: prev, title: document.title, timestamp: Date.now() }); saveState(); // 预加载可能的目标页面 prefetchPossibleTargets(); } }; } history.pushState = wrapper(rawPush); history.replaceState = wrapper(rawReplace); window.addEventListener('popstate', () => { const cur = location.href; if (stack.length && stack[stack.length - 1].url === cur) { stack.pop(); saveState(); } // 恢复滚动位置 if (scrollPositions[cur]) { const { x, y } = scrollPositions[cur]; setTimeout(() => { window.scrollTo(x, y); showScrollRestored(y); }, 100); } }); })(); /* ---------- 5. 智能预加载系统 ---------- */ function prefetchPage(url) { if (cache[url] && cache[url].expiry > Date.now()) return; GM_xmlhttpRequest({ method: "GET", url: url, headers: { "X-Purpose": "prefetch" }, onload: function(response) { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, "text/html"); const content = extractMainContent(doc); cache[url] = { content: content, title: doc.title, expiry: Date.now() + CACHE_EXPIRY }; saveState(); } }); } function prefetchPossibleTargets() { // 预加载栈顶3个页面 stack.slice(-3).forEach(item => { if (!cache[item.url] || cache[item.url].expiry < Date.now()) { prefetchPage(item.url); } }); } function extractMainContent(doc) { const selectors = [ 'article', 'main', '.article', '.post', '.content', '#content', 'body' ]; for (const sel of selectors) { const el = doc.querySelector(sel); if (el) return el.innerHTML; } return doc.body.innerHTML; } /* ---------- 6. 零刷新导航 ---------- */ async function navigateTo(url) { cleanCache(); // 保存当前页面的滚动位置 saveScrollPosition(); // 1. 尝试从缓存加载 if (cache[url]) { updatePage(cache[url]); history.replaceState(null, '', url); // 恢复目标页面的滚动位置 restoreScrollForUrl(url); return; } // 2. 显示加载状态 btn.textContent = '⌛'; toast('快速加载中...'); // 3. 尝试快速获取 try { const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const content = extractMainContent(doc); // 更新缓存 cache[url] = { content: content, title: doc.title, expiry: Date.now() + CACHE_EXPIRY }; saveState(); updatePage(cache[url]); history.replaceState(null, '', url); // 恢复目标页面的滚动位置 restoreScrollForUrl(url); } catch (e) { // 回退到传统导航 sessionStorage.setItem(BACK_FLAG, '1'); location.href = url; } finally { btn.textContent = '←'; } } function restoreScrollForUrl(url) { if (scrollPositions[url]) { const { x, y } = scrollPositions[url]; setTimeout(() => { window.scrollTo({ left: x, top: y, behavior: 'smooth' // 平滑滚动 }); showScrollRestored(y); }, 100); } } function updatePage(data) { document.title = data.title; const container = document.querySelector( 'article, main, .article, .post, .content, #content' ) || document.body; container.innerHTML = data.content; // 重新插入按钮(因为替换了内容) insertBtn(); // 触发可能的脚本重新执行 if (typeof window.onPageUpdated === 'function') { window.onPageUpdated(); } } /* ---------- 7. 优化按钮实现 ---------- */ const btn = createButton(); function createButton() { const btn = document.createElement('div'); btn.textContent = '←'; Object.assign(btn.style, { position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)', width: '52px', height: '52px', lineHeight: '52px', textAlign: 'center', fontSize: '26px', fontWeight: 'bold', borderRadius: '50%', color: '#000', background: 'rgba(255,255,255,0.85)', boxShadow: '0 2px 10px rgba(0,0,0,0.25)', userSelect: 'none', cursor: 'pointer', transition: 'all .2s', zIndex: 2147483646 }); btn.addEventListener('mouseenter', () => { btn.style.transform = 'translateX(-50%) scale(1.1)'; btn.style.background = 'rgba(255,255,255,0.95)'; }); btn.addEventListener('mouseleave', () => { btn.style.transform = 'translateX(-50%) scale(1)'; btn.style.background = 'rgba(255,255,255,0.85)'; }); btn.addEventListener('click', async (e) => { e.stopPropagation(); // 保存当前滚动位置 saveScrollPosition(); // 先把连续的伪页全部弹掉 while (stack.length && isPseudoTurn(stack[stack.length - 1].url, location.href)) { stack.pop(); } if (stack.length) { const target = stack.pop().url; saveState(); await navigateTo(target); } else if (history.length > 1) { history.back(); } else if (document.referrer) { await navigateTo(document.referrer); } else { toast('没有可返回的页面'); } }); return btn; } function insertBtn() { if (!document.body.contains(btn)) { document.body.appendChild(btn); } } /* ---------- 8. Toast 提示 ---------- */ function toast(msg) { const t = document.createElement('div'); t.textContent = msg; Object.assign(t.style, { position: 'fixed', bottom: '90px', left: '50%', transform: 'translateX(-50%)', background: 'rgba(0,0,0,0.8)', color: '#fff', padding: '10px 16px', borderRadius: '20px', fontSize: '14px', zIndex: 2147483647, opacity: '0', transition: 'opacity .3s', maxWidth: '80vw', wordBreak: 'break-word' }); document.body.appendChild(t); requestAnimationFrame(() => t.style.opacity = '1'); setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 1800); } // 显示滚动恢复提示 function showScrollRestored(scrollY) { if (scrollY > 100) { // 只有滚动超过100像素才显示提示 const indicator = document.createElement('div'); indicator.textContent = '↓ 已恢复到上次位置'; Object.assign(indicator.style, { position: 'fixed', top: '20px', left: '50%', transform: 'translateX(-50%)', background: 'rgba(76, 175, 80, 0.9)', color: '#fff', padding: '8px 16px', borderRadius: '20px', fontSize: '13px', zIndex: 2147483647, opacity: '0', transition: 'opacity .3s', pointerEvents: 'none' }); document.body.appendChild(indicator); requestAnimationFrame(() => indicator.style.opacity = '1'); setTimeout(() => { indicator.style.opacity = '0'; setTimeout(() => indicator.remove(), 300); }, 2000); } } /* ---------- 初始化 ---------- */ insertBtn(); prefetchPossibleTargets(); cleanCache(); // 启动时清理过期数据 })();