news 2026/9/8 3:00:23

自制浏览器标签页管理扩展:MV3开发实战与踩坑记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自制浏览器标签页管理扩展:MV3开发实战与踩坑记录

简介:面向初、中级前端开发者,一套自制Edge和Chrome标签页扩展插件的实战资源,系统讲解manifest.json、background.js、content scripts、popup页面等核心结构,并覆盖jQuery、CSS在前端界面中的应用,以及chrome.tabs、chrome.storage、chrome.webNavigation等API的调用方法。内容还包含跨浏览器兼容差异、调试技巧、权限安全原则与打包上架流程,帮助读者从零构建可发布的标签页增强工具。资源包以zip压缩包形式提供,大小约14.64MB,由于上游未提供文件清单,文件总数和类型可解压后自行查看。目前已有1025人学习、下载,适合希望用前端技术扩展浏览器效率的开发者参考。

开篇:为什么我动手写了一个标签页扩展

浏览器标签页一多,整个人的状态就像端着十几杯咖啡过独木桥——明明知道某个页面开着,就是找不到;明明记得刚才关闭了一个重要页面,翻遍历史记录也捞不回来;更别提Edge和Chrome来回切换时,那套互不相通的标签页管理逻辑有多折磨人。

说实话,市面上标签页管理扩展并不少,从老牌的OneTab到后来的Tabby,我都试过一遍。但它们要么太重,要么太“聪明”——把标签页自动折叠、自动休眠,反而干扰了正常的工作流。后来我想明白了,与其在别人的设计理念里将就,不如自己动手写一个刚好契合自己习惯的浏览器扩展。这篇文章就把我自制Edge、Chrome标签页扩展插件的完整过程拆开讲,包括方案选型、项目结构、核心功能实现、调试发布以及各种踩坑实录。如果你也受够了标签页混乱,或者想入门浏览器扩展开发,这篇应该能给你省下不少试错时间。

1. 整体设计与方案选型

1.1 为什么选了Manifest V3而不是V2

刚开始做扩展时,我查了不少资料,网上大量老教程还在沿用Manifest V2(MV2)的写法。这里必须提醒一句:2024年开始,Chrome和Edge都已经全面转向Manifest V3(MV3),新提交的扩展不再支持MV2,现有MV2扩展也在逐步被强制迁移。如果你想长期维护一个扩展,直接上MV3是唯一合理的选择。

MV3相对MV2最大的变化在于后台脚本。MV2时代用常驻的background page,JavaScript在后台持续运行;MV3改成了service worker,平时不活跃,事件触发时才被唤醒,用完就休眠。这种做法大幅降低了扩展对浏览器内存的占用,正好踩中了很多人关心的"Edge浏览器内存占用高"这个痛点。代价是,你不能像以前那样在后台随便挂全局变量,一切状态都得显式存下来,要么存storage,要么存在tab对象本身上。

另外,MV3的权限模型更严格。比如跨域请求需要声明host_permissions,脚本注入也从tabs.executeScript改成了chrome.scripting.executeScript。第一次迁移时我踩了不少坑,后面会详细说。

1.2 功能定位:轻量、精准、不打扰

既然是自用扩展,功能上我不打算做重。核心需求就三条:

  • 标签页去重:同一网址开了多个标签时,自动合并或高亮提示。
  • 快速定位:通过弹出的面板,输入关键词过滤当前窗口的所有标签页,点击直达。
  • 标签页分组与休眠:按域名自动分组,一键休眠当前窗口内不活跃的标签页,降低内存占用。

这三个功能对应了绝大多数人标签页爆炸的真实场景。与其堆砌十几个华而不实的开关,不如把这三条做到顺手。

设计上,我让所有功能都通过popup面板和快捷键触发,没有后台的常驻逻辑,也没有对页面内容的注入操作。这让扩展在权限申请上非常克制——只需要tabs、storage、tabGroups、scripting这几个权限,用户安装时看到的权限提示非常干净,信任成本低。

1.3 项目目录结构设计

自用扩展的好处是,不需要打包成复杂的工程化项目。我用的是纯原生JavaScript,没有引入框架和构建工具,整个项目就五个文件:

tab-manager/ ├── manifest.json ├── background.js # service worker,处理快捷键和后台逻辑 ├── popup.html # 点击扩展图标弹出的面板 ├── popup.js # 面板逻辑 ├── styles.css # 面板样式 └── icons/ ├── icon16.png ├── icon48.png └── icon128.png

我自己用着够用,但如果你想做得更完善,可以再拆一个options.html用来做设置页,后面我也会讲到哪些设置项值得加。

2. 核心功能拆解与实现思路

2.1 标签页信息获取:tabs API的正确打开方式

所有标签页管理功能的基础,是正确拿到当前窗口所有标签页的信息。chrome.tabs.query是最核心的接口:

async function getAllTabs() { const tabs = await chrome.tabs.query({ currentWindow: true }); return tabs; }

注意这里有一个大坑:chrome.tabs.query返回的Tab对象里,url和title字段并非常规情况下都能直接读到。只有当扩展拥有tabs权限,或者在当前页面注入过脚本的情况下,你才能拿到完整的url。如果没有tabs权限,大部分标签页的url会是undefined。

很多扩展为了省权限不申请tabs,结果只能依据title去重,效果大打折扣。考虑到自用,我直接声明了tabs权限,换来了完整的访问能力,这个取舍我觉得很值。

另外一个实用技巧是在popup打开时主动刷新数据:

document.addEventListener('DOMContentLoaded', async () => { const tabs = await getAllTabs(); renderTabList(tabs); });

每次打开面板实时拉取最新标签页列表,而不是依赖缓存,这样就不会出现“明明刚关了标签页,面板里还显示”的陈旧数据问题。

2.2 标签页去重:判断标准与合并策略

去重的核心是判断两个标签页是否“重复”。最简单的标准是规范化后的URL完全一致。但实际场景里经常出现同一个页面带不同查询参数(比如带utm_source的推广链接和原始链接),这种要不要算重复,取决于你的容忍度。

我的实现分两档:

  • 严格模式:规范化后URL完全相同,判定为重复。
  • 宽松模式:同域名且路径相同,忽略query差异,判定为重复。

具体逻辑写在background.js里,因为需要在快捷键触发时直接执行:

function normalizeUrl(url) { try { const u = new URL(url); u.hash = ''; u.search = ''; return u.href.replace(/\/$/, ''); } catch { return url; } } async function findDuplicateTabs(tabs, mode) { const map = new Map(); const duplicates = []; for (const tab of tabs) { if (!tab.url) continue; const key = mode === 'strict' ? normalizeUrl(tab.url) : getLooseKey(tab.url); if (map.has(key)) { duplicates.push({ source: map.get(key), target: tab }); } else { map.set(key, tab); } } return duplicates; }

找到重复标签后,我的策略是保留最早打开的标签,聚焦它并关闭后来的重复项。这里有个体验细节:关闭重复标签后要主动调用chrome.tabs.update把窗口焦点切到保留的那个标签上,否则浏览器会停在某个被关闭标签的空白页上,很突兀。

2.3 快捷键:把常用操作绑定到键盘

做标签页管理工具,最忌讳的是每次都得点开扩展图标再操作。我提供了两组快捷键:一组用来给当前标签页加“星标”(把标签移动到窗口最前),另一组用来触发“去重并清理”。

快捷键在manifest.json的commands字段里声明:

"commands": { "focus-first-tab": { "suggested_key": { "default": "Alt+Shift+1" }, "description": "聚焦第一个标签页" }, "close-duplicate-tabs": { "suggested_key": { "default": "Alt+Shift+D" }, "description": "关闭重复标签页" } }

background.js里监听chrome.commands.onCommand:

chrome.commands.onCommand.addListener(async (command) => { if (command === 'close-duplicate-tabs') { const tabs = await chrome.tabs.query({ currentWindow: true }); const duplicates = await findDuplicateTabs(tabs, 'strict'); if (duplicates.length === 0) return; const ids = duplicates.map(d => d.target.id); await Promise.all(ids.map(id => chrome.tabs.remove(id))); const keepId = duplicates[0].source.id; await chrome.tabs.update(keepId, { active: true }); } });

有个细节值得注意:快捷键的默认配置只对当前窗口生效,如果你想对“所有窗口的标签页”去重,需要把query里的currentWindow参数去掉。但在大多数场景下,跨窗口去重要慎重——两个窗口可能放着不同任务的同网址页面,贸然合并不一定是好事。

2.4 标签页分组与内存优化:tabGroups与discard的妙用

Edge和Chrome从Chromium 90之后都支持了tabGroups分组API。把相同域名的标签页自动归到一组,视觉上就很清爽。实现方式非常简单:

async function groupTabsByDomain(tabs) { const map = new Map(); for (const tab of tabs) { if (!tab.url) continue; const domain = new URL(tab.url).hostname; if (!map.has(domain)) map.set(domain, []); map.get(domain).push(tab.id); } for (const [domain, tabIds] of map.entries()) { if (tabIds.length < 2) continue; const groupId = await chrome.tabs.group({ tabIds }); await chrome.tabGroups.update(groupId, { title: domain, color: getColorByString(domain) }); } }

chrome.tabs.group返回groupId,然后可以设置组的标题和颜色。自动分组的体验依赖于浏览器的native UI,Edge和Chrome都能正常展示,不用额外写页面。

另一个降低内存占用的手段是chrome.tabs.discard——把标签页的内容丢弃,只保留标题和URL,刷新时才重新加载。这和休眠一类的功能原理一致。我做成了一键操作:省略号按钮点击后,将当前窗口除活动标签外的所有标签页discard掉。

chrome.tabs.discard(tabs.filter(t => !t.active).map(t => t.id));

实测下来,开20个标签页的窗口执行discard后,内存下降非常明显。注意discard之后的标签页,图标会变成灰色,这是浏览器常规行为,不是bug。

2.5 覆盖新标签页:一个坑多但值得做的小功能

热搜词里多次出现“Edge新标签页被篡改”和“为什么总是自动添加拓展”这类问题。抛开恶意推广不谈,从技术角度讲,扩展确实可以覆盖新标签页,方式是manifest里声明chrome_url_overrides:

"chrome_url_overrides": { "newtab": "newtab.html" }

这个功能我做过一版,后来砍掉了。原因有两个:第一,一旦覆盖新标签页,浏览器自己的新标签页设置(比如快捷方式、背景图)全部失效,调整起来不够灵活;第二,很多用户遇到的问题其实是第三方扩展强制覆盖了newtab,反而让“新标签页被篡改”的观感更严重。如果你确实想做一个干净清爽的新标签页扩展,技术上完全没问题,但需要自己实现搜索框、快捷方式和壁纸展示,工作量至少翻一倍。

我个人的建议是,优先把标签页管理功能做好差异化,新标签页覆盖可以在后期以“可选模块”的形式单独发布,不要和核心功能耦合。

3. 实操过程与核心实现

3.1 搭建骨架:manifest.json配置文件详解

扩展的第一步是写manifest.json。所有扩展的灵魂都在这个文件里。我提供一个可直接用的最小版本,每一行都有注释说明:

{ "manifest_version": 3, "name": "标签页管家", "version": "1.0.0", "description": "标签页去重、自动分组、一键休眠,轻量高效的标签页管理工具", "permissions": ["tabs", "storage", "tabGroups", "scripting"], "background": { "service_worker": "background.js" }, "action": { "default_popup": "popup.html", "default_title": "标签页管家", "default_icon": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } }, "commands": { "close-duplicate-tabs": { "suggested_key": { "default": "Alt+Shift+D" }, "description": "关闭重复标签页" }, "discard-inactive-tabs": { "suggested_key": { "default": "Alt+Shift+S" }, "description": "休眠非活动标签页" } }, "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } }

说一下几个关键的选型逻辑。

permissions里的tabs权限是我斟酌过的最优解——如果不要这个权限,tab.url经常拿不到,去重直接变成空中楼阁。storage权限用于保存用户设置,比如去重模式是严格还是宽松。tabGroups权限是分组功能的前提。scripting权限这次只用来做可选的高亮注入,如果你不需要注入脚本,也可以去掉。

background.service_worker对应的background.js在MV3里是一个事件驱动的service worker,它不常驻,只在收到事件通知时被唤醒。所以任何“立即生效”的逻辑都必须通过事件监听触发,不能依赖定时轮询。如果你需要在特定时刻执行操作,需要用chrome.alarms。

3.2 popup面板:标签页列表的实时渲染

popup.html是用户点开扩展图标时看到的面板。我的设计目标是:信息密度高但不过载,一眼扫过去就知道当前窗口有什么标签页,点击就能跳转,还有两个快捷按钮负责去重和休眠。

面板结构简化如下:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="styles.css"> </head> <body> <div class="container"> <div class="toolbar"> <input type="text" id="searchInput" placeholder="搜索标签页..."> <button id="btnDedupe">去重</button> <button id="btnDiscard">休眠</button> </div> <ul id="tabList"></ul> </div> <script src="popup.js"></script> </body> </html>

popup.js的核心任务是:加载标签页数据、按关键词过滤、渲染列表、绑定点击事件。

async function renderTabs(filterText = '') { const tabs = await chrome.tabs.query({ currentWindow: true }); const listEl = document.getElementById('tabList'); listEl.innerHTML = ''; const filtered = tabs.filter(tab => { if (!filterText) return true; const text = (tab.title + ' ' + tab.url).toLowerCase(); return text.includes(filterText.toLowerCase()); }); for (const tab of filtered) { const li = document.createElement('li'); li.className = 'tab-item' + (tab.active ? ' active' : ''); li.innerHTML = ` <img class="favicon" src="${tab.favIconUrl || ''}" alt=""> <span class="tab-title">${escapeHtml(tab.title || '无标题')}</span> <span class="tab-meta">${shortUrl(tab.url)}</span> <button class="close-btn">function shortUrl(url) { try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return url || ''; } }

escapeHtml函数用来防止标题里的特殊字符破坏HTML结构。这个安全习惯非常重要,尤其是标签page标题可能来自网页内容,不可信。

function escapeHtml(str) { return str.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]); }

3.3 快捷键触发的后台逻辑:service worker里的事件驱动

真正让扩展“好用”的,其实是快捷键。你不用打开面板,直接Alt+Shift+S一键休眠当前窗口所有非活动标签页,Alt+Shift+D一键清理重复标签,这种“不打断上下文流”的操作体验才是标签页管理工具的价值所在。

background.js里的逻辑非常简单:

chrome.commands.onCommand.addListener(async (command) => { const tabs = await chrome.tabs.query({ currentWindow: true }); const activeTab = tabs.find(t => t.active); switch (command) { case 'discard-inactive-tabs': { const inactiveIds = tabs.filter(t => !t.active).map(t => t.id); if (inactiveIds.length > 0) { await chrome.tabs.discard(inactiveIds); } break; } case 'close-duplicate-tabs': { const settings = await chrome.storage.sync.get({ dedupeMode: 'strict' }); const duplicates = await findDuplicateTabs(tabs, settings.dedupeMode); if (duplicates.length === 0) return; const removeIds = duplicates.map(d => d.target.id); await chrome.tabs.remove(removeIds); const keepId = duplicates[0].source.id; await chrome.tabs.update(keepId, { active: true }); break; } default: break; } });

注意service worker的执行环境:每次事件触发都可能是全新的执行上下文,所以background.js里所有托管状态的变量都是“一次性”的。如果你想保存用户上一次的去重模式,必须用chrome.storage,不能保存在全局变量里。这也是MV3区别于MV2的重要心智模型转变。

3.4 以调试视角加载扩展

写完代码后,在Edge或Chrome中加载扩展的方式基本一致。Chrome打开地址栏输入chrome://extensions/,Edge对应的地址是edge://extensions/,然后把右上角“开发者模式”开关打开,点击“加载已解压的扩展程序”,选中你的项目目录即可。

注意两个细节:第一,目录里不要放无关的临时文件,比如.DS_Store、node_modules之类,虽然不影响加载,但会让目录变乱;第二,改完代码后要点击扩展卡片上的刷新图标重新加载,否则浏览器里跑的永远是旧版本。开发期间如果修改了manifest.json里的权限声明,还必须在扩展卡片里点一下“重新加载”才能生效。

加载后右键弹出的气泡可以打开“检查视图”,里面能看到service worker的控制台日志。这是调试后台逻辑的唯一窗口,而popup部分可以直接右键点panel区域选“检查”,用DevTools直接调试。

4. 常见问题与排查技巧实录

4.1 MV3里dynamic import为什么加载不了

开发过程中我一度想把去重逻辑抽成单独模块,于是用动态import加载一个dedupe.js。结果在popup里怎么都跑不通,控制台报错指向模块解析失败。

原因在于MV3的service worker和popup都不支持动态导入本地脚本。Google官方明确禁止dynamic import,因为这在某些情况下会绕过CSP(内容安全策略)。解决办法有两个:

  • 把公共逻辑全部写在一个文件里,用不同的命名空间分隔。
  • 使用静态import声明,并在manifest.json里声明为web_accessible_resources。

对于自用扩展而言,第一种最省事。我后来把所有工具函数都放进了background.js,popup内部自己维护一份轻量版去重逻辑,避免跨文件引用的复杂度。

4.2 快捷键为什么不生效

如果你注册的快捷键没反应,先检查两点:第一,浏览器本身是否占用了这个组合键。比如Alt+Shift+I在Windows上可能被某些输入法占用,建议避开系统常用组合。第二,是否在chrome://extensions/shortcuts(Edge是edge://extensions/shortcuts)自定义过快捷键优先级,如果你手动设置过别的扩展占用同样的组合,浏览器会在这两个扩展之间做冲突裁决。

我踩过的坑是设置Alt+D作为“聚焦地址栏”的浏览器原生快捷键,所以“去重”我特意改成了Alt+Shift+D,实测没有冲突。

4.3 打开面板就能看到所有窗口的标签页吗

不能。chrome.tabs.query({ currentWindow: true })只查当前窗口。浏览器扩展的popup虽然在功能上是独立的浏览器UI,但它和一个标签页隶属于同一个窗口,currentWindow就是指用户点击扩展图标的那个窗口。

如果你需要跨窗口展示所有标签页,去掉currentWindow参数即可。但要注意:很多标签页操作(比如group、discard)是按窗口维度来体验才自然,跨窗口分组会让用户产生“我的标签页去哪了”的困惑。我的建议是popup默认显示当前窗口,在工具栏加一个开关“显示所有窗口”,按需切换。

4.4 storage.sync同步的容量问题

chrome.storage.sync可以在登录同一浏览器账号的设备间同步数据,但容量限制是每项8KB,总容量100KB。设置项少的时候完全够用,但如果你打算保存用户的分组方案、自定义域名高亮颜色,很快就会超。

超容量后写入不会报常规异常,而是静默失败,这会导致你的设置看似保存了,换一台设备后发现效果不一致,非常隐蔽。解决办法是改用chrome.storage.local,本地容量大得多(通常是10MB,实际以浏览器实现为准),代价是不跨设备同步。对于自用扩展,local存储是更稳的选择。我去重模式和颜色映射都存local,实测没有数据丢失。

4.5 加载扩展时提示“清单文件缺失或不可读”

最常见的三个原因:路径不对、JSON格式错误、manifest里用了MV2才支持的字段。我排查时用的最快方法是把manifest.json拖到任意JSON在线校验工具里看一眼,语法错误一目了然。还有一个容易忽略的点:manifest_version字段值必须是整数3,不能写成字符串"3",否则浏览器会直接拒绝加载。

4.6 扩展图标消失或popup空白

popup空白通常意味着popup.js运行报错。右键扩展图标打开“检查弹出式窗口”,控制台会显示未捕获的错误。我的经验是,很可能出在某个API在当前版本里不存在,比如chrome.tabGroups在旧版Chromium上不支持。开发时注意浏览器版本,Chrome 90以下的版本不要用分组API。

如果扩展图标整个消失,检查manifest.json里action字段是否写错,以及icons目录下的图片是否真实存在。icon路径写错会导致图标加载失败,扩展虽然能加载但显示成灰色默认图标。

4.7 新标签页被第三方扩展篡改的修复思路

热搜词里那条“新标签页被篡改”很典型。从技术角度讲,那些“篡改”新标签页的扩展和恶意脚本,本质上就是通过chrome_url_overrides覆盖了newtab页面。自用扩展一般不碰这个字段,但如果你的浏览器出现了这个问题,修复方向有三个:

  • 打开chrome://extensions(或edge://extensions),逐一点开各扩展详情,检查是否有“网站权限”或“阅读和更改所有网站数据”这类高危权限,无用的扩展直接移除。
  • 检查浏览器快捷方式是否被加了启动参数,比如某些推广版浏览器会在目标路径后面塞一个指向恶意站点的地址,这个要手工清理。
  • 如果以上都无效,把chrome://flags里与NTP相关的实验性开关重置为默认值,然后重启浏览器。

顺带一提,“Edge打开是360怎么改变”这类搜索词,多数情况也是第三方扩展或绿色版浏览器套壳导致。正规渠道安装的Edge或Chrome本体不会自行篡改新标签页,遇到这种问题优先怀疑浏览器快捷方式和扩展列表。

5. 实操总结与后续扩展方向

写到这里,这个标签页管理扩展已经可以正常完成“去重、分组、休眠、搜索跳转”四个核心动作。前前后后我用了一个周末的时间写完第一版,日常使用了一个多月,从最初每天开着二三十个标签页的状态,到现在稳定在十几个以内,最直观的感受是找页面不用再眯着眼扫标签栏了。

如果你也想照这个思路做一个属于自己的标签页工具,我强烈建议保持“能用就行”的心态,先做一版跑起来,再慢慢打磨。扩展开发最大的特点就是反馈链路极短——写完刷新一下,立刻就能用上,这种即时满足感比写普通Web项目强太多。

只做一个MVP版本会有一种特别上瘾的成就感,你可以顺手做更多的实验,比如把AI能力融进去,或者加一个“预测性关闭”功能。我自己也在尝试把去重逻辑里加上基于访问时间的权重,比如“同一个网址开了两个标签,但一个很久没看,一个刚打开,那关闭的时候优先关掉旧的那个”。这些都是后续可以慢慢扩展的小功能,改起来完全不影响已经跑通的核心链路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 2:59:24

QQ宠物怀旧服自动化:雷电模拟器+GG宠物助手配置与排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 2:59:23

Linux权限管理进阶:umask、隐藏权限与特殊权限实战指南

在 Linux 系统中&#xff0c;权限管理一直是日常运维和开发绕不开的核心话题。很多初学者在掌握了基本的 rwx 权限和 chmod 、 chown 命令之后&#xff0c;会觉得权限这块已经学得差不多了。但真正到了多用户服务器、项目协作目录、安全加固等场景时&#xff0c;才发现水…

作者头像 李华
网站建设 2026/9/8 2:57:05

免费纯净系统重装指南:U盘启动盘制作与安装排查

新电脑开箱、旧电脑卡顿、二手设备翻新&#xff0c;第一件事基本都是重装系统。很多人对“装机”的印象还停留在光盘、PE、分区、引导修复这一大堆名词上&#xff0c;觉得只有会修电脑的人才搞得定。实际上&#xff0c;现在的免费纯净装机工具已经把流程压缩到了三步&#xff1…

作者头像 李华
网站建设 2026/9/8 2:56:52

大模型训练像做面包?一文看懂预训练、微调与对齐全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 2:56:17

R-FCN源码解析:位置敏感得分图与全卷积目标检测网络实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华