
打开 Chrome 浏览器在地址栏输入下面这一行内容按回车data:text/html,h1 styletext-align:center;margin-top:40vhHello, Browser/h1浏览器没有去访问某个网站而是直接渲染了一个网页。很多第一次看到这个操作的人都会脱口而出这个谷歌浏览器的网页真好玩。但实际上这不是某个特定网页而是浏览器自身把 URL 当成了一块可以实时渲染的 HTML 画布。这件事背后隐藏着一个值得每个前端开发者掌握的能力浏览器不只是用来浏览网页它本身就是可以随时执行 JavaScript、绘制 Canvas、修改 DOM 的完整运行时。这篇文章想把“好玩”这件事拆开来讲。我会用三种由易到难的方式把任意网页变成特效舞台第一种是在 DevTools 控制台直接注入一段“黑客帝国字符雨”特效第二种是把落雪特效封装成一个书签小工具点一下就能在任何网页上下雪第三种是做一个最小可用的 Chrome 扩展用一个按钮给当前网页添加字符雨、下雪或清除特效。前两种适合日常玩票和给别人展示第三种能让你理解浏览器扩展的基本工作原理。读完这篇文章你不仅能复现这些特效还会知道常见的坑在哪里为什么有些页面注入不了、为什么动画会卡、为什么特效层会挡住点击、怎么清理最干净。这篇文章不依赖 Node.js、不依赖框架只需要一个 Chromium 内核浏览器、一个文本编辑器和原生 JavaScript。1. 这篇文章真正要解决的问题很多人觉得“好玩的网页”是别人的创意作品自己只能看不能造。但实际上浏览器提供了一整套开放的接口让任何访问者都能在已加载的页面上临时添加自己的视觉层。这就是“注入”能力。理解这种能力能帮你解决三个实际场景的问题。第一个场景是学习和演示。你不需要从零搭建一个前端项目只要在任意网站上执行一段脚本就能看到 Canvas 动画、DOM 操作和定时器的实时效果。这对前端初学者来说比反复刷新一个静态页面直观得多。第二个场景是临时工具化。把常用脚本保存成书签或者做成一个小扩展就能随时给演示页面加提示、加遮罩、加雪花、加文字弹幕不用改业务代码。第三个场景是理解浏览器扩展的运行机制。Chrome 扩展本质上就是通过标准 API 向页面注入 JavaScript掌握了这一点后续做翻译插件、阅读模式插件、自动化工具都会顺利很多。这篇文章更适合这样的人阅读刚开始学前端想知道浏览器除了打开网页还能干什么或者想做一个属于自己的 Chrome 扩展但不知道从哪个最小项目开始。如果你是要在生产环境上向用户页面注入内容那需要额外考虑权限、安全、合规和回滚方案这篇文章的重点是个人学习和演示场景不会涉及生产级系统设计。2. 三种“注入特效”方案对比与核心概念在动手之前先梳理几个必须理解的基础概念否则后面看代码容易一头雾水。第一个概念是 DOM。浏览器会把网页解析成一棵节点树所有可见的标题、按钮、图片都是树上的节点。JavaScript 可以在任意时刻往这棵树里新增或删除节点。我们注入特效的本质就是在当前页面 DOM 上添加一个全屏的canvas画布节点。第二个概念是 Canvas。它是 HTML5 提供的一块像素绘图区域通过getContext(2d)拿到绘图上下文后可以绘制矩形、圆形、文字也可以每帧重绘形成动画。字符雨、下雪、粒子爆炸这类特效本质都是 Canvas 上的逐帧动画。第三个概念是“注入”。控制台直接执行脚本、书签小工具的javascript:协议、Chrome 扩展的chrome.scriptingAPI这三种方式执行的都是页面上下文里的 JavaScript所以能直接访问当前页面的document和window。这也是为什么它们能在别人的页面上生效。还需要区分两对容易混淆的概念。第一对是“控制台执行”和“书签小工具”。控制台执行依赖开发者工具只在你当前打开的页面上生效书签小工具把代码保存成一个书签点击一次就在当前标签页执行分享起来更方便。第二对是“普通网页脚本”和“Chrome 扩展脚本”。网页脚本受同源策略限制扩展脚本则通过浏览器扩展 API 被注入到指定页面可以在扩展配置的权限范围内操作更多内容但也会受到更严格的权限限制比如 Manifest V3 要求明确声明scripting和activeTab权限。下面用一张表快速对比三种方案方便你选择先从哪个入手。方案使用门槛可控性可分发性适合场景控制台注入最低只需 F12 和复制粘贴中手动执行、刷新页面即恢复差依赖手动操作临时调试、学习验证书签小工具低建立书签后一键执行中同样依赖刷新恢复中把链接发给别人即可使用分享给同事、快速演示Chrome 扩展中需要了解扩展文件和 API高可以加按钮、做状态管理高打包后可通过商店分发做成长期使用的工具3. 环境准备只需要一个浏览器这篇文章的实操部分不需要安装任何额外的开发环境。你需要准备的东西如下。第一个是浏览器。推荐安装最新稳定版 Chrome 或 Edge两者都是 Chromium 内核扩展 API 基本一致。如果你还在用旧版本浏览器建议先升级因为下面的扩展示例基于 Manifest V3旧版本可能不支持。第二个是开发者工具。在任意网页按F12或者右键选择“检查”可以打开 DevTools。控制台控制台会自动显示一个光标直接输入 JavaScript 并执行。需要说明的是Chrome 为了防止恶意脚本诱导用户粘贴第一次粘贴多行代码时可能会提示“Don‘t paste code”你需要根据提示敲入allow pasting才能继续这是正常的安全策略不是代码问题。第三个是一个文本编辑器。VSCode、Sublime Text、记事本都可以因为扩展只需要三个纯文本文件不涉及编译和构建。第四个是可选项一个用于保存书签测试的普通网页建议选择一个内容较多、滚动顺畅的页面比如新闻门户或文档页这样特效的视觉冲击更强。下面的所有代码都是原生 JavaScript不依赖 npm、webpack、Babel 等任何工具链。你完全可以把这些代码当作学习 Canvas 和浏览器脚本注入的入门材料。4. 方案一控制台直接注入字符雨特效4.1 为什么选字符雨作为第一个示例字符雨是最经典的浏览器特效之一看起来很有视觉冲击力但核心逻辑并不复杂。它用 Canvas 绘制一列一列的字母和符号每列从不同高度开始下落每隔一段时间刷新一帧形成流动的视觉效果。这个示例涉及 Canvas 的文字绘制、随机数、帧动画和窗口自适应非常适合用来理解浏览器动态绘图的基本套路。4.2 核心实现原理字符雨特效的步骤可以拆成四步创建一个全屏的canvas元素放到页面最顶层并设置position: fixed让它铺满整个可视窗口。将画布尺寸设置为窗口宽高计算在固定字号下能分成多少列。用一个数组保存每一列当前的下落位置初始化时全部从顶部开始。每 50 毫秒重绘一次先画一层半透明黑色遮罩制造“拖影”然后在每一列的下落位置随机取一个字符画成绿色文字并让列的位置往下移动当一列超过屏幕底部时有一定概率回到顶部。这里有一个很关键的细节特效层必须设置pointer-events: none否则会把页面上的点击、滚动全部拦截用户会以为网页卡死了。还要记得用>(function () { // 创建全屏画布 const canvas document.createElement(canvas); canvas.setAttribute(data-effect-layer, matrix-effect); canvas.style.cssText [ position: fixed, top: 0, left: 0, width: 100%, height: 100%, z-index: 2147483647, pointer-events: none ].join(;); document.documentElement.appendChild(canvas); const ctx canvas.getContext(2d); const fontSize 16; let columns 0; let drops []; function init() { canvas.width window.innerWidth; canvas.height window.innerHeight; columns Math.floor(canvas.width / fontSize); drops new Array(columns).fill(1); } init(); window.addEventListener(resize, init); const chars ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789アイウエオカキクケコサシスセソタチツテトナニヌネノ; function draw() { ctx.fillStyle rgba(0, 0, 0, 0.05); ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle #00ff41; ctx.font fontSize px monospace; for (let i 0; i drops.length; i) { const text chars[Math.floor(Math.random() * chars.length)]; ctx.fillText(text, i * fontSize, drops[i] * fontSize); if (drops[i] * fontSize canvas.height Math.random() 0.975) { drops[i] 0; } drops[i]; } } setInterval(draw, 50); })();4.4 使用方法与验证打开任意一个普通网页按F12进入 DevTools切换到 Console 面板如果是多行代码粘贴先输入allow pasting回车授权再粘贴上面的代码并回车。执行后页面会立刻出现绿色字符雨层而且鼠标仍然可以正常点击页面滚动也没有被挡住。刷新页面后字符雨就会消失因为所有创建的 DOM 节点都只存在于当前页面内存中。这里有一个容易误解的地方这段代码不是在修改原网页的 DOM 结构而是在页面顶部新增了一个覆盖层。原网页的 HTML 还在只是视觉上多了一层动画。你可以把这个效果理解成给网页贴了一张可交互的全屏“动图贴纸”。5. 方案二书签小工具一键给网页下雪5.1 书签小工具原理书签小工具也叫 Bookmarklet本质上是一个以javascript:开头的 URL 书签。普通书签保存的是网址点击后浏览器会导航过去javascript:书签点击后浏览器会把后面的字符串当作 JavaScript 代码执行。因为这个原因书签小工具可以非常方便地分享发给别人别人新建一个书签就能用不需要打开开发者工具。不过javascript:协议有两条限制需要注意。第一条是代码必须是一行不能包含换行否则部分浏览器保存书签时可能截断。第二条是代码执行上下文是当前页面的全局作用域能访问document但也会受到页面自身的脚本策略影响。换句话说如果页面通过 CSP 明确禁止了内联脚本执行那么书签小工具可能不会生效。5.2 落雪特效的完整代码这里我准备了一个落雪特效效果比字符雨更柔和适合演示分享。压缩版本的代码可以复制到书签 URL 中javascript:((){const cdocument.createElement(canvas);c.setAttribute(data-effect-layer,snow-effect);c.style.cssTextposition:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none;document.documentElement.appendChild(c);const xc.getContext(2d);c.widthwindow.innerWidth;c.heightwindow.innerHeight;window.addEventListener(resize,(){c.widthwindow.innerWidth;c.heightwindow.innerHeight});const f[];for(let i0;i120;i){f.push({x:Math.random()*c.width,y:Math.random()*c.height,r:Math.random()*31,s:Math.random()*10.5,w:Math.random()*0.5-0.25})}function d(){x.clearRect(0,0,c.width,c.height);x.beginPath();for(const p of f){p.yp.s;p.xp.w;if(p.yc.height)p.y-5;if(p.xc.width)p.x0;if(p.x0)p.xc.width;x.moveTo(p.x,p.y);x.arc(p.x,p.y,p.r,0,Math.PI*2)}x.fillStyle#ffffff;x.fill();requestAnimationFrame(d)}d()})();如果你想理解这段代码在做什么可以把压缩版还原成可读形式。核心思路是先创建一个全屏画布再生成 120 个随机位置、随机大小、随机速度的雪花粒子然后使用requestAnimationFrame循环更新每个粒子的位置并重新绘制。遇到窗口缩放时重新设置画布尺寸避免动画显示不全。5.3 如何把代码变成书签在 Chrome 中打开书签管理器快捷键是CtrlShiftO点击右上角的“添加新书签”。名称随意比如“网页下雪”URL 字段粘贴上面的压缩代码保存即可。之后打开任何一个普通网页点击书签栏里的“网页下雪”页面就会开始下雪。如果想测试分享效果把这段代码发给同事让对方也建一个同名书签点击后效果完全一样。注意书签小工具依赖 JavaScript 运行时因此一些不允许脚本执行的页面不会生效。另外如果你把代码粘贴到地址栏而不是书签管理器Chrome 出于安全考虑会忽略javascript:代码这是正常行为。6. 方案三开发一个 Chrome 扩展把特效做成产品6.1 为什么扩展更值得做书签小工具虽然方便但只能做“执行一次”的事情。如果我们想要一个按钮来切换多个特效或者将来想加上快捷键、设置页、自动在某些网站生效就需要 Chrome 扩展。扩展的本质是一个被浏览器信任的独立应用它可以申请权限通过浏览器提供的 API 与网页互动。从开发成本看一个“给当前页面注入特效”的最小扩展非常简单。我们不用写后台页面也不用常驻后台逻辑只需要三个文件manifest.json声明扩展信息popup.html做弹窗界面popup.js负责点击按钮后向当前页面注入脚本。这种架构非常适合作为 Chrome 扩展的第一个练手项目。6.2 项目结构与 manifest.json在本地新建一个文件夹名字可以叫browser-toy-extension在里面创建三个文件browser-toy-extension/ ├── manifest.json ├── popup.html └── popup.js先看manifest.json这是 Chrome 扩展的配置文件类似“身份证 权限清单”{ manifest_version: 3, name: 网页特效播放器, description: 给任意网页一键添加字符雨、下雪等趣味特效适合学习演示使用, version: 1.0.0, permissions: [activeTab, scripting], action: { default_popup: popup.html, default_title: 网页特效播放器 } }这里只申请了两个权限。activeTab表示扩展在当前激活标签页上临时获得访问权限scripting表示允许扩展通过脚本注入 API 向页面注入 JavaScript。这种最小权限声明是 Manifest V3 的推荐做法既满足功能需求又不会要求太多不必要的权限。6.3 popup.html弹窗界面扩展工具栏的弹窗界面使用普通 HTML。我做了三个按钮分别触发字符雨、下雪和清除特效!DOCTYPE html html langzh-CN head meta charsetUTF-8 / style body { width: 220px; margin: 0; padding: 16px; font-family: system-ui, sans-serif; } h3 { margin: 0 0 12px; font-size: 15px; } button { display: block; width: 100%; margin-bottom: 8px; padding: 8px 0; font-size: 14px; cursor: pointer; border: 1px solid #d0d0d0; border-radius: 6px; background: #fff; } button:hover { background: #f5f5f5; } /style /head body h3网页特效播放器/h3 button idrain字符雨/button button idsnow下雪/button button idclear清除特效/button script srcpopup.js/script /body /html在弹窗中直接点击按钮即可。需要注意弹窗的 DOM 和当前网页是隔离的popup.js不能直接操作网页 DOM必须通过chrome.tabs和chrome.scripting这样的扩展 API 与浏览器通信。6.4 popup.js核心逻辑popup.js的任务是监听按钮点击获取当前激活标签页调用chrome.scripting.executeScript注入一个包含特效逻辑的函数到当前页面。由于executeScript的func参数会被序列化到目标页面执行所以我要把特效的所有逻辑都写在同一个函数里通过args传入特效名称。function injectEffect(effectName) { const selector [data-effect-layer]; document.querySelectorAll(selector).forEach((node) node.remove()); if (effectName clear) { return; } const canvas document.createElement(canvas); canvas.setAttribute(data-effect-layer, browser-toy-effect); canvas.style.cssText [ position: fixed, top: 0, left: 0, width: 100%, height: 100%, z-index: 2147483647, pointer-events: none ].join(;); document.documentElement.appendChild(canvas); const ctx canvas.getContext(2d); canvas.width window.innerWidth; canvas.height window.innerHeight; window.addEventListener(resize, () { canvas.width window.innerWidth; canvas.height window.innerHeight; }); if (effectName rain) { const fontSize 16; const columns Math.floor(canvas.width / fontSize); const drops new Array(columns).fill(1); const chars ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789アイウエオカキクケコサシスセソタチツテトナニヌネノ; function draw() { ctx.fillStyle rgba(0, 0, 0, 0.05); ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle #00ff41; ctx.font fontSize px monospace; for (let i 0; i columns; i) { const text chars[Math.floor(Math.random() * chars.length)]; ctx.fillText(text, i * fontSize, drops[i] * fontSize); if (drops[i] * fontSize canvas.height Math.random() 0.975) { drops[i] 0; } drops[i]; } requestAnimationFrame(draw); } draw(); } else if (effectName snow) { const flakes []; for (let i 0; i 120; i) { flakes.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, r: Math.random() * 3 1, speed: Math.random() * 1 0.5, wind: Math.random() * 0.5 - 0.25 }); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); for (const flake of flakes) { flake.y flake.speed; flake.x flake.wind; if (flake.y canvas.height) flake.y -5; if (flake.x canvas.width) flake.x 0; if (flake.x 0) flake.x canvas.width; ctx.moveTo(flake.x, flake.y); ctx.arc(flake.x, flake.y, flake.r, 0, Math.PI * 2); } ctx.fillStyle #ffffff; ctx.fill(); requestAnimationFrame(draw); } draw(); } } chrome.tabs.query({ active: true, currentWindow: true }, (tabs) { const tabId tabs[0].id; document.getElementById(rain).addEventListener(click, () { chrome.scripting.executeScript({ target: { tabId }, func: injectEffect, args: [rain] }); }); document.getElementById(snow).addEventListener(click, () { chrome.scripting.executeScript({ target: { tabId }, func: injectEffect, args: [snow] }); }); document.getElementById(clear).addEventListener(click, () { chrome.scripting.executeScript({ target: { tabId }, func: injectEffect, args: [clear] }); }); });这里最核心的是injectEffect函数。它在目标页面执行时会先查找所有带有>