web-to-figma-chrome-extension

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Web to Figma Chrome Extension

Web to Figma Chrome扩展程序

Skill by ara.so — Design Skills collection
ara.so提供的技能——设计技能合集

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
    .json
    for Figma workflows
Web to Figma是一款Chrome扩展程序,可捕获任意网页并将其导出为Figma兼容的JSON文件。它提供了页面内浮动工具栏,支持一键捕获;支持跨域图片代理获取,以解决CORS问题;还提供可配置的图片下载并发设置。
核心功能:
  • 捕获DOM元素、样式和布局的整页内容
  • 针对特定元素的捕获,实现精准设计提取
  • 跨域图片代理模式,避免图片缺失
  • 可配置的图片获取并发数(4/6/8/10/12/16/20/无限)
  • 导出为
    .json
    文件,适配Figma工作流

Installation

安装

Developer Mode (Local)

开发者模式(本地)

  1. Clone the repository:
bash
git clone https://github.com/Paidax01/web-to-figma.git
cd web-to-figma
  1. Open Chrome and navigate to
    chrome://extensions/
  2. Enable Developer mode (toggle in top-right)
  3. Click Load unpacked
  4. Select the
    web-to-figma
    directory
The extension icon should now appear in your Chrome toolbar.
  1. 克隆仓库:
bash
git clone https://github.com/Paidax01/web-to-figma.git
cd web-to-figma
  1. 打开Chrome浏览器,导航至
    chrome://extensions/
  2. 启用开发者模式(右上角开关)
  3. 点击加载已解压的扩展程序
  4. 选择
    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 icons
web-to-figma/
├── manifest.json          # 扩展程序配置文件
├── background.js          # 用于代理和协调的服务工作线程
├── capture.js             # 核心捕获逻辑(DOM遍历、样式提取)
├── runner.js              # 协调捕获流程
├── inpage-toolbar.js      # 网页中的浮动UI工具栏
├── popup.html/css/js      # 扩展程序弹窗UI
└── logo/                  # 扩展程序图标

Key Components

核心组件

  • capture.js
    : Main capture engine that traverses the DOM, extracts computed styles, handles images, text nodes, and layout information
  • background.js
    : Background service worker that proxies cross-origin image requests
  • runner.js
    : Coordinates the capture flow and message passing between components
  • inpage-toolbar.js
    : Injects floating toolbar UI into webpages for quick access
  • capture.js
    :主捕获引擎,负责遍历DOM、提取计算样式、处理图片、文本节点和布局信息
  • background.js
    :后台服务工作线程,代理跨域图片请求
  • runner.js
    :协调捕获流程及组件间的消息传递
  • inpage-toolbar.js
    :向网页注入浮动工具栏UI,便于快速访问

Core Capture Flow

核心捕获流程

1. Extension Popup Configuration

1. 扩展程序弹窗配置

The popup (
popup.html
) provides settings:
javascript
// 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.html
)提供以下设置:
javascript
// 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:
  1. Enable cross-origin proxy mode:
javascript
const config = { useProxy: true, imageConcurrency: 8 };
  1. Verify background.js has proper permissions in manifest.json:
json
{
  "host_permissions": ["<all_urls>"]
}
  1. 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中图片显示为损坏或缺失。
解决方案:
  1. 启用跨域代理模式:
javascript
const config = { useProxy: true, imageConcurrency: 8 };
  1. 验证manifest.json中background.js是否拥有正确权限:
json
{
  "host_permissions": ["<all_urls>"]
}
  1. 检查图片是否为懒加载:
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:
  1. Reduce image concurrency:
javascript
const config = { imageConcurrency: 4 }; // Lower value
  1. Skip hidden elements:
javascript
function shouldCaptureElement(element) {
  const style = window.getComputedStyle(element);
  return style.display !== 'none' && 
         style.visibility !== 'hidden' &&
         style.opacity !== '0';
}
  1. Limit DOM depth:
javascript
function captureElementWithDepth(element, config, currentDepth = 0) {
  if (currentDepth > config.maxDepth) {
    return null;
  }
  // ... capture logic
}
问题: 捕获耗时过长或超时。
解决方案:
  1. 降低图片并发数:
javascript
const config = { imageConcurrency: 4 }; // 更低的数值
  1. 跳过隐藏元素:
javascript
function shouldCaptureElement(element) {
  const style = window.getComputedStyle(element);
  return style.display !== 'none' && 
         style.visibility !== 'hidden' &&
         style.opacity !== '0';
}
  1. 限制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:
  1. 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"
  }]
}
  1. Verify no CSP (Content Security Policy) blocks:
javascript
// Check console for CSP errors
// May need to adjust for strict CSP pages
  1. Reload extension after code changes:
bash
undefined
问题: 工具栏或捕获功能未在页面上显示。
解决方案:
  1. 检查manifest.json中的内容脚本注入配置:
json
{
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["inpage-toolbar.js", "capture.js", "runner.js"],
    "run_at": "document_idle"
  }]
}
  1. 验证是否存在CSP(内容安全策略)拦截:
javascript
// 在控制台查看CSP错误
// 可能需要针对严格CSP页面进行调整
  1. 修改代码后重新加载扩展程序:
bash
undefined

In chrome://extensions/, click reload icon

在chrome://extensions/中点击重新加载图标

undefined
undefined

Memory Issues with Large Pages

大型页面内存问题

Problem: Browser crashes or slows down on large/complex pages.
Solutions:
  1. 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();
}
  1. Exclude large elements:
javascript
const config = {
  minimumSize: { width: 10, height: 10 },
  excludeSelectors: ['.ad-container', '.comments-section']
};
问题: 浏览器在处理大型/复杂页面时崩溃或变慢。
解决方案:
  1. 分块捕获:
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();
}
  1. 排除大型元素:
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:
  1. 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;
}
  1. 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文件过大,无法下载或处理。
解决方案:
  1. 压缩数据:
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;
}
  1. 分页输出:
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
undefined
bash
undefined

Remove development files

删除开发文件

zip -r web-to-figma-extension.zip .
-x ".DS_Store"
-x ".git/
"
-x "node_modules/"
-x "
.md"
-x "tests/*"
undefined
zip -r web-to-figma-extension.zip .
-x ".DS_Store"
-x ".git/
"
-x "node_modules/"
-x "
.md"
-x "tests/*"
undefined

Version 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

最佳实践

  1. Always test on sample pages first before capturing production sites
  2. Use proxy mode for public websites with image CDNs
  3. Adjust concurrency based on network speed (slower connection = lower concurrency)
  4. Clear browser cache if getting stale captures
  5. Respect robots.txt and terms of service when capturing third-party sites
  6. Handle errors gracefully - not all pages will capture perfectly
  7. Version your capture format for backward compatibility
  1. 先在示例页面测试,再捕获生产环境站点
  2. 对使用图片CDN的公共网站启用代理模式
  3. 根据网络速度调整并发数(网络越慢,并发数越低)
  4. 如果捕获结果过时,清除浏览器缓存
  5. 捕获第三方站点时遵守robots.txt和服务条款
  6. 优雅处理错误——并非所有页面都能完美捕获
  7. 为捕获格式添加版本,保证向后兼容性

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及其他隐私法规
  • 仅用于学习、研究或授权的设计工作流