大家好!我是一个喜欢前端的~菜鸡H
需求
- 完成安装插件后、对整个浏览器的页面请求进行拦截、并且可以通过插件配置指定拦截接口进行展示、包括下载和导出拦截的数据内容、这里面可以配合后端去做很多很多事情
 
问题
- 拦截所有请求组装请求信息和结果
 - 插件与页面的互相通信、做对应操作
 
效果图




首先我们先认识一个文件叫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_resourcesscript.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 valuethis._open = undefined; // the origin functionthis._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;//重写设置请求头openwindow.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 functionsXMLReq._requestID = id;XMLReq._method = method;XMLReq._url = url;// mock onreadystatechangelet _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_RECEIVEDitem.header = {};let header = XMLReq.getAllResponseHeaders() || '',headerArr = header.split("\n");// extract plain text to key-value formatfor (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);};// 重写设置请求头setRequestHeaderwindow.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);};// 重写sendwindow.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 dataif (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` contentif (tool.isString(input)) { // when `input` is a stringmethod = init.method ? init.method : 'GET';url = input;requestHeader = init.headers ? init.headers : null} else { // when `input` is a `Request` objectmethod = 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]';}}// UNSENTif (!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通信 ```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);
- content_script 与 background(后台永久注入服务)通信```javascript//content_script/install.jsconst 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页面jsconst 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后台收到消息')});
完结
基本上一些核心的内容都在这里、接下来就是根据自己的实际业务场景去配置去完成他 、下面我把开发文档贴出来
