Chrome Extension 开发上手(附GPT搜索辅助插件示例)
前言
前段时间,设计提了一个需求,希望使用loops制作邮件的html,而不是使用semplates。由于我们采用aws ses发送邮件,而loops不支持将模板邮件直接上传到aws ses上。以往流程中,需要手动在浏览器控制台导出preview状态下的email,在经过各种格式处理后手动上传到aws ses,比较费时。于是我花了点时间研究了一下chrome extension 的开发流程,将这一耗时流程进行了自动化。最终该插件可以在loops页面中一键自动提取email html,完成邮件打开率,点击率,退订按钮和logo的html标签替换,并自动上传到aws ses。
本文旨在记录chrome extension开发中的一些收获。并且教大家实现一个gpt-search-chrome-extension插件,该插件可以在使用google搜索时自动就搜索内容向gpt提问,并将gpt生成结果显示在搜索结果右侧。
仓库地址: https://github.com/BertramRay/chatgpt-search-chrome-extension
chrome extension开发官方文档: https://developer.chrome.com/docs/extensions
最终结果:
过程
克隆项目
将项目克隆至本地,打开chrome扩展插件页面chrome://extensions,选择加载已解压的扩展程序,选择刚克隆下来的仓库目录即可。
目录结构
插件文件夹的目录结构如下所示
background.js
content.js
gpt-response.html
manifest.json
popup.html
popup.js
README.md
manifest.json
这是扩展的配置文件,定义了扩展的名称、版本、权限需求、文件位置等核心信息。它是任何Chrome扩展或Web扩展必须包含的文件,告诉浏览器如何加载和运行扩展。文件内容如下:
{
"manifest_version": 3,
"name": "GPT Search Companion",
"version": "1.0",
"description": "Add GPT-generated insights to Google Search results.",
"permissions": [
"activeTab",
"storage"
],
"content_scripts": [
{
"matches": ["*://*.google.com/*"],
"js": ["content.js"]
}
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html"
},
"host_permissions": [
"<all_urls>"
],
"web_accessible_resources": [
{
"resources": ["gpt-response.html"],
"matches": ["<all_urls>"]
}
]
}
manifest_version
: 指定了清单文件的版本。当前大多数现代扩展使用manifest_version
3。name
: 扩展的名称。version
: 扩展的版本号。开发者应该在每次发布更新时增加此版本号。description
: 对扩展功能的简短描述。permissions
: 列出了扩展所需的权限。"activeTab"
允许扩展访问用户当前活动标签页的URL等信息;"storage"
允许扩展使用浏览器的存储功能。content_scripts
: 定义了一组脚本("js": ["content.js"]
),这些脚本将按照"matches"
字段指定的模式注入到特定页面中。在这里,它会匹配所有的*://*.google.com/*
地址,表示这个脚本会运行在所有Google搜索页面上。background
: 指定了一个后台脚本 ("service_worker": "background.js"
)。这个脚本常驻后台运行,用于管理扩展的长期运行逻辑。action
: 定义了扩展的图标被点击时显示的默认弹出页面为"popup.html"
。host_permissions
: 申请对所有网站(<all_urls>
)的访问权限。这允许扩展访问并修改所有网站的数据。web_accessible_resources
: 在这个数组中列出的资源可以被任意网页通过创建<script>
,<link>
,<img>
, 等方式访问。在此例中,"gpt-response.html"
被列为一个可访问资源,使得任何页面都可以请求这个文件。"matches": ["<all_urls>"]
表明该资源对所有URL都可访问。
background.js (背景脚本)
背景脚本是扩展运行的核心部分之一,主要用于处理扩展的后台逻辑。它在扩展被安装后以隐式方式常驻运行,即使所有扩展页面都被关闭了,它仍然能够执行。由于其持久性和全局性的特点,背景脚本通常用于以下场景:
监听来自浏览器的事件,如标签页的更新、窗口的创建等。
处理来自其他脚本(如内容脚本或弹出脚本)的消息。
执行定时任务,如使用
setInterval
或setTimeout
。维持或管理全局状态,例如用户的偏好设置。
在Manifest V3中,背景脚本通过定义为服务工作线程(service worker)来实现。
本插件中,background.js
主要负责监听google搜索页上运行的content.js
发送的消息,从local storage中获取存储的api key,并向OpenAI调用流式API,最后向content.js
发送消息。
// background.js
console.log('chatgpt-search-chrome-extension/background.js loaded');
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log('Received query:', request.query);
chrome.storage.local.get('openai_api_key', async function(data) {
const apiKey = data.openai_api_key;
if (!apiKey) {
alert("OpenAI API Key is not set.");
return;
}
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages: [
{role: "system", content: "You should answer like goole search engine, user will provide prompt as search query."},
{role: "user", content: request.query}
],
stream: true
})
});
// 注意:由于stream参数为true,响应将是一个流式的。
// 在实际应用中,你需要处理这个流式响应,这里仅作演示。
const reader = response.body.getReader();
let answer = '';
reader.read().then(function processText({ done, value }) {
if (done) return;
// 将Uint8Array转换为字符串
const textChunk = new TextDecoder("utf-8").decode(value);
// 移除 `data: ` 前缀并分割字符串
const dataLines = textChunk.split('\n').map(line => line.substring(6).trim()).filter(line => line !== '[DONE]');
dataLines.forEach(line => {
if (!line) return; // 忽略空行
try {
const responseData = JSON.parse(line); // 将行解析为JSON对象
if (responseData.choices && responseData.choices.length > 0) {
const content = responseData.choices[0].delta.content;
if (content) { // 确保content存在
answer += content; // 将内容拼接到完整的回答中
}
}
} catch (error) {
console.error('Error parsing response data:', error);
}
});
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
var activeTab = tabs[0];
chrome.tabs.sendMessage(activeTab.id, {"text": answer});
});
// 下一次迭代
return reader.read().then(processText);
});
return true; // 必须返回true表示异步响应
});
}
);
content.js (内容脚本)
内内容脚本与网页直接交互,可以读取或修改网页的DOM,相当于在目标网页上运行的用户脚本。它们在指定的页面上下文中执行,但是与网页原生脚本隔离,这样既可以操作网页内容,又不会直接暴露扩展的内部逻辑给外部脚本。内容脚本的典型用途包括:
修改或美化网页UI。
提取页面信息。
向页面注入自定义元素或功能。
捕捉用户行为并响应。
内容脚本通过匹配模式在特定的URL上自动或按需执行,这些匹配模式在扩展的manifest.json
文件中配置。
本插件中,content.js
用于向Google搜索结果页面注入GPT生成的见解。
// content.js
console.log('chatgpt-search-chrome-extension/content.js loaded');
const query = document.querySelector('textarea').value; // 获取Google搜索框的内容
let gpt_response_div = false;
// 向background.js发送消息
chrome.runtime.sendMessage({query: query});
chrome.runtime.onMessage.addListener(async function(message, sender, sendResponse) {
// 假设message中包含了要显示的文本 "text"
if (message.text) {
const gptResponseArea = document.querySelector('div[id="gpt-response"]');
if (!gpt_response_div) {
gpt_response_div = true;
let sidebar = document.querySelector('div[id="rhs"]');
if (!sidebar) {
const newSidebar = document.createElement('div');
newSidebar.id = 'rhs';
const rcnt = document.querySelector('div[id="rcnt"]');
rcnt.appendChild(newSidebar);
sidebar = newSidebar;
}
// add gpt response div
fetch(chrome.runtime.getURL('gpt-response.html'))
.then(response => response.text())
.then(data => {
const newElement = document.createElement('div');
newElement.innerHTML = data;
// 将新创建的div插入到父元素的最前面
if (sidebar.firstChild) {
sidebar.insertBefore(newElement, sidebar.firstChild);
} else {
// 如果parent没有子元素,就直接添加
sidebar.appendChild(newElement);
}
})
.catch(err => console.error(err));
}
gptResponseArea.innerText = message.text;
}
});
popup.js (弹出脚本)
弹出脚本与弹出页面(Popup Page)相关联,负责处理扩展图标旁的小弹出窗口(通常是用户界面)的逻辑。当用户点击浏览器工具栏中的扩展图标时,弹出页面就会显示,弹出脚本则负责使这个界面动起来,它可以包括:
监听和处理用户在弹出页面上的操作。
与背景脚本或内容脚本通信,请求数据或触发特定行为。
动态更新弹出页面的DOM以反映状态变化或展示信息。
弹出页面和弹出脚本为用户提供了与扩展交互的直接界面,非常适合进行设置更改、展示信息快照或提供简单控制。
本插件中,popup.js
用于保存用户输入的api key到local storage。
// popup.js
console.log('chatgpt-search-chrome-extension/popup.js loaded');
document.getElementById('save').addEventListener('click', function() {
const apiKey = document.getElementById('apiKey').value;
chrome.storage.local.set({'openai_api_key': apiKey}, function() {
console.log('API Key saved.');
});
});
popup.html
这是扩展的弹出界面HTML文件。当用户点击浏览器工具栏中的扩展图标时,这个界面将被显示。它通常用于提供给用户一个交互界面,例如允许用户激活特定功能、查看额外信息或更改设置。本插件中,用户可以在popup.html
设置用于请求OpenAI API的api key,如图所示。
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
<title>GPT Search Settings</title>
</head>
<body>
<div>
<label for="apiKey">OpenAI API Key:</label>
<input type="text" id="apiKey" />
<button id="save">Save</button>
</div>
<script src="popup.js"></script>
</body>
</html>
gpt-response.html
这是一个HTML文件,被设计为显示GPT生成的数据或见解的模板。由于它被列为web_accessible_resources,它可以被内容脚本动态加载到用户当前访问的网页中,作为展示信息的一部分。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>gpt-response</title>
<style>
#gpt-response {
padding: 20px;
margin-top: 20px;
background-color: #f9f9f9; /* 浅灰色背景 */
border: 1px solid #ddd; /* 浅灰色边框 */
border-radius: 8px; /* 圆角边框 */
box-shadow: 0 4px 6px rgba(0,0,0,0.1); /* 轻微阴影效果 */
color: #333; /* 深灰色文字,提高对比度 */
font-family: Arial, sans-serif; /* 使用无衬线字体 */
font-size: 16px; /* 适中的字体大小 */
line-height: 1.5; /* 行高,提高可读性 */
min-height: 100px; /* 最小高度 */
max-width: 90%; /* 防止元素过宽 */
overflow-y: auto; /* 内容过多时显示滚动条 */
}
/* 深色模式时的样式 */
@media (prefers-color-scheme: dark) {
#gpt-response {
background-color: #333; /* 深色背景 */
color: #f9f9f9; /* 浅色文字 */
border-color: #444; /* 边框颜色 */
}
}
</style>
</head>
<body>
<!-- 此处可以插入其他内容 -->
<div id="gpt-response">这里将显示GPT的回复文本。</div>
</body>
</html>
输出调试
需要注意的是,每次修改背景脚本或内容脚本后都需要在扩展页点击刷新方可生效。
在Chrome插件开发中,使用控制台(Console)输出来调试是一种常见且有效的方法。对于background.js
、content.js
和popup.js
这三个不同类型的脚本,它们的控制台输出可以在不同的地方查看:
background.js
- 背景脚本
背景脚本的输出需要在扩展的背景页控制台中查看:
在浏览器地址栏输入
chrome://extensions/
并回车,进入扩展管理页面。确保开发者模式已经被启用(页面右上角的开关)。
找到扩展,点击“详情”按钮。
在详情页面中找到“背景页”(可能标注为"background page"或"service worker"),点击旁边的“检查视图”或“Inspect views”链接。
这将打开一个开发者工具窗口,其中包含了背景脚本的控制台输出。
content.js
- 内容脚本
内容脚本的输出会出现在其所影响的网页的控制台中:
前往内容脚本运行的网页。
右键点击页面,选择“检查”(Inspect)或通过快捷键(例如,在Windows上是
Ctrl+Shift+I
,在Mac上是Cmd+Opt+I
)打开开发者工具。切换到“控制台”(Console)标签页。
在这里可以看到内容脚本的所有控制台输出。
popup.js
- 弹出脚本
弹出脚本的输出可以在弹出页面的控制台中查看。要查看弹出脚本的控制台输出,请按照以下步骤操作:
点击浏览器工具栏中的扩展图标以打开弹出页面。
在弹出页面上,右键点击并选择“检查”(Inspect)以打开开发者工具。(或者,如果弹出窗口太小,你可能需要先在
chrome://extensions/
的您的扩展详情页内直接打开弹出页面的检查视图)切换到开发者工具的“控制台”(Console)标签页。
这个控制台会显示弹出脚本的输出。
脚本通信
这部分简单介绍一下插件中不同脚本的通信,在Chrome扩展中,不同脚本之间的通信是实现复杂功能和数据共享的重要机制。Chrome提供了几种不同的通信方式,主要包括:
1. 消息传递 (Message Passing)
最常用的通信机制是通过消息传递。这可以在内容脚本、背景脚本、弹出脚本等之间进行。
单次请求: 使用
chrome.runtime.sendMessage
发送消息,使用chrome.runtime.onMessage.addListener
接收消息。
发送消息示例:
// 在content.js或popup.js中
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
接收消息示例:
javascript复制代码// 在background.js中
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.greeting === "hello") {
sendResponse({farewell: "goodbye"});
}
return true;
});
长连接: 适用于需要多次或持续通信的场景。
建立连接示例:
// 在popup.js或content.js中
var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
if (msg.question === "Who's there?")
port.postMessage({answer: "Madame"});
});
2. 使用存储 (Using Storage)
虽然不是直接的通信机制,但扩展组件可以通过chrome.storage
API共享和存储数据。一个脚本写入数据,另一个脚本读取数据。这对于保存设置和共享状态尤其有用。
// 写入数据
chrome.storage.sync.set({key: value}, function() {
console.log('Value is set to ' + value);
});
// 读取数据
chrome.storage.sync.get(['key'], function(result) {
console.log('Value currently is ' + result.key);
});
扩展发布
如果只打算个人使用,到此即可完成,如果想要将扩展发布到chrome 网上应用商店,需要注册一个chrome开发者账号,需要有一张visa卡和缴纳5美元注册费。
受限于篇幅,这里不再赘述
参考文档
chrome extension helloworld tutorial https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world