下面我将为你提供一个完整的 Chrome 扩展项目结构、所有必需的代码文件以及详细的安装和使用说明。
项目结构
首先,在你的电脑上创建一个文件夹,例如 tutorial-maker-extension。在这个文件夹中,创建以下文件和子文件夹:
tutorial-maker-extension/ ├── manifest.json # 扩展的配置文件 (核心) ├── background.js # 后台脚本,用于监听图标点击 ├── content.js # 内容脚本,注入到网页中实现核心功能 ├── style.css # 注入到网页中的样式文件 ├── icons/ # 存放图标的文件夹 │ ├── icon16.png │ ├── icon48.png │ └── icon128.png └── libs/ # 存放第三方库的文件夹 ├── html2canvas.min.js └── jspdf.umd.min.js
准备工作:
下载库文件:
下载 html2canvas: 点击这里 (右键 -> 另存为...),保存到 libs 文件夹。
下载 jsPDF: 点击这里 (右键 -> 另存为...),保存到 libs 文件夹。
准备图标:
你需要准备三个尺寸的 PNG 图标:16x16, 48x48, 128x128 像素。你可以使用任何你喜欢的图标,或者在网上找一个免费的 "tutorial" 或 "camera" 图标。将它们命名为 icon16.png, icon48.png, icon128.png 并放入 icons 文件夹。
第1步: manifest.json
这是扩展的配置文件,告诉 Chrome 扩展的名称、权限、需要运行的脚本等。
{ "manifest_version": 3, "name": "网页图文教程生成器", "version": "1.0", "description": "点击网页元素,自动生成带截图和文本的步骤教程,并导出为PDF。", "permissions": [ "activeTab", "scripting" ], "background": { "service_worker": "background.js" }, "action": { "default_icon": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" }, "default_title": "制作图文教程" }, "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" }, "content_scripts": [ { "matches": ["<all_urls>"], "js": [ "libs/html2canvas.min.js", "libs/jspdf.umd.min.js", "content.js" ], "css": ["style.css"] } ] }
关键点解释:
"manifest_version": 3: 使用最新的 Manifest V3 规范。
"permissions": 我们只需要 activeTab 和 scripting 权限,这比请求所有网站的权限更安全。activeTab 允许我们在用户点击扩展图标时与当前活动的标签页交互。
"background": 指定了后台服务工作线程,它将处理点击扩展图标的事件。
"action": 定义了浏览器工具栏中扩展图标的行为。
"content_scripts": 这是核心部分。它告诉 Chrome 自动将 libs 里的库文件、我们自己的 content.js 和 style.css 注入到所有页面 (<all_urls>) 中。注意JS文件的顺序很重要,必须先加载库文件。
第2步: background.js
这个脚本在后台运行,它的唯一任务是监听用户点击扩展图标的动作,并向当前页面的内容脚本发送一条消息,告诉它“该干活了”。
// background.js chrome.action.onClicked.addListener((tab) => { // 当用户点击扩展图标时 // 向当前激活的标签页发送一个消息 chrome.tabs.sendMessage(tab.id, { action: "toggle_tutorial_maker" }); });
第3步: style.css
将之前油猴脚本中的所有 CSS 规则复制到这个文件中。
/* style.css */ .wtm-sidebar { position: fixed; top: 0; right: -450px; /* Initially hidden */ width: 400px; height: 100%; background-color: #f8f9fa; border-left: 1px solid #dee2e6; box-shadow: -5px 0 15px rgba(0,0,0,0.1); z-index: 2147483647; /* Max z-index */ transition: right 0.5s ease; display: flex; flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .wtm-sidebar.show { right: 0; } .wtm-sidebar-header { padding: 15px; background-color: #e9ecef; border-bottom: 1px solid #ced4da; display: flex; justify-content: space-between; align-items: center; } .wtm-sidebar-header h3 { margin: 0; font-size: 18px; } .wtm-sidebar-header .wtm-controls button { margin-left: 10px; padding: 5px 10px; cursor: pointer; border-radius: 4px; border: 1px solid transparent; font-size: 14px; } .wtm-record-btn { background-color: #007bff; color: white; } .wtm-record-btn.recording { background-color: #dc3545; } .wtm-export-btn { background-color: #28a745; color: white; } .wtm-steps-container { flex-grow: 1; overflow-y: auto; padding: 10px; } .wtm-step { background-color: white; border: 1px solid #ddd; border-radius: 5px; padding: 15px; margin-bottom: 10px; position: relative; cursor: grab; } .wtm-step.dragging { opacity: 0.5; } .wtm-step-header { font-weight: bold; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; } .wtm-step-delete-btn { background: none; border: none; color: #dc3545; font-size: 20px; cursor: pointer; padding: 0 5px; } .wtm-step textarea { width: 100%; min-height: 50px; border: 1px solid #ccc; border-radius: 4px; padding: 8px; font-size: 14px; resize: vertical; margin-bottom: 10px; box-sizing: border-box; } .wtm-step img { max-width: 100%; border: 1px solid #eee; margin-top: 10px; border-radius: 4px; } .wtm-click-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 123, 255, 0.1); border: 2px dashed #007bff; z-index: 2147483646; pointer-events: none; /* Important */ cursor: crosshair; } .wtm-loader { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 20px; border-radius: 8px; z-index: 2147483647; display: none; /* Initially hidden */ }
第4步: content.js
这是扩展的核心逻辑,与油猴脚本非常相似,但做了一些调整以适应扩展的架构。
// content.js // 使用 jspdf 库 const { jsPDF } = window.jspdf; let isRecording = false; let stepCounter = 1; let tutorialStepsContainer; let sidebar; let recordButton; let uiInitialized = false; /** * 创建并初始化UI界面 (只执行一次) */ function createUI() { if (document.querySelector('.wtm-sidebar')) return; // 防止重复创建 // 创建侧边栏 sidebar = document.createElement('div'); sidebar.className = 'wtm-sidebar'; // 侧边栏头部 const header = document.createElement('div'); header.className = 'wtm-sidebar-header'; header.innerHTML = ` <h3>教程编辑器</h3> <div class="wtm-controls"> <button class="wtm-record-btn">开始录制</button> <button class="wtm-export-btn">导出 PDF</button> </div> `; sidebar.appendChild(header); // 步骤容器 tutorialStepsContainer = document.createElement('div'); tutorialStepsContainer.className = 'wtm-steps-container'; sidebar.appendChild(tutorialStepsContainer); document.body.appendChild(sidebar); // 添加头部按钮事件 recordButton = sidebar.querySelector('.wtm-record-btn'); recordButton.addEventListener('click', toggleRecording); sidebar.querySelector('.wtm-export-btn').addEventListener('click', exportToPDF); // 添加拖拽排序功能 enableDragAndDrop(); uiInitialized = true; } /** * 切换录制状态 */ function toggleRecording() { isRecording = !isRecording; if (isRecording) { recordButton.textContent = '停止录制'; recordButton.classList.add('recording'); document.addEventListener('click', handlePageClick, true); addRecordingOverlay(); } else { recordButton.textContent = '开始录制'; recordButton.classList.remove('recording'); document.removeEventListener('click', handlePageClick, true); removeRecordingOverlay(); } } /** * 切换侧边栏的显示/隐藏 */ function toggleSidebar() { if (!uiInitialized) { createUI(); } sidebar.classList.toggle('show'); // 如果侧边栏被关闭时仍在录制,则停止录制 if (!sidebar.classList.contains('show') && isRecording) { toggleRecording(); } } // --- 以下代码与油猴脚本几乎完全相同 --- /** * 处理页面点击事件,用于捕获步骤 * @param {MouseEvent} event */ function handlePageClick(event) { if (event.target.closest('.wtm-sidebar')) { return; } event.preventDefault(); event.stopPropagation(); const clickedElement = event.target; let description = clickedElement.innerText || clickedElement.value || clickedElement.alt || '点击页面区域'; description = description.trim().substring(0, 100); showLoader('正在截取屏幕...'); setTimeout(() => { html2canvas(document.body, { windowWidth: document.documentElement.clientWidth, windowHeight: document.documentElement.clientHeight, x: window.scrollX, y: window.scrollY, useCORS: true // 尝试解决跨域图片问题 }).then(canvas => { const imageDataUrl = canvas.toDataURL('image/png'); addStepToSidebar(description, imageDataUrl); hideLoader(); }).catch(err => { console.error("html2canvas error:", err); alert("截图失败,如果页面包含跨域图片可能导致此问题。请查看控制台日志。"); hideLoader(); }); }, 100); } function addStepToSidebar(text, imageDataUrl) { const stepDiv = document.createElement('div'); stepDiv.className = 'wtm-step'; stepDiv.setAttribute('draggable', true); const currentStep = tutorialStepsContainer.children.length + 1; stepDiv.innerHTML = ` <div class="wtm-step-header"> <span class="wtm-step-number">步骤 ${currentStep}:</span> <button class="wtm-step-delete-btn" title="删除此步骤">×</button> </div> <textarea>${text}</textarea> <img src="${imageDataUrl}" alt="步骤 ${currentStep} 的截图"> `; tutorialStepsContainer.appendChild(stepDiv); stepDiv.querySelector('.wtm-step-delete-btn').addEventListener('click', () => { stepDiv.remove(); updateStepNumbers(); }); // 更新编号,并滚动到底部 updateStepNumbers(); tutorialStepsContainer.scrollTop = tutorialStepsContainer.scrollHeight; } function updateStepNumbers() { const steps = tutorialStepsContainer.querySelectorAll('.wtm-step'); steps.forEach((step, index) => { step.querySelector('.wtm-step-number').textContent = `步骤 ${index + 1}:`; step.querySelector('img').alt = `步骤 ${index + 1} 的截图`; }); } function enableDragAndDrop() { let draggedItem = null; tutorialStepsContainer.addEventListener('dragstart', e => { if (e.target.classList.contains('wtm-step')) { draggedItem = e.target; setTimeout(() => e.target.classList.add('dragging'), 0); } }); tutorialStepsContainer.addEventListener('dragend', () => { if (draggedItem) { draggedItem.classList.remove('dragging'); draggedItem = null; updateStepNumbers(); } }); tutorialStepsContainer.addEventListener('dragover', e => { e.preventDefault(); const afterElement = getDragAfterElement(tutorialStepsContainer, e.clientY); const currentDragged = document.querySelector('.dragging'); if (currentDragged) { if (afterElement == null) { tutorialStepsContainer.appendChild(currentDragged); } else { tutorialStepsContainer.insertBefore(currentDragged, afterElement); } } }); } function getDragAfterElement(container, y) { const draggableElements = [...container.querySelectorAll('.wtm-step:not(.dragging)')]; return draggableElements.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if (offset < 0 && offset > closest.offset) { return { offset: offset, element: child }; } else { return closest; } }, { offset: Number.NEGATIVE_INFINITY }).element; } async function exportToPDF() { const steps = tutorialStepsContainer.querySelectorAll('.wtm-step'); if (steps.length === 0) { alert('没有可导出的步骤!'); return; } showLoader('正在生成 PDF...'); const doc = new jsPDF(); const page_margin = 15; const page_width = doc.internal.pageSize.getWidth() - 2 * page_margin; let y_pos = 20; doc.setFontSize(22); doc.text("网页操作教程", doc.internal.pageSize.getWidth() / 2, y_pos, { align: 'center' }); y_pos += 20; for (let i = 0; i < steps.length; i++) { const step = steps[i]; const stepNumberText = step.querySelector('.wtm-step-number').textContent; const description = step.querySelector('textarea').value; const imgData = step.querySelector('img').src; if (y_pos > 250) { doc.addPage(); y_pos = 20; } doc.setFontSize(16); doc.setFont(undefined, 'bold'); doc.text(stepNumberText, page_margin, y_pos); y_pos += 8; doc.setFontSize(12); doc.setFont(undefined, 'normal'); const splitDescription = doc.splitTextToSize(description, page_width); doc.text(splitDescription, page_margin, y_pos); y_pos += splitDescription.length * 5 + 5; if (y_pos > 200) { doc.addPage(); y_pos = 20; } try { const img = new Image(); img.src = imgData; await new Promise(resolve => img.onload = resolve); const imgHeight = (img.height * page_width) / img.width; doc.addImage(imgData, 'PNG', page_margin, y_pos, page_width, imgHeight); y_pos += imgHeight + 15; } catch (error) { console.error("Error adding image to PDF: ", error); doc.text("图片加载失败", page_margin, y_pos); y_pos += 10; } } doc.save('图文教程.pdf'); hideLoader(); } function addRecordingOverlay() { if (document.querySelector('.wtm-click-overlay')) return; const overlay = document.createElement('div'); overlay.className = 'wtm-click-overlay'; document.body.appendChild(overlay); document.body.style.cursor = 'crosshair'; } function removeRecordingOverlay() { const overlay = document.querySelector('.wtm-click-overlay'); if (overlay) overlay.remove(); document.body.style.cursor = 'default'; } function showLoader(message) { let loader = document.querySelector('.wtm-loader'); if (!loader) { loader = document.createElement('div'); loader.className = 'wtm-loader'; document.body.appendChild(loader); } loader.textContent = message; loader.style.display = 'block'; } function hideLoader() { const loader = document.querySelector('.wtm-loader'); if (loader) loader.style.display = 'none'; } // --- 扩展消息监听器 --- chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === "toggle_tutorial_maker") { toggleSidebar(); } });
与油猴脚本的主要区别:
启动方式: 不再是页面右下角的悬浮按钮。而是通过监听来自 background.js 的消息 (chrome.runtime.onMessage.addListener) 来触发 toggleSidebar() 函数。
UI创建时机: UI(侧边栏等)是懒加载的。只有在用户第一次点击扩展图标时,createUI() 才会被调用。
按钮变化: "制作图文教程" 按钮被移到了侧边栏的头部,并改名为 "开始录制"。整个扩展的控制中心都在侧边栏内,页面更加整洁。
Z-index: 为UI元素设置了非常高的 z-index (2147483647),以确保它能显示在绝大多数网站内容的上方。
第5步: 安装和测试扩展
打开扩展管理页面: 在 Chrome 浏览器中,输入 chrome://extensions 并回车。
启用开发者模式: 在页面右上角,打开“开发者模式”的开关。
加载扩展: 点击“加载已解压的扩展程序”按钮。
选择文件夹: 在弹出的文件选择器中,选择你创建的 tutorial-maker-extension 整个文件夹。
完成: 如果一切顺利,你应该能在扩展列表中看到“网页图文教程生成器”,并在浏览器工具栏上看到它的图标。
如何使用
打开任何你想要制作教程的网站。
点击浏览器工具栏上的扩展图标。
右侧会滑出教程编辑器侧边栏。
点击侧边栏顶部的“开始录制”按钮。
现在,你在页面上的每一次点击都会被记录下来,生成一个带截图和描述的步骤。
你可以随时在侧边栏编辑文本、拖拽调整步骤顺序或删除步骤。
完成后,点击“停止录制”,然后点击“导出 PDF”即可下载你的教程。
再次点击扩展图标可以隐藏侧边栏。
网友回复
阿里通义大模型哪些是支持多模态的api的ai模型?
js如何实现浏览器中离线语音唤醒语音聊天小助手?
浏览器中如何将WebM视频转成mp4视频?
parlant如何改成qwen 的apikey与baseurl?
如何写一个chrome插件实现截屏自动生成步骤图文教程转成pdf或网页?
python如何通过阿里云的api对域名进行解析和ecs主机服务器进行启动停止等操作?
Tesla Robotaxi可以让特斯拉车自动无人驾驶跑滴滴为车主赚钱,国内以后也会这样吗?
有没有可以监控安卓手机上的app打开后偷偷摸摸做了啥的监控软件?
webrtc进行p2p连接发送的文本音视频文件是否是加密的?
如何让一个可爱的三维动物通过three在浏览器中有表情动作的自然说话?