区块链钱包开发(八)—— 创建内容脚本contentscript.js

64 阅读4分钟

什么是内容脚本

我们在前面的章节提到过,内容脚本是在我们打开网页时自动注入的脚本,metamask的内容脚本在manifest文件中定义如下:

  // 内容脚本配置(注入到网页的JS)
  "content_scripts": [
    {
      "matches": ["file://*/*", "http://*/*", "https://*/*"], // 注入目标URL模式
      "js": [                                                 // 按顺序注入的脚本,脚本的详细作用后续会讲
        "scripts/disable-console.js",    // 禁用危险API
        "scripts/lockdown-install.js",   // 安全沙箱初始化
        "scripts/lockdown-run.js",       // 执行环境隔离
        "scripts/lockdown-more.js",      // 增强安全策略
        "scripts/contentscript.js"       // 主业务逻辑
      ],
      "run_at": "document_start",  // 注入时机:DOM加载前
      "all_frames": true           // 注入所有iframe
    },
    {  // 特殊处理Trezor硬件钱包,不需要了解
      "matches": ["*://connect.trezor.io/*/popup.html*"],
      "js": ["vendor/trezor/content-script.js"] 
    }
  ],

内容脚本详解

disable-console.js

源码:github.com/MetaMask/me…

console.log = noop;
console.info = noop;
console.warn = noop;

function noop() {
  return undefined;
}

将内容脚本中的console.log、console.info和console.warn方法替换为一个空函数(noop),只返回undefined。 其实就是禁用控制台。

为什么要禁用:

  • 后面加载的SES(Secure ECMAScript)和lockdown在初始化时可能会输出日志,这些日志可能包含敏感信息或内部实现细节
  • 容易和网页本身的日志混淆

lockdown-***

lockdown-install.js/lockdown-run.js/lockdown-more.js 这三个文件的作用我们在第三章节-钱包的初始化部分有过讲解,这里不做讲解。值得注意的是我们之前讲的是这些文件在service worker后台会被加载,这里在内容脚本又被加载一遍,因为content scripts和service worker的运行环境是隔离的,我们需要保证它们各自的执行环境都是安全的。

contentscript.js

源码:github.com/MetaMask/me… 此为核心文件,实现为:

const start = () => {
  // 检查当前网站是否为钓鱼网站
  if (isDetectedPhishingSite) {
    // 如果是钓鱼网站,初始化钓鱼警告流,不注入provider
    initPhishingStreams();
    return;
  }

  // 检查当前网站是否为需要处理cookie营销的网站
  if (isDetectedCookieMarketingSite) {
    // 初始化cookie处理流
    initializeCookieHandlerSteam();
  }

  // 检查是否应该向当前页面注入provider
  if (shouldInjectProvider()) {
    // 初始化provider通信流
    initStreams();

    // 处理页面预渲染情况
    if (document.prerendering && getIsBrowserPrerenderBroken()) {
      // 如果页面正在预渲染且浏览器预渲染存在问题
      // 监听预渲染状态变化事件
      document.addEventListener('prerenderingchange', () => {
        // 当预渲染页面变为活动状态时,销毁并重建流
        onDisconnectDestroyStreams(
          new Error('Prerendered page has become active.'),
        );
      });
    }

    // 处理页面从缓存恢复的情况(Back-Forward Cache)
    window.addEventListener('pageshow', (event) => {
      if (event.persisted) {
        // 如果页面是从BFCache恢复的,重新设置扩展流
        console.warn('BFCached page has become active. Restoring the streams.');
        setupExtensionStreams();
      }
    });

    // 处理页面可能被放入缓存的情况
    window.addEventListener('pagehide', (event) => {
      if (event.persisted) {
        // 如果页面将被放入BFCache,销毁流以防止内存泄漏
        console.warn('Page may become BFCached. Destroying the streams.');
        destroyStreams();
      }
    });
  }
};

// 执行启动函数
start();

这个脚本主要做了三件事:

  • 通信流的建立
  • 检测是否需要注入Provider
  • 处理页面预渲染和缓存

通信流的建立

这部分我们在第四章节有详细的讲解,这里不再赘述。

检测是否需要注入Provider

检测项包括:是否是.html页面,是否在黑名单中等。

处理页面预渲染和缓存

预渲染 (Prerendering) 处理

if (document.prerendering && getIsBrowserPrerenderBroken()) {
  document.addEventListener('prerenderingchange', () => {
    onDisconnectDestroyStreams(
      new Error('Prerendered page has become active.'),
    );
  });
}

什么是预渲染?

  • 浏览器在用户点击链接前预先加载和渲染页面
  • 页面在后台运行,但用户看不到
  • 当用户实际访问时,页面立即显示

为什么需要处理?

预渲染页面中的流连接可能无法正常工作,从而导致与扩展后台的通信可能失败。

Back-Forward Cache (BFCache) 处理

什么是 BFCache?

浏览器缓存机制,保存页面状态,用户使用前进/后退按钮时,页面从缓存恢复。

为什么需要处理?

从 BFCache 恢复的页面,之前的流连接可能已经断开,扩展后台可能已经清理了相关连接。

内存泄漏预防

window.addEventListener('pagehide', (event) => {
  if (event.persisted) {
    console.warn('Page may become BFCached. Destroying the streams.');
    destroyStreams();
  }
});

页面进入 BFCache 时,流连接可能继续存在,这些连接会占用内存,即使页面不再活跃可能导致内存泄漏和性能问题。

inpage.js

除了上面这些脚本,其实还有一个内容脚本被注入到了网页中,我们在钱包初始化的章节提到过,它是在初始化阶段动态注册的,就是inpage.js。它和上面的contentscript脚本的区别是运行环境不一样,inpage.js运行在和网页相同的上下文环境,而contentscript.js运行在单独的隔离环境。

脚本类型执行环境访问权限主要职责
contentscript.js隔离环境(ISOLATED)有限DOM访问安全检查、流管理
inpage.js主世界(MAIN)完整网页访问Provider注入、通信建立

源码:github.com/MetaMask/me…

if (shouldInjectProvider()) {
  // 建立和contentscript的通信流
  const metamaskStream = new WindowPostMessageStream({
    name: INPAGE,
    target: CONTENT_SCRIPT,
  });

  const mux = new ObjectMultiplex();
  pipeline(metamaskStream, mux, metamaskStream, (error) => {
    let warningMsg = `Lost connection to "${METAMASK_EIP_1193_PROVIDER}".`;
    if (error?.stack) {
      warningMsg += `\n${error.stack}`;
    }
    console.warn(warningMsg);
  });
  // 初始化Provider
  initializeProvider({
    connectionStream: mux.createStream(METAMASK_EIP_1193_PROVIDER),
    logger: log,
    shouldShimWeb3: true,
    providerInfo: {
      uuid: uuid(),
      name: process.env.METAMASK_BUILD_NAME,
      icon: process.env.METAMASK_BUILD_ICON,
      rdns: process.env.METAMASK_BUILD_APP_ID,
    },
  });
}

主要做三件事:

  • 检测是否需要注入Provider(与contentscript中的逻辑一致)
  • 建立通信流(详情参考第四章)
  • 初始化Provider(详情参考第七章)

学习交流请添加vx: gh313061

下期预告:构建后台主服务background.js