什么是内容脚本
我们在前面的章节提到过,内容脚本是在我们打开网页时自动注入的脚本,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
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注入、通信建立 |
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