一篇文章让你读懂Figma的原始数据结构

0 阅读10分钟

最近使用AI做还原视觉相关的提效项目,在AI还没出来之前也刚好做过Figma相关的实践和项目,也趁此机会温故知新一下,分享给有需要的童鞋~

一、Figma 原始数据结构

目前通过AI获取到的figma数据,通常都经过一层转换,不是figma的原始数据,要想获取figma的原始数据结构,你需要通过api获取,返回的应该是一个JSON格式的数据结构,主要包含以下内容:

1. 文件基本信息

{
    "name": "haha 教程【一期】",// 文件名称
    "lastModified": "2022-03-05T03:04:14Z",// 最近修改时间
    "thumbnailUrl": "https://xxxxx",// 缩略图
    "version": "2327290316033362691",
    "role": "viewer",
    "editorType": "figma",
    "linkAccess": "inherit",
    "nodes": {} // 内容信息
}

2. 内容信息(nodes)

"nodes": {
    "988:122835": {
        "document": {},// 文档信息
        "components": {// 组件相关
            "543:105272": {
                "key": "a859937fe23b4182a41301d18670ff3d722a0c85",
                "name": "按钮",
                "description": "",
                "remote": true,
                "documentationLinks": []
            },
            "14:450": {
                "key": "82d6496dfd8eb1f47924bf4e77acd32effc6506c",
                "name": "tipsicon",
                "description": "",
                "remote": true,
                "documentationLinks": []
            }
        },
        "componentSets": {},
        "schemaVersion": 0,
        "styles": {}// 样式信息
    }
}

3. 文档结构 (document)

"document": {
    "id": "988:122835",
    "name": "Frame 2036096513",
    "type": "FRAME",
    "scrollBehavior": "SCROLLS",
    "children": [],// 页面内容
}

4. 节点信息

也就是上述children里面的内容信息,里面会包含各种各样的节点,节点类型总览如下:

类型说明有子节点特殊属性
DOCUMENT文档根-
CANVAS页面backgroundColor
FRAME画板layoutMode, clipsContent
GROUP分组-
SECTION分区FigJam 功能
COMPONENT组件定义componentPropertyDefinitions
COMPONENT_SET变体集-
INSTANCE组件实例componentId, overrides
RECTANGLE矩形cornerRadius
ELLIPSE椭圆arcData
LINE线条-
VECTOR矢量fillGeometry
TEXT文本characters, style
BOOLEAN_OPERATION布尔运算booleanOperation
SLICE切片导出用

4.1. 节点通用属性

{
  "id": "953:118218",             // 节点唯一ID
  "name": "节点名称",              // 图层名称
  "type": "FRAME",                // 节点类型
  "visible": true,                // 是否可见
  "locked": false,                // 是否锁定
  "opacity": 1.0,                 // 透明度 0-1
  "blendMode": "PASS_THROUGH",    // 混合模式
  "absoluteBoundingBox": {        // 绝对定位和尺寸
    "x": -2981.0,
    "y": 6883.0,
    "width": 96.0,
    "height": 96.0
   },
  "absoluteRenderBounds": {       // 渲染定位和尺寸
    "x": -2981.0,
    "y": 6883.0,
    "width": 96.0,
    "height": 96.0
  },
  "constraints": {                // 约束
    "vertical": "TOP",
    "horizontal": "LEFT"
  },
  "effects": [],                  // 效果(阴影、模糊等)
  "fills": [],                    // 填充
  "strokes": [],                  // 描边
  "children": []                  // 子节点(容器类型)
}

4.2. 各节点类型详解

1. DOCUMENT(文档根节点)
{
    "id": "0:0",
    "name": "Document",
    "type": "DOCUMENT",
    "children": [] // CANVAS 节点数组
}

2. CANVAS(页面/画布)
{
    "id": "0:1",
    "name": "Page Canvas",
    "type": "CANVAS",
    "backgroundColor": {
        "r": 0.96,
        "g": 0.96,
        "b": 0.96,
        "a": 1
    },
    "children": [] // 页面内的所有元素
}

每个 CANVAS 代表一个页面


3. FRAME(画板/框架)

{
    "type": "FRAME",
    "clipsContent": true, // 是否裁剪超出内容
    "layoutMode": "VERTICAL", // 自动布局方向
    "primaryAxisSizingMode": "AUTO",
    "counterAxisSizingMode": "FIXED",
    "paddingLeft": 16,
    "paddingRight": 16,
    "paddingTop": 16,
    "paddingBottom": 16,
    "itemSpacing": 8, // 子元素间距
    "layoutAlign": "STRETCH",
    "cornerRadius": 8, // 圆角
    "children": []
}

这是一种最常见的节点,绝大部分节点的类型都是它


4. GROUP(组)
{
    "id": "953:118473",
    "name": "Group 2036096687",
    "type": "GROUP",
    "scrollBehavior": "SCROLLS",
    "children": []
}

通常children会有很多内容,但它纯粹的分组,没有自己的样式,样式来自子元素


5. RECTANGLE(矩形)
{
    "id": "953:118218",
    "name": "2",
    "type": "RECTANGLE",
    "scrollBehavior": "SCROLLS",
    "cornerRadius": 8, // 统一圆角
    "rectangleCornerRadii": [
        8,
        8,
        0,
        0
    ], // 分别设置四角
    "fills": [
        {
            "type": "SOLID",
            "color": {
                "r": 1,
                "g": 0,
                "b": 0,
                "a": 1
            }
        }
    ],
    "strokes": [
        {
            "type": "SOLID",
            "color": {
                "r": 0,
                "g": 0,
                "b": 0,
                "a": 1
            }
        }
    ],
    "strokeWeight": 1,
    "strokeAlign": "INSIDE" // INSIDE | OUTSIDE | CENTER
}

6. ELLIPSE(椭圆/圆形)
{
    "id": "I953:118474;953:115456",
    "name": "Ellipse 12175",
    "type": "ELLIPSE",
    "effects": [
        {
            "type": "LAYER_BLUR",
            "visible": true,
            "radius": 4.0
        }
    ],
    "arcData": {
        "startingAngle": 0.0,
        "endingAngle": 6.2831854820251465, // 2π = 完整圆
        "innerRadius": 0.0 // >0 为环形
    },
    "interactions": [],
    "complexStrokeProperties": {
        "strokeType": "BASIC"
    }
}

7. TEXT(文本)
{
    "id": "953:118226",
    "name": "95% OFF",
    "type": "TEXT",
    "strokes": [],
    "strokeWeight": 1.3939393758773804,
    "strokeAlign": "OUTSIDE",
    "absoluteBoundingBox": {
        "x": -2971.0,
        "y": 6882.847167968750,
        "width": 33.0,
        "height": 25.0
    },
    "absoluteRenderBounds": {
        "x": -2969.893066406250,
        "y": 6886.439941406250,
        "width": 30.792480468750,
        "height": 19.55957031250
    },
    "constraints": {
        "vertical": "TOP",
        "horizontal": "LEFT"
    },
    "characters": "95%\nOFF", // 文本内容
    "characterStyleOverrides": [
        21,
        21,
        21,
        21,
        20,
        20,
        20
    ],
    "styleOverrideTable": {
        "21": {
            "fontSize": 13.0
        },
        "20": {
            "fontSize": 8.0,
            "fills": [
                {
                    "blendMode": "NORMAL",
                    "type": "SOLID",
                    "color": {
                        "r": 1.0,
                        "g": 1.0,
                        "b": 1.0,
                        "a": 1.0
                    }
                }
            ]
        }
    },
    "lineTypes": [
        "NONE",
        "NONE"
    ],
    "lineIndentations": [
        0,
        0
    ],
    "style": { // 文本样式
        "fontFamily": "SF Pro",
        "fontPostScriptName": "SFPro-Heavy",
        "fontStyle": "Heavy",
        "fontWeight": 860,
        "textAutoResize": "WIDTH_AND_HEIGHT",
        "fontSize": 13.939393997192383,
        "textAlignHorizontal": "CENTER",
        "textAlignVertical": "BOTTOM",
        "letterSpacing": 0.0,
        "lineHeightPx": 16.634706497192383,
        "lineHeightPercent": 100.0,
        "lineHeightUnit": "INTRINSIC_%"
    }
}

重点关注:

  • characters: 纯文本内容
  • style: 文本样式
  • characterStyleOverrides + styleOverrideTable: 处理富文本

8. VECTOR(矢量路径)
{
    "type": "VECTOR",
    "strokeCap": "ROUND",
    "strokeJoin": "ROUND",
    "fillGeometry": [], // 填充路径数据 
    "strokeGeometry": [] // 描边路径数据 
}

9. BOOLEAN_OPERATION(布尔运算)
{
    "id": "953:118223",
    "name": "Union",
    "type": "BOOLEAN_OPERATION",
    "scrollBehavior": "SCROLLS",
    "children": [],
    "blendMode": "PASS_THROUGH",
    "fills": [],
    "strokes": [],
    "strokeWeight": 1.3939393758773804,
    "strokeAlign": "OUTSIDE",
    "booleanOperation": "UNION",
    "exportSettings": [
        {
            "suffix": "",
            "format": "PNG",
            "constraint": {
                "type": "SCALE",
                "value": 1.0
            }
        }
    ],
    "effects": [
        {
            "type": "DROP_SHADOW",
            "visible": true,
            "color": {
                "r": 0.0,
                "g": 0.0,
                "b": 0.0,
                "a": 0.250
            },
            "blendMode": "NORMAL",
            "offset": {
                "x": -1.3939393758773804,
                "y": 0.0
            },
            "radius": 2.7878787517547607,
            "showShadowBehindNode": false
        }
    ],
    "interactions": []
}

10. INSTANCE(组件实例)
{
    "id": "953:118542",
    "name": "Mask group",
    "type": "INSTANCE",
    "scrollBehavior": "SCROLLS",
    "componentId": "953:117624", // 引用的组件ID
    "overrides": [], // 覆盖的属性
    "children": []
}

这个也比较重要,通过 componentId 可以找到对应的 COMPONENT


11. COMPONENT(组件定义)
{
    "type": "COMPONENT",
    "componentPropertyDefinitions": {
        "Text#1:1": {
            "type": "TEXT",
            "defaultValue": "Button"
        },
        "Variant": {
            "type": "VARIANT",
            "variantOptions": [
                "Primary",
                "Secondary"
            ]
        }
    },
    "children": []
}

12. COMPONENT_SET(组件集/变体)
{
    "type": "COMPONENT_SET",
    "componentPropertyDefinitions": {
        "State": {
            "type": "VARIANT",
            "variantOptions": [
                "Default",
                "Hover",
                "Active"
            ]
        },
        "Size": {
            "type": "VARIANT",
            "variantOptions": [
                "Small",
                "Medium",
                "Large"
            ]
        }
    },
    "children": []
}

这个不多见,跟上述11的COMPONENT一样,只是它的一种集合体


5. 图片信息

有没有发现,上述的节点信息中并没有图片信息,但图片作为设计稿中最不可或缺的元素,怎么能没有图片类型的节点呢?

这是个好问题,然而Figma 中确实没有独立的 IMAGE 节点类型,下面让我们来了解一下图片信息是如何在Figma中存储的吧。

Figma 图片的存储方式

在 Figma 中,图片不是一种独立的节点类型,而是作为填充属性(fills) 附加在其他形状节点上:

你以为的图片存储方式或许是这样:

{ "type": "IMAGE", "src": "xxx.png" } 
 ❌ 没有这种节点

✅ 实际是这样的:

{
    "id": "953:118218",
    "type": "RECTANGLE", // 载体是矩形 
    "fills": [
        {
            "blendMode": "NORMAL", // 填充类型是图片 
            "type": "IMAGE", // 表示填充类型是图片
            "scaleMode": "FILL", // 图片缩放模式:`FILL`、`FIT`、`STRETCH`、`TILE`
            "imageRef": "fb233719d2b7c04499361d3052e21e9f32a1ca8d", // 图片的唯一哈希标识符,用于获取实际图片
            "imageTransform": [ // 2x3 变换矩阵,控制图片的缩放、旋转、位移
                [
                    0.81347656250,
                    0.0,
                    0.09082031250
                ],
                [
                    0.0,
                    0.86035162210464478,
                    0.093750
                ]
            ]
        }
    ]
}

如何获取实际图片

Figma 不直接存储图片数据,而是通过 imageRef 引用。要获取实际图片,需要调用:

# 方法1:获取文件中所有图片 
GET https://api.figma.com/v1/files/{file_key}/images 
# 方法2:导出指定节点为图片 
GET https://api.figma.com/v1/images/{file_key}?ids={node_id}&format=png

图片引用机制

┌─────────────────────────────────────────────────────┐
│  Figma JSON 数据                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │ imageRef: "843b1885be37478f2e04f7f82e52d481..." │  │
│  └───────────────────────┬───────────────────────┘  │
└──────────────────────────┼──────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────┐
│  Figma 图片服务器                                    │
│  通过 /v1/files/{key}/images API 获取               │
│  返回: { imageRef: "https://s3-xxx.amazonaws.com/..."} │
└─────────────────────────────────────────────────────┘

imageTransform 变换矩阵

"imageTransform": [ 
    [0.81347656250, 0.0, 0.09082031250], // [scaleX, skewX, translateX] 
    [0.0, 0.86035162210464478, 0.093750] // [skewY, scaleY, translateY] 
]
  • 第一行:X 轴缩放 81.3%,X 方向偏移 9.1%
  • 第二行:Y 轴缩放 86%,Y 方向偏移 9.4%

总结:Figma 通过 imageRef 哈希值引用图片,实际图片数据存储在 Figma 的云端服务器,需要通过 API 单独获取图片 URL,因此这里需要考虑token过期的问题。

6. 节点解析

我们拿到这些原始数据之后,是需要处理的,因此我们需要对各种节点信息解析

const parseNode = (node) => {
  if (!node) return null
  // console.log('parseNode node==', node)
  
  const base = {
    id: node.id,
    name: node.name,
    type: node.type,
    bounds: node.absoluteBoundingBox,
  };

  switch (node.type) {
    case "TEXT":
      return {
        ...base,
        text: node.characters,
        style: node.style,
      };

    case "RECTANGLE":
    case "ELLIPSE":
      return {
        ...base,
        fills: parseFills(node.fills),
        cornerRadius: node.cornerRadius,
      };

    case "FRAME":
    case "GROUP":
    case "COMPONENT":
    case "INSTANCE":
      return {
        ...base,
        children: node.children?.map(parseNode) || [],
      };

    case "VECTOR":
      return {
        ...base,
        fills: parseFills(node.fills),
        strokes: node.strokes,
      };

    default:
      return base;
  }
};

const parseFills = (fills) => {
  return fills
    ?.filter((f) => f.visible !== false)
    .map((fill) => {
      console.log('fill===', fill)
      switch (fill.type) {
        case "SOLID":
          return { type: "solid", color: rgbaToHex(fill.color) };
        case "IMAGE":
          return { type: "image", ref: fill.imageRef };
        case "GRADIENT_LINEAR":
          return { type: "gradient", stops: fill.gradientStops };
        default:
          return fill;
      }
    });
};

二、Figma位置信息转换

1. 位置信息

a. 核心位置属性

{
    "layoutMode": "HORIZONTAL",
    "counterAxisSizingMode": "FIXED",
    "primaryAxisSizingMode": "FIXED",
    "counterAxisAlignItems": "CENTER",
    "primaryAxisAlignItems": "CENTER",
    "paddingLeft": 8.0,
    "paddingRight": 8.0,
    "paddingTop": 3.0,
    "paddingBottom": 3.0,
    "itemSpacing": 8.0,
    "layoutWrap": "NO_WRAP",
    "absoluteBoundingBox": { // 绝对位置(相对于画布原点)
        "x": 100,
        "y": 200,
        "width": 300,
        "height": 150
    },
    "absoluteRenderBounds": { // 实际渲染边界(包含阴影等效果)
        "x": 95,
        "y": 195,
        "width": 310,
        "height": 160
    },
    "relativeTransform": [ // 相对于父节点的变换矩阵 
        [4, 0, 16], // [scaleX, skewX, translateX] 
        [0, 8, 12] // [skewY, scaleY, translateY] 
    ]
}

b. 属性对比

属性说明用途
absoluteBoundingBox相对画布的绝对坐标全局定位
absoluteRenderBounds包含效果的实际渲染区域导出时使用
relativeTransform相对父节点的变换矩阵转换相对位置的关键

2、计算相对位置

方法1:通过 relativeTransform 获取

const getRelativePosition = (node) => {
  const transform = node?.relativeTransform;
  // 处理位置换换
  if (transform && transform.length && transform?.[0].length) {
    return {
      x: transform[0][2], // translateX
      y: transform[1][2], // translateY
    };
  }
  return null;
};

方法2:通过绝对坐标计算

// 绝对位置计算
const calculateRelativePosition = (child, parent) => {
  console.log('=====calculateRelativePosition', child, parent)
  if (!child) return
  const childBox = child?.absoluteBoundingBox;
  const parentBox = parent?.absoluteBoundingBox;
  return {
    x: childBox?.x - parentBox?.x || 0,
    y: childBox?.y - parentBox?.y || 0,
    width: childBox?.width,
    height: childBox?.height,
  };
};


3、转换为 CSS 的几种方式

场景1:绝对定位

function toAbsoluteCSS(node, parent) {
  const rel = calculateRelativePosition(node, parent);
  return {
    position: "absolute",
    left: `${rel.x}px`,
    top: `${rel.y}px`,
    width: `${rel.width}px`,
    height: `${rel.height}px`,
  };
}

场景2:Flex 布局(Auto Layout)

当 Figma 使用 Auto Layout 时:

const toFlexCSS = (node) => {
  if (node.layoutMode === "NONE") return null;
  console.log('node==', node)
  return {
    display: "flex",
    flexDirection: node.layoutMode === "VERTICAL" ? "column" : "row",
    gap: `${node.itemSpacing}px`,
    padding: `${node.paddingTop}px ${node.paddingRight}px ${node.paddingBottom}px ${node.paddingLeft}px`,
    alignItems: mapAlignItems(node.counterAxisAlignItems),
    justifyContent: mapJustifyContent(node.primaryAxisAlignItems),
  };
};

const mapAlignItems = (value) => {
  consle.log('=====value', value);
  // 这里处理下转换
  const map = {
    MIN: "flex-start",
    CENTER: "center",
    MAX: "flex-end",
    BASELINE: "baseline",
  };
  
  return map[value] || "flex-start";
};

场景3:响应式约束(constraints)

const constraintsToCSS = (node, parent) => {
  // console.log('constraintsToCSS', node, parent)
  const { constraints } = node || {};
  const rel = calculateRelativePosition(node, parent);
  const css = { position: "absolute" }; 
  // 水平约束
  switch (constraints?.horizontal) {
    case "LEFT":
      css.left = `${rel.x}px`;
      css.width = `${rel.width}px`;
      break;
    case "RIGHT":
      css.right = `${parent?.absoluteBoundingBox?.width - rel.x - rel.width}px`;
      css.width = `${rel.width}px`;
      break;
    case "LEFT_RIGHT": // 左右拉伸
      css.left = `${rel.x}px`;
      css.right = `${parent?.absoluteBoundingBox?.width - rel.x - rel.width}px`;
      break;
    case "CENTER":
      css.left = "50%";
      css.transform = "translateX(-50%)";
      css.width = `${rel.width}px`;
      break;
    case "SCALE": // 按比例缩放
      css.left = `${(rel.x / parent?.absoluteBoundingBox?.width) * 100}%`;
      css.width = `${(rel.width / parent?.absoluteBoundingBox?.width) * 100}%`;
      break;
  } 
  //console.log('=====css1', css)
  // 垂直约束(类似逻辑)
  switch (constraints?.vertical) {
    case "TOP":
      css.top = `${rel.y}px`;
      css.height = `${rel.height}px`;
      break;
    case "BOTTOM":
      css.bottom = `${parent?.absoluteBoundingBox?.height - rel.y - rel.height}px`;
      css.height = `${rel.height}px`;
      break;
    case "TOP_BOTTOM":
      css.top = `${rel.y}px`;
      css.bottom = `${parent?.absoluteBoundingBox?.height - rel.y - rel.height}px`;
      break;
    case "CENTER":
      css.top = "50%";
      css.transform = (css.transform || "") + " translateY(-50%)";
      css.height = `${rel.height}px`;
      break;
    case "SCALE":
      css.top = `${(rel.y / parent?.absoluteBoundingBox?.height) * 100}%`;
      css.height = `${(rel.height / parent?.absoluteBoundingBox?.height) * 100}%`;
      break;
  }
  console.log('=====css2', css)
  return css;
}


4、完整转换示例

const figmaToCSS = (node, parent) => {
  const rel = calculateRelativePosition(node, parent); // 1. 基础样式
  const css = { width: `${rel.width}px`, height: `${rel.height}px` }; // 2. 判断布局方式
  console.log('====rel', rel)
  if (parent?.layoutMode !== "NONE") {
    // 父元素是 Auto Layout,子元素不需要定位
    // 使用 flex item 属性
    if (node.layoutAlign === "STRETCH") {
      css.alignSelf = "stretch";
    }
    if (node.layoutGrow === 1) {
      css.flexGrow = 1;
    }
  } else {
    // debugger
    // 父元素不是 Auto Layout,使用绝对定位
    Object.assign(css, constraintsToCSS(node, parent));
  } // 3. 如果当前节点是 Auto Layout 容器
  
  if (node?.layoutMode !== "NONE") {
    Object.assign(css, toFlexCSS(node));
  }
  console.log('=====figmaToCSS css', css)
  return css;
};

5、常见转换映射表

尺寸与位置

Figma 属性CSS 属性说明
absoluteBoundingBox.xleft需计算相对值
absoluteBoundingBox.ytop需计算相对值
absoluteBoundingBox.widthwidth直接使用
absoluteBoundingBox.heightheight直接使用

Flex 布局(Auto Layout)

Figma 属性CSS 属性说明
layoutMode: VERTICALflex-direction: column垂直排列
layoutMode: HORIZONTALflex-direction: row水平排列
itemSpacinggap子元素间距
paddingLeftpadding-left左内边距
paddingRightpadding-right右内边距
paddingToppadding-top上内边距
paddingBottompadding-bottom下内边距
layoutAlign: STRETCHalign-self: stretch拉伸填充
layoutGrow: 1flex-grow: 1自动扩展

约束定位(Constraints)

Figma 属性CSS 属性说明
constraints.horizontal: LEFTleft: Npx左对齐固定
constraints.horizontal: RIGHTright: Npx右对齐固定
constraints.horizontal: LEFT_RIGHTleft: Npx; right: Npx左右拉伸
constraints.horizontal: CENTERleft: 50%; transform: translateX(-50%)水平居中
constraints.horizontal: SCALEleft: N%; width: N%按比例缩放
constraints.vertical: TOPtop: Npx顶部固定
constraints.vertical: BOTTOMbottom: Npx底部固定
constraints.vertical: TOP_BOTTOMtop: Npx; bottom: Npx上下拉伸
constraints.vertical: CENTERtop: 50%; transform: translateY(-50%)垂直居中
constraints.vertical: SCALEtop: N%; height: N%按比例缩放

样式属性

Figma 属性CSS 属性说明
opacityopacity透明度
cornerRadiusborder-radius圆角
fills[].colorbackground-color背景色
strokes[].colorborder-color边框色
strokeWeightborder-width边框宽度
effects[] (DROP_SHADOW)box-shadow投影
effects[] (LAYER_BLUR)filter: blur()模糊
clipsContent: trueoverflow: hidden裁剪内容

自从AI面世以来,目前真正要用到figma原始数据结构的其实已经并不多,大多数情况下并不需要直接接触figma的原始JSON数据,这里分享一下相关经验,留给需要定制化处理Figma的童鞋。