import type { ComponentInternalInstance, Plugin, App } from 'vue'; /** * 全局文本混淆插件 * 自动查找并替换所有文本节点 */ // 修改Vue混淆插件以确保处理整个组件树 export const TextObfuscatorPlugin: Plugin = { install(app: App) { // 安装全局样式和解码脚本 addObfuscationStyle(); // 注册全局指令,直接处理元素内容 app.directive('odata', { mounted(el) { processTextNodes(el, null); if (window.decodeObfuscatedContent) { setTimeout(() => window.decodeObfuscatedContent(el), 0); } setupMutationObserver(el); }, updated(el) { processTextNodes(el, null); if (window.decodeObfuscatedContent) { setTimeout(() => window.decodeObfuscatedContent(el), 0); } } }); // 在 app.mixin 中修改处理 $el 的部分 app.mixin({ mounted() { // 安全地获取组件的根元素(s) const rootElements = this.$el ? (this.$el.nodeType === Node.ELEMENT_NODE ? [this.$el] : (Array.isArray(this.$el) ? this.$el : [])) : []; // 处理每个根元素 rootElements.forEach((rootElement: Element) => { if (!rootElement || !(rootElement instanceof Element)) return; // 1. 首先处理当前组件的根元素 processTextNodes(rootElement, this); // 2. 递归处理所有子元素,确保覆盖所有文本节点 const processAllChildNodes = (element: Element) => { if (!(element instanceof Element)) return; try { // 为每个子元素单独处理文本节点 const childElements = element.querySelectorAll('*'); childElements.forEach(childEl => { processTextNodes(childEl, null); }); // 立即解码当前处理的元素 if (window.decodeObfuscatedContent) { setTimeout(() => window.decodeObfuscatedContent(element), 0); } } catch (error) { console.error('Error processing child nodes:', error, element); } }; // 处理整个组件树 processAllChildNodes(rootElement); // 设置监听 setupMutationObserver(rootElement); }); // 添加:深度扫描所有包含纯文本的元素 setTimeout(() => { const scanPureTextElements = (rootElement: Element) => { // 跳过已处理过的元素 if (rootElement.hasAttribute && rootElement.hasAttribute('data-obfuscated')) { return; } // 检查是否为包含纯文本的元素(只有文本节点,没有元素节点) let hasOnlyTextNodes = false; let hasElementNodes = false; if (rootElement.childNodes && rootElement.childNodes.length > 0) { hasOnlyTextNodes = Array.from(rootElement.childNodes).some( node => node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() ); hasElementNodes = Array.from(rootElement.childNodes).some( node => node.nodeType === Node.ELEMENT_NODE ); } // 如果元素只包含文本节点,并且不包含其他元素节点,则混淆它 if (hasOnlyTextNodes && !hasElementNodes && !SKIP_TAGS.includes(rootElement.tagName) && !Array.from(rootElement.classList || []).some(cls => SKIP_CLASSES.includes(cls))) { processTextNodes(rootElement, null); if (window.decodeObfuscatedContent) { window.decodeObfuscatedContent(rootElement); } } // 递归处理子元素 if (rootElement.children) { Array.from(rootElement.children).forEach(child => { scanPureTextElements(child); }); } }; // 从body开始扫描 scanPureTextElements(document.body); }, 10); // 给DOM足够的时间渲染 }, updated() { // 安全地获取组件的根元素(s) const rootElements = this.$el ? (this.$el.nodeType === Node.ELEMENT_NODE ? [this.$el] : (Array.isArray(this.$el) ? this.$el : [])) : []; // 处理每个根元素 rootElements.forEach((rootElement: Element | undefined) => { if (!rootElement || !(rootElement instanceof Element)) return; processTextNodes(rootElement, this); try { // 递归处理所有子元素 const childElements = rootElement.querySelectorAll('*'); childElements.forEach((childEl: Element) => { processTextNodes(childEl, null); }); if (window.decodeObfuscatedContent) { setTimeout(() => window.decodeObfuscatedContent(rootElement), 0); } } catch (error) { console.error('Error processing updated component:', error, rootElement); } }); } }); } }; // 声明全局函数类型 declare global { interface Window { decodeObfuscatedContent: (rootElement?: Element) => void; } } // 需要跳过的标签 const SKIP_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'PRE', 'CODE']; // 需要跳过的类名 const SKIP_CLASSES = ['no-obfuscate']; /** * 处理元素中的所有文本节点 */ function processTextNodes(element: Element, instance: ComponentInternalInstance | null) { if (!element || element.nodeType !== Node.ELEMENT_NODE) { return; } // 为 DIV 元素增加优先处理逻辑 if (element.tagName === 'DIV' && !element.hasAttribute('data-obfuscated')) { // 对于DIV元素,特殊处理其直接子文本节点 let hasTextContent = false; for (let i = 0; i < element.childNodes.length; i++) { const node = element.childNodes[i]; if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') { hasTextContent = true; break; } } // 如果DIV中有直接的文本内容,标记它需要被处理 if (hasTextContent) { // 只处理未混淆过的DIV const textContent = Array.from(element.childNodes) .filter(node => node.nodeType === Node.TEXT_NODE && node.textContent) .map(node => node.textContent).join('').trim(); if (textContent) { // 替换整个DIV的内容 const originalHTML = element.innerHTML; const processedHTML = obfuscateText(textContent); element.innerHTML = processedHTML + originalHTML.replace(textContent, ''); element.setAttribute('data-obfuscated', 'true'); } } } // 跳过带有特定标记的元素(避免重复处理) if (element.hasAttribute('data-obfuscated') || !element || SKIP_TAGS.includes(element.tagName) || Array.from(element.classList).some(cls => SKIP_CLASSES.includes(cls))) { return; } // 使用 TreeWalker 遍历所有文本节点 const walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, { acceptNode(node) { // 不处理完全空的文本节点 if (!node.textContent) { return NodeFilter.FILTER_REJECT; } // 检查父节点是否应该被跳过 const parent = node.parentElement; if (parent && ( SKIP_TAGS.includes(parent.tagName) || parent.hasAttribute('data-obfuscated') || Array.from(parent.classList || []).some(cls => SKIP_CLASSES.includes(cls)) )) { return NodeFilter.FILTER_REJECT; } // 如果只包含空白且不是段落的首个节点,则跳过 if (node.textContent.trim() === '' && !(parent?.tagName === 'P' && node === parent.firstChild)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } } ); // 收集需要处理的文本节点 const textNodes: Text[] = []; let currentNode: Node | null = walker.nextNode(); while (currentNode) { textNodes.push(currentNode as Text); currentNode = walker.nextNode(); } // 预先计算所有混淆内容,减少DOM操作次数 const fragments: DocumentFragment[] = []; const nodesToReplace: Text[] = []; // 在处理文本节点前,移除所有前导空格 for (const textNode of textNodes) { const text = textNode.textContent; if (!text) continue; // 重要修改:检测是否是段落的首个文本节点,无条件移除开头的空白 let processedText = text; if (textNode.parentElement?.tagName === 'P' && textNode === textNode.parentElement.firstChild) { // 去除开头的空白,无论什么情况 processedText = text.replace(/^\s+/, ''); // 如果去除空白后为空,直接跳过这个节点 if (!processedText) continue; } try { // 创建文档碎片来存储混淆后的内容 const fragment = document.createDocumentFragment(); const tempContainer = document.createElement('div'); tempContainer.innerHTML = obfuscateText(processedText); // 将内容移动到碎片中 while (tempContainer.firstChild) { fragment.appendChild(tempContainer.firstChild); } fragments.push(fragment); nodesToReplace.push(textNode); } catch (error) { console.error('Error processing text node:', error); } } // 批量替换节点,减少回流 for (let i = 0; i < nodesToReplace.length; i++) { const textNode = nodesToReplace[i]; const fragment = fragments[i]; if (textNode.parentNode) { textNode.parentNode.replaceChild(fragment, textNode); } } } /** * 设置 MutationObserver 来监听 DOM 变化 */ function setupMutationObserver(element: Element) { if (!element) return; const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length) { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { processTextNodes(node as Element, null); // 观察到新元素后立即解码 if (window.decodeObfuscatedContent) { window.decodeObfuscatedContent(node as Element); } } }); } } }); observer.observe(element, { childList: true, subtree: true }); } /** * 生成随机噪声标签和注释 */ function getRandomNoise() { const noiseTypes = [ // HTML注释 () => ``, // 空的自定义元素 () => { const tags = ['z-nil', 'z-void', 'z-null', 'z-fake', 'z-empty']; const tag = tags[Math.floor(Math.random() * tags.length)]; return `<${tag}>`; }, // 带随机属性的自定义元素 () => { const attr = `data-${Math.random().toString(36).substring(2, 7)}`; const value = Math.random().toString(36).substring(2, 10); return ``; }, // 带随机文本的隐藏元素 () => { const text = Math.random().toString(36).substring(2, 8); return `${text}`; } ]; // 随机选择一种噪声类型 const noiseGenerator = noiseTypes[Math.floor(Math.random() * noiseTypes.length)]; return noiseGenerator(); } /** * 混淆文本 - 将文本拆分成单词并分别存储,使用自定义z-标签 */ function obfuscateText(text: string): string { if (!text) return ''; // 首先移除文本开头的所有空白字符以解决段落首行缩进问题 text = text.replace(/^\s+/, ''); if (!text) return ''; // 使用正则表达式将文本拆分为单词和空格,保持完整性 const words = text.split(/(\s+)/); let result = ''; // 过滤掉空字符串,避免生成空的z-span元素 const filteredWords = words.filter(word => word.length > 0); // 为每个单词创建一个独立的自定义元素 filteredWords.forEach((word, index) => { // 对空格和特殊字符进行特殊处理 if (/^\s+$/.test(word)) { // 改进:更精确地处理各种换行符 if (/[\n\r]/.test(word)) { // 将所有类型的换行符分割出来,但仅在实际有换行符时才添加z-break元素 result += ''; } else { // 纯空格的情况 - 对连续空格合并处理,避免添加过多z-space result += ''; } return; } // 随机在单词前添加噪声,但减少频率 if (Math.random() > 0.85) { result += getRandomNoise(); } // 为了提高效率,固定属性名 const attrId = `data`; // 将单词编码为Base64 const encodedWord = btoa(encodeURIComponent(word)); // 随机选择z-span或z-strong标签增加混淆度 const tagName = Math.random() > 0.5 ? 'z-span' : 'z-strong'; // 添加标签开始部分 result += `<${tagName} data-${attrId}="${encodedWord}" data-preload="true">`; // 随机在标签内添加隐藏内容,但减少频率 if (Math.random() > 0.8) { const fakeText = Math.random().toString(36).substring(2, 5 + Math.floor(Math.random() * 5)); result += `${fakeText}`; } // 闭合标签 result += ``; // 随机在单词后添加噪声,但减少频率 if (Math.random() > 0.85) { result += getRandomNoise(); } }); result += ''; return result; } /** * 添加显示混淆内容的样式和解码脚本 */ function addObfuscationStyle() { // 添加样式 if (!document.getElementById('obfuscation-style')) { const style = document.createElement('style'); style.id = 'obfuscation-style'; style.textContent = ` /* 自定义元素基本样式 */ z-wrap { display: inline; white-space: normal; user-select: none; /* 阻止文本选择 */ text-indent: 0 !important; /* 确保无缩进 */ } /* 添加一个类来允许选择文本的情况 */ .allow-select z-wrap { user-select: text; } /* 确保段落中的混淆内容没有开头缩进 */ p > z-wrap:first-child { text-indent: 0 !important; margin-left: 0 !important; padding-left: 0 !important; } /* 处理所有p标签,确保没有多余空间 */ p { text-indent: 0; } z-span, z-strong { display: inline-block; position: relative; opacity: 1; transition: opacity 0.1s ease; margin: 0; padding: 0; } /* 控制右侧间距 */ z-span + z-span, z-strong + z-strong, z-span + z-strong, z-strong + z-span { margin-left: 0.1em; } /* 移除最后一个元素的右侧间距 */ z-span:last-child, z-strong:last-child { margin-right: 0; } z-span::after, z-strong::after { content: attr(data-content); position: relative; pointer-events: none; } /* 空格元素 - 精确控制宽度 */ z-space { display: inline-block; width: 0.25em; margin: 0; padding: 0; } /* 空的z-span/z-strong元素不应该显示 */ z-span:not([data-content]), z-strong:not([data-content]) { display: none; } /* 换行元素 - 强制换行且完全没有尺寸 */ z-break { display: block !important; height: 0 !important; width: 0 !important; margin: 0 !important; padding: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; } /* 隐藏所有噪声元素 */ z-nil, z-void, z-null, z-fake, z-empty, z-attr, z-text, z-hidden { display: none; width: 0; height: 0; opacity: 0; overflow: hidden; position: absolute; visibility: hidden; } /* 预加载状态 */ [data-preload="true"] { min-width: 0.5em; min-height: 1em; } `; document.head.appendChild(style); } // 添加自定义元素注册,确保所有浏览器都能正确处理 const scriptCustomElements = document.createElement('script'); scriptCustomElements.textContent = ` // 注册所有自定义元素 if ('customElements' in window) { customElements.define('z-wrap', class extends HTMLElement {}); customElements.define('z-span', class extends HTMLElement {}); customElements.define('z-strong', class extends HTMLElement {}); customElements.define('z-space', class extends HTMLElement {}); customElements.define('z-break', class extends HTMLElement {}); // 注册噪声元素 customElements.define('z-nil', class extends HTMLElement {}); customElements.define('z-void', class extends HTMLElement {}); customElements.define('z-null', class extends HTMLElement {}); customElements.define('z-fake', class extends HTMLElement {}); customElements.define('z-empty', class extends HTMLElement {}); customElements.define('z-attr', class extends HTMLElement {}); customElements.define('z-text', class extends HTMLElement {}); customElements.define('z-hidden', class extends HTMLElement {}); } `; document.head.appendChild(scriptCustomElements); // 添加提前解码的脚本,放在顶部优先加载 if (!document.getElementById('obfuscation-script')) { const script = document.createElement('script'); script.id = 'obfuscation-script'; script.textContent = ` (function() { // 定义解码函数并暴露为全局函数 window.decodeObfuscatedContent = function(rootElement) { const root = rootElement || document.body; const elements = root.querySelectorAll('z-span[data-preload="true"], z-strong[data-preload="true"]'); if (elements.length === 0) return; // 使用requestIdleCallback或setTimeout在空闲时运行,避免阻塞渲染 const runWhenIdle = window.requestIdleCallback || function(cb) { setTimeout(cb, 1); }; runWhenIdle(() => { elements.forEach(el => { // 避免重复解码 if (el.hasAttribute('data-content')) return; // 找到编码数据 const dataAttr = el.getAttribute('data-data'); if (dataAttr) { try { // 解码并设置 const decodedWord = decodeURIComponent(atob(dataAttr)); el.setAttribute('data-content', decodedWord); el.removeAttribute('data-preload'); } catch (e) { // 解码失败时跳过 } } }); }); }; // 页面加载完成后立即执行一次全局解码 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { window.decodeObfuscatedContent(); }); } else { window.decodeObfuscatedContent(); } // 使用IntersectionObserver优化解码性能 if ('IntersectionObserver' in window) { const decodeObserver = new IntersectionObserver( (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { // 容器进入视口时解码其内容 window.decodeObfuscatedContent(entry.target); observer.unobserve(entry.target); } }); }, { rootMargin: '200px 0px' } // 提前200像素开始解码 ); // 监听所有混淆容器 function observeContainers() { document.querySelectorAll('z-wrap').forEach(container => { decodeObserver.observe(container); }); } // 初始观察 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', observeContainers); } else { observeContainers(); } // 定期检查新容器 setInterval(observeContainers, 2000); } else { // 降级方案:定期全局检查 setInterval(() => window.decodeObfuscatedContent(), 1000); } })(); `; // 将脚本添加到head的最前面,确保尽早加载 if (document.head.firstChild) { document.head.insertBefore(script, document.head.firstChild); } else { document.head.appendChild(script); } } }