普通视图

发现新文章,点击刷新页面。
昨天以前首页

Chrome插件开发:从零开始了解Chrome Extension

作者 那小孩儿
2025年4月13日 16:30

Chrome Extension官方开发文档:developer.chrome.com/docs/extens…

什么是Chrome Extension

首先给出一些官方定义和解释:

官方定义:Chrome 扩展程序可通过自定义界面、监控浏览器事件和修改网页来提升浏览体验。

如何构建:您可以使用创建 Web 应用时所用的 Web 技术构建扩展程序:HTMLCSS 和 JavaScript

可以做些什么:除了 Web API 之外,扩展程序还可以使用 Chrome 扩展程序 API 来执行不同的任务

综上,我觉得Chrome Extension其实就是一个由 HTML、CSS、JavaScript 和配置文件组成的包,通过Chrome 的扩展 API 与浏览器深度交互,完成一些正常web页面不能完成的功能,比如操作浏览器界面、后台任务处理,以及一些硬件交互等,所以能做什么,不能做什么,都是是扩展API决定的

基本组成结构

一个典型 Chrome 扩展包含以下核心组件:

  • manifest.json:配置文件,定义扩展名称、版本、权限、资源路径等。
  • 内容脚本(Content Scripts) :注入到网页中的脚本,可直接操作 DOM。
  • 后台脚本(Background Scripts) :独立于页面的 JavaScript 环境,用于处理全局逻辑。
  • 用户界面组件:如弹出页(Popup)、选项页(Options Page)、侧边栏等。
  • 静态资源:图标、图片、样式表等。

以上只有manifest.json文件是必须的,当然插件的图标也是要提供的,算半必须吧,后面这些组成都会详细介绍,这里了解一下就好

Manifest V3 与 V2 的主要区别

其实我觉得能点进来看这篇文章的人,没有必要去详细了解V2,因为V3 是当前强制要求的最新版本(自 2023 年起新扩展必须使用 V3),所以V2相当于已经被淘汰了,但是毕竟没有过去很久,可能有一些项目需要做一些迁移,还是给大家介绍一下,后面也会专门出一篇迁移的文章

manifest.json 文件其实是Extension的“身份证”和“说明书”,决定了Extension如何与浏览器交互。不同版本的 Manifest 对应不同的 API 和权限规则。那又为什么放弃V2,需要V3呢?

首先得说说V2存在的问题,在V2版本存续期间,Chrome Extension生态长期面临两大问题:

  • 安全风险:恶意Extension滥用权限,窃取用户数据
  • 性能损耗:常驻后台脚本导致内存占用过高,性能损耗过于严重

Manifest V3 的变革本质是浏览器厂商对扩展生态的一次“技术纠偏”,其核心目标是通过限制自由度换取更高的安全性和性能。

V3版本又是怎么解决问题的呢?

  • 资源效率:优化后台任务的生命周期管理
  • 最小权限模型:限制Extension的过度权限申请
  • 代码可信性:禁止远程代码动态执行

接下来,详细说说V2→V3究竟有哪些核心变革

1. 后台脚本的颠覆:Service Worker 取代 Background Pages

  • V2 的 Background Pages

    本质是一个常驻内存的隐藏页面,通过 background.html 和 background.js 运行,即使无页面活动也可持续执行代码,只要扩展安装后,就会持续运行,直至浏览器关闭或扩展被禁用

    典型问题

    // V2 示例:常驻后台监听消息
    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
      // 长期占用内存的逻辑
    });
    
  • V3 的 Service Worker

    改用 Service Worker 作为后台脚本载体,其生命周期由浏览器严格管理:

    • 无持久化运行:仅在需要时激活(如收到事件、定时任务触发)。
    • 无 DOM 访问权限:无法操作页面元素。
    • 内存限制:执行完成后会被终止以节省资源。

开发者影响:需重构后台逻辑,避免依赖长时运行的任务,改用事件驱动模式。

2. 权限模型收紧

网络请求处理

V2:通过 chrome.webRequest API 动态拦截和修改请求

// V2 动态广告拦截示例
chrome.webRequest.onBeforeRequest.addListener(
  (details) => ({ cancel: details.url.includes("adserver.com") }),
  { urls: ["<all_urls>"] },
  ["blocking"]
);

V3:改用 Declarative Net Request 静态规则

// manifest.json 中声明规则文件
"declarative_net_request": {
  "rule_resources": [{
    "id": "block_ads",
    "enabled": true,
    "path": "rules.json"
  }]
}
// rules.json 定义静态规则
[{
  "id": 1,
  "priority": 1,
  "action": { "type": "block" },
  "condition": {
    "urlFilter": "||adserver.com^",
    "resourceTypes": ["script"]
  }
}]
权限声明收紧

V2:允许运行时动态申请权限

chrome.permissions.request({ permissions: ["tabs"] });

V3:所有权限必须在 manifest.json 中预声明,无法动态添加

{
  "permissions": ["activeTab", "scripting"],
  "optional_permissions": ["downloads"] // 仅部分权限可动态请求
}

开发者影响:需提前规划权限需求,放弃动态注入远程脚本的“偷懒”方案。

3. 安全策略:代码可信性与执行限制

禁止远程代码

V2:允许通过 eval() 或远程加载脚本

// V2 中动态执行远程代码(高风险!)
fetch("https://example.com/code.js")
  .then(response => response.text())
  .then(code => eval(code));

V3:所有逻辑必须打包至扩展本地,禁止以下操作

  • eval()new Function()
  • 动态插入 <script> 标签加载远程 JS。
  • 使用 chrome.scripting.executeScript 执行远程代码。
内容安全策略(CSP)强化

V2:允许宽松策略

"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"

V3:强制禁用 unsafe-eval 和内联脚本

"content_security_policy": {
  "extension_pages": "script-src 'self'; object-src 'self'"
}

Chrome Extension开发和一般网页开发

Extension的开发语言就是前端中大家都会接触的HTML、CSS和JavaScript,那这两者究竟有什么区别呢

1. 运行环境与执行上下文差异

Chrome Extension 普通网页
执行上下文 多环境并行: - 内容脚本(Content Scripts) - Service Worker(后台脚本) - 弹出页(Popup)、选项页等 单一页面上下文: 所有代码在页面 DOM 环境中运行
DOM 访问权限 内容脚本可直接操作页面 DOM,但和页面原始脚本隔离(独立 JS 环境) 直接操作当前页面的 DOM
浏览器 API 访问 可调用 Chrome 扩展专属 API(如 chrome.tabschrome.storage 仅限 Web 标准 API(如 fetchlocalStorage
跨域请求限制 可通过声明权限(如 "<all_urls>")绕过同源策略 严格受同源策略限制,需依赖 CORS 或代理服务
生命周期管理 Service Worker 按需激活,浏览器控制其启停 页面关闭即终止所有逻辑

2. 架构设计差异

Chrome Extension 普通网页
代码组织 多文件协同: - manifest.json 配置文件 - 内容脚本、Service Worker、UI 组件分离 通常集中在单页应用(SPA)或传统多页面架构
通信机制 依赖 chrome.runtime API 实现不同组件间通信: - sendMessageconnect 基于 window.postMessage 或自定义事件总线
状态管理 需跨组件同步状态(如使用 chrome.storage 持久化) 可通过 Redux、Context API 等前端框架管理

3. 异步编程与事件驱动差异

Chrome API 几乎全部以异步形式提供(回调或 Promise),需处理多组件协作的异步逻辑;一般网页开发异步操作集中在数据请求(如 fetch)和用户交互事件,架构相对简单

4. 权限模型差异

Chrome Extension 普通网页
权限范围 可申请浏览器底层权限: - 访问所有网站内容 - 管理下载、书签、历史记录等 仅限用户交互授权: - 地理位置 - 摄像头/麦克风 - 通知权限等
权限申请方式 必须在 manifest.json 中静态声明(Manifest V3 限制动态申请) 可通过 Permissions API 动态请求(如 navigator.permissions.request()
安全审查强度 上架需通过 Chrome 商店严格审核(代码扫描 + 人工复核) 部署在自有服务器,无强制审核(依赖用户信任)

5. 安全策略差异

Chrome Extension 普通网页
内容安全策略(CSP) 强制严格策略: - 禁止内联脚本(如 onclick="...") - 禁止 eval() 可自由定义 CSP(如允许内联脚本)
代码来源限制 所有逻辑必须打包在扩展内,禁止远程代码执行(Manifest V3) 可自由加载外部 CDN 资源(如 <script src="https://cdn.example.com/lib.js">
沙箱隔离 内容脚本与页面脚本隔离(无法直接共享变量) 所有脚本共享同一全局环境

6. 调试与部署差异

Chrome Extension 普通网页
调试环境 需分别调试多个组件: - Service Worker(通过 chrome://serviceworker-internals) - 内容脚本(在网页开发者工具中) - 弹出页(独立窗口) 单一页面调试(浏览器开发者工具)
热重载支持 无原生热重载,需手动刷新扩展或使用第三方工具(如 webpack-chrome-extension-reloader 主流框架(如 React、Vue)支持热重载
错误追踪 错误分散在多个上下文,需结合 chrome://extensions 的日志和浏览器控制台 所有错误集中在页面控制台
打包方式 必须压缩为 ZIP 文件(不含根目录),依赖 manifest.json 定义结构 无强制打包格式,通常直接部署 HTML/CSS/JS 文件或使用构建工具(如 Webpack)打包
发布流程 必须提交至 Chrome 网上应用店,通过审核后发布(1-7 天) 直接部署到服务器或 CDN,实时生效
版本更新 用户自动接收更新,但需重新审核新版本 用户访问时立即获取最新版本(可能受缓存影响)

以上差不多就是宏观上的一些了解了,打个预告,下一篇文章会介绍环境搭建,完成经典的“Hello World”,同时介绍基本的本地加载与开发调试技巧

魔改chromium源码——新增自定义变量到windows属性

2025年4月11日 18:34

在进行以下操作之前,请确保已完成之前文章中提到的 源码拉取及编译 部分。

如果已顺利完成相关配置,即可继续执行后续操作。


目标

在 Chromium 中添加一个全局变量 myCode,值为 "你好!我是来自C++的字符串",并通过 JavaScript 的 console.log(window.myCodeApi.myCode()) 直接访问。

实现步骤

步骤 1:定义全局变量

在 src/base 模块中定义全局变量 myCode,以便在整个 Chromium 项目中复用

在src/base目录下,创建文件:my_globals.h

文件内容:

#ifndef BASE_MY_GLOBALS_H_
#define BASE_MY_GLOBALS_H_

#include <string>

namespace base {
extern const char* const kMyCode;
}

#endif

base目录下创建文件:my_globals.cc

文件内容:

#include "base/my_globals.h"

namespace base {
const char* const kMyCode = "你好!我是来自C++的字符串";
}

在这里插入图片描述

修改 base/BUILD.gn

文件路径: src/base/BUILD.gn 操作: 在 component("base") 的 sources 列表中添加新文件的文件名

在这里插入图片描述

步骤 2:创建 JavaScript 绑定

在 src/content/renderer 目录中,创建文件:my_code_binding.h

文件内容:

#ifndef CONTENT_RENDERER_MY_CODE_BINDING_H_
#define CONTENT_RENDERER_MY_CODE_BINDING_H_

#include "v8/include/v8.h"

namespace content {

class MyCodeBinding {
 public:
  // 安装绑定到指定的 V8 上下文中
  static void Install(v8::Local<v8::Context> context);

 private:
  // 获取 my_code 值的 JavaScript 函数
  static void GetMyCode(const v8::FunctionCallbackInfo<v8::Value>& args);
};
}

#endif

在 src/content/renderer 目录中,创建文件:my_code_binding.cc

#include "content/renderer/my_code_binding.h"

#include "base/my_globals.h"
#include "third_party/blink/public/web/blink.h"
#include "v8/include/v8.h"

namespace content {

void MyCodeBinding::Install(v8::Local<v8::Context> context) {
  // 从 context 获取 Isolate
  v8::Isolate* isolate = context->GetIsolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Object> global = context->Global();
  v8::Local<v8::Object> my_code_api = v8::Object::New(isolate);
  my_code_api->Set(
      context,
      v8::String::NewFromUtf8(isolate, "myCode").ToLocalChecked(),
      v8::Function::New(context, &MyCodeBinding::GetMyCode).ToLocalChecked())
      .Check();

  global->Set(
      context,
      v8::String::NewFromUtf8(isolate, "myCodeApi").ToLocalChecked(),
      my_code_api)
      .Check();
}

void MyCodeBinding::GetMyCode(const v8::FunctionCallbackInfo<v8::Value>& args) {
  v8::Isolate* isolate = args.GetIsolate();
  v8::HandleScope handle_scope(isolate);
  args.GetReturnValue().Set(
      v8::String::NewFromUtf8(isolate, base::kMyCode)
          .ToLocalChecked());
}

}

修改 content/renderer/BUILD.gn

文件路径: src/content/renderer/BUILD.gn 操作: 在 target(link_target_type, "renderer") 的 sources 列表中添加新文件

在这里插入图片描述

步骤 3:绑定到 RenderFrameImpl

在 RenderFrameImpl 中调用绑定逻辑,将 myCode 属性安装到脚本上下文中

修改文件路径:src/content/renderer/render_frame_impl.cc

在文件顶部添加 my_code_binding.h 头文件,可以按文件头字母顺序添加

#include "content/renderer/my_code_binding.h"

在这里插入图片描述

在RenderFrameImpl::DidCreateScriptContext中添加如下代码

void RenderFrameImpl::DidCreateScriptContext(v8::Local<v8::Context> context,
                                             int world_id) {
   // 新增代码
  if (world_id == 0) {
    MyCodeBinding::Install(context);
  }
  // 新增代码

  TRACE_EVENT_WITH_FLOW0("navigation",
                         "RenderFrameImpl::DidCreateScriptContext",
                         TRACE_ID_LOCAL(this),
                         TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT);
  v8::MicrotasksScope microtasks(GetAgentGroupScheduler().Isolate(),
                                 context->GetMicrotaskQueue(),
                                 v8::MicrotasksScope::kDoNotRunMicrotasks);
  if (((enabled_bindings_.Has(BindingsPolicyValue::kMojoWebUi)) ||
       enable_mojo_js_bindings_) &&
      IsMainFrame() && world_id == ISOLATED_WORLD_ID_GLOBAL) {
    // We only allow these bindings to be installed when creating the main
    // world context of the main frame.
    blink::WebV8Features::EnableMojoJS(context, true);

    if (mojo_js_features_) {
      if (mojo_js_features_->file_system_access)
        blink::WebV8Features::EnableMojoJSFileSystemAccessHelper(context, true);
    }
  }

  if (world_id == ISOLATED_WORLD_ID_GLOBAL &&
      mojo_js_interface_broker_.is_valid()) {
    // MojoJS interface broker can be enabled on subframes, and will limit the
    // interfaces JavaScript can request to those provided in the broker.
    blink::WebV8Features::EnableMojoJSAndUseBroker(
        context, std::move(mojo_js_interface_broker_));
  }

  for (auto& observer : observers_)
    observer.DidCreateScriptContext(context, world_id);
}

在这里插入图片描述

最后,在src目录下,执行 gn gen out/Default ,重新生成构建文件

构建成功之后运行一下命令进行编译

autoninja -C out/Default chrome

在这里插入图片描述

如果你想实现console.log(window.myCode);这样的效果

将 my_code_binding.h 修改为

#ifndef CONTENT_RENDERER_MY_CODE_BINDING_H_
#define CONTENT_RENDERER_MY_CODE_BINDING_H_

#include "v8/include/v8.h"

namespace content {

class MyCodeBinding {
 public:
  static void Install(v8::Local<v8::Context> context);
};

}
#endif

将前面的 my_code_binding.cc 修改为

#include "content/renderer/my_code_binding.h"

#include "base/my_globals.h"
#include "v8/include/v8.h"

namespace content {

void MyCodeBinding::Install(v8::Local<v8::Context> context) {
  v8::Isolate* isolate = context->GetIsolate();
  v8::HandleScope handle_scope(isolate);

  v8::Local<v8::Object> global = context->Global();

  global->Set(
      context,
      v8::String::NewFromUtf8(isolate, "myCode").ToLocalChecked(),
      v8::String::NewFromUtf8(isolate, base::kMyCode).ToLocalChecked())
      .Check();
}

}

在这里插入图片描述

❌
❌