下面我将为你提供一个完整的 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”即可下载你的教程。
再次点击扩展图标可以隐藏侧边栏。
网友回复


