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

需求

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

问题

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

效果图

开发chrome插件实现爬虫 - 图1

开发chrome插件实现爬虫 - 图2

开发chrome插件实现爬虫 - 图3

开发chrome插件实现爬虫 - 图4

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

第一个插件

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

manifest.json

  1. {
  2. //你的插件名称
  3. "name": "chrome",
  4. //描述
  5. "description": "chrome插件",
  6. //版本
  7. "version": "1.0",
  8. //必填项、而且填2
  9. "manifest_version": 2,
  10. //你可以理解为你的插件注入在浏览器的一个后台服务器
  11. "background": {
  12. "page": "/background/index.html"
  13. },
  14. //插件点击后显示的页面
  15. "browser_action": {
  16. "default_icon": "/icon/logo.png",
  17. "default_title": "chrome插件",
  18. "default_popup": "/browser_action/index.html"
  19. },
  20. //icons
  21. "icons": {
  22. "16": "/icon/logo.png",
  23. "32": "/icon/logo.png",
  24. "48": "/icon/logo.png",
  25. "128": "/icon/logo.png"
  26. }
  27. }

browser_action/index.html && index.css

  1. //index.html
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  7. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  8. <title>Document</title>
  9. /*注意一下这里引入css的路径规则 */
  10. <link rel="stylesheet" type="text/css" href="/browser_action/index.css">
  11. </head>
  12. <body>
  13. <div class="app">
  14. 我的第一个chrome插件
  15. </div>
  16. </body>
  17. </html>
  18. //index.css
  19. .app{
  20. width: 200px;height: 100px;background: yellow;
  21. }

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

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

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

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

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

install.js

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

重写请求拦截方法 network.js

  1. const tool = {
  2. isString(value) {
  3. return Object.prototype.toString.call(value) == '[object String]';
  4. },
  5. isPlainObject(obj) {
  6. let hasOwn = Object.prototype.hasOwnProperty;
  7. // Must be an Object.
  8. if (!obj || typeof obj !== 'object' || obj.nodeType || isWindow(obj)) {
  9. return false;
  10. }
  11. try {
  12. if (obj.constructor && !hasOwn.call(obj, 'constructor') && !hasOwn.call(obj.constructor.prototype, 'isPrototypeOf')) {
  13. return false;
  14. }
  15. } catch (e) {
  16. return false;
  17. }
  18. let key;
  19. for (key in obj) {}
  20. return key === undefined || hasOwn.call(obj, key);
  21. }
  22. }
  23. //这个类是基于腾讯开源vconsole(https://github.com/Tencent/vConsole)、写的适用本插件的一个类
  24. class RewriteNetwork {
  25. constructor() {
  26. this.reqList = {}; // URL as key, request item as value
  27. this._open = undefined; // the origin function
  28. this._send = undefined;
  29. this._setRequestHeader = undefined;
  30. this.status = false;
  31. this.mockAjax();
  32. this.mockFetch();
  33. }
  34. onRemove() {
  35. if (window.XMLHttpRequest) {
  36. window.XMLHttpRequest.prototype.open = this._open;
  37. window.XMLHttpRequest.prototype.send = this._send;
  38. window.XMLHttpRequest.prototype.setRequestHeader = this._setRequestHeader;
  39. this._open = undefined;
  40. this._send = undefined;
  41. this._setRequestHeader = undefined
  42. }
  43. }
  44. /**
  45. * mock ajax request
  46. * @private
  47. */
  48. mockAjax() {
  49. let _XMLHttpRequest = window.XMLHttpRequest;
  50. if (!_XMLHttpRequest) { return; }
  51. const that = this;
  52. //保存原生_XMLHttpRequest方法、用于下方重写
  53. const _open = window.XMLHttpRequest.prototype.open,
  54. _send = window.XMLHttpRequest.prototype.send,
  55. _setRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
  56. that._open = _open;
  57. that._send = _send;
  58. that._setRequestHeader = _setRequestHeader;
  59. //重写设置请求头open
  60. window.XMLHttpRequest.prototype.open = function() {
  61. let XMLReq = this;
  62. let args = [].slice.call(arguments),
  63. method = args[0],
  64. url = args[1],
  65. id = that.getUniqueID();
  66. let timer = null;
  67. // may be used by other functions
  68. XMLReq._requestID = id;
  69. XMLReq._method = method;
  70. XMLReq._url = url;
  71. // mock onreadystatechange
  72. let _onreadystatechange = XMLReq.onreadystatechange || function() {};
  73. //定时轮询去查看状态 每次 readyState 属性改变的时候调用的事件句柄函数。当 readyState 为 3 时,它也可能调用多次。
  74. let onreadystatechange = function() {
  75. let item = that.reqList[id] || {};
  76. //恢复初始化
  77. item.readyState = XMLReq.readyState;
  78. item.status = 0;
  79. //同步XMLReq状态
  80. if (XMLReq.readyState > 1) {
  81. item.status = XMLReq.status;
  82. }
  83. item.responseType = XMLReq.responseType;
  84. //初始化状态。XMLHttpRequest 对象已创建或已被 abort() 方法重置。
  85. if (XMLReq.readyState == 0) {
  86. if (!item.startTime) {
  87. item.startTime = (+new Date());
  88. }
  89. //open() 方法已调用,但是 send() 方法未调用。请求还没有被发送
  90. } else if (XMLReq.readyState == 1) {
  91. if (!item.startTime) {
  92. item.startTime = (+new Date());
  93. }
  94. //Send() 方法已调用,HTTP 请求已发送到 Web 服务器。未接收到响应。
  95. } else if (XMLReq.readyState == 2) {
  96. // HEADERS_RECEIVED
  97. item.header = {};
  98. let header = XMLReq.getAllResponseHeaders() || '',
  99. headerArr = header.split("\n");
  100. // extract plain text to key-value format
  101. for (let i=0; i<headerArr.length; i++) {
  102. let line = headerArr[i];
  103. if (!line) { continue; }
  104. let arr = line.split(': ');
  105. let key = arr[0],
  106. value = arr.slice(1).join(': ');
  107. item.header[key] = value;
  108. }
  109. //所有响应头部都已经接收到。响应体开始接收但未完成
  110. } else if (XMLReq.readyState == 3) {
  111. //HTTP 响应已经完全接收。
  112. } else if (XMLReq.readyState == 4) {
  113. clearInterval(timer);
  114. item.endTime = +new Date(),
  115. item.costTime = item.endTime - (item.startTime || item.endTime);
  116. item.response = XMLReq.response;
  117. item.method = XMLReq._method;
  118. item.url = XMLReq._url;
  119. item.req_type = 'xml';
  120. item.getData = XMLReq.getData;
  121. item.postData = XMLReq.postData;
  122. that.filterData(item)
  123. } else {
  124. clearInterval(timer);
  125. }
  126. return _onreadystatechange.apply(XMLReq, arguments);
  127. };
  128. XMLReq.onreadystatechange = onreadystatechange;
  129. //轮询查询状态
  130. let preState = -1;
  131. timer = setInterval(function() {
  132. if (preState != XMLReq.readyState) {
  133. preState = XMLReq.readyState;
  134. onreadystatechange.call(XMLReq);
  135. }
  136. }, 10);
  137. return _open.apply(XMLReq, args);
  138. };
  139. // 重写设置请求头setRequestHeader
  140. window.XMLHttpRequest.prototype.setRequestHeader = function() {
  141. const XMLReq = this;
  142. const args = [].slice.call(arguments);
  143. const item = that.reqList[XMLReq._requestID];
  144. if (item) {
  145. if (!item.requestHeader) { item.requestHeader = {}; }
  146. item.requestHeader[args[0]] = args[1];
  147. }
  148. return _setRequestHeader.apply(XMLReq, args);
  149. };
  150. // 重写send
  151. window.XMLHttpRequest.prototype.send = function() {
  152. let XMLReq = this;
  153. let args = [].slice.call(arguments),
  154. data = args[0];
  155. let item = that.reqList[XMLReq._requestID] || {};
  156. item.method = XMLReq._method ? XMLReq._method.toUpperCase() : 'GET';
  157. let query = XMLReq._url ? XMLReq._url.split('?') : []; // a.php?b=c&d=?e => ['a.php', 'b=c&d=', 'e']
  158. item.url = XMLReq._url || '';
  159. item.name = query.shift() || ''; // => ['b=c&d=', 'e']
  160. item.name = item.name.replace(new RegExp('[/]*$'), '').split('/').pop() || '';
  161. if (query.length > 0) {
  162. item.name += '?' + query;
  163. item.getData = {};
  164. query = query.join('?'); // => 'b=c&d=?e'
  165. query = query.split('&'); // => ['b=c', 'd=?e']
  166. for (let q of query) {
  167. q = q.split('=');
  168. item.getData[ q[0] ] = decodeURIComponent(q[1]);
  169. }
  170. }
  171. if (item.method == 'POST') {
  172. // save POST data
  173. if (tool.isString(data)) {
  174. let arr = data.split('&');
  175. item.postData = {};
  176. for (let q of arr) {
  177. q = q.split('=');
  178. item.postData[ q[0] ] = q[1];
  179. }
  180. } else if (tool.isPlainObject(data)) {
  181. item.postData = data;
  182. } else {
  183. item.postData = '[object Object]';
  184. }
  185. }
  186. XMLReq.getData = item.getData || "";
  187. XMLReq.postData = item.postData || "";
  188. return _send.apply(XMLReq, args);
  189. };
  190. };
  191. /**
  192. * mock fetch request
  193. * @private
  194. */
  195. mockFetch() {
  196. const _fetch = window.fetch;
  197. if (!_fetch) { return ""; }
  198. const that = this;
  199. const prevFetch = function(input, init){
  200. let id = that.getUniqueID();
  201. that.reqList[id] = {};
  202. let item = that.reqList[id] || {};
  203. let query = [],
  204. url = '',
  205. method = 'GET',
  206. requestHeader = null;
  207. // handle `input` content
  208. if (tool.isString(input)) { // when `input` is a string
  209. method = init.method ? init.method : 'GET';
  210. url = input;
  211. requestHeader = init.headers ? init.headers : null
  212. } else { // when `input` is a `Request` object
  213. method = input.method || 'GET';
  214. url = input.url;
  215. requestHeader = input.headers;
  216. }
  217. query = url.split('?');
  218. item.id = id;
  219. item.method = method;
  220. item.requestHeader = requestHeader;
  221. item.url = url;
  222. item.name = query.shift() || '';
  223. item.name = item.name.replace(new RegExp('[/]*$'), '').split('/').pop() || '';
  224. if (query.length > 0) {
  225. item.name += '?' + query;
  226. item.getData = {};
  227. query = query.join('?'); // => 'b=c&d=?e'
  228. query = query.split('&'); // => ['b=c', 'd=?e']
  229. for (let q of query) {
  230. q = q.split('=');
  231. item.getData[ q[0] ] = q[1];
  232. }
  233. }
  234. if (item.method === "post") {
  235. if (tool.isString(input)) {
  236. if (tool.isString(init.body && init.body)) {
  237. let arr = init.body.split('&');
  238. item.postData = {};
  239. for (let q of arr) {
  240. q = q.split('=');
  241. item.postData[ q[0] ] = q[1];
  242. }
  243. } else if (tool.isPlainObject(init.body && init.body)) {
  244. item.postData = init.body && init.body;
  245. } else {
  246. item.postData = '[object Object]';
  247. }
  248. } else {
  249. item.postData = '[object Object]';
  250. }
  251. }
  252. // UNSENT
  253. if (!item.startTime) { item.startTime = (+new Date()); }
  254. return _fetch(url, init).then((response) => {
  255. response.clone().json().then((json) => {
  256. item.endTime = +new Date(),
  257. item.costTime = item.endTime - (item.startTime || item.endTime);
  258. item.status = response.status;
  259. item.header = {};
  260. for (let pair of response.headers.entries()) {
  261. item.header[pair[0]] = pair[1];
  262. }
  263. item.response = json;
  264. item.readyState = 4;
  265. const contentType = response.headers.get('content-type');
  266. item.responseType = contentType.includes('application/json') ? 'json' : contentType.includes('text/html') ? 'text' : '';
  267. item.req_type = 'fetch';
  268. that.filterData(item)
  269. return json;
  270. })
  271. return response;
  272. })
  273. }
  274. window.fetch = prevFetch;
  275. }
  276. filterData({ url,method,req_type,response,getData,postData}){
  277. if(!url) return;
  278. const req_data = {
  279. url,
  280. method,
  281. req_type,
  282. response,
  283. getData, //query参数
  284. postData
  285. }
  286. console.log('拦截的结果',req_data)
  287. }
  288. /**
  289. * generate an unique id string (32)
  290. * @private
  291. * @return string
  292. */
  293. getUniqueID() {
  294. let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
  295. let r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
  296. return v.toString(16);
  297. });
  298. return id;
  299. }
  300. }
  301. const network = new RewriteNetwork();

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

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

  • inject_js(实际插入到页面的js) 与 content_script通信 ```javascript //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);

  1. - content_script background(后台永久注入服务)通信
  2. ```javascript
  3. //content_script/install.js
  4. const sendBgMessage = (data) =>{
  5. chrome.runtime.sendMessage({ type:'page_request',data}, function(response) {
  6. console.log('后台的回复:' + response);
  7. });
  8. }
  9. //background(后台永久注入服务)接收
  10. chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
  11. console.log('background接收数据',request)
  12. //答复
  13. sendResponse('bg后台收到消息')
  14. });
  • browser_action页面与 background.js通信、基本差不多
  1. //browser_action页面js
  2. const sendMes = (data) =>{
  3. return new Promise( resolve =>{
  4. chrome.runtime.sendMessage( data, (res)=> { resolve(res) });
  5. })
  6. }
  7. //background(后台永久注入服务)接收
  8. chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
  9. console.log('background接收数据',request)
  10. //答复
  11. sendResponse('bg后台收到消息')
  12. });

完结

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

chrome中文文档

chrome英文文档