不懂前端?没开发过插件?
没关系!现在有了AI助手,比如 ChatGPT(o3-mini-high) 和 Cursor+Claude-3.7-sonnet,
开发一个实用Chrome插件,只需要几个Prompt!
今天带你亲身体验:
从0开始,生成Chrome插件
遇坑、调试、修复
最后对比不同AI模型的开发体验
🧠 项目目标
浏览网页时输入关键词
点击【高亮】,标出所有匹配的文本
点击【清除】,一键恢复正常页面
第一阶段|用ChatGPT(o3-mini-high)实战开发(含调试过程)
1️⃣ 初次生成
Prompt:
写一个Chrome扩展程序,支持Manifest V3。用户可以输入一段文字,点击按钮后,在当前网页中高亮所有包含这段文字的内容。
ChatGPT(o3-mini-high)快速生成了三份核心文件:
📄 manifest.json(初版)
{
"manifest_version": 3,
"name": "文本高亮器",
"version": "1.0",
"permissions": ["scripting", "activeTab"],
"action": {
"default_popup": "popup.html"
}
}
✅ 使用了正确的 Manifest V3
📄 popup.html(初版)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文本高亮器</title>
</head>
<body>
<input type="text" id="searchText" placeholder="请输入要高亮的文字" />
<button id="highlightButton">高亮</button>
<script src="popup.js"></script>
</body>
</html>
很基础,一个输入框+一个按钮。
📄 popup.js(初版)
document.getElementById('highlightButton').addEventListener('click', async () => {
const searchText = document.getElementById('searchText').value.trim();
if (!searchText) return;
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) return;
chrome.scripting.executeScript({
target: { tabId: tab.id },
function: highlightText,
args: [searchText]
});
});
function highlightText(searchText) {
const regex = newRegExp(searchText, 'gi');
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
let node;
while ((node = walker.nextNode())) {
if (node.parentNode && !['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.parentNode.nodeName)) {
node.parentNode.innerHTML = node.parentNode.innerHTML.replace(regex, match =>
`<span style="background: yellow;">${match}</span>`);
}
}
}
2️⃣ 第一次测试:出现问题
加载后,初步测试可以高亮,但很快发现严重问题:
连续搜索时,之前的高亮内容不会清除!
页面会越叠越多高亮,阅读体验变差。
3️⃣ 尝试修正:引入清除高亮逻辑
于是我让ChatGPT帮我补充:
Prompt:
请在每次高亮前,清除上次的高亮内容。
ChatGPT生成了一个新的removeHighlights
方法,并在highlightText
中先调用它。
但是!在测试时又遇到新报错:
Uncaught ReferenceError: removeHighlights is not defined
原因:在 Chrome 插件中注入的函数,只能使用自身作用域内定义的函数,不能引用外部函数!
4️⃣ 正确修正:内嵌removeHighlights
于是进一步优化,把removeHighlights
写成highlightText函数内部函数。
最终修正后的 popup.js:
📄 popup.js(修正版 ✅ 正常运行)
document.getElementById('highlightButton').addEventListener('click', async () => {
const searchText = document.getElementById('searchText').value.trim();
if (!searchText) return;
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) return;
chrome.scripting.executeScript({
target: { tabId: tab.id },
function: highlightText,
args: [searchText]
});
});
function highlightText(searchText) {
function removeHighlights() {
document.querySelectorAll('span.my-highlighter-highlight').forEach(span => {
span.replaceWith(span.textContent);
});
document.querySelectorAll('span[data-highlighter]').forEach(span => {
span.replaceWith(span.textContent);
});
}
removeHighlights();
if (!searchText) return;
const escapeRegExp = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escapedText = escapeRegExp(searchText);
const regex = newRegExp(escapedText, 'gi');
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
let node;
const nodesToHighlight = [];
while ((node = walker.nextNode())) {
if (node.parentNode && !['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.parentNode.nodeName)) {
if (regex.test(node.nodeValue)) {
nodesToHighlight.push(node);
}
}
}
nodesToHighlight.forEach(textNode => {
const wrapper = document.createElement('span');
wrapper.setAttribute('data-highlighter', 'true');
wrapper.innerHTML = textNode.nodeValue.replace(regex, match =>
`<span class="my-highlighter-highlight" style="background-color: yellow;">${match}</span>`
);
textNode.parentNode.replaceChild(wrapper, textNode);
});
}
✅ 这次测试完全OK:
每次点击前清除旧高亮
新的关键词正常高亮
页面干净不乱
第二阶段|用Cursor + Claude-3.7-sonnet开发进阶版插件
同样的Prompt,我使用Cursor编辑器调用Claude-3.7-sonnet模型重新生成。
这次Claude输出了更完整、更专业的项目结构,而且代码一次生成就能直接用,不需要再调试!
📦 项目结构(Claude版)
TextHighlighterClaude/
├── manifest.json
├── popup.html
├── popup.js
├── content.js
📄 manifest.json
{
"manifest_version": 3,
"name": "Text Highlighter",
"version": "1.0",
"description": "Highlights specified text on the current webpage",
"action": {
"default_popup": "popup.html"
},
"permissions": ["activeTab", "scripting"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}
📄 popup.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Text Highlighter</title>
<style>
body {
width: 300px;
padding: 15px;
font-family: Arial, sans-serif;
}
#searchText {
width: 100%;
padding: 8px;
margin-bottom: 10px;
}
#highlightButton, #clearButton {
width: 100%;
padding: 8px;
margin-top: 5px;
border: none;
border-radius: 4px;
cursor: pointer;
}
#highlightButton {
background-color: #4285f4;
color: white;
}
#clearButton {
background-color: #f44336;
color: white;
}
</style>
</head>
<body>
<h2>Text Highlighter</h2>
<p>Enter text to highlight on the current page:</p>
<input type="text" id="searchText" placeholder="Enter text...">
<button id="highlightButton">Highlight Text</button>
<button id="clearButton">Clear Highlights</button>
<script src="popup.js"></script>
</body>
</html>
📄 popup.js
document.addEventListener('DOMContentLoaded', function() {
const highlightButton = document.getElementById('highlightButton');
const clearButton = document.getElementById('clearButton');
const searchText = document.getElementById('searchText');
highlightButton.addEventListener('click', function() {
const textToHighlight = searchText.value.trim();
if (!textToHighlight) {
alert('Please enter text to highlight');
return;
}
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.scripting.executeScript({
target: {tabId: tabs[0].id},
function: highlightTextOnPage,
args: [textToHighlight]
});
});
});
clearButton.addEventListener('click', function() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.scripting.executeScript({
target: {tabId: tabs[0].id},
function: clearHighlightsOnPage
});
});
});
});
function highlightTextOnPage(searchText) {
const event = new CustomEvent('highlight-text', { detail: { text: searchText } });
document.dispatchEvent(event);
}
function clearHighlightsOnPage() {
const event = new CustomEvent('clear-highlights');
document.dispatchEvent(event);
}
📄 content.js
document.addEventListener('highlight-text', function(e) {
const searchText = e.detail.text;
highlightText(searchText);
});
document.addEventListener('clear-highlights', function() {
clearHighlights();
});
let highlightElements = [];
function highlightText(searchText) {
clearHighlights();
if (!searchText) return;
const searchRegex = newRegExp(escapeRegExp(searchText), 'gi');
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
let node;
const textNodes = [];
while (node = walker.nextNode()) {
if (!['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.parentNode.nodeName)) {
textNodes.push(node);
}
}
textNodes.forEach(function(textNode) {
const matches = textNode.nodeValue.match(searchRegex);
if (matches && matches.length > 0) {
const fragment = document.createDocumentFragment();
let lastIndex = 0;
let match;
searchRegex.lastIndex = 0;
while ((match = searchRegex.exec(textNode.nodeValue)) !== null) {
const matchIndex = match.index;
if (matchIndex > lastIndex) {
fragment.appendChild(document.createTextNode(textNode.nodeValue.substring(lastIndex, matchIndex)));
}
const highlightSpan = document.createElement('span');
highlightSpan.className = 'text-highlighter-extension';
highlightSpan.style.backgroundColor = '#ffff00';
highlightSpan.style.color = '#000000';
highlightSpan.appendChild(document.createTextNode(match[0]));
highlightElements.push(highlightSpan);
fragment.appendChild(highlightSpan);
lastIndex = matchIndex + match[0].length;
}
if (lastIndex < textNode.nodeValue.length) {
fragment.appendChild(document.createTextNode(textNode.nodeValue.substring(lastIndex)));
}
textNode.parentNode.replaceChild(fragment, textNode);
}
});
}
function clearHighlights() {
highlightElements.forEach(function(el) {
if (el.parentNode) {
const textNode = document.createTextNode(el.textContent);
el.parentNode.replaceChild(textNode, el);
}
});
highlightElements = [];
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
✅ Claude版不仅功能更全,还额外支持【一键清除】,并且代码模块化、维护性更强!
📈 最后总结|ChatGPT vs Claude体验对比
体验项目 |
ChatGPT(o3-mini-high) |
Cursor+Claude-3.7-sonnet |
---|---|---|
开发速度 |
快,但需中途多次调试 |
直接一版成型 |
功能完整度 |
需要自己补充清除功能 |
自带完整高亮+清除 |
代码规范性 |
简单直接 |
工程结构标准、可扩展性高 |
适合场景 |
快速原型、练手 |
正式产品、小工具开发 |
AI已经大大提升了开发生产力,每个人都有机会用简单的指令打造自己的小工具、小产品!