web-to-figma-chrome-extension
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWeb to Figma Chrome Extension
Web to Figma Chrome扩展程序
Overview
概述
Web to Figma is a Chrome extension that captures any webpage and exports it as a Figma-compatible JSON file. It provides an in-page floating toolbar for one-click capture, supports cross-origin image proxy fetching to handle CORS issues, and offers configurable concurrency settings for image downloads.
Key capabilities:
- Full-page capture of DOM elements, styles, and layout
- Element-specific capture for targeted design extraction
- Cross-origin image proxy mode to avoid missing images
- Configurable image fetch concurrency (4/6/8/10/12/16/20/infinite)
- Export as for Figma workflows
.json
Web to Figma是一款Chrome扩展程序,可捕获任意网页并将其导出为Figma兼容的JSON文件。它提供了页面内浮动工具栏,支持一键捕获;支持跨域图片代理获取,以解决CORS问题;还提供可配置的图片下载并发设置。
核心功能:
- 捕获DOM元素、样式和布局的整页内容
- 针对特定元素的捕获,实现精准设计提取
- 跨域图片代理模式,避免图片缺失
- 可配置的图片获取并发数(4/6/8/10/12/16/20/无限)
- 导出为文件,适配Figma工作流
.json
Installation
安装
Developer Mode (Local)
开发者模式(本地)
- Clone the repository:
bash
git clone https://github.com/Paidax01/web-to-figma.git
cd web-to-figma-
Open Chrome and navigate to
chrome://extensions/ -
Enable Developer mode (toggle in top-right)
-
Click Load unpacked
-
Select thedirectory
web-to-figma
The extension icon should now appear in your Chrome toolbar.
- 克隆仓库:
bash
git clone https://github.com/Paidax01/web-to-figma.git
cd web-to-figma-
打开Chrome浏览器,导航至
chrome://extensions/ -
启用开发者模式(右上角开关)
-
点击加载已解压的扩展程序
-
选择目录
web-to-figma
扩展程序图标现在应该会出现在Chrome工具栏中。
Project Architecture
项目架构
web-to-figma/
├── manifest.json # Extension configuration
├── background.js # Service worker for proxy and coordination
├── capture.js # Core capture logic (DOM traversal, style extraction)
├── runner.js # Orchestrates capture process
├── inpage-toolbar.js # Floating UI toolbar on webpage
├── popup.html/css/js # Extension popup UI
└── logo/ # Extension iconsweb-to-figma/
├── manifest.json # 扩展程序配置文件
├── background.js # 用于代理和协调的服务工作线程
├── capture.js # 核心捕获逻辑(DOM遍历、样式提取)
├── runner.js # 协调捕获流程
├── inpage-toolbar.js # 网页中的浮动UI工具栏
├── popup.html/css/js # 扩展程序弹窗UI
└── logo/ # 扩展程序图标Key Components
核心组件
- : Main capture engine that traverses the DOM, extracts computed styles, handles images, text nodes, and layout information
capture.js - : Background service worker that proxies cross-origin image requests
background.js - : Coordinates the capture flow and message passing between components
runner.js - : Injects floating toolbar UI into webpages for quick access
inpage-toolbar.js
- :主捕获引擎,负责遍历DOM、提取计算样式、处理图片、文本节点和布局信息
capture.js - :后台服务工作线程,代理跨域图片请求
background.js - :协调捕获流程及组件间的消息传递
runner.js - :向网页注入浮动工具栏UI,便于快速访问
inpage-toolbar.js
Core Capture Flow
核心捕获流程
1. Extension Popup Configuration
1. 扩展程序弹窗配置
The popup () provides settings:
popup.htmljavascript
// Example popup.js structure
document.getElementById('startCapture').addEventListener('click', async () => {
const useProxy = document.getElementById('proxyMode').checked;
const concurrency = document.getElementById('concurrency').value;
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.tabs.sendMessage(tab.id, {
action: 'startCapture',
config: {
useProxy: useProxy,
imageConcurrency: parseInt(concurrency)
}
});
});弹窗()提供以下设置:
popup.htmljavascript
// Example popup.js structure
document.getElementById('startCapture').addEventListener('click', async () => {
const useProxy = document.getElementById('proxyMode').checked;
const concurrency = document.getElementById('concurrency').value;
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.tabs.sendMessage(tab.id, {
action: 'startCapture',
config: {
useProxy: useProxy,
imageConcurrency: parseInt(concurrency)
}
});
});2. Triggering Capture
2. 触发捕获
From the in-page toolbar or popup:
javascript
// inpage-toolbar.js pattern
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'startCapture') {
const config = message.config || {};
// Start capture process
captureWebpage(config).then(result => {
// Generate download
downloadJSON(result, `figma-capture-${Date.now()}.json`);
});
}
});通过页面内工具栏或弹窗触发:
javascript
// inpage-toolbar.js pattern
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'startCapture') {
const config = message.config || {};
// 启动捕获流程
captureWebpage(config).then(result => {
// 生成下载文件
downloadJSON(result, `figma-capture-${Date.now()}.json`);
});
}
});3. DOM Traversal and Data Extraction
3. DOM遍历与数据提取
The capture engine recursively walks the DOM:
javascript
// capture.js core pattern
function captureElement(element, config) {
const computedStyle = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
const nodeData = {
type: element.tagName.toLowerCase(),
id: element.id || null,
className: element.className || null,
position: {
x: rect.left + window.scrollX,
y: rect.top + window.scrollY,
width: rect.width,
height: rect.height
},
styles: extractStyles(computedStyle),
text: extractText(element),
children: []
};
// Handle images
if (element.tagName === 'IMG') {
nodeData.src = element.src;
if (config.useProxy) {
nodeData.proxyUrl = await fetchImageViaProxy(element.src, config);
}
}
// Recursively capture children
for (const child of element.children) {
if (shouldCaptureElement(child)) {
nodeData.children.push(captureElement(child, config));
}
}
return nodeData;
}
function extractStyles(computedStyle) {
return {
color: computedStyle.color,
backgroundColor: computedStyle.backgroundColor,
fontSize: computedStyle.fontSize,
fontFamily: computedStyle.fontFamily,
fontWeight: computedStyle.fontWeight,
lineHeight: computedStyle.lineHeight,
padding: {
top: computedStyle.paddingTop,
right: computedStyle.paddingRight,
bottom: computedStyle.paddingBottom,
left: computedStyle.paddingLeft
},
margin: {
top: computedStyle.marginTop,
right: computedStyle.marginRight,
bottom: computedStyle.marginBottom,
left: computedStyle.marginLeft
},
border: {
width: computedStyle.borderWidth,
style: computedStyle.borderStyle,
color: computedStyle.borderColor,
radius: computedStyle.borderRadius
},
display: computedStyle.display,
position: computedStyle.position,
zIndex: computedStyle.zIndex
};
}捕获引擎递归遍历DOM:
javascript
// capture.js core pattern
function captureElement(element, config) {
const computedStyle = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
const nodeData = {
type: element.tagName.toLowerCase(),
id: element.id || null,
className: element.className || null,
position: {
x: rect.left + window.scrollX,
y: rect.top + window.scrollY,
width: rect.width,
height: rect.height
},
styles: extractStyles(computedStyle),
text: extractText(element),
children: []
};
// 处理图片
if (element.tagName === 'IMG') {
nodeData.src = element.src;
if (config.useProxy) {
nodeData.proxyUrl = await fetchImageViaProxy(element.src, config);
}
}
// 递归捕获子元素
for (const child of element.children) {
if (shouldCaptureElement(child)) {
nodeData.children.push(captureElement(child, config));
}
}
return nodeData;
}
function extractStyles(computedStyle) {
return {
color: computedStyle.color,
backgroundColor: computedStyle.backgroundColor,
fontSize: computedStyle.fontSize,
fontFamily: computedStyle.fontFamily,
fontWeight: computedStyle.fontWeight,
lineHeight: computedStyle.lineHeight,
padding: {
top: computedStyle.paddingTop,
right: computedStyle.paddingRight,
bottom: computedStyle.paddingBottom,
left: computedStyle.paddingLeft
},
margin: {
top: computedStyle.marginTop,
right: computedStyle.marginRight,
bottom: computedStyle.marginBottom,
left: computedStyle.marginLeft
},
border: {
width: computedStyle.borderWidth,
style: computedStyle.borderStyle,
color: computedStyle.borderColor,
radius: computedStyle.borderRadius
},
display: computedStyle.display,
position: computedStyle.position,
zIndex: computedStyle.zIndex
};
}4. Cross-Origin Image Handling
4. 跨域图片处理
Background proxy pattern:
javascript
// background.js proxy implementation
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'fetchImage') {
fetch(message.url, {
method: 'GET',
mode: 'cors',
credentials: 'omit'
})
.then(response => response.blob())
.then(blob => {
const reader = new FileReader();
reader.onloadend = () => {
sendResponse({
success: true,
dataUrl: reader.result
});
};
reader.readAsDataURL(blob);
})
.catch(error => {
sendResponse({
success: false,
error: error.message
});
});
return true; // Async response
}
});
// capture.js usage
async function fetchImageViaProxy(url, config) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({
action: 'fetchImage',
url: url
}, response => {
if (response.success) {
resolve(response.dataUrl);
} else {
reject(new Error(response.error));
}
});
});
}后台代理实现:
javascript
// background.js proxy implementation
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'fetchImage') {
fetch(message.url, {
method: 'GET',
mode: 'cors',
credentials: 'omit'
})
.then(response => response.blob())
.then(blob => {
const reader = new FileReader();
reader.onloadend = () => {
sendResponse({
success: true,
dataUrl: reader.result
});
};
reader.readAsDataURL(blob);
})
.catch(error => {
sendResponse({
success: false,
error: error.message
});
});
return true; // 异步响应
}
});
// capture.js usage
async function fetchImageViaProxy(url, config) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({
action: 'fetchImage',
url: url
}, response => {
if (response.success) {
resolve(response.dataUrl);
} else {
reject(new Error(response.error));
}
});
});
}5. Concurrency Control
5. 并发控制
Managing parallel image fetches:
javascript
// Concurrency limiter pattern
class ConcurrencyQueue {
constructor(limit) {
this.limit = limit === 'infinite' ? Infinity : limit;
this.running = 0;
this.queue = [];
}
async add(fn) {
while (this.running >= this.limit) {
await new Promise(resolve => this.queue.push(resolve));
}
this.running++;
try {
return await fn();
} finally {
this.running--;
const resolve = this.queue.shift();
if (resolve) resolve();
}
}
}
// Usage in capture
async function captureImagesWithConcurrency(images, config) {
const queue = new ConcurrencyQueue(config.imageConcurrency || 4);
return Promise.all(
images.map(img =>
queue.add(() => fetchImageViaProxy(img.src, config))
)
);
}管理并行图片请求:
javascript
// Concurrency limiter pattern
class ConcurrencyQueue {
constructor(limit) {
this.limit = limit === 'infinite' ? Infinity : limit;
this.running = 0;
this.queue = [];
}
async add(fn) {
while (this.running >= this.limit) {
await new Promise(resolve => this.queue.push(resolve));
}
this.running++;
try {
return await fn();
} finally {
this.running--;
const resolve = this.queue.shift();
if (resolve) resolve();
}
}
}
// Usage in capture
async function captureImagesWithConcurrency(images, config) {
const queue = new ConcurrencyQueue(config.imageConcurrency || 4);
return Promise.all(
images.map(img =>
queue.add(() => fetchImageViaProxy(img.src, config))
)
);
}Configuration
配置
manifest.json
manifest.json
json
{
"manifest_version": 3,
"name": "Web to Figma",
"version": "1.0.0",
"description": "Convert any webpage into an editable Figma file",
"permissions": [
"activeTab",
"storage",
"downloads"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["inpage-toolbar.js", "capture.js", "runner.js"],
"run_at": "document_idle"
}
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "logo/icon16.png",
"48": "logo/icon48.png",
"128": "logo/icon128.png"
}
}
}json
{
"manifest_version": 3,
"name": "Web to Figma",
"version": "1.0.0",
"description": "Convert any webpage into an editable Figma file",
"permissions": [
"activeTab",
"storage",
"downloads"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["inpage-toolbar.js", "capture.js", "runner.js"],
"run_at": "document_idle"
}
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "logo/icon16.png",
"48": "logo/icon48.png",
"128": "logo/icon128.png"
}
}
}Runtime Configuration
运行时配置
Pass config object to capture functions:
javascript
const captureConfig = {
useProxy: true, // Enable cross-origin image proxy
imageConcurrency: 8, // Parallel image fetches (4/6/8/10/12/16/20/'infinite')
fullPage: true, // Capture entire page vs. viewport
includeHidden: false, // Capture elements with display:none
maxDepth: 50, // Maximum DOM traversal depth
captureIframes: false, // Whether to capture iframe content
captureBackgrounds: true, // Include CSS background images
minimumSize: { width: 1, height: 1 } // Skip tiny elements
};将配置对象传入捕获函数:
javascript
const captureConfig = {
useProxy: true, // 启用跨域图片代理
imageConcurrency: 8, // 并行图片请求数(4/6/8/10/12/16/20/'infinite')
fullPage: true, // 捕获整页而非视口
includeHidden: false, // 捕获display:none的元素
maxDepth: 50, // 最大DOM遍历深度
captureIframes: false, // 是否捕获iframe内容
captureBackgrounds: true, // 包含CSS背景图片
minimumSize: { width: 1, height: 1 } // 跳过极小元素
};Common Patterns
常用模式
Full-Page Capture
整页捕获
javascript
async function captureFullPage() {
const config = {
useProxy: true,
imageConcurrency: 8,
fullPage: true
};
// Scroll to top
window.scrollTo(0, 0);
// Capture from root
const rootElement = document.body;
const captureData = await captureElement(rootElement, config);
// Add metadata
const result = {
version: '1.0',
timestamp: new Date().toISOString(),
url: window.location.href,
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
document: {
width: document.documentElement.scrollWidth,
height: document.documentElement.scrollHeight
},
tree: captureData
};
return result;
}javascript
async function captureFullPage() {
const config = {
useProxy: true,
imageConcurrency: 8,
fullPage: true
};
// 滚动到顶部
window.scrollTo(0, 0);
// 从根元素开始捕获
const rootElement = document.body;
const captureData = await captureElement(rootElement, config);
// 添加元数据
const result = {
version: '1.0',
timestamp: new Date().toISOString(),
url: window.location.href,
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
document: {
width: document.documentElement.scrollWidth,
height: document.documentElement.scrollHeight
},
tree: captureData
};
return result;
}Element-Specific Capture
特定元素捕获
javascript
function captureSpecificElement(selector) {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
const config = {
useProxy: true,
imageConcurrency: 6,
fullPage: false
};
return captureElement(element, config);
}
// Usage
const headerData = await captureSpecificElement('header.main-header');javascript
function captureSpecificElement(selector) {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
const config = {
useProxy: true,
imageConcurrency: 6,
fullPage: false
};
return captureElement(element, config);
}
// 使用示例
const headerData = await captureSpecificElement('header.main-header');Download JSON Result
下载JSON结果
javascript
function downloadJSON(data, filename) {
const jsonString = JSON.stringify(data, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
// Cleanup
document.body.removeChild(a);
URL.revokeObjectURL(url);
}javascript
function downloadJSON(data, filename) {
const jsonString = JSON.stringify(data, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
// 清理资源
document.body.removeChild(a);
URL.revokeObjectURL(url);
}Handling SVGs
SVG处理
javascript
function captureSVG(svgElement) {
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svgElement);
const svgDataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`;
return {
type: 'svg',
content: svgString,
dataUrl: svgDataUrl,
viewBox: svgElement.getAttribute('viewBox'),
width: svgElement.width.baseVal.value,
height: svgElement.height.baseVal.value
};
}javascript
function captureSVG(svgElement) {
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svgElement);
const svgDataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`;
return {
type: 'svg',
content: svgString,
dataUrl: svgDataUrl,
viewBox: svgElement.getAttribute('viewBox'),
width: svgElement.width.baseVal.value,
height: svgElement.height.baseVal.value
};
}Canvas Capture
Canvas捕获
javascript
function captureCanvas(canvasElement) {
try {
const dataUrl = canvasElement.toDataURL('image/png');
return {
type: 'canvas',
dataUrl: dataUrl,
width: canvasElement.width,
height: canvasElement.height
};
} catch (error) {
// Canvas may be tainted by cross-origin content
return {
type: 'canvas',
error: 'Tainted canvas - cross-origin content',
width: canvasElement.width,
height: canvasElement.height
};
}
}javascript
function captureCanvas(canvasElement) {
try {
const dataUrl = canvasElement.toDataURL('image/png');
return {
type: 'canvas',
dataUrl: dataUrl,
width: canvasElement.width,
height: canvasElement.height
};
} catch (error) {
// Canvas可能被跨域内容污染
return {
type: 'canvas',
error: 'Tainted canvas - cross-origin content',
width: canvasElement.width,
height: canvasElement.height
};
}
}Advanced Usage
进阶用法
Custom Style Extraction
自定义样式提取
javascript
function extractCustomProperties(element) {
const styles = window.getComputedStyle(element);
const customProps = {};
// Extract CSS custom properties (variables)
for (let i = 0; i < styles.length; i++) {
const prop = styles[i];
if (prop.startsWith('--')) {
customProps[prop] = styles.getPropertyValue(prop);
}
}
return customProps;
}javascript
function extractCustomProperties(element) {
const styles = window.getComputedStyle(element);
const customProps = {};
// 提取CSS自定义属性(变量)
for (let i = 0; i < styles.length; i++) {
const prop = styles[i];
if (prop.startsWith('--')) {
customProps[prop] = styles.getPropertyValue(prop);
}
}
return customProps;
}Text Node Extraction
文本节点提取
javascript
function extractTextNodes(element) {
const textNodes = [];
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
if (text) {
const range = document.createRange();
range.selectNode(node);
const rect = range.getBoundingClientRect();
textNodes.push({
text: text,
position: {
x: rect.left + window.scrollX,
y: rect.top + window.scrollY,
width: rect.width,
height: rect.height
}
});
}
}
return textNodes;
}javascript
function extractTextNodes(element) {
const textNodes = [];
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
if (text) {
const range = document.createRange();
range.selectNode(node);
const rect = range.getBoundingClientRect();
textNodes.push({
text: text,
position: {
x: rect.left + window.scrollX,
y: rect.top + window.scrollY,
width: rect.width,
height: rect.height
}
});
}
}
return textNodes;
}Background Image Extraction
背景图片提取
javascript
function extractBackgroundImages(element) {
const style = window.getComputedStyle(element);
const bgImage = style.backgroundImage;
if (bgImage && bgImage !== 'none') {
const urlMatch = bgImage.match(/url\(['"]?(.*?)['"]?\)/);
if (urlMatch && urlMatch[1]) {
return {
url: urlMatch[1],
size: style.backgroundSize,
position: style.backgroundPosition,
repeat: style.backgroundRepeat
};
}
}
return null;
}javascript
function extractBackgroundImages(element) {
const style = window.getComputedStyle(element);
const bgImage = style.backgroundImage;
if (bgImage && bgImage !== 'none') {
const urlMatch = bgImage.match(/url\(['"]?(.*?)['"]?\)/);
if (urlMatch && urlMatch[1]) {
return {
url: urlMatch[1],
size: style.backgroundSize,
position: style.backgroundPosition,
repeat: style.backgroundRepeat
};
}
}
return null;
}Progressive Capture with Progress Callback
带进度回调的渐进式捕获
javascript
async function captureWithProgress(element, config, onProgress) {
const totalElements = element.querySelectorAll('*').length;
let processedElements = 0;
async function captureWithCallback(el) {
processedElements++;
if (onProgress) {
onProgress({
processed: processedElements,
total: totalElements,
percentage: (processedElements / totalElements * 100).toFixed(2)
});
}
return captureElement(el, config);
}
return await captureWithCallback(element);
}
// Usage
const result = await captureWithProgress(
document.body,
{ useProxy: true, imageConcurrency: 8 },
(progress) => {
console.log(`Capturing: ${progress.percentage}%`);
}
);javascript
async function captureWithProgress(element, config, onProgress) {
const totalElements = element.querySelectorAll('*').length;
let processedElements = 0;
async function captureWithCallback(el) {
processedElements++;
if (onProgress) {
onProgress({
processed: processedElements,
total: totalElements,
percentage: (processedElements / totalElements * 100).toFixed(2)
});
}
return captureElement(el, config);
}
return await captureWithCallback(element);
}
// 使用示例
const result = await captureWithProgress(
document.body,
{ useProxy: true, imageConcurrency: 8 },
(progress) => {
console.log(`捕获进度: ${progress.percentage}%`);
}
);Troubleshooting
故障排查
Images Not Capturing
图片未捕获
Problem: Images appear as broken or missing in exported JSON.
Solutions:
- Enable cross-origin proxy mode:
javascript
const config = { useProxy: true, imageConcurrency: 8 };- Verify background.js has proper permissions in manifest.json:
json
{
"host_permissions": ["<all_urls>"]
}- Check if images are lazy-loaded:
javascript
// Scroll through page to trigger lazy loading
async function scrollToLoadImages() {
const scrollStep = window.innerHeight;
const scrollMax = document.body.scrollHeight;
for (let y = 0; y < scrollMax; y += scrollStep) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 200));
}
window.scrollTo(0, 0);
}
await scrollToLoadImages();
const result = await captureFullPage();问题: 导出的JSON中图片显示为损坏或缺失。
解决方案:
- 启用跨域代理模式:
javascript
const config = { useProxy: true, imageConcurrency: 8 };- 验证manifest.json中background.js是否拥有正确权限:
json
{
"host_permissions": ["<all_urls>"]
}- 检查图片是否为懒加载:
javascript
// 滚动页面触发懒加载
async function scrollToLoadImages() {
const scrollStep = window.innerHeight;
const scrollMax = document.body.scrollHeight;
for (let y = 0; y < scrollMax; y += scrollStep) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 200));
}
window.scrollTo(0, 0);
}
await scrollToLoadImages();
const result = await captureFullPage();Capture Timeout or Slow Performance
捕获超时或性能缓慢
Problem: Capture takes too long or times out.
Solutions:
- Reduce image concurrency:
javascript
const config = { imageConcurrency: 4 }; // Lower value- Skip hidden elements:
javascript
function shouldCaptureElement(element) {
const style = window.getComputedStyle(element);
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0';
}- Limit DOM depth:
javascript
function captureElementWithDepth(element, config, currentDepth = 0) {
if (currentDepth > config.maxDepth) {
return null;
}
// ... capture logic
}问题: 捕获耗时过长或超时。
解决方案:
- 降低图片并发数:
javascript
const config = { imageConcurrency: 4 }; // 更低的数值- 跳过隐藏元素:
javascript
function shouldCaptureElement(element) {
const style = window.getComputedStyle(element);
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0';
}- 限制DOM遍历深度:
javascript
function captureElementWithDepth(element, config, currentDepth = 0) {
if (currentDepth > config.maxDepth) {
return null;
}
// ... 捕获逻辑
}Extension Not Injecting
扩展程序未注入
Problem: Toolbar or capture functionality not appearing on page.
Solutions:
- Check content script injection in manifest.json:
json
{
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["inpage-toolbar.js", "capture.js", "runner.js"],
"run_at": "document_idle"
}]
}- Verify no CSP (Content Security Policy) blocks:
javascript
// Check console for CSP errors
// May need to adjust for strict CSP pages- Reload extension after code changes:
bash
undefined问题: 工具栏或捕获功能未在页面上显示。
解决方案:
- 检查manifest.json中的内容脚本注入配置:
json
{
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["inpage-toolbar.js", "capture.js", "runner.js"],
"run_at": "document_idle"
}]
}- 验证是否存在CSP(内容安全策略)拦截:
javascript
// 在控制台查看CSP错误
// 可能需要针对严格CSP页面进行调整- 修改代码后重新加载扩展程序:
bash
undefinedIn chrome://extensions/, click reload icon
在chrome://extensions/中点击重新加载图标
undefinedundefinedMemory Issues with Large Pages
大型页面内存问题
Problem: Browser crashes or slows down on large/complex pages.
Solutions:
- Capture in chunks:
javascript
async function captureInChunks(rootElement, chunkSize = 100) {
const allElements = Array.from(rootElement.querySelectorAll('*'));
const chunks = [];
for (let i = 0; i < allElements.length; i += chunkSize) {
const chunk = allElements.slice(i, i + chunkSize);
chunks.push(await Promise.all(
chunk.map(el => captureElement(el, config))
));
// Allow event loop to process
await new Promise(resolve => setTimeout(resolve, 0));
}
return chunks.flat();
}- Exclude large elements:
javascript
const config = {
minimumSize: { width: 10, height: 10 },
excludeSelectors: ['.ad-container', '.comments-section']
};问题: 浏览器在处理大型/复杂页面时崩溃或变慢。
解决方案:
- 分块捕获:
javascript
async function captureInChunks(rootElement, chunkSize = 100) {
const allElements = Array.from(rootElement.querySelectorAll('*'));
const chunks = [];
for (let i = 0; i < allElements.length; i += chunkSize) {
const chunk = allElements.slice(i, i + chunkSize);
chunks.push(await Promise.all(
chunk.map(el => captureElement(el, config))
));
// 让事件循环处理其他任务
await new Promise(resolve => setTimeout(resolve, 0));
}
return chunks.flat();
}- 排除大型元素:
javascript
const config = {
minimumSize: { width: 10, height: 10 },
excludeSelectors: ['.ad-container', '.comments-section']
};JSON Export Too Large
JSON导出文件过大
Problem: Generated JSON file is too large to download or process.
Solutions:
- Compress data:
javascript
function compressStyles(styles) {
// Only include non-default values
const compressed = {};
const defaults = {
color: 'rgb(0, 0, 0)',
backgroundColor: 'rgba(0, 0, 0, 0)',
// ... other defaults
};
for (const [key, value] of Object.entries(styles)) {
if (value !== defaults[key]) {
compressed[key] = value;
}
}
return compressed;
}- Paginate output:
javascript
function exportInPages(captureData, elementsPerFile = 500) {
const files = [];
const elements = flattenTree(captureData);
for (let i = 0; i < elements.length; i += elementsPerFile) {
const chunk = elements.slice(i, i + elementsPerFile);
files.push({
filename: `figma-capture-page-${Math.floor(i / elementsPerFile) + 1}.json`,
data: { elements: chunk }
});
}
return files;
}问题: 生成的JSON文件过大,无法下载或处理。
解决方案:
- 压缩数据:
javascript
function compressStyles(styles) {
// 仅包含非默认值
const compressed = {};
const defaults = {
color: 'rgb(0, 0, 0)',
backgroundColor: 'rgba(0, 0, 0, 0)',
// ... 其他默认值
};
for (const [key, value] of Object.entries(styles)) {
if (value !== defaults[key]) {
compressed[key] = value;
}
}
return compressed;
}- 分页输出:
javascript
function exportInPages(captureData, elementsPerFile = 500) {
const files = [];
const elements = flattenTree(captureData);
for (let i = 0; i < elements.length; i += elementsPerFile) {
const chunk = elements.slice(i, i + elementsPerFile);
files.push({
filename: `figma-capture-page-${Math.floor(i / elementsPerFile) + 1}.json`,
data: { elements: chunk }
});
}
return files;
}Packaging for Distribution
打包分发
Create Distribution Build
创建分发版本
bash
undefinedbash
undefinedRemove development files
删除开发文件
zip -r web-to-figma-extension.zip .
-x ".DS_Store"
-x ".git/"
-x "node_modules/"
-x ".md"
-x "tests/*"
-x ".DS_Store"
-x ".git/"
-x "node_modules/"
-x ".md"
-x "tests/*"
undefinedzip -r web-to-figma-extension.zip .
-x ".DS_Store"
-x ".git/"
-x "node_modules/"
-x ".md"
-x "tests/*"
-x ".DS_Store"
-x ".git/"
-x "node_modules/"
-x ".md"
-x "tests/*"
undefinedVersion Management
版本管理
Update version in manifest.json:
json
{
"version": "1.0.1",
"version_name": "1.0.1 Beta"
}更新manifest.json中的版本:
json
{
"version": "1.0.1",
"version_name": "1.0.1 Beta"
}Integration with Figma API
与Figma API集成
While this extension exports JSON, you can process it for Figma import:
javascript
// Example post-processing for Figma plugin
function convertToFigmaNodes(captureData) {
return {
name: captureData.type || 'Frame',
type: mapToFigmaType(captureData.type),
x: captureData.position.x,
y: captureData.position.y,
width: captureData.position.width,
height: captureData.position.height,
fills: convertFills(captureData.styles.backgroundColor),
strokes: convertStrokes(captureData.styles.border),
children: captureData.children.map(convertToFigmaNodes)
};
}
function mapToFigmaType(htmlType) {
const typeMap = {
'div': 'FRAME',
'span': 'TEXT',
'img': 'RECTANGLE', // With image fill
'svg': 'VECTOR'
};
return typeMap[htmlType] || 'FRAME';
}虽然本扩展程序导出JSON,但你可以对其进行处理以适配Figma导入:
javascript
// Figma插件示例后处理逻辑
function convertToFigmaNodes(captureData) {
return {
name: captureData.type || 'Frame',
type: mapToFigmaType(captureData.type),
x: captureData.position.x,
y: captureData.position.y,
width: captureData.position.width,
height: captureData.position.height,
fills: convertFills(captureData.styles.backgroundColor),
strokes: convertStrokes(captureData.styles.border),
children: captureData.children.map(convertToFigmaNodes)
};
}
function mapToFigmaType(htmlType) {
const typeMap = {
'div': 'FRAME',
'span': 'TEXT',
'img': 'RECTANGLE', // 带图片填充
'svg': 'VECTOR'
};
return typeMap[htmlType] || 'FRAME';
}Best Practices
最佳实践
- Always test on sample pages first before capturing production sites
- Use proxy mode for public websites with image CDNs
- Adjust concurrency based on network speed (slower connection = lower concurrency)
- Clear browser cache if getting stale captures
- Respect robots.txt and terms of service when capturing third-party sites
- Handle errors gracefully - not all pages will capture perfectly
- Version your capture format for backward compatibility
- 先在示例页面测试,再捕获生产环境站点
- 对使用图片CDN的公共网站启用代理模式
- 根据网络速度调整并发数(网络越慢,并发数越低)
- 如果捕获结果过时,清除浏览器缓存
- 捕获第三方站点时遵守robots.txt和服务条款
- 优雅处理错误——并非所有页面都能完美捕获
- 为捕获格式添加版本,保证向后兼容性
Legal and Ethical Considerations
法律与伦理考量
- Only capture content you have permission to use
- Respect copyright and intellectual property rights
- Follow website terms of service
- Do not capture sensitive or personal information without consent
- Comply with GDPR, CCPA, and other privacy regulations
- Use for learning, research, or authorized design workflows only
- 仅捕获你有权使用的内容
- 尊重版权和知识产权
- 遵守网站服务条款
- 未经同意,不得捕获敏感或个人信息
- 遵守GDPR、CCPA及其他隐私法规
- 仅用于学习、研究或授权的设计工作流