共计 12055 个字符,预计需要花费 31 分钟才能阅读完成。
概述
最近在边学边开发一个 chrome 插件,帮人家爬取网站的内容,所以总结下开发时遇到的问题,怎么从零开始开发一个 chrome 插件,实现爬取网站内容、脚本间数据通信、脚本权限和 Tab 的管制等性能。
其实 chrome
的文档有很具体的疾速开始的教程,有趣味的能够看官网教程:Getting started。本篇总结的内容,次要记录我在开发插件时遇到的问题,会比官网的疾速教程波及的内容更多点。
筹备
咱们要做一个 chrome 插件,性能是依据网站的列表,点击对应的详情,获取外面的具体内容,最初在一个自定义页面查看爬取的内容。
我这里曾经把上面的内容整合成一个我的项目了,如果嫌麻烦,能够间接 clone 这个仓库:chrome 插件 demo
首先须要新建一个我的项目,增加对应的文件,文件构造如下:
.
├── data.json # list.html 和 detail.html 应用到的数据
├── demo
│ ├── background.js
│ ├── common.js
│ ├── getDetail.js
│ ├── getList.js
│ ├── jquery.min.js
│ ├── manifest.json
│ ├── popup.html
│ └── popup.js
├── detail.html # 要爬取的详情页
└── list.html # 要爬取的列表页
整个插件的开发,下面的文件都会用到,前面会一一对其补充内容,用到哪个文件,再解释其作用。
筹备爬取的网页内容
我这里截取了一部分的数据,但也足够应用,批改 data.json
:
const DATA = {
"array": [
{
"name": "Amy Rodriguez",
"id": "63000019860511796X"
},
{
"name": "Jose Harris",
"id": "14000019891212344X"
},
{
"name": "Kenneth Jones",
"id": "150000200908039343"
},
{
"name": "Elizabeth Wilson",
"id": "630000197205295754"
},
{
"name": "Elizabeth Hall",
"id": "32000020040726080X"
},
{
"name": "Frank Garcia",
"id": "410000199606123660"
},
{
"name": "George White",
"id": "320000198909170249"
},
{
"name": "Susan Walker",
"id": "610000198001276878"
},
{
"name": "Deborah Brown",
"id": "810000200504028347"
},
{
"name": "Angela Hernandez",
"id": "630000197207149056"
},
{
"name": "Jason Hall",
"id": "620000198209225078"
},
{
"name": "Robert Clark",
"id": "350000197212127591"
},
{
"name": "Shirley Thomas",
"id": "50000020100911686X"
},
{
"name": "Mark Miller",
"id": "650000200506212412"
},
{
"name": "Dorothy Jones",
"id": "610000200708203138"
},
{
"name": "Gary Lopez",
"id": "140000198703280844"
},
{
"name": "Margaret Davis",
"id": "520000200206107072"
},
{
"name": "Steven Allen",
"id": "330000201501316327"
},
{
"name": "Brenda Lewis",
"id": "520000199709270757"
}
]
}
list.html
有一些我本人测试的逻辑,不必关怀这个文件,批改 list.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.item {
display: flex;
width: 500px;
}
.page {display: flex;}
.prev, .next {
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
margin: 20px;
}
</style>
<script src="./data.json"></script>
</head>
<body>
<div class="wrap">
<div class="content"></div>
<div class="page">
<button class="prev"> 上 </button>
<div class="pageNum">1</div>
<button class="next"> 下 </button>
</div>
</div>
<script>
const oContent = document.querySelector('.content')
const oNext = document.querySelector('.next')
const page = 10
// const total = Math.ceil(DATA.array / page)
const total = 10
let pageNum = 1
let isPending = false
function insertItem(pageNum) {const time = (300 - 100) * Math.random() + 100
console.log(time)
isPending = true
setTimeout(() => {const dom = DATA.array.filter((_, i) => i >= ((pageNum - 1) * page) && i < (pageNum * page)).map((v) => {
return `<div class="item">
<div class="item-title">${v.name}</div>
<a class="item-id" href="./detail.html?id=${v.id}" target="_blank">${v.id}</a>
</div>`
}).join('')
oContent.innerHTML = dom
updateDom()
isPending = false
}, time);
}
function updateDom() {document.querySelector('.pageNum').innerHTML = pageNum
}
insertItem(pageNum)
document.querySelector('.prev').addEventListener('click', () => {if (isPending) return
oNext.removeAttribute('disabled')
pageNum -= 1
if (pageNum < 1) pageNum = 1
insertItem(pageNum)
})
oNext.addEventListener('click', () => {if (isPending || oNext.getAttribute('disabled')) return
pageNum += 1
oNext.removeAttribute('disabled')
if (pageNum > total) {
pageNum = total
oNext.setAttribute('disabled', 'disabled')
}
insertItem(pageNum)
})
</script>
</body>
</html>
批改 detail.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./data.json"></script>
</head>
<body>
<div class="wrap">
</div>
<script>
const {search} = location
const id = search.replace('?id=', '')
function getData() {return DATA.array.find(v => v.id === id)
}
setTimeout(() => {const data = getData()
const oWrap = document.querySelector('.wrap')
oWrap.innerHTML = `<div class="name">${data.name}</div><div class="id">${data.id}</div>`
}, (1000 - 500) * Math.random() + 1000);
</script>
</body>
</html>
list.html
和 detail.html
为了模仿更实在的状况,我采纳异步渲染数据的模式。
mainfest.json 配置
首先咱们批改 manifest.json
{
"name": "Test",
"description": "chrome 插件",
"version": "1.0",
"manifest_version": 3,
"background": {"service_worker": "background.js"},
"permissions": [
"storage",
"tabs"
],
"action": {"default_popup": "popup.html"},
"content_scripts": [
{"matches": ["file:///*/chrome-extension-demo/detail.html*"],
"js": ["getDetail.js"]
},
{"matches": ["file:///*/chrome-extension-demo/list.html"],
"js": ["getList.js"]
}
]
}
像 name
、description
、version
这些字段,就是字面意思,对该插件的一些信息申明。
background
:相当于整个插件的外围,治理事件。
permissions
: 通知浏览器你须要用到什么权限。
popup
:弹窗页面,其实都是 html,只不过它的关上模式是通过点击在浏览器右上方的 icon,一个弹窗的模式。
content_scripts
:接管一个数组,依据你的 matches
注入对应的 js
到页面内执行。
导入到 chrome
咱们当初能够把插件导入到 chrome,在浏览器输出 chrome://extensions/ 并关上,会看到下图:
咱们关上开发者模式,并点击加载扩大程序,抉择咱们的插件根目录
会看到插件导入胜利,当初是启动状态,而后在浏览器盯住显示:
当初插件导入胜利了。
根本运行原理
咱们的筹备工作实现了,在开发前要明确咱们 chrome 插件根本的运行逻辑:
简略来说:
当咱们将插件导入到 chrome
并启动后,background.js
就会执行。
当咱们点击右上角的 icon 时,就会渲染 popup.html
,当弹窗敞开后,popup.html
就会销毁。
当咱们拜访网页时,如果命中 manifest.json
的 content_scripts
时,就会执行对应的脚本,每当刷新页面,都会从新执行脚本,这个要留神。
来自于 chorme 官网文档:https://developer.chrome.com/…
这图是来自于官网文档的,是对于不同的脚本之间是如何通信的,能够看到整个通信的流程,外围是 background.js
。
总结一下,popup
是帮忙咱们更不便地应用插件,不是必须;contentscript
是咱们的解决网页内容的外围,background.js
是用于治理整个插件的事件通信和做一些全局性的操作。
开发
应用 content_scripts
获取列表和详情的内容
第一步,咱们要实现上面的三个性能:
- 通过
getList.js
查看列表页的内容 - 找到详情链接点击
- 在详情页应用
getDetail.js
获取详情内容并存储到插件缓存 - 详情页发送后,敞开以后页面
列表页的脚本
批改 getList.js
:
function checkData(executeNum, callback, maxNum = 10) {if (executeNum >= maxNum) {console.log('100 秒过来了,但没有查看到内容')
return
}
setTimeout(() => {if (!!document.querySelector('.item')) {callback()
} else {checkData(executeNum + 1, callback)
}
}, 1000);
}
function getData() {const items = document.querySelectorAll('.item')
console.log(items)
items.forEach(item => {item.querySelector('.item-id').click()})
}
checkData(0, getData)
咱们逐行剖析,为什么要这样写。
首先,整个 js
文件的代码是没有被嵌套的,但这些生命的函数是不会被注入到页面的全局对象,chrome
会提供相似沙箱的成果进行隔离的。但如果你想在页面的全局对象增加属性或办法,能够间接 widnow.a = 1
进行增加。
该文件有两个个函数,checkData
和 getData
。
先看下 checkData
,它是用于检测页面的内容是否合乎咱们须要,因为咱们解决的网页大部分都是异步的,当浏览器执行你的脚本时,你的数据可能还没渲染进去,所以这时候须要一个轮询的逻辑,一直地检测页面内容,是否符合要求
function checkData(executeNum, callback, maxNum = 10) {if (executeNum >= maxNum) {console.log('100 秒过来了,但没有查看到内容')
return
}
setTimeout(() => {if (!!document.querySelector('.item')) {callback()
} else {checkData(executeNum + 1, callback)
}
}, 1000);
}
每次期待 1s,再次进行检测。咱们检测的根据,就是这个 dom
是否存在,当发现存在时,则只需回调。这里的回调,咱们用的是 getData
函数:
function getData() {document.querySelectorAll('.item').forEach(item => {item.querySelector('.item-id').click()})
}
咱们这里是简略解决,间接关上所有的内容详情页,但我集体比拟偏向于设计一个工作队列,按量去执行工作。
到这里,getList.js
工作曾经实现了,咱们再看下 getDetail.js
:
详情页的脚本
function checkData(executeNum, callback, maxNum = 10) {if (executeNum >= maxNum) {console.log('100 秒过来了,但没有查看到内容')
return
}
setTimeout(() => {if (!!document.querySelector('.id')) {callback()
} else {checkData(executeNum + 1, callback)
}
}, 1000);
}
function getData() {const name = document.querySelector('.name').innerHTML
const id = document.querySelector('.id').innerHTML
chrome.runtime.sendMessage({data: { name, id}, close: true })
}
checkData(0, getData)
和列表有点相似,同样是一个检测函数和一个获取数据函数。咱们次要看下这个:
chrome.runtime.sendMessage({data: { name, id}, close: true })
chrome
这属性,如果没有注入对应的脚本进去,是不会找到的,这行代码的性能是将数据发送给 background
。
详情页只做一个获取数据并发送给 background
的操作,这里发送了爬取的数据和 close
。close
是通知 background
这次的发送事件,要把对应的 tab 敞开。
存储数据和敞开 Tab
发送事件写好了,当初要增加一个监听事件,这能力获取详情页发送的数据,批改 background.js
:
chrome.runtime.onMessage.addListener(async (msg, sender) => {if (msg.close) {chrome.tabs.remove(sender.tab.id)
}
if (msg.data) {chrome.storage.sync.get(['data'], function(result) {chrome.storage.sync.set({ data: [...result.data, msg.data] }, function() {console.log('设置数据胜利')
});
});
}
})
这里增加了一个监听事件,当任何一个页面应用了 sendMessage
都会在这里接管到。
当 msg.close
存在时,就会依据 sender.tab.id
,应用 chrome.tabs.remove
敞开对应的 tab。
当 msg.data
存在时,应用 chrome.storage.sync
API,进行数据缓存。因为咱们是增量增加,所以每次存储前都须要先获取之前的值。
咱们能够关上 list.html
试试成果。在关上前,要先对插件进行刷新操作,只有你批改了代码,就须要进行刷新。
能够看到当初曾经实现了下面所说的性能,进入列表页,会主动关上详情页,而详情页会爬取内容发送给 background
,background
将数据缓存并敞开对应的 tab。
查看爬取的内容
当初曾经将内容爬取完了,须要查看爬取数据,咱们应用 popup.html
把爬取内容展现进去。
批改 popup.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.wrap {
width: 300px;
padding: 24px;
}
</style>
</head>
<body>
<div class="wrap">
<button id="show"> 显示数据 </button>
<div class="log"></div>
</div>
<script src="./popup.js"></script>
</body>
</html>
批改 popup.js
:
const btn = document.querySelector('#show')
function log(txt) {document.querySelector('.log')
.appendChild(document.createElement('div'))
.innerText = ">" + txt;
}
btn.addEventListener('click', () => {document.querySelector('.log').innerHTML = ''chrome.storage.sync.get(['data'], function(result) {(result.data || []).forEach(({name}) => {log(name)
})
})
})
这里的逻辑很简略,popup.html
就是弹窗渲染的内容,popup.js
是对应执行的脚本,点击按钮,把 chrome.storage.sync
的数据展现进去。
要留神一点,在结尾有说过,咱们所用的 chrome
API 基本上都要在 manifest.json
进行申明:
{
"permissions": [
"storage",
"tabs"
]
}
至此,咱们这个小 demo 曾经开发完了,性能很简略,但曾经笼罩了很多罕用的性能,我这里没有对每个应用到的 API 进行具体解释,只是简略阐明而已,具体的 API 阐明还是要去官网文档查阅。
注意事项
在整个开发插件的过程中,还是遇到不少的问题。
加载第三方库
如果咱们要开发一个略微简单的插件,可能须要引入一些第三方库,这里咱们来演示下,引入 jquery 并在 getDetail.js
应用。首先批改 background.js
的 content_scripts
:
{
"content_scripts": [
{"matches": ["file:///*/chrome-extension-demo/detail.html*"],
"js": ["jquery.min.js", "getDetail.js"]
},
{"matches": ["file:///*/chrome-extension-demo/list.html"],
"js": ["getList.js"]
}
]
}
增加了之后,批改 getDetail.js
的 getData
:
function getData() {const name = $('.name').text()
const id = $('.id').text()
chrome.runtime.sendMessage({data: { name, id}, close: true })
}
刷新插件,运行一下,能够看到数据是失常上报的。
"js": ["jquery.min.js", "getDetail.js"]
这段的配置,当命中对应的配置后,会按程序加载你的 js
文件。
优化通用代码
在开发时,多个文件大部分状况都会呈现通用代码或配置的,但 chrome
插件不反对原生的 import
,我这里想到的解决办法有两个:
- 应用
webpack
等工具,每次批改完代码,先编译,再刷新应用 - 通过
contents_script
可加载多个脚本的特点,先加载通用代码文件,这样后加载的文件都能够应用通用代码文件申明的对象、函数。
第一种这里就不多说了,有点麻烦,但这个是最完满的,完满解决通用代码的问题。
第二种,咱们别离批改 common.js
、getList.js
、getDetail.js
和 manifest.json
文件。
批改 common.js
:
function checkData(checkDomTxt, executeNum, callback, maxNum = 10) {if (executeNum >= maxNum) {console.log('100 秒过来了,但没有查看到内容')
return
}
setTimeout(() => {if (!!document.querySelector(checkDomTxt)) {callback()
} else {checkData(checkDomTxt, executeNum + 1, callback, maxNum)
}
}, 1000);
}
getList.js
全笼罩:
function getData() {const items = document.querySelectorAll('.item')
items.forEach(item => {item.querySelector('.item-id').click()})
}
checkData('.item', 0, getData)
getDetail.js
全笼罩:
function getData() {const name = $('.name').text()
const id = $('.id').text()
chrome.runtime.sendMessage({data: { name, id}, close: true })
}
checkData('.id', 0, getData)
manifest.json
批改 content_scripts
:
{
"content_scripts": [
{"matches": ["file:///*/chrome-extension-demo/detail.html*"],
"js": ["common.js", "jquery.min.js", "getDetail.js"]
},
{"matches": ["file:///*/chrome-extension-demo/list.html"],
"js": ["common.js", "getList.js"]
}
]
}
咱们刷新下插件和 list.html
页面,能够看到插件还是失常运行的,证实了这种加载通用代码的办法是可行的。
当然这种办法尽管相对来说不便,但也有不少问题:
- 当代码量多起来后,不晓得哪个变量是来自哪个文件
- 命名抵触问题
- 像
background.js
和popup.js
不能通过这个办法解决
权限与通信
咱们关注三个对象:background
、popup
和页面的 content_scripts
,这三者之间是如何通信的。
background
和 popup
通信
background
或 popup
发送:
chrome.runtime.sendMessage(...)
background
或 popup
监听:
chrome.runtime.onMessage.addListener(async (msg, sender) => {})
background
和 popup
的通信是最简略的,间接应用 chrome.runtime
的 sendMessage
和 onMessage
就行了。
background
与 popup
和 content_scripts
通信
background
或 popup
发送:
// 这里改成了 tabs,不是 runtime
chrome.tabs.sendMessage(tabId, data)
content_scripts
监听:
chrome.runtime.onMessage.addListener(async (msg, sender) => {})
background
和 popup
不能相似播送那样发送给 content_scripts
,只能依据你要发送的那个网页的 tabId
,调用 chrome.tabs.sendMessage
。
你能够在 background.js
和 popup
增加对应的代码:
chrome.tabs.query({}, function(tabs){const tab = tabs.find(v => v.url.includes('list.html'))
if (!tab) return
chrome.tabs.sendMessage(tab.id, 'ok')
});
找到对应的要发送的 tab,进行指定发送。
background
或 popup
监听:
chrome.runtime.onMessage.addListener(async (msg, sender) => {})
content_scripts
发送:
chrome.runtime.sendMessage(...)
content_scripts
之间通信
这里就略微麻烦点,须要通过 background.js
进行转发,咱们在 getList.js
增加发送的事件,在 getDetail.js
进行监听。
getList.js
:
chrome.runtime.sendMessage('list send')
background.js
:
chrome.runtime.onMessage.addListener(async (msg, sender) => {if (msg === 'list send') {chrome.tabs.query({}, function(tabs){const tab = tabs.find(v => v.url.includes('detail.html'))
if (!tab) return
chrome.tabs.sendMessage(tab.id, 'to detail')
});
}
})
detail.js
:
chrome.runtime.onMessage.addListener(async (msg, sender) => {console.log(msg)
})
这样就能够了,当 getList.js
发送了 list send
信息后,background.js
会承受到,转发给 detail.js
,实现了 content_scripts
之间的通信。
总结
这篇文章,次要总结我在开发时遇到的一些问题,是如何解决的,没有具体的解释为什么这样做,心愿能够给到大家一个参考,解决在开发 chrome 插件时遇到的问题。