// ==UserScript== // @name 非同源链接新标签页打开 // @namespace https://viayoo.com/lissl0 // @version 1.5 // @description 外部链接新标签页打开,并防止在当前页跳转 // @author DeepSeek + Grok // @match *://*/* // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; const processedLinks = new WeakSet(); const isSameOrigin = (url) => { return url.protocol === location.protocol && url.hostname === location.hostname && url.port === location.port; }; const openSafely = (href) => { const win = window.open(href, '_blank'); if (win) win.opener = null; }; const handleClick = (e) => { const link = e.currentTarget; const href = link.href; try { const url = new URL(href, location.href); if (url.protocol !== 'http:' && url.protocol !== 'https:') return; if (isSameOrigin(url)) return; } catch { return; } // 完全阻止在当前页打开 e.preventDefault(); e.stopImmediatePropagation(); // 阻止其他事件处理器 openSafely(href); return false; // 额外保护 }; const processLink = (link) => { if (processedLinks.has(link)) return; try { const url = new URL(link.href, location.href); if (url.protocol !== 'http:' && url.protocol !== 'https:') return; if (isSameOrigin(url)) return; // 添加安全属性 link.target = '_blank'; link.rel = link.rel ? `${link.rel} noopener noreferrer` : 'noopener noreferrer'; // 捕获阶段监听,尽早处理 link.addEventListener('click', handleClick, true); // 移除可能存在的危险属性 link.removeAttribute('onclick'); link.removeAttribute('onmousedown'); processedLinks.add(link); } catch { // 无效 href,忽略 } }; const processAll = () => { document.querySelectorAll('a[href]').forEach(processLink); }; const observer = new MutationObserver((mutations) => { // 优化:只检查新增的节点 mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { // Element节点 if (node.tagName === 'A' && node.hasAttribute('href')) { processLink(node); } node.querySelectorAll?.('a[href]').forEach(processLink); } }); }); }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { processAll(); observer.observe(document.body, { childList: true, subtree: true }); }); } else { processAll(); observer.observe(document.body, { childList: true, subtree: true }); } })();