如何开发一款方便快速查询数据的Google浏览器插件
前言
事情是这样的,有一天我正在上班,突然有一天,领导前来买瓜,问我:“小赵啊,你这瓜保熟吗?” 我一猜就知道,突然找我肯定是没有啥好事


插件:Gitee地址 仓库中的Stdio项目为本文实例插件 (建议根据实例代码进行阅读)
分析
- 领导的想法是不想手动去复制Excel中的(姓名、身份证号)等用户信息,想让它们自动填充到输入框中,领导只需要点击查询和滑动验证就行
- 查询出结果后,要根据用户查询出来的状态进行标注,最后进行导出
构思
经过深思熟虑以后,我决定使用Google插件来实现这一个功能
整体的界面设计为如下
页面有一个table数据表格,用来展示Excel表格中的需要查询的一户数据,其中包括姓名、身份证号、标注状态、以及当前的学生索引位置等等(效果图如下)
开发准备
既然需要用到Google插件,那就需要对Google插件开发有一定的了解,下面我就简单的介绍一下本次案例用到的Google插件的一些功能和API(Google提供了很多API供我们选择,想了解更多可以去官网进行学习和使用)
Google插件开发模块介绍
Google插件开发主要分三个模块组成, 分别为
background
、popup
以及content
模块,这三个模块是相对独立的模块,他们不仅有不同的调试方式,还遵循着不同的通讯协议
background
background
顾名思义就是后台
模块,这个模块是用户看不见的模块,一般在此模块内进行处理数据,后台
模块可以访问绝大部分Google
提供的API
,不能操作页面上的DOM
元素,因为模块都是独立的,后台模块触碰不到DOM
popup
popup
顾名思义就是弹出层
模块,当然这个模块并不是依赖于DOM元素的弹出层,而是依赖于Chrome中的拓展程序中的插件,点击插件时弹出的模块,效果如图下
当然popup
模块也是独立的,虽然用户看得见但是他不能够访问渲染页面上的DOM
元素
content
content
模块就是内容
模块了,在这个模块里,可以随意操作DOM
元素,对DOM
元素进行控制、添加、删除或者修改,案例中插入了两个模块(按钮盒子、table表)到页面上,实际上就是在content
中使用了DOM
添加操作方法
介绍完以上三个模块以后,现在可能会有一些疑问,那既然这三个模块他们都是相对独立的,当在background
后台模块或者popup
弹出层模块获取到异步数据以后,怎么让他动态加载到页面DOM
中去呢?
答案:因为background
和popup
是不具备操作DOM
的功能的,所以没有办法直接添加。这个时候,就需要用到content
模块来进行加载到DOM
中去。
那么这样就会有一个新的问题,就是怎么将background
后台模块或者popup
弹出层模块获取到的异步数据传递给background
模块呢?
其实很简单,Google提供了一些API,便于background
、popup
和content
之间进行通讯。
Google 模块之间的通讯
前面讲述了,既然每个模块都是独立的且每个模块的权限各不相同,那么各个模块之间的通讯也会有一些细微的差别,下面就来详细的讲解一下
在进入正式编写代码插件之前,可以暂时将background
、popup
和content
理解为三个在不同地方执行的js
文件
popup 通讯 background
方法一:使用长连接
popup.js
// 使用长连接 name 一定是唯一的
let port = chrome.runtime.connect({ name: 'popup-name' });
port.onMessage.addListener(function (message) {
// 处理从 background 发来的消息
console.log('background 发来了消息:', message);
});
// 发送消息给 background
port.postMessage('Hi~background, 我是popup');
background.js
let port = null;
// 监听Connect长连接
chrome.runtime.onConnect.addListener(function (externalPort) {
// 监听连接name
if (externalPort.name === 'popup-name') {
port = externalPort;
// 连接监听
port.onMessage.addListener(function (message) {
// 接收 popup 发来的消息
console.log('popup 发来了消息:', message);
// 接收到消息以后 发送响应消息给 popup
port.postMessage('Hi~popup');
});
}
});
点击popup
弹窗后,即可进行通信,出现以下状况,即可表示通信成功
方法二:API
popup.js
// 向后台脚本发送消息
function sendMessageToBackground(message) {
chrome.runtime.sendMessage(message);
}
// 监听来自后台脚本的消息
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
console.log('Message from background:', message);
// 这里可以进行处理或者发送响应消息给后台脚本
});
// 发送消息给后台脚本
sendMessageToBackground('Hello from popup!');
background.js
// 向 popup 发送消息
function sendMessageToPopup(message) {
chrome.runtime.sendMessage(message);
}
// 监听来自 popup 的消息
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
console.log('Message from popup:', message);
// 这里可以进行处理或者发送响应消息给 popup
});
// 监听插件的安装事件
chrome.runtime.onInstalled.addListener(function(details) {
// 发送消息给 popup
sendMessageToPopup('Hello from background!');
});
popup 通讯 content
popup.js
// 向 content script 发送消息
function sendMessageToContentScript(message) {
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, message);
});
}
// 监听来自 content script 的消息
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
console.log('Message from content script:', message);
// 这里可以进行处理或者发送响应消息给 content script
});
// 发送消息给 content script
sendMessageToContentScript('Hello from popup!');
content.js
// 向 popup 发送消息
function sendMessageToPopup(message) {
chrome.runtime.sendMessage(message);
}
// 监听来自 popup 的消息
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
console.log('Message from popup:', message);
// 这里可以进行处理或者发送响应消息给 popup
sendMessageToPopup('Hello from content script!');
});
content 通讯 background
content.js
// 向后台脚本发送消息
function sendMessageToBackground(message) {
chrome.runtime.sendMessage(message);
}
// 监听来自后台脚本的消息
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
console.log('Message from background:', message);
// 这里可以进行处理或者发送响应消息给后台脚本
});
// 发送消息给后台脚本
sendMessageToBackground('Hello from content script!');
background.js
// 向内容脚本发送消息
function sendMessageToContentScript(tabId, message) {
chrome.tabs.sendMessage(tabId, message);
}
// 监听来自内容脚本的消息
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
console.log('Message from content script:', message);
// 这里可以进行处理或者发送响应消息给内容脚本
});
// 监听标签页的切换
chrome.tabs.onActivated.addListener(function(activeInfo) {
// 获取当前活动标签页的信息
chrome.tabs.get(activeInfo.tabId, function(tab) {
// 发送消息给当前活动标签页的内容脚本
sendMessageToContentScript(tab.id, 'Hello from background!');
});
});
至此,常用模块的基本通讯方法已经介绍完了,下面就开始一步一步的去开发一个Google插件吧
创建项目
首先我们来看一下,配置文件
manifest.json
文件
{
"name": "快速查询成绩",
"manifest_version": 2,
"version": "1.0.0",
"description": "查询成绩插件1.0版本",
"icons": {
"16": "img/lookup.png",
"48": "img/lookup.png",
"128": "img/lookup.png"
},
"browser_action": {
"default_icon": "img/lookup.png",
"default_title": "成绩查询",
"default_popup": "html/popup.html"
},
"background": {
"scripts": [
"js/background.js",
"js/popup.js"
],
"persistent": false
},
"content_scripts": [
{
"js": [
"js/content.js",
"js/contentMessaging.js",
"js/contentHttp.js"
],
"css": [
"css/contentAllStyleContent.css",
"css/bootstrap.css"
],
"matches": [
"http://*.zscx.osta.org.cn/"
]
}
],
"permissions": [
"<all_urls>",
"tabs",
"storage",
"bookmarks",
"*://*/*",
"http://*/*",
"https://*/*"
]
}
manifest.json
是 Chrome 插件的清单文件,它描述了插件的元数据、功能和权限。它告诉 Chrome 浏览器如何加载和运行插件。以下是一些常见的参数及其用途:
"manifest_version"
:指定插件清单文件的版本号,当前版本为 2。"name"
:插件的名称,将显示在 Chrome 浏览器的插件管理器中。"version"
:插件的版本号。"description"
:插件的描述信息,将显示在插件管理器中。"icons"
:定义插件在不同位置和尺寸的图标。"background"
:指定插件的后台脚本文件。"browser_action"
:定义插件的浏览器动作,通常是点击插件图标后弹出的浮窗。"page_action"
:定义插件的页面动作,可以在特定页面上显示插件图标和浮窗。"content_scripts"
:定义插件的内容脚本,用于向页面注入脚本或样式。"permissions"
:声明插件需要的权限,如访问特定网站、读取浏览历史等。"background"
:指定插件的后台脚本文件。
接着让我们一步一步的来分析这个配置文件的作用:
browser_action
配置项
"browser_action": {
"default_icon": "img/lookup.png",
"default_title": "成绩查询",
"default_popup": "html/popup.html"
},
根据配置我们可以看出,这一个配置文件,主要是配置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">
<link rel="stylesheet" href="../css/popup.css">
<link rel="stylesheet" href="../css/bootstrap.css">
<title>Document</title>
</head>
<input id="uploadExcel" type="file" hidden>
<div class="panels">
<ul class="list-group">
<li class="list-group-item" id="uploadExcelBtn">
<img src="转存失败,建议直接上传图片文件 ../img/Upload.png" alt="转存失败,建议直接上传图片文件">
上传信息表
</li>
<li class="list-group-item">
<a href="../检索查询模板.xlsx">
<img src="转存失败,建议直接上传图片文件 ../img/Download.png" alt="转存失败,建议直接上传图片文件">
下载信息表模板
</a>
</li>
</ul>
</div>
<script src="../js/xlsx.core.min.js"></script>
<script src="../js/popup.js"></script>
</html>
正是我们点击右侧的图标,弹出的模板信息内容
我们在页面中,引入了脚本popup.js
和xlsx
操作excel表格的数据文件
其中popup.js
脚本就是我们用于通讯的弹出层
模块
background
配置项
"background": {
"scripts": [
"js/background.js"
],
"persistent": false
},
"background"
参数主要是指定了插件的后台脚本是谁,代码中指定了脚本文件为 "js/background.js"
,表示background.js
就是运行在后台
模块中的脚本文件,可以在这个文件内实现与其他模块的通信监听等逻辑
content_scripts
配置项
"content_scripts": [
{
"js": [
"js/content.js"
],
"css": [
"css/contentAllStyleContent.css",
"css/bootstrap.css"
],
"matches": [
"http://*.zscx.osta.org.cn/"
]
}
],
顾名思义,这个配置项就是配置content
模块的,上面我们说了,content
模块是可以直接操作DOM
元素的,所以可以定义一些Css
样式来美化创建在页面上的DOM
元素,以及定义一些脚本文件绑定到元素上,并且和其他模块进行通讯
matches
就是匹配插件在那些网站下执行脚本,我这里使用了zscx.osta.org.cn/ 这个网站,所以我定义了如下配置
"matches": [
"http://*.zscx.osta.org.cn/"
]
可以根据自行修改
例如:如果想修改为只有打开掘金网站的时候启用插件可以配置为 https://*.juejin.cn/
popup.js
上述代码中,展示了
popup.html
页面,并且引入了一个popup.js
的脚本文件用作popup
的通讯脚本文件
这里我定义了两个按钮,一个是上传信息表
,一个是下载信息表模板
, 这样做的目的是为了定义一个标准,避免随便上传造成的解析出错
popup.js
window.onload = function () {
// 点击上传
const UplaodBtn = getDomById('uploadExcelBtn')
// console.log('UplaodBtn ===> ', UplaodBtn)
// 监听点击上传
UplaodBtn.addEventListener('click', uploadExcelBtn)
// 监听选择完文件
const uploadExcelSuccess = getDomById('uploadExcel')
uploadExcelSuccess.addEventListener('change', function (event) {
const selectedFile = event.target.files[0];
console.log('selectedFile ====> ', selectedFile)
if (selectedFile) {
// 文件已选择
readWorkbookFromLocalFile(selectedFile, (data) => {
const worksheet = data.Sheets[data.SheetNames[0]];
const jsonData = window.XLSX.utils.sheet_to_json(worksheet, { header: 1 });
console.log('jsonData ===> ', jsonData)
// 处理json数据后 交互给页面
formatingData(jsonData)
})
// 在这里可以执行你需要的操作,如上传文件等
}
});
}
// 点击上传文件按钮
function uploadExcelBtn() {
const Upload = getDomById('uploadExcel')
// 手动触发上传
Upload.click()
}
//
function getDomById(id) {
return document.getElementById(id)
}
// 读取本地excel文件
function readWorkbookFromLocalFile(file, callback) {
var reader = new FileReader();
reader.onload = function (e) {
var data = e.target.result;
var workbook = window.XLSX.read(data, { type: 'binary' });
if (callback) callback(workbook);
};
reader.readAsBinaryString(file);
}
// 处理数据
function formatingData(dataSource) {
var header = dataSource[0];
var result = [];
for (var i = 1; i < dataSource.length; i++) {
if (dataSource[i].length === 0) {
continue; // 跳过空数组
}
var obj = {};
for (var j = 0; j < header.length; j++) {
var value = dataSource[i][j] || '';
obj[header[j]] = value;
}
result.push(obj);
}
// Excel数据处理好了,通信给页面 result
toContent(result)
}
function toContent(dresult) {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
let message = { info: '来自popup的信息', dresult }
chrome.tabs.sendMessage(tabs[0].id, message, res => {
console.log('popup=>content')
console.log(res)
})
})
}
因为在popup.html
引用了XLSX
处理Excel的第三方插件,所以直接使用window.XLSX
来进行解析文件
当点击触发上传信息表
按钮时,会执行uploadExcelBtn
方法,打开文件选择器,
当文件选择完毕后,会被uploadExcelSuccess.addEventListener('change',xxx)
监听到,进而执行解析Excel
文件的readWorkbookFromLocalFile
方法,数据解析完成后,如下图所示
此时还需要对数据进行进一步处理
// 处理数据
function formatingData(dataSource) {
var header = dataSource[0];
var result = [];
for (var i = 1; i < dataSource.length; i++) {
if (dataSource[i].length === 0) {
continue; // 跳过空数组
}
var obj = {};
for (var j = 0; j < header.length; j++) {
var value = dataSource[i][j] || '';
obj[header[j]] = value;
}
result.push(obj);
}
// Excel数据处理好了,通信给页面 result
toContent(result)
}
function toContent(dresult) {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
let message = { info: '来自popup的信息', dresult }
chrome.tabs.sendMessage(tabs[0].id, message, res => {
console.log('popup=>content')
console.log(res)
})
})
}
首先通过formatingData
方法,对数据进行转换,转换以后通过模块通讯
将数据传递给content
来渲染试图 \
解析后的数据为如下
content.js
popup.js
中,将数据通过chrome.tabs.sendMessage
API通讯方式传递给了content.js
,那么就需要在content.js
注册监听事件chrome.runtime.onMessage.addListener
来接受通讯传递的数据
// 姓名
const Names = []
// 身份证号
const IDcards = []
// 初始数据对象
const UserObj = {}
// 监听导入的数据
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
// console.log('message ===> ', message)
for (const item of message.dresult) {
const sfzh = '身份证号'
const xm = '姓名'
const remark = '备注'
// 存储数据
UserObj[item[sfzh]] = { sfzh: item[sfzh], xm: item[xm], remark: item[remark] }
// 存储姓名和身份证号
Names.push(item[xm])
IDcards.push(item[sfzh])
}
// 数据面板
initPanel()
// 导入数据后,初始化
initTable()
// 同步面板
syncPanelActive()
})
以上代码中,声明了三个变量,来分别存储和记录数据:姓名、身份证号以及对象形式的存储(根据身份证号为key的键值对存放数据,方便记录用户信息)
// 姓名
const Names = []
// 身份证号
const IDcards = []
// 初始数据对象
const UserObj = {}
现在就已经获取到了数据,就可以开始渲染DOM
元素了
createRoot
生成底部按钮:上一个、下一个、查询、标记、导出等
function createRoot() {
const Dom = document.getElementsByTagName('body')[0]
const Button1 = cDom({
domName: 'button',
id: "last",
textContent: '上一个',
className: 'cDombtn btn btn-primary',
})
const Button2 = cDom({
domName: 'button',
id: "next",
textContent: '下一个',
className: 'cDombtn btn btn-primary',
})
// 查询
const Button3 = cDom({
domName: 'button',
id: "query",
textContent: '查询国家资格证书',
className: 'cDombtn btn btn-success'
})
const Button6 = cDom({
domName: 'button',
id: "queryZydj",
textContent: '查询职业技能等级证书',
className: 'cDombtn btn btn-success'
})
// 标记该学生
const Button4 = cDom({
domName: 'button',
id: "sign",
textContent: '标记通过',
className: 'cDombtn btn btn-success'
})
const Button5 = cDom({
domName: 'button',
id: "signLook",
textContent: '取消标记',
className: 'cDombtn btn btn-danger'
})
const Button7 = cDom({
domName: 'button',
id: "exportExcel",
textContent: '标记文档导出',
className: 'cDombtn btn btn-success'
})
// 父盒子
const parentBox = cDom({
domName: 'div',
id: "parentBox",
})
// 按钮盒子
const btnBox = cDom({
domName: 'div',
id: "btnBox",
})
const addEvent = {
last: LastLook,
next: NextLook,
query: queryFun,
queryZydj: queryZydjFun,
sign: signFun,
signLook: signLookFun,
exportExcel: exportExcelFun
}
const Doms = [Button1, Button2, Button3, Button6, Button4, Button5, Button7]
Doms.map((v, i) => {
console.log('v.id ===> ', v.id)
v.addEventListener('click', addEvent[v.id])
})
btnBox.prepend(...Doms)
parentBox.prepend(btnBox)
Dom.prepend(parentBox)
// 初始化标记
// initTable()
}
initPanel
生成数据展示面板
function initPanel() {
const Dom = document.getElementsByTagName('body')[0]
var table = document.createElement('table');
table.id = 'table23Excel'
var tbody = document.createElement('tbody');
table.appendChild(tbody);
// 表头
var rowTh = document.createElement('tr');
var nameCell = document.createElement('td');
nameCell.classList = 'table_xm'
nameCell.textContent = '姓名';
rowTh.appendChild(nameCell);
var sfzhCell = document.createElement('td');
sfzhCell.classList = 'table_sfzh'
sfzhCell.textContent = '身份证号';
rowTh.appendChild(sfzhCell);
var remarkCell = document.createElement('td');
remarkCell.classList = 'table_remark'
remarkCell.textContent = '备注';
rowTh.appendChild(remarkCell);
tbody.appendChild(rowTh);
// 表单内容
for (const liItem in UserObj) {
const data = UserObj[liItem]
var row = document.createElement('tr');
row.classList = 'rowTrAll'
var nameCell = document.createElement('td');
nameCell.textContent = data.xm;
row.appendChild(nameCell);
var sfzhCell = document.createElement('td');
sfzhCell.textContent = data.sfzh;
row.appendChild(sfzhCell);
var remarkCell = document.createElement('td');
remarkCell.textContent = data.remark;
remarkCell.classList = 'remarkCell'
row.appendChild(remarkCell);
tbody.appendChild(row);
}
Dom.prepend(table)
}
initTable
初始化数据,将数据动态添加到表单输入框中
function initTable() {
// 查询国家级别
const Index = getIndex()
const CID = getDom("CID")
const Name = getDom("Name")
Name.html(Names[Index])
CID.html(IDcards[Index])
// 职业技能级别
const D = document.getElementById('search_box')
const inputArr = D.getElementsByTagName('input')
inputArr[1].value = IDcards[Index]
inputArr[2].value = Names[Index]
}
syncPanelActive
同步面板选择
在初始状态或者切换上一位、下一位的时候,同步右侧面板的选中状态,当然,为了保证选中的数据始终出现在视图中,我们需要判断,如果选中的数据的位置超出了可视区域,就让其滚动到盒子顶部,让选中的数据始终出现在视图中,实现方法为scrollView
function syncPanelActive() {
// rowTrAll
const rowTrAll = document.querySelectorAll('.rowTrAll')
const index = getIndex()
if (index >= 0 && index < rowTrAll.length) {
for (var i = 0; i < rowTrAll.length; i++) {
if (i === index) {
rowTrAll[i].classList.add('rowActive');
// 滚动元素到视图范围内的顶部
scrollView(rowTrAll[i])
} else {
rowTrAll[i].classList.remove('rowActive');
}
}
}
}
监测数据滚动
// 滚动到可视化范围内
function scrollView(Dom) {
// 判断元素是否在可视化范围内
var rect = Dom.getBoundingClientRect();
const panelBox = document.getElementById('table23Excel')
var viewportHeight = panelBox.clientHeight;
// 鉴定滚动
if (rect.top >= viewportHeight || rect.top < 100) {
// 不在可视化范围内
Dom.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
导入后,插件展示效果如下
标记
、导出
标注学生是否通过,其实就是修改UserObj
对象中的remark
字段,然后渲染到对应的面板当中去
// 标记学生 通过
function signFun() {
let i = getIndex()
UserObj[IDcards[i]].remark = '通过'
// 同步panel页面
signSet()
}
// 标记学生 未通过
function signLookFun() {
let i = getIndex()
UserObj[IDcards[i]].remark = ''
// 同步panel页面
signSet()
}
导出数据为Excel
导出数据这一块没有用到XLSX,而是使用的原生js进行导出,逻辑也很简单,就是利用浏览器本身的功能进行导出
// 导出标记的学生信息
function exportExcelFun() {
function dataAnalysis(obj) {
// 将对象处理成数组
const arr = []
for (const key in obj) {
const item = obj[key]
arr.push({
xm: item.xm,
sfzh: item.sfzh,
remark: item.remark
})
}
return arr
}
const jsonData = dataAnalysis(UserObj)
console.log('dataSource ===> ', jsonData)
// 列标题,逗号隔开,每一个逗号就是隔开一个单元格
let str = `姓名,身份证号,备注\n`;
// 增加\t为了不让表格显示科学计数法或者其他格式
for (let i = 0; i < jsonData.length; i++) {
for (const key in jsonData[i]) {
// console.log('jsonData ===> ', jsonData[i], key)
str += `${jsonData[i][key] + '\t'},`;
}
str += '\n';
}
// encodeURIComponent解决中文乱码
const uri = 'data:text/csv;charset=utf-8,\ufeff' + encodeURIComponent(str);
// 通过创建a标签实现
const link = document.createElement("a");
link.href = uri;
// 对下载的文件命名
link.download = "标记文档导出.xlsx";
link.click();
}
其他方法
// 获取dom
function getDom(id) {
const Dom = document.getElementById(id)
return {
html: (v) => Dom.value = v
}
}
// 上一个
function LastLook() {
let I = getIndex()
if (I == 0) {
alert('已经到头了')
} else {
setIndex(I - 1)
// 切换表单数据
initTable()
// 同步表单选中的
syncPanelActive()
}
}
// 下一个
function NextLook() {
let I = getIndex()
// console.log('Names ===> ', Names)
if (I >= Names.length - 1) {
alert('已经到底了')
} else {
setIndex(I + 1)
// 切换表单数据
initTable()
// 同步表单选中的
syncPanelActive()
}
}
// 查询
const queryHistory = {}
function queryFun() {
const btn = document.getElementById('bt1')
btn.click()
}
// 查询职业
function queryZydjFun() {
const D = document.getElementById('search_box')
D.getElementsByTagName('input')[3].click()
}
// 创建一个dom
function cDom(args = {}) {
if (!args.domName) {
console.error('domName is undefined')
return
}
const Dom = document.createElement(args.domName)
for (const key in args) {
if (Object.hasOwnProperty.call(args, key)) {
Dom[key] = args[key];
}
}
return Dom
}
// 存储查询到了哪一个?
function setIndex(index) {
window.localStorage.setItem('CacleIndex', index)
}
function getIndex() {
return +(window.localStorage.getItem('CacleIndex') || '0')
}
到这里是不是才发现 貌似少了一个 background 模块,事实上,本示例并没有用到这个模块........
基本到这里我们就实现了一个很简单的Google浏览器插件的开发,也满足了领导的需求,但是Google插件能做的还有很多,包括实现浏览器录屏、抓包等等等,这些功能大家可以自行探索,也可以一起交流。
交付
当我信心满满的交付给领导的时候, 幻想着自己不用多久,就会升职加薪,当上总经理,出任CEO,迎娶白富美,走上人生巅峰,但是领导却.....

结尾
本文涉及的Google插件开发深度比较浅,适合新手学习,如果你对google插件有一定的了解,可以直接跳出本文,直接阅读代码即可,当然本文涉及到的内容如果出现错误,劳烦各位指出,感谢
转载自:https://juejin.cn/post/7256726151280115749