很多做 Chrome 扩展的朋友这两年都在关注同一个东西从 Chrome 114 开始浏览器终于把“侧边栏”这个结构级的 UI 能力开放给了扩展开发者。以前我们想在浏览器右侧放一块常驻面板只能靠 popup 或者 content script 在页面上硬塞一个 iframe体验又割裂又容易崩。现在官方给了 chrome.sidePanel API做助手类工具、AI 对话面板、信息聚合面板都方便了很多。这篇文章我就拿一个实际能跑的示例把侧边栏开发从配置到通信、再到几个容易踩的坑完整拆一遍适合有基础前端能力、想快速上手 Chrome 侧边栏扩展的开发者参考。1. 项目背景从需求到方案选型1.1 什么场景适合用侧边栏先聊个实际问题什么时候你会想到用侧边栏而不是继续用 popuppopup 的痛点很明显——点一下工具栏图标弹个小窗焦点再点一下页面其他地方它就没影了。你要是想做一款“用户会一直开着、边浏览边操作”的工具比如笔记助手、翻译面板、AI 对话框、网址收藏夹popup 根本撑不住。用户不可能每次都点开图标、输内容、再点其他地方关掉这种交互太反人类了。侧边栏不一样。它一旦打开就保持可见用户可以同时操作网页内容和侧边栏工具两个区域互不遮挡。Chrome 官方给的定位也很直接辅助多任务处理。对这个需求sidePanel 是目前唯一合理的扩展方案没有之一。我在实际项目里用下来它所在的场景基本是用户需要看着当前网页内容同时在旁边做记录、查资料、调用工具。当然也不是所有场景都适合侧边栏。如果你的工具功能很单一比如就是一个“生成二维码”“翻译选中文字”这种用完即走的工具那 popup 还是更轻量、更合理的选择。侧边栏会额外占用浏览器宽度用户不一定会买账。1.2 侧边栏开发的三种形态Chrome 的侧边栏开发不是只有一种写法根据面板的展示逻辑可以分成三种形态全局启用所有标签页都显示同一个侧边栏不区分站点。适合工具类面板比如自己的笔记工具、JSON 格式化工具。按站点启用只在指定的网站上显示侧边栏其它站点自动隐藏。适合做购物助手、学习辅助这类跟站点强绑定的工具。多面板切换同一扩展可以定义多个侧边栏页面通过 setPath 动态切换。适合一个扩展里包含多个功能模块的场景比如既有“笔记”又有“AI 对话”。这篇示例我会按“全局启用 按站点动态开关”的方式来做因为这两种是最核心也是最容易绕晕的配置方式。多面板切换只需要在 setPath 上做文章理解了前两种之后上手就很快。对了还要提一句版本前提。sidePanel API 是 Chrome 114 才有的Chrome 116 之后又补充了 setPath、getOptions 这些增强能力。如果你要兼容 109 这种老版本就别想了老老实实做 popup 或者 content script。开发之前先确认目标用户群用的 Chrome 版本这是很现实的问题。版本对了后面才谈得上什么入口、什么通信方案。1.3 为什么建议用 MV3现在写 Chrome 扩展官方的推荐已经是 Manifest V3MV3了。MV2 虽然还可以加载但新提交的扩展已经不允许上架商店后续也会逐步被淘汰。这个示例我直接按 MV3 来写本质上也是让你从一开始就走在官方推荐的路线上省得以后迁移还要改 service worker、改造权限声明方式。MV3 和侧边栏搭配有个需要注意的点MV3 扩展的“后台逻辑”不是常驻后台页而是 service worker它随时可能被浏览器休眠。这对侧边栏本身没有太大影响因为侧边栏页面是持久的但如果你的侧边栏需要依赖后台逻辑做事件处理就要格外注意了。后面的通信章节我会专门讲怎么应对。2. 开发环境与工程基础2.1 准备工作本地加载和调试写侧边栏扩展的开发环境和普通 Web 前端没有本质区别一个顺手的编辑器VS Code 或者 WebStorm 都行、一个 Chrome 浏览器就够了不需要额外的构建工具。如果你想用 TypeScript 或者 React那另说可以自己搭 Vite 或者 Webpack但为了最大化还原“能直接跑起来”的体验我这里用的是纯 HTML JS零构建依赖直接把目录拖进浏览器就能调试。第一次加载扩展的操作很固定打开chrome://extensions/右上角打开“开发者模式”点“加载已解压的扩展程序”选择项目目录扩展就会出现在工具栏上。这里有一个实用小技巧你改完代码后不用每次手动去扩展管理页面点那个刷新按钮。在 service worker 的调试面板里执行chrome.runtime.reload()就能快速重载整个扩展不过侧边栏面板本身的更新稍微特殊一点如果面板一直开着改 HTML 不会自动热更新你需要在面板上右键选择“检查”在 DevTools 里的控制台执行location.reload()单独刷新面板页面。这个操作我在开发中几乎天天用比一遍遍重新加载扩展快太多了。2.2 manifest.json 配置一个位置都不能错MV3 扩展的配置全写在 manifest.json 里。我做侧边栏项目时配置经历了从“怎么一直报错”到“搞清楚每个字段意义”的过程所以下面把关键字段逐个拆开说明。{ manifest_version: 3, name: 站点速览助手, version: 1.0.0, description: 在侧边栏查看当前页面信息并快速记录, permissions: [sidePanel, tabs, storage], background: { service_worker: service-worker.js }, action: { default_title: 打开侧边栏 }, side_panel: { default_path: sidepanel.html } }先说权限部分sidePanel权限是核心中的核心。如果不声明它chrome.sidePanel整个 API 都是不存在的调用时直接报Cannot read properties of undefined。我第一次做的时候就漏了这一点还以为是 API 写错了。tabs权限用来读取标签页的 URL 和标题。这里有个容易被误解的点只声明tabs权限不代表你能拿到所有标签页的完整信息。如果你想要读取 URL 这些敏感字段在 MV3 里需要用户在扩展本身的权限弹窗里给tabs打勾或者在用户主动与扩展交互时通过activeTab获取临时的权限。对侧边栏开发来说如果面板打开时能感知到“当前活跃标签页”并读取它就能覆盖绝大多数场景。后面代码里我会给出一个用query({ active: true, currentWindow: true })查询的写法这是最典型的读取方式。storage权限是给笔记持久化用的。如果你的侧边栏不依赖存储也可以不声明但我这个示例会用到。然后注意action部分。这里只给了default_title没有配置default_popup。这是有意为之——一旦配置了 popup点击图标弹出来的就是小窗而不是侧边栏。所以做侧边栏扩展时千万不能写default_popup。配合后的 service worker 会通过setPanelBehavior把“点击图标打开侧边栏”的行为绑定在action上。side_panel字段里的default_path是侧边栏默认加载的 HTML 页面。这个路径是相对于扩展根目录的不能填外部网址也不能填绝对路径只能填项目里的相对路径。最后是 manifest 版本号。如果你照抄网上的旧教程看到manifest_version: 2那建议直接放弃那篇教程。MV2 的侧边栏不是这个写法。2.3 service worker 里设置默认行为service worker 是 MV3 扩展的事件指挥中枢。侧边栏的“点击图标自动展开”这个行为是要在这里配置的。// service-worker.js chrome.runtime.onInstalled.addListener(() { console.log(扩展已安装); }); chrome.sidePanel .setPanelBehavior({ openPanelOnActionClick: true }) .catch((error) console.error(error));setPanelBehavior里的openPanelOnActionClick设为true意味着用户在工具栏点击我们的扩展图标时浏览器会直接展开侧边栏而不是弹出 popup。这个 API 是全局生效的对一个扩展来说它只有一个面板行为配置不管你这个扩展定义了多个侧边栏页面点击图标打开的始终是当前面板。这段代码还有一个细节setPanelBehavior返回的是一个 Promise建议用catch兜底。因为某些情况下比如在浏览器内置页面上点击图标侧边栏可能无法正常打开这时候 API 会抛错。把这个错误打出来开发和排查问题时很有帮助。3. 核心逻辑sidePanel API 的关键细节3.1 三种触发方式怎么按需选择侧边栏的打开方式不止“点击图标”一种。我实际使用中把触发方式分成了三类不同场景用不同的入口组合。第一种是点击工具栏图标也就是上一节说的setPanelBehavior配置。这种方式最直观用户一眼就能看到扩展的存在。第二种是通过右键菜单触发。你可以用contextMenusAPI 在右键菜单里加一个“打开侧边栏”的选项点击后调用chrome.sidePanel.open()主动打开面板。这种方式对“需要在特定上下文里唤起工具”的场景特别实用但要注意chrome.sidePanel.open()必须在用户手势的上下文响应里调用不能在异步回调里过一会儿再调用否则会被浏览器拦截。第三种是键盘快捷键。用commandsAPI 注册全局快捷键比如按AltShiftS直接打开侧边栏。配置方式是在 manifest 里加一段commands: { _execute_action: { suggested_key: { default: AltShiftS } } }_execute_action是特殊命令名它会模拟一次工具栏图标的点击。配合前面openPanelOnActionClick: true的设置按快捷键就能展开侧边栏实现得很优雅。这三种触发方式我在项目中一般会同时上——图标点击是主入口快捷键给深度用户用右键菜单则是锦上添花。至于实际选哪种取决于你的用户习惯不能一概而论。3.2 按站点动态开关面板很多侧边栏扩展的需求是“只在某些网站显示”。比如做一个购物比价助手只在淘宝、京东这类购物网站上显示才有意义在无关的新闻网站上显示反而碍眼。chrome.sidePanel.setOptions就是干这个用的。它接收的参数是一个对象关键字段有三个tabId指定哪个标签页生效path指定用哪个侧边栏页面enabled指定侧边栏在该标签页是否可用要注意的是setOptions的调用方式有点讲究。当不传tabId时配置是全局默认的传了tabId就是针对某个标签页单独覆盖。Chrome 官方文档里说如果你想对每个标签页单独设置必须在tabs.onUpdated事件里对每个标签页挨个调用setOptions。这也是初学者最容易踩的坑。举个例子做一个“只在百度搜索结果页显示”的面板chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) { const isBaiduResult tab.url?.includes(www.baidu.com/s); chrome.sidePanel.setOptions({ tabId, enabled: isBaiduResult, }); });这段代码监听标签页更新事件一旦某个标签页变成百度搜索结果页就针对这个标签页启用侧边栏访问别的网站时侧边栏自动对当前标签页禁用。这种“跟随标签页”的感觉才是扩展侧边栏真正有魅力的地方。但我需要给你提个醒setPanelBehavior和setOptions的enabled字段不是同一个概念。openPanelOnActionClick控制的是“点击图标会不会打开面板”enabled控制的是“面板在某个标签页里是否可用”。如果你没设置openPanelOnActionClick即使enabled为 true点图标也不会打开侧边栏。两个行为是独立配置、叠加生效的这一点一定要想清楚。3.3 侧边栏和后台、内容脚本通信链路设计侧边栏本质上是一个嵌在浏览器 UI 里的独立页面它有自己独立的 DOM、独立的 JS 上下文。它和扩展的 service worker、以及网页里的 content script是三套互相隔离的环境通信必须走消息通道。先说侧边栏和 service worker 之间的通信。因为侧边栏页面本身是扩展页面它可以直接用chrome.runtime.sendMessage发送消息在 service worker 里用chrome.runtime.onMessage监听。这个链路非常稳定也是做数据持久化的主要途径。然后是侧边栏和 content script 之间的通信。这里有个典型的场景用户点击侧边栏里某个按钮希望在当前网页里执行一段脚本比如高亮选中文本、抓取页面数据。这个交互链路是侧边栏先告诉 service worker再由 service worker 告诉指定的 content script最后把结果回传给侧边栏。消息路径大概是这样的// 侧边栏中 chrome.runtime.sendMessage({ type: GRAB_PAGE, tabId: currentTabId }, (response) { console.log(response); });// service worker 中 chrome.runtime.onMessage.addListener((message, sender, sendResponse) { if (message.type GRAB_PAGE) { chrome.tabs.sendMessage(message.tabId, { type: EXECUTE_GRAB }, (response) { sendResponse(response); }); return true; // 异步响应必须返回 true } });为什么不能直接从侧边栏发消息给 content script因为chrome.tabs.sendMessage必须在有扩展上下文的场景下调用侧边栏页面拥有扩展上下文理论上是可以调用chrome.tabs.sendMessage的。但路径设计上我习惯统一走 service worker 中转好处有两个一是权限和逻辑集中在 service worker好排查问题二是如果以后某个 content script 没注入你至少可以在 service worker 里统一兜底。这段通信逻辑里容易踩的坑就是返回值问题。MV3 里如果发送端用sendResponse接收异步返回监听函数必须return true表示“我还会异步回复你”。新手经常忘这行结果sendResponse执行了但发送端永远收不到。还有一个细节是 content script 的注入方式。现在 MV3 推荐用chrome.scripting服务把脚本注入到目标页面比如chrome.scripting.executeScript。这种编程式注入的好处是你可以在用户点击侧边栏按钮时才真正注入不污染页面加载过程。声明式注入虽然简单但只能写死匹配规则不够灵活。3.4 面板内状态保存的注意事项侧边栏页面是持久的但也有一个现实问题它不像是 content script 在页面里可以被刷新它有自己的生命周期可以被用户关闭或切换标签页。面板内 JS 里的变量在面板关闭再打开后就全没了。如果你有一些用户配置、笔记内容、甚至面板 UI 状态需要保存一定要用chrome.storage而且我推荐用chrome.storage.session。它有两个好处一是读写是异步的不影响 UI 渲染二是数据存放在浏览器进程里关闭面板再打开还在扩展更新后才会清空。存储的代码很简单// 保存 await chrome.storage.session.set({ notes: noteList }); // 读取 const { notes } await chrome.storage.session.get({ notes: [] });不要把这些状态放在 service worker 的全局变量里。MV3 的 service worker 随时可能被浏览器杀掉一旦被回收所有内存状态就都没了。这是我踩过一次的坑后来统一改成了 storage 方案。4. 实操过程从零实现一个“站点速览助手”4.1 需求与页面结构为了把上面的 API 串成一个能运行、能体验的完整示例我实现一个小工具叫“站点速览助手”。核心功能有三块展示当前标签页的标题、URL、favicon一键把当前页面标题和链接复制成 Markdown 格式的链接在侧边栏里做一个简短的备忘笔记自动保存这三个功能不算复杂但覆盖了 sidePanel 开发里最重要的几个能力点读取标签页、与后台通信、Chrome 存储、面板 UI 渲染。sidepanel.html 的页面结构我保持得非常简单!DOCTYPE html html langzh-CN head meta charsetUTF-8 / style body { font-family: system-ui, sans-serif; padding: 16px; } #page-info { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; } #favicon { width: 24px; height: 24px; } #page-title { font-weight: 600; word-break: break-all; } #page-url { color: #888; font-size: 12px; word-break: break-all; } #copy-btn { margin-top: 8px; } #note-area { width: 100%; min-height: 160px; margin-top: 16px; box-sizing: border-box; } /style /head body h2站点速览/h2 div idpage-info img idfavicon altfavicon / div div idpage-title当前页面标题/div div idpage-url当前页面地址/div /div /div button idcopy-btn复制 Markdown 链接/button textarea idnote-area placeholder给这个页面记点笔记…/textarea script srcsidepanel.js/script /body /html样式没必要追求多好看重点在于跑通逻辑。这里一个小的 UI 要点是侧边栏宽度一般只有 250px 到 400px所以所有文本都要设置word-break不然长 URL 会把布局撑破。4.2 核心代码实现sidepanel.js 是整个示例的大脑我会把主要逻辑拆成三段来说。第一段是面板打开时获取当前标签页信息。这里有一个很关键的念头侧边栏虽然和网页并排显示但它并不是 content script它没有访问当前网页 DOM 的权限要拿页面信息只能通过chrome.tabs.query。// sidepanel.js async function getCurrentTab() { const [tab] await chrome.tabs.query({ active: true, currentWindow: true }); return tab; } async function refreshPageInfo() { const tab await getCurrentTab(); if (!tab || !tab.url) return; document.querySelector(#page-title).textContent tab.title || 无标题; document.querySelector(#page-url).textContent tab.url; try { const url new URL(tab.url); document.querySelector(#favicon).src https://www.google.com/s2/favicons?domain${url.hostname}sz32; } catch { // 忽略非法 URL } } refreshPageInfo();需要注意query的过滤参数active: true表示当前激活标签页currentWindow: true表示当前浏览器窗口。如果不加这两个条件可能会查出一堆标签页取到的还不一定是用户正在看的那个。第二段是复制 Markdown 链接。这里我用的是 Clipboard API比document.execCommand(copy)干净得多。document.querySelector(#copy-btn).addEventListener(click, async () { const [tab] await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab || !tab.url) return; const markdown [${tab.title}](${tab.url}); await navigator.clipboard.writeText(markdown); const btn document.querySelector(#copy-btn); btn.textContent 已复制; setTimeout(() (btn.textContent 复制 Markdown 链接), 1500); });对tabs.query的调用必须在用户交互的事件回调里吗不是强制要求chrome.tabs.query本身没有用户手势限制。但navigator.clipboard.writeText对权限要求更严格它要求页面处于聚焦状态且是安全上下文。扩展侧边栏页面天然拥有扩展页面的安全上下文所以这里能直接用。如果你在 content script 里复制文本反而会因为页面协议是http或https而触发权限限制这种情况就要走chrome.runtime消息让后台来写剪贴板。我一般是能用扩展页面剪贴板就用扩展页面省得踩内容脚本的坑。第三段是备忘录逻辑用chrome.storage.session做自动保存const noteArea document.querySelector(#note-area); function bindNoteEvents() { noteArea.addEventListener(input, async (e) { await chrome.storage.session.set({ currentNote: e.target.value }); }); refreshNote(); } async function refreshNote() { const { currentNote } await chrome.storage.session.get({ currentNote: }); noteArea.value currentNote; } bindNoteEvents();这里有个小细节input事件触发很频繁每次都写 storage 虽然不会有太大性能问题但如果以后要存到chrome.storage.local甚至同步到远程最好加个 300ms 的防抖。比如用setTimeout延迟保存防止频繁的磁盘写入。三段逻辑合起来这个示例就已经是一个能用的侧边栏工具了。4.3 加上按站点开关的逻辑为了让示例更好地演示 sidePanel 的“动态开关”能力我再给 service worker 加一段逻辑只有在访问github.com相关的页面时才让侧边栏可用其它网站点击图标会提示不可用。在 service-worker.js 里加监听chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) { if (!tab.url) return; const isGitHub tab.url.startsWith(https://github.com); chrome.sidePanel .setOptions({ tabId, enabled: isGitHub }) .catch(() {}); });这段配置会实时生效。当你从 GitHub 切到百度搜索时侧边栏应该会自动禁用切回 GitHub侧边栏又恢复了。这种动态开关是 popup 完全做不到的也是侧边栏扩展里体验最好的一种能力。不过要注意如果你在 manifest 里没有声明sidePanel权限这里调用chrome.sidePanel.setOptions会直接抛错页面控制台会看到一个红色报错。所以在修改代码之前先确认 manifest 里的权限列表是完整的。4.4 调试与验证流程整个示例跑起来之后调试流程很重要。我会按下面的顺序检查问题先在chrome://extensions/页面确认普通扩展已经加载成功没有红字错误。然后点击工具栏图标如果侧边栏没打开右键图标选择“检查弹出”或直接打开 service worker 的控制台看有没有报错。页面打开后在侧边栏面板内右键选择“检查”打开面板自己的 DevTools。这里要特别说明一下侧边栏面板是一个独立的页面它的 console 和 DOM 检查器是独立的很多人以为点侧边栏的右键会打开页面 DevTools其实弹出来的是面板自身的检查器。在这个检查器里你可以执行location.reload()刷新面板或者直接打断点调试。信息读取逻辑、复制逻辑、存储逻辑分别验证一遍。我每次测试的时候还会特意试一个场景面板开着的时候切到别的标签页再切回来确认面板是不是一直保持打开状态。这个场景正是侧边栏的亮点——如果哪次切换标签页后面板自动关了那大概率是你在tabs.onUpdated里不小心把setOptions({ tabId, enabled: false })写错了。5. 常见问题与排查技巧实录5.1 点击图标没反应侧边栏不出现这是最高频的问题通常原因有三个按概率排序第一没有调用setPanelBehavior({ openPanelOnActionClick: true })。如果你只配置了 manifest 里的side_panel.default_path点击图标只会激活扩展并不会展开侧边栏。必须在 service worker 里显式调用一次。第二代码里写了default_popup。只要 manifest 的action字段里声明了default_popup点击图标就会优先弹出 popup侧边栏自然打不开。两者是互斥的。第三在受限制的页面上点击。Chrome Web Store、chrome://内置页面、edge://如果你是 Edge 浏览器这些页面扩展侧边栏是不支持的。在这些页面点击图标行为会被忽略或者抛错。这不是你的代码 bug是浏览器的安全限制。5.2 报错或日志里出现Cannot read properties of undefined基本可以断定是chrome.sidePanel对象本身不存在。原因也简单你也忘了在 manifest.json 的permissions里加sidePanel权限。这是初学者最容易犯的错误也是最容易排查的——检查权限列表加上就好了。如果权限列表没问题但还是报错那考虑浏览器版本。Chrome 114 之前不存在这个 API低于 114 的版本调用必然报错。这种情况下建议写个特性检测if (chrome.sidePanel) { chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); }这样至少不会整段 service worker 因为一个 API 不存在而崩溃。5.3 侧边栏和 content script 通信时消息收不到这个问题的排查思路要按链路一层层拆。首先确认 content script 是否真的注入了目标页面。在目标页面的 DevTools 里看一眼 Sources 面板或者直接执行typeof someContentVariable如果没定义说明脚本没注入。然后确认消息事件监听器是否注册了。如果 content script 是在页面加载之后才通过chrome.scripting.executeScript注入的那么注入时机之前的addListener注册是不会被执行的。最后也是最隐蔽的一点MV3 中 content script 和扩展页面之间的消息默认是不带「页面 URL」信息的但带sender.tab。你在监听函数里如果判断了sender.tab.url的域名要注意sender.tab在部分消息场景下可能是 undefined。做健壮性处理时先判空再取字段。我一般都建议把常见的消息链路整理成一张速查表方便排查发送方接收方调用方式监听方式侧边栏service workerchrome.runtime.sendMessagechrome.runtime.onMessageservice worker侧边栏chrome.runtime.sendMessagechrome.runtime.onMessageservice workercontent scriptchrome.tabs.sendMessage(tabId, ...)chrome.runtime.onMessagecontent scriptservice workerchrome.runtime.sendMessagechrome.runtime.onMessagecontent script侧边栏经 service worker 中转中转后由 onMessage 接收5.4 面板一直显示白屏或者控制台报错白屏这块我遇到过的原因基本都是 JS 运行时报错导致脚本中断。比如document.querySelector(#page-title).textContent这行如果 HTML 里没有对应的 id 元素就可能抛 null 引用错误。出现白屏时先在面板的 DevTools Console 里看报错信息这是最快定位路径。另外要注意sidepanel.html 里引入的 JS、CSS 文件路径都是相对于扩展根目录的不要用绝对路径/sidepanel.js。用绝对路径在某些内部页面里会解析失败。统一用相对路径最保险。还有一个很容易忽略的坑如果你在 sidepanel.html 里使用了外部 CDN 的脚本比如在线的 React、VueChrome 扩展默认有内容安全策略CSP会禁止加载远程脚本。MV3 下对外部脚本的限制非常严格建议把所有资源都打包进扩展目录或者使用离线构建产物。5.5 开发时怎么提升刷新效率侧边栏面板开发有个很独特的问题扩展本体和面板页面是两套缓存。改 manifest.json 或 service worker 后需要在扩展管理页面点刷新按钮改面板的 HTML/JS 后面板页面不会跟随扩展刷新自动更新得手动在面板里执行一次location.reload()。我现在的开发习惯是写一个 word 级别的调试工具在 service worker 里监听键盘事件按CtrlShiftR时执行chrome.runtime.reload()配合chrome.tabs.reload()机制来整体刷新。这虽然是一个偏 hack 的做法但确实能省不少时间。更妥的方案还是像我前面提到的把面板的刷新函数绑定到一个临时按钮上或者直接用location.reload()快捷键操作。结尾按我自己的经历来说做 Chrome 侧边栏扩展不是说把 API 调用一遍就算完真正的门槛在于交互逻辑的取舍。侧边栏的优势是持久可见但它也占屏幕空间用户的容忍度没有想象中那么高。如果面板打开后只是展示一些静态信息没有顺手到让人愿意一直开着那效果就大打折扣。我强烈建议你在做侧边栏功能时多考虑“用户为什么要把这个面板一直放在旁边”而不是简单地把一个 popup 换个地方展示。把面板的信息密度、操作效率做上去用起来才会舒服。最后分享一个很实用的小技巧如果你用快捷键作为侧边栏入口尽量选择AltShift字母这种组合避免和 Chrome 自带快捷键冲突另外在面板里给所有按钮加上title提示hover 时给用户一个明确的预期这个小细节虽然没有技术含量但在实测中能明显降低用户误操作的概率。