开发chrome插件实现爬虫

吐槽君 分类:javascript

大家好!我是一个喜欢前端的~菜鸡H

需求

  • 完成安装插件后、对整个浏览器的页面请求进行拦截、并且可以通过插件配置指定拦截接口进行展示、包括下载和导出拦截的数据内容、这里面可以配合后端去做很多很多事情

问题

  • 拦截所有请求组装请求信息和结果
  • 插件与页面的互相通信、做对应操作

效果图

微信图片_20210414171235.png

微信图片_20210414171240.png

微信图片_20210414171244.png

微信图片_20210414171505.png

首先我们先认识一个文件叫manifest.json、他是一个配置文件、chrome插件先读取这个配置文件去做初始化工作、比如一些插件的图标配置、插件点击显示的页面、插件需要的权限授权、等等....

第一个插件

  • 创建配置文件manifest.json
  • 创建展示图标
  • 创建展示页面
    //chromePlugin文件结构
    - icon 
        - logo.png  //图片尺寸不能大于129...好像是吧...如果不显示的话就弄小一些
    - background
        - index.html
        - index.js
    - browser_action
        - index.html
        - index.js
        - index.css
    - manifest.json
    
 

manifest.json

{
    //你的插件名称
    "name": "chrome",
    //描述
    "description": "chrome插件",
    //版本
    "version": "1.0",
    //必填项、而且填2
    "manifest_version": 2,
    //你可以理解为你的插件注入在浏览器的一个后台服务器
    "background": {
        "page": "/background/index.html"
    },
    //插件点击后显示的页面
    "browser_action": {
        "default_icon": "/icon/logo.png",
        "default_title": "chrome插件",
        "default_popup": "/browser_action/index.html"
   },
   //icons
   "icons": {
        "16": "/icon/logo.png",
        "32": "/icon/logo.png",
        "48": "/icon/logo.png",
        "128": "/icon/logo.png"
    }
}

 

browser_action/index.html && index.css

//index.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>
    /*注意一下这里引入css的路径规则 */
    <link rel="stylesheet" type="text/css" href="/browser_action/index.css">
</head>
<body>
    <div class="app">
        我的第一个chrome插件
    </div>
</body>
</html>

//index.css
.app{
    width: 200px;height: 100px;background: yellow;
}

 

简单描述下配置文件流程、background你可以理解为你的插件一直驻留在浏览器的代码块、里面可以放一些共享给插件页面的数据等等... browser_action就是指插件点击后显示的页面内容、你可以尝试去写一些你自己想要展示的内容部分、然后打开浏览器、更多工具-> 扩展程序、右上角开发模式打开 然后把你的项目直接拖入进去、会自动识别、不出意外的话你的插件就安装好了、然后点击插件、那么恭喜你、你的第一个chrome插件已经完成!?

问题1:拦截所有请求组装请求信息和结果

思路是:重写XMLHttpRequest和fetch、重写后通过chrome提供的配置文件去往每个页面把重写的代码注入进去,到拦截效果、先认识一个 content_scripts 配置、他是一个告诉chrome插件我需要在当前网页去加载我的js 的一个配置 往manifest.json添加下面代码

//注入到页面js的规则配置
"content_scripts": [
    {
    // 定义哪些页面需要注入content script "<all_urls>"所有页面 
        "matches": ["<all_urls>"],
        //css文件地址
        "css": [],
        //注入的js文件地址
        "js": ["/contentScript/install.js"],
        //控制content script注入的时机。可以是document_start, document_end或者document_idle。默认document_idle。
        "run_at":"document_start"
       }
 ],
 //通过chrome.extension.getURL来获取包内资源的路径。需要在manifest.json文件中设置访问权限web_accessible_resources
 "web_accessible_resources": [
     "/contentScript/network.js"
  ]
 

ok 这样我就添加了注入js的脚本命令、现在我们需要在相对应的路径下创建好文件夹和文件/contentScript/install.js 然后在contentScript文件夹下在创建一个network.js 和 install.js

install.js

setTimeout(() => {
    const script = document.createElement('script');
    script.setAttribute('type', 'text/javascript');
    //通过chrome.extension.getURL来获取包内资源的路径。需要在manifest.json文件中设置访问权限web_accessible_resources
    script.setAttribute('src', chrome.extension.getURL('/contentScript/network.js'));
    document.head.appendChild(script);
});
 

重写请求拦截方法 network.js

const tool = {
isString(value) {
return Object.prototype.toString.call(value) == '[object String]';
},
isPlainObject(obj) {
let hasOwn = Object.prototype.hasOwnProperty;
// Must be an Object.
if (!obj || typeof obj !== 'object' || obj.nodeType || isWindow(obj)) {
return false;
}
try {
if (obj.constructor && !hasOwn.call(obj, 'constructor') && !hasOwn.call(obj.constructor.prototype, 'isPrototypeOf')) {
return false;
}
} catch (e) {
return false;
}
let key;
for (key in obj) {}
return key === undefined || hasOwn.call(obj, key);
}
}
//这个类是基于腾讯开源vconsole(https://github.com/Tencent/vConsole)、写的适用本插件的一个类
class RewriteNetwork {
constructor() {
this.reqList = {}; // URL as key, request item as value
this._open = undefined; // the origin function
this._send = undefined;
this._setRequestHeader = undefined;
this.status = false;
this.mockAjax();
this.mockFetch();
}
onRemove() {
if (window.XMLHttpRequest) {
window.XMLHttpRequest.prototype.open = this._open;
window.XMLHttpRequest.prototype.send = this._send;
window.XMLHttpRequest.prototype.setRequestHeader = this._setRequestHeader;
this._open = undefined;
this._send = undefined;
this._setRequestHeader = undefined
}
}
/**
* mock ajax request
* @private
*/
mockAjax() {
let _XMLHttpRequest = window.XMLHttpRequest;
if (!_XMLHttpRequest) { return; }
const that = this;
//保存原生_XMLHttpRequest方法、用于下方重写
const _open = window.XMLHttpRequest.prototype.open,
_send = window.XMLHttpRequest.prototype.send,
_setRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
that._open = _open;
that._send = _send;
that._setRequestHeader = _setRequestHeader;
//重写设置请求头open
window.XMLHttpRequest.prototype.open = function() {
let XMLReq = this;
let args = [].slice.call(arguments),
method = args[0],
url = args[1],
id = that.getUniqueID();
let timer = null;
// may be used by other functions
XMLReq._requestID = id;
XMLReq._method = method;
XMLReq._url = url;
// mock onreadystatechange
let _onreadystatechange = XMLReq.onreadystatechange || function() {};
//定时轮询去查看状态 每次 readyState 属性改变的时候调用的事件句柄函数。当 readyState 为 3 时,它也可能调用多次。
let onreadystatechange = function() {
let item = that.reqList[id] || {};
//恢复初始化
item.readyState = XMLReq.readyState;
item.status = 0;
//同步XMLReq状态
if (XMLReq.readyState > 1) {
item.status = XMLReq.status;
}
item.responseType = XMLReq.responseType;
//初始化状态。XMLHttpRequest 对象已创建或已被 abort() 方法重置。
if (XMLReq.readyState == 0) {
if (!item.startTime) {
item.startTime = (+new Date());
}
//open() 方法已调用,但是 send() 方法未调用。请求还没有被发送
} else if (XMLReq.readyState == 1) {
if (!item.startTime) {
item.startTime = (+new Date());
}
//Send() 方法已调用,HTTP 请求已发送到 Web 服务器。未接收到响应。
} else if (XMLReq.readyState == 2) {
// HEADERS_RECEIVED
item.header = {};
let header = XMLReq.getAllResponseHeaders() || '',
headerArr = header.split("\n");
// extract plain text to key-value format
for (let i=0; i<headerArr.length; i++) {
let line = headerArr[i];
if (!line) { continue; }
let arr = line.split(': ');
let key = arr[0],
value = arr.slice(1).join(': ');
item.header[key] = value;
}
//所有响应头部都已经接收到。响应体开始接收但未完成
} else if (XMLReq.readyState == 3) {
//HTTP 响应已经完全接收。
} else if (XMLReq.readyState == 4) {
clearInterval(timer);
item.endTime = +new Date(),
item.costTime = item.endTime - (item.startTime || item.endTime);
item.response = XMLReq.response;
item.method = XMLReq._method;
item.url = XMLReq._url;
item.req_type = 'xml';
item.getData = XMLReq.getData;
item.postData = XMLReq.postData;
that.filterData(item)
} else {
clearInterval(timer);
}
return _onreadystatechange.apply(XMLReq, arguments);
};
XMLReq.onreadystatechange = onreadystatechange;
//轮询查询状态
let preState = -1;
timer = setInterval(function() {
if (preState != XMLReq.readyState) {
preState = XMLReq.readyState;
onreadystatechange.call(XMLReq);
}
}, 10);
return _open.apply(XMLReq, args);
};
// 重写设置请求头setRequestHeader
window.XMLHttpRequest.prototype.setRequestHeader = function() {
const XMLReq = this;
const args = [].slice.call(arguments);
const item = that.reqList[XMLReq._requestID];
if (item) {
if (!item.requestHeader) { item.requestHeader = {}; }
item.requestHeader[args[0]] = args[1];
}
return _setRequestHeader.apply(XMLReq, args);
};
// 重写send
window.XMLHttpRequest.prototype.send = function() {
let XMLReq = this;
let args = [].slice.call(arguments),
data = args[0];
let item = that.reqList[XMLReq._requestID] || {};
item.method = XMLReq._method ? XMLReq._method.toUpperCase() : 'GET';
let query = XMLReq._url ? XMLReq._url.split('?') : []; // a.php?b=c&d=?e => ['a.php', 'b=c&d=', 'e']
item.url = XMLReq._url || '';
item.name = query.shift() || ''; // => ['b=c&d=', 'e']
item.name = item.name.replace(new RegExp('[/]*$'), '').split('/').pop() || '';
if (query.length > 0) {
item.name += '?' + query;
item.getData = {};
query = query.join('?'); // => 'b=c&d=?e'
query = query.split('&'); // => ['b=c', 'd=?e']
for (let q of query) {
q = q.split('=');
item.getData[ q[0] ] = decodeURIComponent(q[1]);
}
}
if (item.method == 'POST') {
// save POST data
if (tool.isString(data)) {
let arr = data.split('&');
item.postData = {};
for (let q of arr) {
q = q.split('=');
item.postData[ q[0] ] = q[1];
}
} else if (tool.isPlainObject(data)) {
item.postData = data;
} else {
item.postData = '[object Object]';
}
}
XMLReq.getData = item.getData || "";
XMLReq.postData = item.postData || "";
return _send.apply(XMLReq, args);
};
};
/**
* mock fetch request
* @private
*/
mockFetch() {
const _fetch = window.fetch;
if (!_fetch) { return ""; }
const that = this;
const prevFetch = function(input, init){
let id = that.getUniqueID();
that.reqList[id] = {};
let item = that.reqList[id] || {};
let query = [],
url = '',
method = 'GET',
requestHeader = null;
// handle `input` content
if (tool.isString(input)) { // when `input` is a string
method = init.method ? init.method : 'GET';
url = input;
requestHeader = init.headers ? init.headers : null
} else { // when `input` is a `Request` object
method = input.method || 'GET';
url = input.url;
requestHeader = input.headers;
}
query = url.split('?');
item.id = id;
item.method = method;
item.requestHeader = requestHeader;
item.url = url;
item.name = query.shift() || '';
item.name = item.name.replace(new RegExp('[/]*$'), '').split('/').pop() || '';
if (query.length > 0) {
item.name += '?' + query;
item.getData = {};
query = query.join('?'); // => 'b=c&d=?e'
query = query.split('&'); // => ['b=c', 'd=?e']
for (let q of query) {
q = q.split('=');
item.getData[ q[0] ] = q[1];
}
}
if (item.method === "post") { 
if (tool.isString(input)) {
if (tool.isString(init.body && init.body)) {
let arr = init.body.split('&');
item.postData = {};
for (let q of arr) {
q = q.split('=');
item.postData[ q[0] ] = q[1];
}
} else if (tool.isPlainObject(init.body && init.body)) {
item.postData = init.body && init.body;
} else {
item.postData = '[object Object]';
}
} else { 
item.postData = '[object Object]';
}
}
//   UNSENT
if (!item.startTime) {  item.startTime = (+new Date()); }
return _fetch(url, init).then((response) => {
response.clone().json().then((json) => {
item.endTime = +new Date(),
item.costTime = item.endTime - (item.startTime || item.endTime);
item.status = response.status;
item.header = {};
for (let pair of response.headers.entries()) {
item.header[pair[0]] = pair[1];
}
item.response = json;
item.readyState = 4;
const contentType = response.headers.get('content-type');
item.responseType  = contentType.includes('application/json') ? 'json' : contentType.includes('text/html') ? 'text' : '';
item.req_type = 'fetch';
that.filterData(item)
return json;
})
return response;
})
}
window.fetch = prevFetch;
}
filterData({ url,method,req_type,response,getData,postData}){
if(!url) return;
const req_data = {
url,
method,
req_type,
response,
getData, //query参数
postData
}
console.log('拦截的结果',req_data)
}
/**
* generate an unique id string (32)
* @private
* @return string
*/
getUniqueID() {
let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
return id;
}
}
const network = new RewriteNetwork();

随便打开一个测试网站、f12打开控制台然后里面就会有拦截的结果输出、这样我们就完成了页面接口拦截了、接下来我们就需要完成、插件与页面的互相通信、做对应操作

问题2:插件与页面的互相通信

  • inject_js(实际插入到页面的js) 与 content_script通信
//inject_js利用postMessage方法和content_script进行通信、在拦截请求的方法里面去发送数据给content_script
//network.js
const senMes = (data) =>{
window.postMessage(data, '*');
}
....
console.log('拦截的结果',req_data)
senMes(req_data)
//install.js
//接收inject页面消息
....
window.addEventListener("message", function(e){
const { data } = e;
console.log('接收networkJS数据',data)
}, false);
  • content_script 与 background(后台永久注入服务)通信
//content_script/install.js
const sendBgMessage = (data) =>{
chrome.runtime.sendMessage({ type:'page_request',data}, function(response) {
console.log('后台的回复:' + response);
});
}
//background(后台永久注入服务)接收
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
console.log('background接收数据',request)
//答复
sendResponse('bg后台收到消息')
});
  • browser_action页面与 background.js通信、基本差不多
//browser_action页面js
const sendMes = (data) =>{
return new Promise( resolve =>{
chrome.runtime.sendMessage( data, (res)=> {  resolve(res) });
})
}
//background(后台永久注入服务)接收
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
console.log('background接收数据',request)
//答复
sendResponse('bg后台收到消息')
});

完结

基本上一些核心的内容都在这里、接下来就是根据自己的实际业务场景去配置去完成他 、下面我把开发文档贴出来

chrome中文文档

chrome英文文档

回复

我来回复
  • 暂无回复内容