尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从零开发B站美化插件:基于Manifest V3的浏览器扩展实战指南

从零开发B站美化插件:基于Manifest V3的浏览器扩展实战指南 1. 背景与核心概念你是否曾觉得B站网页版的默认界面有些单调或者某些交互操作不够便捷例如想要快速调整视频播放速度、一键下载封面或者仅仅是让页面布局更符合个人审美却发现官方并未提供这些功能。这种对个性化体验和效率提升的需求正是浏览器扩展插件能够大显身手的地方。一个B站美化插件本质上是一个运行在你浏览器如Chrome、Edge、Firefox中的小型JavaScript程序。它通过注入自定义的CSS样式和JavaScript脚本来修改B站网页的视觉呈现和交互逻辑。这意味着在不改动B站服务器端任何代码的前提下你可以在本地实现界面主题切换、功能增强、广告屏蔽、快捷键自定义等一系列个性化改造。这类插件的核心价值在于个性化定制打破千篇一律的官方界面允许用户根据喜好调整颜色、布局、字体等。效率提升通过添加快捷键、一键操作等功能简化常用流程提升使用效率。功能增强补充官方未提供但用户呼声较高的功能如视频下载辅助需注意版权、数据展示增强等。学习与实践对于开发者而言开发此类插件是学习Web前端技术、浏览器扩展API以及逆向分析实际网站的优秀实践项目。本文将从一个开发者视角系统性地介绍如何从零开始构思、开发并发布一个功能完整的B站美化插件。我们将涵盖从环境搭建、核心API使用、样式与脚本注入到调试、打包和发布的完整闭环。无论你是前端新手想做一个有趣的练手项目还是有一定经验的开发者希望深入浏览器扩展开发都能从中获得一套可复用的实战方案。2. 环境准备与版本说明在开始编码之前我们需要准备好开发环境。浏览器扩展开发主要依赖现代浏览器和代码编辑器对操作系统没有特殊要求。核心环境操作系统Windows 10/11, macOS, Linux 均可。浏览器Google Chrome 或 Microsoft Edge推荐Chrome因其开发者工具最全面。本文示例基于 Chrome/Chromium 内核的扩展规范Manifest V3该规范也适用于新版Edge。代码编辑器Visual Studio Code (VS Code)并安装诸如 “Live Server”、 “Chrome Extension Developer Tools” 等插件以提升效率。Node.js非必须但若项目涉及复杂的构建流程如使用React/Vue框架、SCSS等则需要安装。本文基础示例不依赖Node.js。项目结构预览一个最简化的浏览器扩展项目通常包含以下文件bilibili-enhancer/ ├── manifest.json # 扩展的配置文件核心 ├── popup.html # 扩展弹出窗口的页面 ├── popup.js # 弹出窗口的逻辑脚本 ├── content.js # 注入到B站页面的内容脚本 ├── background.js # 后台服务脚本Manifest V3中改为service_worker ├── styles.css # 注入到B站页面的样式文件 ├── icons/ # 扩展图标文件夹 │ ├── icon16.png │ ├── icon48.png │ └── icon128.png └── _locales/ # 国际化文件夹可选 └── zh_CN/ └── messages.json关键版本说明Manifest V3这是Chrome扩展平台的最新版本。与V2相比V3更注重安全性、隐私和性能主要变化包括用Service Worker替代后台页面(background page)限制远程代码执行以及修改了部分API的权限模型。新项目强烈建议从Manifest V3开始。本文示例将基于Manifest V3。B站页面结构B站前端代码会持续迭代选择器CSS Selector和页面结构可能发生变化。开发时需注意选择器的健壮性并做好后续维护的准备。3. 核心原理与API拆解浏览器扩展通过一组特定的API与浏览器和网页进行交互。理解以下几个核心概念是开发的基础。3.1 扩展的骨架manifest.jsonmanifest.json是扩展的“身份证”和“说明书”定义了扩展的基本信息、权限、资源文件和运行规则。{ manifest_version: 3, name: B站视觉与功能增强插件, version: 1.0.0, description: 自定义B站界面样式并添加实用功能如快捷键、下载封面等。, icons: { 16: icons/icon16.png, 48: icons/icon48.png, 128: icons/icon128.png }, action: { default_popup: popup.html, default_title: B站增强设置 }, permissions: [ storage, activeTab, scripting ], host_permissions: [ https://*.bilibili.com/* ], content_scripts: [ { matches: [https://*.bilibili.com/*], js: [content.js], css: [styles.css], run_at: document_idle } ], background: { service_worker: background.js }, web_accessible_resources: [{ resources: [injected-style.css], matches: [https://*.bilibili.com/*] }] }manifest_version: 必须为3。permissions: 声明扩展需要的权限。storage用于存储用户设置activeTab和scripting用于在用户与页面交互时执行脚本。host_permissions: 声明扩展可以访问的网站这里是所有B站域名。content_scripts:核心配置。指定在哪些页面(matches)自动注入哪些JS(js)和CSS(css)文件。run_at表示注入时机document_idleDOM加载完成后通常是最佳选择。background.service_worker: 后台服务脚本用于处理全局事件、管理状态等。它独立于任何页面运行。web_accessible_resources: 声明哪些扩展内的资源可以被网页访问。例如如果你想通过content.js动态加载一个CSS文件就需要在这里声明。3.2 与页面交互Content Scriptscontent.js是直接运行在B站网页上下文中的脚本。它可以完全访问和操作当前页面的DOM就像页面自身的JavaScript一样但通常运行在一个独立的“隔离环境”中无法直接访问页面原始的全局变量如window.jQuery反之亦然。// content.js - 示例修改页面标题颜色并添加一个按钮 (function() { use strict; // 1. 修改样式 document.querySelector(.bili-video-card__info--tit)?.style.color #00a1d6; // 2. 监听页面事件 window.addEventListener(scroll, function() { console.log(B站页面滚动了); }); // 3. 创建一个功能按钮并添加到页面 function addDownloadCoverButton() { const coverElement document.querySelector(.bpx-player-video-wrap img, .video-cover img); if (coverElement !document.getElementById(my-download-cover-btn)) { const btn document.createElement(button); btn.id my-download-cover-btn; btn.textContent 保存封面; btn.style.cssText position: absolute; top: 10px; right: 10px; z-index: 1000; padding: 5px 10px; background: #fb7299; color: white; border: none; border-radius: 4px; cursor: pointer;; coverElement.parentElement.style.position relative; coverElement.parentElement.appendChild(btn); btn.addEventListener(click, function(e) { e.stopPropagation(); const imgUrl coverElement.src.replace(/.*$/, ); // 尝试获取高清原图 if (imgUrl) { chrome.runtime.sendMessage({action: downloadImage, url: imgUrl}); } }); } } // 使用MutationObserver监听动态加载的内容 const observer new MutationObserver(addDownloadCoverButton); observer.observe(document.body, { childList: true, subtree: true }); // 初始执行一次 addDownloadCoverButton(); })();为什么使用IIFE立即调用函数表达式(function(){...})()可以将变量封装在函数作用域内避免污染页面的全局命名空间。3.3 后台处理与消息通信Service Workerbackground.js(在Manifest V3中是Service Worker) 用于处理需要长期运行或跨页面协调的任务。它不能直接操作DOM。// background.js - 示例处理下载请求和快捷键命令 chrome.runtime.onMessage.addListener((request, sender, sendResponse) { console.log(收到消息:, request); if (request.action downloadImage) { // 使用downloads API下载图片 chrome.downloads.download({ url: request.url, filename: bilibili_cover_${Date.now()}.jpg, saveAs: false // 是否弹出“另存为”对话框 }, (downloadId) { if (chrome.runtime.lastError) { console.error(下载失败:, chrome.runtime.lastError); } }); } // 可以返回一个Promise以支持异步sendResponse // return true; // 表示你会异步调用sendResponse }); // 监听快捷键命令需要在manifest.json的commands字段中声明 chrome.commands.onCommand.addListener((command) { if (command toggle-theme) { // 通知所有B站标签页切换主题 chrome.tabs.query({url: *://*.bilibili.com/*}, (tabs) { tabs.forEach(tab { chrome.tabs.sendMessage(tab.id, {action: toggleDarkMode}); }); }); } });3.4 存储用户设置Storage API扩展可以使用chrome.storageAPI 来持久化保存用户的配置如选择的主题、开关状态等。它比网页的localStorage更强大支持同步跨设备和本地存储。// 在popup.js或content.js中 // 保存设置 chrome.storage.local.set({ theme: dark, autoPlay: false }, () { console.log(设置已保存); }); // 读取设置 chrome.storage.local.get([theme, autoPlay], (result) { console.log(当前主题:, result.theme || light); const autoPlay result.autoPlay ! false; // 默认true });4. 完整实战案例开发一个B站“深色模式”插件我们将开发一个插件它不仅提供深色模式还允许用户自定义主色调并添加一个“一键回到顶部”的悬浮按钮。4.1 创建项目结构与manifest.json首先创建项目文件夹bilibili-dark-plus并按照上文的结构创建文件。先完成manifest.json。{ manifest_version: 3, name: B站深色模式, version: 1.0.0, description: 为B站提供可自定义的深色模式与便捷功能。, icons: { 16: icons/icon16.png, 48: icons/icon48.png, 128: icons/icon128.png }, action: { default_popup: popup.html, default_title: B站深色模式设置 }, permissions: [storage, activeTab], host_permissions: [https://*.bilibili.com/*], content_scripts: [ { matches: [https://*.bilibili.com/*], js: [content.js], css: [styles/injected.css], run_at: document_idle } ], web_accessible_resources: [ { resources: [styles/injected.css], matches: [https://*.bilibili.com/*] } ] }注意我们将CSS放在了styles/injected.css文件夹中。4.2 编写核心样式文件 (styles/injected.css)这个文件定义了深色模式的基础样式和我们的自定义组件样式。/* styles/injected.css */ /* B站深色模式 基础样式 */ :root { --bili-dark-bg: #181818 !important; --bili-dark-text: #e0e0e0 !important; --bili-dark-primary: #00a1d6 !important; /* 默认主色可通过JS覆盖 */ --bili-dark-border: #333 !important; } /* 应用深色背景和文字色 */ body, .bili-header, .main-container, .video-page, .bili-dyn-list__item, .bili-video-card { background-color: var(--bili-dark-bg) !important; color: var(--bili-dark-text) !important; } /* 调整链接和按钮颜色 */ a, .bili-video-card__info--tit, .video-title { color: var(--bili-dark-primary) !important; } /* 调整边框颜色 */ .bili-video-card, .bili-dyn-list__item, .bili-comment { border-color: var(--bili-dark-border) !important; } /* 输入框和搜索框 */ input, textarea, .nav-search-keyword { background-color: #2a2a2a !important; color: var(--bili-dark-text) !important; border-color: var(--bili-dark-border) !important; } /* 自定义的“回到顶部”按钮 */ #bili-dark-plus-back-to-top { position: fixed !important; bottom: 80px !important; right: 20px !important; width: 50px !important; height: 50px !important; border-radius: 50% !important; background-color: var(--bili-dark-primary) !important; color: white !important; border: none !important; cursor: pointer !important; font-size: 24px !important; box-shadow: 0 2px 10px rgba(0,0,0,0.3) !important; z-index: 9999 !important; display: none !important; /* 默认隐藏 */ align-items: center !important; justify-content: center !important; } #bili-dark-plus-back-to-top:hover { opacity: 0.9 !important; }为什么大量使用!important因为B站自身的样式可能具有更高的特异性Specificity使用!important可以确保我们的覆盖样式生效。但这应谨慎使用避免样式冲突难以管理。4.3 编写内容脚本 (content.js)这个脚本负责动态应用用户设置、创建交互元素并监听变化。// content.js (function() { use strict; const STORAGE_KEY biliDarkPlusSettings; let settings { enabled: true, primaryColor: #00a1d6, showBackToTop: true }; // 初始化从存储加载设置并应用 function init() { chrome.storage.local.get([STORAGE_KEY], (result) { if (result[STORAGE_KEY]) { Object.assign(settings, result[STORAGE_KEY]); } applySettings(); if (settings.showBackToTop) { createBackToTopButton(); } setupMutationObserver(); }); } // 应用当前设置到页面 function applySettings() { const root document.documentElement; if (settings.enabled) { root.classList.add(bili-dark-plus-enabled); // 动态更新CSS变量 root.style.setProperty(--bili-dark-primary, settings.primaryColor, important); } else { root.classList.remove(bili-dark-plus-enabled); } // 控制“回到顶部”按钮显示 const btn document.getElementById(bili-dark-plus-back-to-top); if (btn) { btn.style.display settings.showBackToTop ? flex : none; } } // 创建“回到顶部”按钮 function createBackToTopButton() { if (document.getElementById(bili-dark-plus-back-to-top)) return; const btn document.createElement(button); btn.id bili-dark-plus-back-to-top; btn.innerHTML ↑; btn.title 回到顶部; document.body.appendChild(btn); btn.addEventListener(click, () { window.scrollTo({ top: 0, behavior: smooth }); }); // 滚动时显示/隐藏按钮 window.addEventListener(scroll, toggleBackToTopButton); toggleBackToTopButton(); // 初始检查 } function toggleBackToTopButton() { const btn document.getElementById(bili-dark-plus-back-to-top); if (btn) { btn.style.display (window.scrollY 300 settings.showBackToTop) ? flex : none; } } // 监听来自popup或background的消息动态更新设置 chrome.runtime.onMessage.addListener((request, sender, sendResponse) { if (request.action updateSettings) { Object.assign(settings, request.settings); chrome.storage.local.set({ [STORAGE_KEY]: settings }, () { applySettings(); sendResponse({ success: true }); }); return true; // 保持消息通道开放以进行异步响应 } }); // 监听DOM变化确保动态加载的内容也能被正确样式化 function setupMutationObserver() { const observer new MutationObserver((mutations) { // 可以在这里添加对特定新元素的样式修补逻辑 // 例如if (settings.enabled) { patchNewElements(); } }); observer.observe(document.body, { childList: true, subtree: true }); } // 启动 if (document.readyState loading) { document.addEventListener(DOMContentLoaded, init); } else { init(); } })();4.4 编写弹出窗口页面 (popup.html popup.js)弹出窗口是用户与插件交互的主要界面。!-- popup.html -- !DOCTYPE html html head meta charsetutf-8 style body { width: 300px; padding: 15px; font-family: sans-serif; } .switch { position: relative; display: inline-block; width: 50px; height: 24px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top:0;left:0;right:0;bottom:0; background-color: #ccc; transition: .4s; border-radius: 24px; } .slider:before { position: absolute; content: ; height: 16px; width: 16px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } input:checked .slider { background-color: #00a1d6; } input:checked .slider:before { transform: translateX(26px); } .color-picker { margin: 10px 0; } label { display: block; margin-top: 15px; font-weight: bold; } /style /head body h3B站深色模式/h3 label span启用深色模式/span label classswitch input typecheckbox idtoggleEnabled span classslider/span /label /label label forprimaryColor主题主色调/label input typecolor idprimaryColor classcolor-picker value#00a1d6 label input typecheckbox idtoggleBackToTop 显示“回到顶部”按钮 /label script srcpopup.js/script /body /html// popup.js document.addEventListener(DOMContentLoaded, () { const STORAGE_KEY biliDarkPlusSettings; const defaultSettings { enabled: true, primaryColor: #00a1d6, showBackToTop: true }; const toggleEnabled document.getElementById(toggleEnabled); const primaryColor document.getElementById(primaryColor); const toggleBackToTop document.getElementById(toggleBackToTop); // 加载保存的设置 chrome.storage.local.get([STORAGE_KEY], (result) { const settings result[STORAGE_KEY] || defaultSettings; toggleEnabled.checked settings.enabled; primaryColor.value settings.primaryColor; toggleBackToTop.checked settings.showBackToTop; }); // 监听设置变化并保存 function saveSettings() { const newSettings { enabled: toggleEnabled.checked, primaryColor: primaryColor.value, showBackToTop: toggleBackToTop.checked }; chrome.storage.local.set({ [STORAGE_KEY]: newSettings }, () { // 通知所有B站标签页更新 chrome.tabs.query({url: *://*.bilibili.com/*}, (tabs) { tabs.forEach(tab { chrome.tabs.sendMessage(tab.id, {action: updateSettings, settings: newSettings}); }); }); }); } toggleEnabled.addEventListener(change, saveSettings); primaryColor.addEventListener(change, saveSettings); toggleBackToTop.addEventListener(change, saveSettings); });4.5 加载与调试插件打开 Chrome 浏览器进入chrome://extensions/。打开右上角的“开发者模式”开关。点击“加载已解压的扩展程序”按钮。选择你创建的bilibili-dark-plus项目文件夹。插件将被加载并显示在扩展列表中。确保其处于“启用”状态。现在打开B站任意页面如www.bilibili.com你应该能看到页面变为深色主题右下角会出现“回到顶部”按钮。点击浏览器工具栏中的插件图标可以弹出设置窗口尝试开关深色模式、更改主色调观察页面实时变化。调试技巧Content Script调试在B站网页上右键 - “检查”打开开发者工具。在“源代码”(Sources)标签页中找到“内容脚本”(Content scripts)分类可以看到并调试你的content.js。Popup调试右键点击插件图标 - “检查弹出内容”即可调试popup.html和popup.js。Service Worker调试在chrome://extensions/页面找到你的插件点击“service worker”链接即可打开后台脚本的控制台。5. 常见问题与排查思路在开发过程中你可能会遇到以下典型问题问题现象可能原因排查与解决思路插件图标不显示或无法点击1.manifest.json中icons路径错误或图片缺失。2.action配置错误。1. 检查icons文件夹是否存在图片命名是否与manifest.json中一致。2. 确认manifest.json中action的default_popup路径正确。样式或脚本没有在B站页面生效1.manifest.json中content_scripts的matches模式不匹配当前B站URL。2. CSS选择器因B站页面更新而失效。3. 脚本注入时机 (run_at) 不合适。1. 检查浏览器地址栏URL是否匹配https://*.bilibili.com/*。2. 在开发者工具中检查元素确认你使用的CSS选择器是否能正确选中目标元素。3. 尝试将run_at改为document_start或document_end测试。chrome.storage或chrome.tabs.sendMessage报错undefined1. 在错误的作用域调用API如普通网页JS。2. 未在manifest.json的permissions中声明所需权限。1. 确保调用这些API的代码在content.js,popup.js或background.js中。2. 检查manifest.json的permissions字段是否包含了storage和activeTab或tabs。向content.js发送消息失败收不到回复1. 目标标签页未加载content.js可能URL不匹配。2.content.js中未正确添加消息监听器。3. 异步响应未返回true。1. 确认接收消息的标签页URL在matches范围内。2. 检查content.js中chrome.runtime.onMessage.addListener是否正确添加。3. 如果sendResponse是异步的监听器必须return true;。MutationObserver导致性能问题或无限循环观察的回调函数中修改了DOM又触发了新的Mutation形成循环。在回调函数开始进行检查如果修改是自己触发的则直接返回。或者使用更精确的观察配置如attributes,characterData避免subtree: true的过度观察。样式覆盖不完全有“闪白”现象CSS注入时机晚于页面渲染或!important优先级仍不够。1. 将content_scripts的run_at设为document_start让CSS尽早注入。2. 使用更具体的选择器或通过JS在document_start阶段动态添加style标签。插件更新后旧设置丢失或页面无变化1. 存储键名 (STORAGE_KEY) 改变。2. 页面缓存了旧的CSS/JS文件。1. 保持存储键名稳定或编写迁移逻辑。2. 在扩展管理页面 (chrome://extensions/) 点击对应插件的“更新”按钮并硬刷新B站页面 (CtrlF5)。6. 最佳实践与工程建议将一个小插件打磨成健壮、可维护的项目需要遵循一些工程实践。模块化与构建对于复杂插件考虑使用如 Webpack、Parcel 等构建工具。这允许你使用 ES6 模块、SCSS/Less 等并将代码打包优化。将功能拆分为独立的JS模块例如theme-manager.js、ui-components.js、utils.js。健壮的样式注入避免全局污染为你的所有样式规则添加一个独特的前缀类名例如.bili-enhancer-container .some-element减少与页面样式冲突的风险。动态样式对于用户可自定义的样式如主题色最好通过content.js动态修改style标签或CSS变量而不是准备多份巨大的CSS文件。安全的权限管理最小权限原则在manifest.json中只声明插件运行所必需的最少权限。例如如果不需要修改所有网站就不要使用all_urls。敏感操作确认对于下载、获取大量数据等操作应在popup或页面内提供明确的用户确认按钮而不是静默执行。错误处理与日志在content.js和background.js中使用try...catch包裹可能出错的操作。在开发阶段可以使用console.log进行调试。对于发布版本考虑实现一个简单的日志系统将错误信息通过chrome.runtime.sendMessage发送到background.js进行统一处理或上报需用户同意。兼容性与降级B站页面结构多变你的CSS选择器和DOM操作逻辑很可能在未来失效。设计代码时考虑降级方案例如某个功能按钮添加失败不影响核心的样式功能。可以使用特性检测而不是浏览器嗅探。发布与更新打包在Chrome扩展管理页面点击“打包扩展程序”生成.crx文件和.pem私钥文件务必保存好。发布注册 Chrome 网上应用店开发者账号提交打包后的扩展进行审核。更新更新manifest.json中的version字段然后重新打包并上传到商店。用户端会自动更新。尊重版权与用户明确你的插件是辅助工具任何涉及下载、解析视频等功能必须强调其用于个人学习、备份的合法用途并尊重B站和UP主的版权。在插件描述中增加相关声明。清晰告知用户插件收集了哪些数据如存储本地设置并提供隐私政策。通过以上步骤你不仅完成了一个功能性的B站美化插件更掌握了一套完整的浏览器扩展开发工作流。从需求分析、技术选型、编码实现、调试测试到发布维护这个过程涵盖了前端工程化的多个关键环节。你可以在此基础上继续探索更多有趣的功能如自定义播放器控件、高级快捷键集成、数据分析面板等打造属于你自己的终极B站体验工具。
返回列表