概述
最近在边学边开发一个 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,不是 runtimechrome.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 插件时遇到的问题。