详细介绍:Chrome插件学习笔记(二)

Chrome插件学习笔记(二)

参考文章:

  • https://developer.chrome.com/docs/extensions/reference/api/sidePanel?hl=zh-cn
  • https://developer.chrome.com/docs/extensions/reference/api/webRequest?hl=zh-cn
  • https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest?hl=zh-cn

1、什么是sidePanel

上一篇文章Chrome插件学习笔记(一)认识了Chrome插件中的Popup,虽然Popup很轻便并且资源占用很低,但是有时候不能满足诉求,比如不能失去焦点后就会消失,无法长期保持打开状态

如下图浏览器页面右侧即为sidePanel,会占据页面一部分空间,可长期保持打开状态,适合需要持续交互的功能

在这里插入图片描述

2、Chrome插件开发

在浏览器使用过程中有时候打开页面的时候并没有及时打开控制台导致无法及时看到请求信息,这次插件功能是做一个快速复制请求Cookie/Curl,同时可以为修改/阻止请求的插件

2.1、第一个sidePanel插件

1、manifest.json

注意这里不能设置default_popup

{
"name": "NetWorker"
,
"description": "Listen for network requests"
,
"version": "0.0.1"
,
"manifest_version": 3
,
"permissions": [
"sidePanel"
]
,
"host_permissions": [
"*://*/*"
]
,
"action": {
"default_title": "Click to switch side panel"
}
,
"background": {
"service_worker": "background.js"
,
"type": "module"
}
,
"side_panel": {
"default_path": "sidepanel/sidepanel.html"
}
}
2、background.js

这里可以设置openPanelOnActionClick控制sidePanel的展开和折叠,同时也可以通过其他的交互手动调用open方法展开sidePanel(注意这里没有close方法!!!)

chrome.sidePanel.setPanelBehavior({
openPanelOnActionClick: true
}
)
3、sidespanel.html
<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        </head>
        <body>
        <div id="data-container">sidespanel!</div>
        <script type="module" src="sidepanel.js"></script>
        </body>
      </html>
4、页面效果

在这里插入图片描述

2.2、修改请求功能

1、manifest.json

ManifestV3中已经无法使用webRequest修改请求,需要使用declarativeNetRequest

{
"name": "NetWorker"
,
"description": "Listen for network requests"
,
"version": "0.0.1"
,
"manifest_version": 3
,
"permissions": [
"sidePanel"
,
"declarativeNetRequest"
]
,
"host_permissions": [
"*://*/*"
]
,
"action": {
"default_title": "Click to switch side panel"
}
,
"background": {
"service_worker": "background.js"
,
"type": "module"
}
,
"side_panel": {
"default_path": "sidepanel/sidepanel.html"
}
}
2、background.js
chrome.sidePanel.setPanelBehavior({
openPanelOnActionClick: true
}
)
chrome.declarativeNetRequest.getDynamicRules(rules =>
{
let ruleIds = rules.map(rule => rule.id)
;
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: ruleIds,
addRules: [
{
id: 10001
,
priority: 10001
,
action: {
type: "modifyHeaders"
,
requestHeaders: [
{
header: "X-My-Custom-Header"
,
operation: "set"
,
value: "HelloWorld"
}
,
{
header: "Accept-Language"
,
operation: "set"
,
value: "en-US,en;q=0.9"
}
]
}
,
condition: {
urlFilter: "*"
,
resourceTypes: ["main_frame"
, "sub_frame"
, "stylesheet"
, "script"
, "image"
, "font"
, "object"
, "xmlhttprequest"
, "ping"
, "csp_report"
, "media"
, "websocket"
, "other"]
}
}
]
}
)
;
}
)
;
3、页面效果

在这里插入图片描述

2.3、阻止请求功能

1、background.js
chrome.sidePanel.setPanelBehavior({
openPanelOnActionClick: true
}
)
chrome.declarativeNetRequest.getDynamicRules(rules =>
{
let ruleIds = rules.map(rule => rule.id)
;
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: ruleIds,
addRules: [
{
id: 10001
,
priority: 10001
,
action: {
type: "modifyHeaders"
,
requestHeaders: [
{
header: "X-My-Custom-Header"
,
operation: "set"
,
value: "HelloWorld"
}
,
{
header: "Accept-Language"
,
operation: "set"
,
value: "en-US,en;q=0.9"
}
]
}
,
condition: {
urlFilter: "*"
,
resourceTypes: ["main_frame"
, "sub_frame"
, "stylesheet"
, "script"
, "image"
, "font"
, "object"
, "xmlhttprequest"
, "ping"
, "csp_report"
, "media"
, "websocket"
, "other"]
}
}
,
{
id: 10002
,
priority: 1
,
action: {
type: "block"
}
,
condition: {
urlFilter: "wwads.cn"
,
resourceTypes: ["main_frame"
, "sub_frame"
, "stylesheet"
, "script"
, "image"
, "font"
, "object"
, "xmlhttprequest"
, "ping"
, "csp_report"
, "media"
, "websocket"
, "other"]
}
}
]
}
)
;
}
)
;
2、页面效果

未屏蔽广告相关请求

在这里插入图片描述

屏蔽广告相关请求

在这里插入图片描述

2.4、监听请求功能

1、manifest.json
{
"name": "NetWorker"
,
"description": "Listen for network requests"
,
"version": "0.0.1"
,
"manifest_version": 3
,
"permissions": [
"sidePanel"
,
"declarativeNetRequest"
,
"webRequest"
,
"storage"
]
,
"host_permissions": [
"*://*/*"
]
,
"action": {
"default_title": "Click to switch side panel"
}
,
"background": {
"service_worker": "background.js"
,
"type": "module"
}
,
"side_panel": {
"default_path": "sidepanel/sidepanel.html"
}
}
2、background.js

注意onBeforeRequest中只能获取requestBody,onSendHeaders中只能获取requestHeaders、extraHeaders,因此需要进行数据拼接

chrome.sidePanel.setPanelBehavior({
openPanelOnActionClick: true
}
)
chrome.declarativeNetRequest.getDynamicRules(rules =>
{
let ruleIds = rules.map(rule => rule.id)
;
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: ruleIds,
addRules: [
{
id: 10001
,
priority: 10001
,
action: {
type: "modifyHeaders"
,
requestHeaders: [
{
header: "X-My-Custom-Header"
,
operation: "set"
,
value: "HelloWorld"
}
,
{
header: "Accept-Language"
,
operation: "set"
,
value: "en-US,en;q=0.9"
}
]
}
,
condition: {
urlFilter: "*"
,
resourceTypes: ["main_frame"
, "sub_frame"
, "stylesheet"
, "script"
, "image"
, "font"
, "object"
, "xmlhttprequest"
, "ping"
, "csp_report"
, "media"
, "websocket"
, "other"]
}
}
,
{
id: 10002
,
priority: 1
,
action: {
type: "block"
}
,
condition: {
urlFilter: "wwads.cn"
,
resourceTypes: ["main_frame"
, "sub_frame"
, "stylesheet"
, "script"
, "image"
, "font"
, "object"
, "xmlhttprequest"
, "ping"
, "csp_report"
, "media"
, "websocket"
, "other"]
}
}
]
}
)
;
}
)
;
const requestBodys = {
}
chrome.webRequest.onBeforeRequest.addListener(
details =>
{
if ('requestBody'
in details) {
if ('raw'
in details['requestBody']
) {
const requestBodyRaw = details['requestBody']['raw']
const parseRequestBodyRaw = requestBodyRaw.map(r =>
{
if ('bytes'
in r) {
return {
bytes:
new TextDecoder(
).decode(r.bytes)
}
}
return r
}
)
const newDetails = JSON.parse(JSON.stringify(details)
)
newDetails.requestBody.raw = parseRequestBodyRaw
requestBodys[details.requestId] = newDetails
}
}
else {
requestBodys[details.requestId] = details
}
}
,
{
urls: ["<all_urls>"]
  }
  ,
  ["requestBody"
  , "extraHeaders"]
  )
  ;
  chrome.webRequest.onSendHeaders.addListener(
  details =>
  {
  if (!(details.requestId in requestBodys)
  ) {
  return
  }
  chrome.storage.local.get(['networklogs']
  , ({
  networklogs = []
  }
  ) =>
  {
  const newLog = {
  requestHeaders: details['requestHeaders']
  ,
  ...requestBodys[details.requestId]
  ,
  }
  ;
  chrome.storage.local.set({
  networklogs: [newLog, ...networklogs.slice(-99
  )]
  }
  )
  ;
  delete requestBodys[details.requestId]
  ;
  }
  )
  ;
  }
  ,
  {
  urls: ["<all_urls>"]
    }
    ,
    ["requestHeaders"
    , "extraHeaders"]
    )
    ;
    // chrome.webRequest.onCompleted.addListener(
    // details => {
    // console.log('onCompleted:', details);
    // },
    // { urls: ["<all_urls>"] },
      // ["responseHeaders"]
      // );
      chrome.runtime.onMessage.addListener((message, sender, sendResponse
      ) =>
      {
      if (message.action === 'networklogs'
      ) {
      chrome.storage.local.get(['networklogs']
      , ({
      networklogs
      }
      ) =>
      {
      sendResponse(networklogs)
      ;
      // 通过 sendResponse 返回结果
      }
      )
      ;
      return true
      ;
      }
      }
      )
      ;
3、sidepanel.html
<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
            <link rel="stylesheet" href="sidespanel.css">
          </head>
          <body>
          <div id="data-container"></div>
          <script type="module" src="sidepanel.js"></script>
          </body>
        </html>
4、sidepanel.js
import {
timestampToYmshms
}
from '../util/datetime.js'
;
const container = document.getElementById("data-container"
)
document.addEventListener('click'
,
async (e
) =>
{
const item = e.target.closest('.data-item'
)
;
if (item) {
try {
const value = item.dataset.requestid;
await navigator.clipboard.writeText(cookies[value]
)
;
item.style.backgroundColor = '#93b5cf'
;
setTimeout((
) =>
{
item.style.backgroundColor = '#f8f9fa'
;
}
, 500
)
;
}
catch (err) {
console.error('复制失败:'
, err)
;
}
}
}
)
;
document.addEventListener('dblclick'
,
async (e
) =>
{
const item = e.target.closest('.data-item'
)
;
if (item) {
try {
const value = item.dataset.requestid;
await navigator.clipboard.writeText(curls[value]
)
;
item.style.backgroundColor = '#ff9300'
;
setTimeout((
) =>
{
item.style.backgroundColor = '#f8f9fa'
;
}
, 500
)
;
}
catch (err) {
console.error('复制失败:'
, err)
;
}
}
}
)
;
let cookies
let curls
setInterval(
async (
) =>
{
const networklogs =
await chrome.runtime.sendMessage({
action: 'networklogs'
,
}
)
;
cookies = {
}
curls = {
}
container.innerHTML = networklogs.map(n =>
{
const requestId = parseInt(n['requestId']
)
const url = n['url']
const method = n['method']
const requestHeaders = n['requestHeaders']
const requestBody = n?.['requestBody']
const cookieHeader = requestHeaders?.filter(h =>
{
return h['name'] === 'Cookie'
}
)
let cookie
if (cookieHeader?.length >
1
) {
cookie = 'Multiple Cookie'
}
else
if (cookieHeader?.length <
1
) {
cookie = 'No Cookie'
}
else
if (cookieHeader?.length == 1
) {
cookie = cookieHeader[0]['value']
}
cookies[requestId] = cookie
let curl = `curl -X ${method
} '${url
}'`
;
requestHeaders.forEach(header =>
{
curl += ` -H '${header.name.toLowerCase(
)
}: ${header.value
}'`
;
}
)
;
if (requestBody && requestBody.raw) {
curl += ' --data-raw $\''
requestBody.raw.forEach(element =>
{
curl += element.bytes || ''
;
}
)
;
curl += '\''
}
curls[requestId] = curl.replaceAll('\"'
, '"'
).replaceAll('\r\n'
, '\\r\\n'
)
return `
<div class="data-item" data-requestid="${requestId
}">
<div class="copy-badge">Click to Copy</div>
<div class="data-metas">
  <div class="meta">
  <strong>url: </strong>
    
    ${url
    }
  </div>
  <div class="meta">
  <strong>method: </strong>
    
    ${method
    }
  </div>
  <div class="meta">
  <strong>date: </strong>
    
    ${timestampToYmshms(n['timeStamp']
    )
    }
  </div>
</div>
</div>
`
}
).join(''
)
}
, 2000
)
5、sidepanel.css
.data-container {
background: white;
border-radius: 8px;
border: 1px solid #eee;
padding: 10px;
max-height: 400px;
overflow-y: auto;
overflow-x: hidden;
}
.data-container .init{
font-size: 14px;
color: #cccccc;
}
.data-item {
padding: 12px;
margin: 8px 0;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #0078d4;
transition: all 0.2s;
cursor: pointer;
position: relative;
user-select: none;
}
.data-item:hover {
transform: translateX(2px)
;
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.1)
;
}
.data-metas .meta {
padding: 1px 0;
font-size: 12px;
max-height: 50px;
overflow: hidden;
/* 隐藏溢出的内容 */
white-space: nowrap;
/* 不自动换行 */
text-overflow: ellipsis;
/* 用省略号替代超出的文本 */
}
.copy-badge {
position: absolute;
right: 10px;
top: 12px;
transform: translateY(-50%)
;
background: rgba(0, 120, 212, 0.1)
;
color: #0078d4;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
opacity: 0;
transition: opacity 0.3s;
z-index: 999;
}
.data-item:hover .copy-badge {
opacity: 1;
}
6、datetime.js
export
function timestampToYmshms(timestamp
) {
const date =
new Date(timestamp)
;
const year = date.getFullYear(
)
;
const month = String(date.getMonth(
) + 1
).padStart(2
, '0'
)
;
const day = String(date.getDate(
)
).padStart(2
, '0'
)
;
const hours = String(date.getHours(
)
).padStart(2
, '0'
)
;
const minutes = String(date.getMinutes(
)
).padStart(2
, '0'
)
;
const seconds = String(date.getSeconds(
)
).padStart(2
, '0'
)
;
return `${year
}-${month
}-${day
} ${hours
}:${minutes
}:${seconds
}`
;
}
7、页面效果

点击card复制请求cookie,双击card复制请求的curl格式代码

在这里插入图片描述

posted on 2025-06-25 17:02  ljbguanli  阅读(35)  评论(0)    收藏  举报