图示
DiagramExportMenu - 导出菜单
把图带走:PNG、JPEG、WebP、SVG,以及 1200×630 的分享卡。
用法
什么时候用它
来自@misoto22/folio/diagrams
import { DiagramExportMenu } from '@misoto22/folio/diagrams'说明
把图从页面上带走:五个文件,一个菜单。
为什么是菜单自己干活而不是抛回一个格式名:这些导出里的每一个都是同样的六步——找到 <svg>,用 getComputedStyle 遍历它、把自定义属性烘成真实颜色,序列化,栅格化,给文件命名,交给浏览器——它们之间只有最后两步不同。一个抛出 'png'、剩下的留给调用方的菜单,会变成一个每个使用者都要重写一遍、而且都写不对的菜单;而最难的那一半(序列化后的 SVG 里 var(--ink) 解析成空,图整个是隐形的)正是调用方不会知道要写的那一半。
onExport 仍然留着,给自带管线的页面用——服务端渲染、另一种画幅——传了它,下面的一切就都变成空转;这也正是走那条路的结果里 source 是 'caller' 的原因:这个组件看不见那个回调到底有没有产出文件,所以它不会声称有。
每种格式到底是什么,说清楚而不是暗示。
SVG 是颜色已解析的原始图形。它可编辑,也是唯一无损的一种,但它不带 Web 字体:一台没有这些字族的机器会用回退字体渲染,字形度量会不一样。
底色就是读者正看着的那块表面,除非 background 另有说明。background={null} 导出在透明底上——给一张要落到有色页面上的图用——JPEG 除外,它没有 alpha 通道,不管你要什么都会被压平到纸色上。
PNG、JPEG 和 WebP 是浏览器自己的栅格化器按 2 倍重画那份 SVG。不是截图——抗锯齿,以及页面样式表从 <svg> 外面施加的任何效果,都不在里面。JPEG 没有 alpha 通道,所以它会被压平到解析出来的纸色上,而不是压到黑色上——后者正是一张透明 PNG 被交给一个不支持透明的格式时会变成的样子。
分享卡是一个 1200 × 630 的画幅,标题印在上面,整张图以留边方式装进去——绝不裁切。一张为了填满画幅而裁切的卡片,是另一张图的照片。
结构
| 部件 | 说明 |
|---|---|
| 触发器必填 | 一颗写着 Export 的小号 secondary 按钮,或者顶替它的那个触发器——替代品是插槽进来的,所以它成为这个菜单自己的控件,而不是站在一个控件旁边。 |
| 菜单必填 | 一个对齐到触发器行尾的下拉,分成三个带标题、以分隔线隔开的组:Image、Vector、Share。这个分组,就是对「这五个里我要哪一个」的回答。 |
| 格式行必填 | 一种格式一行菜单项,带名字,下面是一行等宽提示——「Lossless, 2× for retina」、「Compact, flattened onto paper」。它们是真正的菜单项,所以方向键走得过去、打字能跳到某一行,而选中一行会在它刚写出的那个文件之上把菜单关掉。正在跑的那一行会加上省略号,而在它跑完之前每一行都是禁用的,所以第二次点击开不出第二次导出。 |
| 序列化器必填 | 没有任何东西可以指的那一部分:画面被克隆一份,每个节点算出来的颜色都被写成内联样式,克隆体再放到一块与画面加内边距同样大小的底上。正是它挡住了 var(--ink) 抵达一个没有样式表的文档、然后什么都画不出来。 |
| 分享卡 | 一个固定 1200 × 630 的框,上面印着标题,整张图以留边的方式装在里面——绝不裁切填满,因为裁切填满的卡片,画的是另一张图。 |
| 结果 | onResult,调用时带上格式,以及一个 ok 或者那个 Error。它是导出失败唯一被报出来的地方。 |
实践建议
推荐
- targetRef 要指向那层包裹元素,而不是你自己找到的某个 svg。导出会先去找画面标记,正是它挡住了工具条和画面在同一个包裹里的页面导出一张箭头图标的图。
- 要等画面量到尺寸之后再导出。序列化器读的是元素的盒子,尺寸还是零时会抛错,所以和挂载在同一个 tick 里发出的调用会大声失败,而不是写出一个空文件。
- 接住 onResult,把失败摆到读者面前。一张还没量到尺寸的图、一块被跨源图片污染的 canvas、一个没有返回 2D 上下文的浏览器——每一种都是在那里报出来的,而不是在点击处抛出来;不接的下场,是一个悄无声息什么也不做的菜单项。
- ok 之外还要读 source。built-in 说的是一个文件确实到了浏览器;caller 说的只是你自己的 onExport 兑现了,而一个什么都没做的处理函数同样会兑现——菜单看不进你的流水线里去,也不会摆出一副看得进的样子来报。
- 要放到有色页面上的图,传 background={null}。其余每一次导出都会在画面背后铺上读者当时那层纸面;而 JPEG 无论这一项怎么写都会被压到那层纸面上,因为一张透明的 JPEG 就是一张黑的。
避免
- 文件里装的是画面,外加读者当时所处的那个主题。颜色是从活着的元素上读下来的,所以从深色页面导出的图,放进浅色文档里就是一张深色的图;而工具条、检查面板以及图自己那份隐藏的摘要列表都是 HTML,所以它们一个也不会跟着走。
- 不要把 SVG 当成像素级精确的记录。隔离出来的那份文档取不到页面的 Web 字体,所以字体会回退到这台机器上有的那一套——文字和折行已经定死了,但在屏幕上刚好塞进板块的一个名字,到了文件里可能撑出自己那条线。
- 输出的尺寸是按画面在屏幕上的盒子算的,不是按它的 viewBox,所以一张在 DiagramCanvas 里以 4× 显示的图,序列化出来大四倍,然后还要在这之上再按 2× 栅格化。导出前先把视图复位,否则读者拿到的文件,尺寸记录的是缩放当时碰巧停在哪儿。
示例
default
一个触发器后面五个文件,按用途分组:PNG、JPEG 和 WebP 是按 2 倍栅格化的画作,SVG 是画作本身、每一个自定义属性都解析成了真实颜色,而分享卡是一张 1200 × 630 的 PNG,标题印在图的上方。targetRef 指向的是图形外面那层容器、不是那个 svg,菜单会在它里面找到图形自己的画作。title 是给下载文件命名的东西,会被 slug 化——「Request path」到手就是 request-path.png。
2 elements and 1 relationships.
- CloudFront (cloud) — CDN
- API (backend) — FastAPI
- CloudFront → API: HTTPS
what lands in the file
这里的 ref 把工具栏也一并裹了进来,而那条工具栏里的每个图标本身都是一个 svg——菜单会先去找图形自己的画作标记,正是这一点让一次「导出这张图」不至于变成一张信息按钮的照片。离开页面的只有画作本身:工具栏、面板,以及任何其他用 HTML 画出来的东西都不在文件里。颜色是从活的元素上读的,所以下载下来的是读者此刻正在看的那个主题;但网页字体不会跟着走,一台没有这个字族的机器会用别的字体来排那些标签。
3 elements and 2 relationships.
- Checkout (backend) — Django
- Orders topic (messagebus) — Kafka
- Payments (external) — Stripe
- Checkout → Orders topic: publish
- Orders topic → Payments: capture
your own pipeline
onExport 把整份活接管过去:序列化、栅格化和下载这几步一概不跑,所以除非这个处理器自己去写,否则不会有任何东西被写到磁盘上。当文件必须来自浏览器去不了的地方时就去拿它——一个服务端渲染器、一个你自己定的画幅尺寸、一个队列。onResult 无论哪种情况都照样触发,也正是页面用来弹一条提示的那道接缝;trigger 替换的是按钮、不是菜单,所以一个只有图标的触发器仍然需要它的 aria-label。
2 elements and 1 relationships.
- CloudFront (cloud) — CDN
- API (backend) — FastAPI
- CloudFront → API: HTTPS
组成部分
在调用处组合,而不是通过 props 配置——所以这个组件没有预料到的布局,依然表达得出来。
exportFigure
一张图、一种格式、一个文件。
导出出来,是为了让页面能把一个快捷键或它自己的按钮接到菜单做的同一件事上,而不必把那六步重写一遍。
这个组件没有自己的属性。
类型
export type ExportFormat = 'png' | 'jpeg' | 'webp' | 'svg' | 'share-card'无障碍
- 导出失败会通过 onResult 报出来而不是被吞掉:一次悄无声息的点击,和一个坏掉的按钮是分不出来的。