Chrome 浏览器插件获取网页 iframe 中的 window 对象

711 阅读5分钟

Chrome 浏览器插件获取网页 iframe 中的 window 对象

前言

之前写了篇《Chrome 浏览器插件获取网页 window 对象》文章,是获取当前页面的 window 对象,但是有些页面是嵌入 iframe 的,特别是系统项目主域一样,那就也需要获取 iframe 内部的 window 对象数据,而且还不能重复加载 content html 页面。这个时候就需要对 content_script 的 js 文件进行特殊处理了。

一、需求整理

1. 没有 iframe 内嵌

可以参考 《Chrome 浏览器插件获取网页 window 对象》文章

2. 嵌套一个 iframe

2.1. 页面如下

2.2. Parent 页面对象数据
<script>
window.coverageData = {
  name: 'parent',
  data: {
    a: 'parent'
  }
}
</script>
2.3. Child 页面对象数据
<script>
window.coverageData = {
  name: 'child1',
  data: {
    a: 'child'
  }
}
</script>

3. 嵌套多个 Iframe

3.1. 页面如下

3.2. Child2 页面对象数据
<script>
  window.coverageData = {
    name: 'child2',
    data: {
      a: 'child2'
    }
  }
</script>

二、需求实现

我们就用 《Chrome 浏览器插件获取网页 window 对象》文章中的第一种方案,使用两个 JS,通过 postMessage 进行消息通讯获取 window 对象数据。

1. 新建项目&文件

.
├── assets
│   └── icon.png
├── index.js
├── lucky.js
├── manifest.json
└── service-worker.js
  • index.js:通过 content_scripts 引入
  • lucky.js:通过 index.js 文件,插入当前页面的 head 标签中

2. manifest.json 文件

{
  "manifest_version": 3,
  "name": "Get Window Data",
  "version": "0.0.1",
  "description": "get window data",
  "action": {
    "default_title": "Get Window Data",
    "default_icon": "assets/icon.png"
  },
  "background": {
    "service_worker": "service-worker.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "js": [
        "index.js"
      ],
      "matches": [
        "http://127.0.0.1:*/*",
        "http://localhost:*/*"
      ],
      "all_frames": true,
      "run_at": "document_end"
    }
  ],
  "host_permissions": [
    "http://127.0.0.1:*/*",
    "http://localhost:*/*"
  ],
  "permissions": [
		"tabs",
    "scripting",
    "activeTab"
  ],
  "web_accessible_resources": [
    {
      "resources": ["lucky.js"],
      "matches": ["http://127.0.0.1:*/*", "http://localhost:*/*"],
      "use_dynamic_url": false
    }
  ]
}
  • content_scripts:
    • 一定要设置 all_frames 为 true,这样才可以透传 iframe
    • matches 匹配的是本地域名,根据项目需要自行修改
  • host_permissions:匹配的是本地域名,根据项目需要自行修改
  • permissions:需要用到的权限,根据项目需要自行修改
  • web_accessible_resources:所有需要在插件代码里面用到的文件,都需要加在 resources 中,不加这个 lucky.js 不能嵌入页面。

3. lucky.js 文件

/**
 * 发送 coverage data 数据
 * @param {boolean} init 是否是初始化数据
 */
const sendCoverageData = (init = false) => {
  window.postMessage({
    type: 'coverage-data',
    data: window.coverageData,
    location: {
      href: window.location.href,
      hostname: window.location.hostname,
      host: window.location.host,
      pathname: window.location.pathname,
      protocol: window.location.protocol,
      port: window.location.port,
      search: window.location.search,
      hash: window.location.hash,
      origin: window.location.origin,
      domain: document.domain,
      title: document.title
    },
    init
  })
}
console.log('luckyjs, window.self === window.top', window.self === window.top)
// JS coverage data
sendCoverageData(true)

/**
 * 监听 message 并进行处理
 */
window.addEventListener('message', (event) => {
  if (event.data?.type === 'get-coverage') {
    sendCoverageData()
  }
})
  • sendCoverageData:通过 postMessage 发送消息
    • init 是值是否第一次发送消息
    • 包含的数据为 window 下的 coverageData、location 对象,type 类型,init 字段
    • 不可以直接把 location 传过来,会报错的
  • addEventListener 进行消息监听,再次发送数据
3.1. 直传 location 报错

Uncaught DataCloneError: Failed to execute 'postMessage' on 'Window': Location object could not be cloned.

3.1.1. 页面报错消息

3.1.2. 插件报错消息

3.2. 为什么要传 location 数据?

因为可能存在多个 iframe,可以通过 location 进行区分

4. index.js 文件

/**
 * 添加 script 标签
 * @param {string} url 路径
 * @param {string} id script ID
 */
const addScript = (url, id) => {
  const script = document.createElement('script')
  id && (script.id = id)
  script.src = chrome.runtime.getURL(url)
  document.head.appendChild(script)
}

/**
 * 添加 JS 和事件监听
 */
const addJSAndEventListener = async () => {
  // 监听从页面上下文发回的消息
  window.addEventListener('message', (event) => {
    console.log('event.data', event.data)
    if (event.data?.type === 'coverage-data') {

    }
  })
  addScript('lucky.js', 'coverage-script')
}

addJSAndEventListener()

  • addScript:添加 JS 文件,把 lucky.js 添加到 head 标签中
  • addEventListener:消息监听

5. 安装插件

6. window 数据

6.1. 当前页面 console 日志

6.2. 当前页面插件 console 日志
6.2.1. 点击下面选项可以插件插件 index.js 的 console 日志

6.2.2. console 日志

6.3. iframe 页面 console 日志

6.4. iframe 页面插件 console 日志
6.4.1. 选择当前 iframe 下的插件

6.4.2. console 日志

7. 把数据传给 service-worker

7.1. service-worker.js 文件
/**
 * service worker 监听 接收消息
 */
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log('message', message)
  switch (message.action) {
    // 从 content 发送消息到 SW,发送数据
    case 'fromContent2SW-sendCoverage': {
			// todo
      break
    }
    default: {
      break
    }
  }
  sendResponse()
  return false
})

7.2. service-worker 背景日志

8. 嵌套多个 iframe

8.1. 查看页面

8.2. service-worker 日志

三、收集 coverage 数据并展示在 popup 中

1. 增加 popup.html 和 popup.js 文件

.
├── assets
│   └── icon.png
├── index.js
├── lucky.js
├── manifest.json
├── popup.html
├── popup.js
└── service-worker.js
1.1. popup.html 文件内容
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Popup</title>
  <style>
    *{
      padding: 0;
      margin: 0;
    }
    body{
      width: 400px;
      max-height: 300px;
      min-height: 200px;
    }
    header{
      text-align: center;
      margin-bottom: 10px;
    }
    main{
      padding: 0 20px;
      #get_window_but{
        margin-bottom: 10px;
      }
      li{
        margin-bottom: 10px;
        word-break: break-all;
        list-style: none;
      }
    }
  </style>
</head>
<body>
  <header>
    <h1>Popup</h1>
  </header>
  <main>
    <button id="get_window_but">获取 window 数据</button>
    <ul id="window_coverage"></ul>
  </main>
</body>
<script src="popup.js"></script>
</html>
1.2. popup.js 文件内容
/**
 * 事件监听
 */
chrome.runtime.onMessage.addListener((e, _, sendResponse) => {
  switch (e.action) {
    case 'fromSW2Popup-sendCoverage': {
      const coverageUl = document.getElementById('window_coverage')
      const li = document.createElement('li')
      li.innerHTML = `
        <div>url: ${e.message.location.href}</div>
        <div>coverage: ${e.message.data ? JSON.stringify(e.message.data) : ''}</div>
      `
      coverageUl.appendChild(li)
      break
    }
    default:
      break
  }
  sendResponse()
  return false
})

const get_window_but = document.getElementById('get_window_but')

/**
 * but 按钮事件
 */
get_window_but.onclick = async () => {
  const tabId = await getCurrentTabId()
  // 从 popup 发送消息到  SW
  tabId && chrome.runtime.sendMessage({
    action: 'fromPopup2SW-getWindowData',
    message: {
      tabId
    }
  })
}

/**
 * 获取当前活动页面的 id
 * @returns {string} tabId
 */
const getCurrentTabId = async () => {
  const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
  return tabs && tabs.length > 0 ? tabs[0].id : ''
}
  • addListener:事件监听,监听从 SW 发送到 popup 的消息,把消息生成 li 在 append 到 ul 中
  • onclick:按钮点击事件,发送消息到 SW
  • getCurrentTabId:获取当前活动的页面的 tabId

2. manifest.json 文件

"action": {
  "default_title": "Get Window Data",
  "default_icon": "assets/icon.png",
  "default_popup": "popup.html"
},
  • 增加 default_popup 字段

3. index.js 文件

/**
 * 添加 script 标签
 * @param {string} url 路径
 * @param {string} id script ID
 */
const addScript = (url, id) => {
  const script = document.createElement('script')
  id && (script.id = id)
  script.src = chrome.runtime.getURL(url)
  document.head.appendChild(script)
}

/**
 * 添加 JS 和事件监听
 */
const addJSAndEventListener = async () => {
  // 监听从页面上下文发回的消息
  window.addEventListener('message', (event) => {
    if (event.data?.type === 'coverage-data') {
      // 从 content 脚本获取到 coverage 数据后,发送给 SW
      chrome.runtime.sendMessage({
        action: 'fromContent2SW-sendCoverage',
        message: {
          ...event.data
        }
      })
    }
  })
  addScript('lucky.js', 'coverage-script')
}

addJSAndEventListener()

// 消息监听
chrome.runtime.onMessage.addListener((e, _, sendResponse) => {
  switch (e.action) {
    // 从 SW 发送消息到 content 脚本,获取 coverage 数据
    case 'fromSW2Content-getWindowData': {
      window.postMessage({
        type: 'get-coverage'
      })
      break
    }
    default:
      break
  }
  sendResponse()
  return false
})
  • addListener:消息监听,并进行 postmessage 消息传递给 lucky.js

4. lucky.js 文件

// sendCoverageData(true)
  • 因为我们是按钮点击在获取,所以可以把初始化就获取数据注释掉

5. service-worker.js

/**
 * service worker 监听 接收消息
 */
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  switch (message.action) {
    // 从 content 发送消息到 SW,发送数据
    case 'fromContent2SW-sendCoverage': {
      // 从 SW 发送消息到 popup,发送数据
      chrome.runtime.sendMessage({
        action: 'fromSW2Popup-sendCoverage',
        message: {
          ...message.message
        }
      })
      break
    }
    // 从 popup 发送消息到 SW,获取数据
    case 'fromPopup2SW-getWindowData': {
      // 从 SW 发送消息到 content,获取数据
      chrome.tabs.sendMessage(
        message.message.tabId,
        {
          action: 'fromSW2Content-getWindowData',
          message: {}
        },
        {},
        () => {}
      )
      break;
    }
    default: {
      break
    }
  }
  sendResponse()
  return false
})

  • 接收 popup 的消息,并发送到 content 文件
    • 需要 tabId 字段
  • 从 content 接收消息,并发送到 popup 页面
  • service-worker 对两者发送消息的方式不一样
    • content 因为是在页面中,所以需要 tabId
    • popup 则不需要 tabId

6. 效果展示

6.1. 小问题

多次点击会重复渲染

我这里就不做处理,需要的话自己判断处理即可

四、总结

  • 获取逻辑和 《Chrome 浏览器插件获取网页 window 对象》中的方案一一样,有兴趣的可以试下其他方案
  • 我这是本地 iframe URL,如果你的 URL 是网页链接,可以在 index.js 中嵌入 lucky.js 做延迟处理
  • 如果你的 iframe URL 是动态的,比如,点击 tab,切换 URL,则可以在 index.js 中进行 MutationObserver 监听
  • 如果你需要部分内容嵌入 iframe 中,则可以使用 window.top === window.self 判断是否是顶层
  • 源码:【Gitee
  • 🎉🎉🎉🎉🎉🎉

拉票

【掘金2024年度人气创作者打榜中,快来帮我打榜吧~】(activity.juejin.cn/rank/2024/w…)