ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

开发浏览器扩展隐藏网页版抖音登录弹框:MutationObserver与Manifest V3实战

开发浏览器扩展隐藏网页版抖音登录弹框:MutationObserver与Manifest V3实战 说实话我一开始没打算给网页版抖音写浏览器扩展插件。直到有晚上靠在床头刷视频刷到第三个视频就弹出一个半透明遮罩上面写着“登录后体验更多精彩”我点了“暂不登录”继续往下滑第五个视频又弹一次。那晚我大概手动关了十几次登录弹框才动了“干脆自己做一个隐藏扩展”的念头。项目核心很简单做一个浏览器扩展插件专门针对网页版抖音的登录弹框做隐藏处理。目标不是绕过登录也不是伪造登录状态而是让那些只想“刷一刷、看一看”的用户不再被弹窗反复打断。这个项目非常适合刚接触浏览器扩展开发的人练手因为你只需要一个内容脚本、一个manifest配置文件再加一个MutationObserver就能跑通不用碰后端不用懂构建工具适合用来理解扩展插件最基础的工作方式。我这次就把整个开发过程和踩坑记录整理出来包括manifest.json该怎么写、如何精准找到抖音网页版的登录弹框、为什么单纯加CSS隐藏不够、以及在火狐浏览器上遇到的“扩展插件地区不可用”问题。文章按我的实际开发顺序来写你能跟着一步步操作。1. 先说清楚网页版抖音的登录弹框到底卡在哪1.1 实际使用场景里的“弹框三连”网页版抖音的登录弹框不是只出现一次。未登录状态下你打开首页能正常刷视频但只要做这几个动作登录弹框马上冒出来把鼠标悬停在右侧的评论按钮上弹框出现点击评论区域想看看别人说了什么弹框出现往下滑到第三个视频左右即使什么都不点弹框也可能自动出现尝试点赞、收藏、转发弹框几乎必现。弹框通常是一整块遮罩层盖在页面上中间是二维码或手机号登录面板右上角有个小叉按钮。设计上它就是一个“结构化组件”有背景遮罩、有内容卡片、有关闭按钮。隐藏它的核心思路就是找到这个组件对应的DOM元素然后让它不显示。但这里有个很容易踩的误区登录弹框是由前端框架抖音用的是类Vue或类React的组件体系动态渲染出来的节点不是写死在HTML里的。如果你只在控制台手动删一次刷新页面或切换路由后它还会回来。所以真正要做的不是删除一次而是持续监听DOM变化发现登录组件出现就立刻隐藏。1.2 隐藏弹框不是绕过登录而是让浏览回归阅读很多第一次接触这个项目的人会问把登录框隐藏了是不是就能偷偷用登录后的功能了不是。隐藏弹框只解决“视觉打扰”和“交互打断”的问题。未登录状态下抖音的接口依然会拦你点赞、评论、收藏这些操作依然会弹提示或者直接静默失败。扩展并不能绕过登录态也不应该往这个方向设计。我们的目标只是让“纯浏览”场景更清爽相当于把“请您登录”的提示牌先遮住但这不代表你有权限进后台。把边界想清楚开发的时候就不容易跑偏。我不会去拦截任何网络请求也不修改登录接口返回更不会注入伪造Token。只是做一个表面功夫让登录弹框组件不渲染、不显示、不拦截滚动。1.3 为什么选择浏览器扩展而不是油猴脚本有的朋友会问这种小功能用油猴脚本一行CSS不就完了吗确实可以比如在Tampermonkey里写一段MutationObserver也能实现。但从实际维护和使用角度独立浏览器扩展插件有几个优势安装之后用户感知度低打开浏览器自动生效不用每次开关油猴权限声明更清晰manifest里明确写了只在www.douyin.com下面运行用户一眼能看明白可以配合浏览器原生的chrome.storage做配置项后续想加开关、白名单都很方便发布到扩展商店后非技术用户也能直接搜索安装不用先装Tampermonkey。当然独立扩展也有代价就是每个浏览器的加载方式略有不同Firefox和Chrome的开发模式入口不一样。不过这也在预期内下面会详细讲。2. 项目准备Manifest V3 下最小扩展的结构与加载方式2.1 manifest.json 的配置文件逐行分析开发浏览器扩展插件第一步不是写隐藏逻辑而是先创建一个文件夹里面放一个manifest.json。这是整个扩展的身份证浏览器通过读取这个文件才知道扩展叫什么、需要什么权限、在哪些页面注入脚本。我使用的配置如下{ manifest_version: 3, name: Hide Douyin Login Modal, version: 1.0.0, description: 隐藏网页版抖音的登录弹框让浏览更沉浸, content_scripts: [ { matches: [*://www.douyin.com/*], css: [content.css], js: [content.js], run_at: document_idle } ], host_permissions: [*://www.douyin.com/*], permissions: [storage], action: { default_popup: popup.html, default_title: 抖音登录框隐藏 } }逐行解释一下manifest_version必须是3这对应Chrome 88之后的MV3规范。MV3相比MV2最大的变化是后台脚本被Service Worker替代但对纯内容脚本的项目来说影响不大。name和version是基础信息name最好用英文避免部分浏览器显示乱码。description会在扩展管理页显示简明扼要即可。content_scripts是核心声明要在哪些页面注入content.css和content.js。matches里写的是页面URL匹配规则*://www.douyin.com/*覆盖了http/https协议下的主站所有路径。run_at设置为document_idle代表页面DOM基本解析完再执行。如果设成document_start可能太早DOM还没准备好但只要代码里做好判空也能跑设成document_end则更接近我们需要的时机。host_permissions在这里其实可以省略因为content_scripts里的matches已经限制了注入范围。保留它更像是一种习惯方便以后扩展API读取页面信息。permissions里的storage是为了后面做开关和设置项用的如果你只做静态隐藏这个权限也可以不申请。MV3强调最小权限原则能少申请就少申请。这个项目只需要读DOM、写DOM不涉及网络请求拦截所以权限非常轻。2.2 content_scripts 和 host_permissions 的边界很多人刚接触扩展开发时会把content_scripts和host_permissions混在一起。这里有一个很关键的区别content_scripts控制的是“哪些页面会注入脚本和样式”属于代码注入清单host_permissions控制的是“扩展可以访问哪些站点的数据/接口”属于权限声明。我们隐藏登录弹框只做DOM层面操作理论上只需要content_scripts就够了。host_permissions更像是给后台脚本或API调用看的。在MV3里如果你在后台脚本里用fetch请求某个站点必须声明对那个站点的host_permissions。但在这个项目里没有这类需求所以完全可以去掉。另外要注意content_scripts里的脚本和页面本身的JavaScript是隔离的。页面里的全局变量、内部函数内容脚本不能直接访问反过来页面也读不到内容脚本里定义的变量。它们共享同一个DOM但各自有独立的JavaScript环境。这意味着我们虽然能操作DOM来隐藏弹框但没法直接调用抖音内部的openLoginModal()或closeLoginModal()这类方法。我们需要靠操作DOM节点本身来实现目的。2.3 在Chrome/Firefox中加载未打包扩展并调试开发阶段不需要发布到应用商店浏览器的“加载已解压的扩展”功能就是我们的调试入口。Chrome/Edge都是Chromium内核的加载方式打开浏览器在地址栏输入chrome://extensionsEdge是edge://extensions打开右上角的“开发者模式”开关点击“加载已解压的扩展程序”选择存放manifest.json的文件夹。加载成功后扩展卡片会出现在工具栏。如果修改了manifest.json需要点击扩展卡片上的刷新按钮如果只改了content.js或content.css直接在目标页面刷新页面即可生效不需要重新加载整个扩展。Firefox的加载方式不太一样在地址栏输入about:debugging并回车左侧选择“此 Firefox”点击“临时载入附加组件”选择项目文件夹里的manifest.json。临时载入的扩展只对当前Firefox会话有效关闭浏览器后再打开就失效了。这适合开发调试但如果你想长期使用需要把项目打包成.xpi或.zip再视情况安装。我建议你至少同时装一个Chrome和一个Firefox分别测试一遍。原因后面会单独说——火狐浏览器在扩展安装上有一些容易被“地区不可用”卡住的情况。3. 核心实现用内容脚本识别并隐藏登录弹框3.1 从DOM中找到登录弹框的多种方法写扩展的第一步实在网页里找到登录弹框对应的节点。我打开抖音网页版的开发者工具在弹框出现时快速检查DOM结构发现弹框的类名不是固定的不同账号、不同时间下看到的类名可能不一样。直接用单一类名选择器很容易踩空。我更推荐采用“候选选择器数组”的方式把所有可能的标识统一收集起来逐个尝试匹配const LOGIN_MODAL_SELECTORS [ .web-login-modal, #login-modal, [class*login-modal], [data-e2elogin-modal], [class*login-container], #douyin-login-modal ];这段代码的核心思路是“模糊匹配”[class*login-modal]意味着只要元素的class属性里包含login-modal字符串就会被命中不管完整类名是什么。但要注意这个做法也有副作用。如果页面某个不相关的弹窗类名里恰好包含login-modal比如用户协议弹窗叫login-modal-agreement也会被一起隐藏。好在我们只在www.douyin.com域名下注入脚本并且computedStyle验证做兜底实际影响不大。配合开发工具我建议你按这个流程定位弹框在抖音网页版按F12打开开发者工具点击工具栏的手机模拟按钮或者把页面窗口拉窄一点更容易触发弹框弹框出现后点击开发者工具左上角的“选择元素”图标再点击弹框区域在Elements面板里查看当前选中的节点一层层往上找直到找到同时包含背景遮罩和内容卡片的父节点把这个节点的标签名、类名、id或>const el document.querySelector(LOGIN_MODAL_SELECTORS[0]); if (el) { el.style.display none; }这段代码单独运行页面刚加载时确实能隐藏一次弹框。问题在于抖音的前端框架会在用户滑动、点击按钮、路由切换时动态创建登录组件而且这个组件可能每触发一次就生成一个新的DOM节点。你只执行一次等于只打了一发子弹后面弹框照样出现。正确思路是用MutationObserver持续监听DOM变化。这个API允许你注册一个回调当指定DOM子树发生节点新增、删除、属性变化时浏览器会自动通知你。实现如下function hideLoginModal() { for (const selector of LOGIN_MODAL_SELECTORS) { document.querySelectorAll(selector).forEach((el) { if (el el.style.display ! none) { el.style.display none; } }); } const body document.body; if (body body.style.overflow hidden) { body.style.overflow ; } } function startObserver() { // 防止重复创建observer if (window.__loginModalObserver) { return; } const observer new MutationObserver((mutations) { hideLoginModal(); }); observer.observe(document.body, { childList: true, subtree: true }); window.__loginModalObserver observer; hideLoginModal(); } startObserver();这段代码有几个关键点MutationObserver的回调不需要遍历mutations里的每一条记录因为我们的隐藏操作是全量扫描每次改动都重新查一遍节点简单粗暴但有效。document.getElementById之类的查询只对单个节点有效而document.querySelectorAll能同时处理多个匹配节点避免弹框被复制出多个实例的情况。监听范围选document.body因为抖音登录弹框无论挂在哪个根节点下最终都会插入到body里。我用window.__loginModalObserver做了标识避免内容脚本被多次注入时创建多个Observer造成重复监听和性能浪费。有人会担心每秒钟触发几十次MutationObserver回调会不会卡页面实测下来querySelectorAll对正常DOM规模的开销很小而且登录弹框不是每帧都出现触发频率并不高。如果你还是不放心可以在hideLoginModal里加一个节流函数比如100毫秒内最多执行一次。3.3 完整代码manifest.json content.js style.css我这里给出一个完整的最小可用版本方便你直接复制。文件结构hide-douyin-login-modal/ ├── manifest.json ├── content.js └── content.cssmanifest.json就是我前面列出的版本这里不再重复。content.css作为静态兜底样式.web-login-modal, #login-modal, [class*login-modal], [data-e2elogin-modal] { display: none !important; visibility: hidden !important; pointer-events: none !important; }content.js完整代码const LOGIN_MODAL_SELECTORS [ .web-login-modal, #login-modal, [class*login-modal], [data-e2elogin-modal], [class*login-container], #douyin-login-modal ]; function hideLoginModal() { for (const selector of LOGIN_MODAL_SELECTORS) { document.querySelectorAll(selector).forEach((el) { if (el el.style.display ! none) { el.style.display none; } if (el el.style.visibility ! hidden) { el.style.visibility hidden; } if (el el.style.pointerEvents ! none) { el.style.pointerEvents none; } }); } const body document.body; if (body body.style.overflow hidden) { body.style.overflow ; } } let lastRunTime 0; function throttledHide() { const now Date.now(); if (now - lastRunTime 100) { return; } lastRunTime now; hideLoginModal(); } function startObserver() { if (window.__loginModalObserver) { return; } const observer new MutationObserver(() { throttledHide(); }); observer.observe(document.body, { childList: true, subtree: true }); window.__loginModalObserver observer; hideLoginModal(); } startObserver();我加了100毫秒的节流避免弹框刚刚出现时连续多次执行隐藏导致性能抖动。从实测效果看弹框几乎不会在屏幕上留下可见残影刷新页面后也是一加载就被隐藏。3.4 保留使用体验隐藏而非删除这里要强调一个容易犯的错不要直接把登录弹框的DOM节点移除。如果你用el.remove()把这个节点从DOM里删掉确实也能隐藏但抖音组件的内部状态会乱掉。比如它有一个遮罩层删除后可能给body留下overflow:hidden页面就会一直锁滚动。我最初试过removeNode结果页面不能滚了必须刷新才恢复正常。所以我后来统一改成“隐藏”策略只设置display:none不移动节点结构。这样组件在内存和DOM树里依然存在但视觉上不可见也不参与交互页面滚动恢复正常。说白了我只是在DOM上盖了一块布而不是把桌子搬走。同样的道理如果用visibility:hidden隐藏元素虽然不可见但仍会占位display:none会从布局中移除更适合弹框这类浮层。pointer-events:none防止某些透明遮罩继续拦截鼠标事件。4. 踩坑实录刷新、路由切换、地区不可用与弹框复发4.1 Firefox 扩展插件地区不可用问题如何面对开发过程中遇到最多的问题不是代码逻辑而是火狐浏览器扩展插件地区不可用。具体表现是在Firefox附加组件商店里搜索某些扩展页面显示“此扩展在您所在的地区不可用”或者点击添加到Firefox时提示无法安装。首先要明确一点这不代表你的代码有问题。Firefox的附加组件商店有一套审核和地区分发规则不同地区能看到的内容不一样。如果你是在企业网络环境下管理员策略也可能阻止安装扩展。遇到这个提示先检查页面右下角的区域设置和账号地区再确认网络环境是否属于正常的公司办公网。如果确认是企业策略限制只能联系管理员添加白名单没有别的正规途径。如果你只是想自己开发或临时自用可以用Firefox官方的临时加载功能在about:debugging页面加载本地项目这个方式完全绕开附加组件商店的地区限制但它只是开发调试用途每次启动浏览器都需要重新加载一次。还有一点火狐对未签名扩展的限制比Chrome更严格。Chrome开发者模式可以加载任意本地扩展而Firefox在普通版本里加载未签名扩展也会报错。我的建议是开发调试用about:debugging临时加载正式发布再走AMO审核签名流程。不要试图用非官方手段强制安装未签名的.xpi既不稳定也不安全。4.2 弹框隐藏后页面滚动与快捷键冲突登录弹框一般会有配套的滚动锁定机制。弹框弹出时抖音会往body上加overflow:hidden同时给整个页面加一层事件监听阻止背景滚动。即便弹框隐藏了这个body样式如果没恢复用户照样滑不动视频。我在hideLoginModal里专门加了一行恢复逻辑const body document.body; if (body body.style.overflow hidden) { body.style.overflow ; }不过这里有个小坑body.style.overflow只能读取内联样式如果样式是写在外部CSS里的style对象里看不到。抖音的做法通常是把overflow:hidden作为内联样式加到body上因为这样动态切换最方便。如果你的版本没生效可以在控制台看下body的computed style再决定是清className还是移除特定样式类。快捷键冲突也出现过一次。登录弹框出现后焦点会移到弹框内的输入框按Esc可以关闭弹框。当我们把它隐藏后输入框虽然不可见但仍然可能持有焦点导致键盘事件被吃掉。解决办法是隐藏后主动让焦点回到页面主体if (document.activeElement document.activeElement.blur) { document.activeElement.blur(); }你可以把这段加到hideLoginModal末尾。实测下来页面滚动和键盘操作都能恢复正常。4.3 不同浏览器对!important和动态样式的处理差异我分别在Chrome、Edge、Firefox三个浏览器里测试整体行为一致但细节有差异。Chrome和Edge同为Chromium内核对内联样式style.display none和外部CSSdisplay:none !important的处理一致。如果外部CSS用了!important内联样式会被覆盖。不过我们的内容脚本同时操作内联样式和CSS所以不存在这个冲突。Firefox对MutationObserver的支持也很好但在某些页面状态下document.body可能还没准备好。保险起见需要在startObserver里加一个if (document.body)判断或者等DOMContentLoaded后再执行。动态样式的生效时机略有不同。Chromium浏览器在节点插入后立刻执行MutationObserver回调弹框不会闪现Firefox偶尔会出现一帧的闪烁肉眼几乎看不见。如果想彻底避免可以在content.css里用[class*login-modal] { display: none !important; }做静态兜底让样式从一开始就生效。如果遇到隐藏后又复现的情况多半是页面SPA路由切换导致的。抖音是单页应用从“推荐”切到“关注”再切回来登录弹框可能重新渲染。我们的MutationObserver一直在监听所以理论上能自动再次隐藏。但要注意startObserver如果在document_idle执行而路由切换发生在之后监听回调依然有效。只要Observer没被销毁就不需要额外处理。5. 体验升级给扩展增加开关、白名单与状态提示5.1 轻量 popup 面板与存储控制只写一个固定隐藏逻辑的扩展能用但不够灵活。比如用户今天想隐藏明天想暂时看看登录界面或者某些页面不想被干扰这时候就需要一个开关。利用chrome.storage和action.default_popup我们可以加一个简单的popup面板。先建一个popup.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 / style body { width: 220px; padding: 12px; font-family: system-ui, sans-serif; } .row { display: flex; align-items: center; justify-content: space-between; } /style /head body div classrow span隐藏登录弹框/span input typecheckbox idtoggle / /div script srcpopup.js/script /body /html再建popup.jsconst toggle document.getElementById(toggle); chrome.storage.sync.get({ enabled: true }, (items) { toggle.checked items.enabled; }); toggle.addEventListener(change, () { const enabled toggle.checked; chrome.storage.sync.set({ enabled }); chrome.tabs.query({ active: true, currentWindow: true }, (tabs) { const tab tabs[0]; // 通知当前页面的content script立即生效或失效 chrome.tabs.sendMessage(tab.id, { type: TOGGLE_ENABLED, enabled }); }); });注意chrome.tabs.sendMessage要求你有tabs权限所以得在manifest.json的permissions里加上tabs。不过chrome.tabs.query也可以不声明tabs权限它只能获取标签的基础信息。为了发消息建议还是把tabs加上。content.js需要监听来自popup的消息chrome.runtime.onMessage.addListener((message, sender, sendResponse) { if (message.type TOGGLE_ENABLED) { if (message.enabled) { startObserver(); hideLoginModal(); } else { stopObserver(); restoreLoginModal(); } } });stopObserver里要做的事情是断开Observer连接并且把之前隐藏的节点恢复显示。恢复显示比较麻烦因为我们可能已经修改了多个节点的style.display。一个简单的做法是只记录我们修改过的节点保存它们在修改前的原始样式恢复时按原样回填。但为了篇幅这里不展开你可以先用“刷新页面后生效”这个方案关闭开关后刷新页面也能恢复。5.2 站点白名单和自动启停的思路扩展的matches已经限定了只在www.douyin.com下运行所以从域名角度来说已经是一个天然的白名单。但如果你后续想通用于其他视频网站就需要把白名单做成可配置的数组。思路大致是在manifest.json里把content_scripts的matches放宽到all_urls但这样权限太大不够安全改为在content.js里判断当前域名是否在允许列表不在就立刻返回将允许列表存储在chrome.storage.sync里popup界面上提供输入框让用户自己添加站点。我不建议一上来就做通用版。原因很简单不同网站的登录弹框结构差异极大选择器完全不能复用。抖音的class里好歹有login-modal关键词有些网站的登录浮层可能叫modal-mask、dialog、layer隐藏了容易误伤正常功能。做好一个站点再做第二个才是务实路线。5.3 后续可以继续做的方向这个项目虽然小但扩展点很多。我自己后续打算做的几个方向登录态自动识别通过请求Cookie或页面上的用户信息节点判断当前是否已登录已登录就完全不需要隐藏弹框减少无用操作。选择器自定义在popup里提供一个输入框用户自己填页面节点选择器让非技术用户也能适配网站改版。按页面路由生效抖音的/、/follow、/user/xxx页面结构不完全相同可以针对不同路由使用不同选择器降低误命中率。统计隐藏次数在popup里展示当前会话一共自动隐藏了多少次登录弹框方便观察这个扩展到底在什么时候起作用。做这个扩展最大的收获不是写了多少行代码而是彻底理解了浏览器扩展插件和网页之间“共享DOM但隔离环境”的关系。你不需要理解抖音的前端框架也不需要逆向它的内部方法只需要通过标准DOM API观察节点变化就能做出一个稳定生效的工具。如果你也被网页版抖音的登录弹框骚扰过完全可以照着这个项目思路自己做一个浏览器扩展插件。先把Chrome版本跑通再考虑Firefox的发布问题。遇到“地区不可用”的时候别急着想歪招本地开发用about:debugging临时加载正式发布走官方审核这个流程既干净也省心。
返回列表