首页
/ Excalidraw 包演进全记录:从 @excalidraw/excalidraw CHANGELOG 解析集成 API 的演进与迁移路径

Excalidraw 包演进全记录:从 @excalidraw/excalidraw CHANGELOG 解析集成 API 的演进与迁移路径

2026-09-04 23:35:54作者:舒璇辛Bertina

packages/excalidraw/CHANGELOG.md 是 Excalidraw 嵌入式包 @excalidraw/excalidraw 的官方变更日志,记录了这个手绘风格虚拟白板库自 0.1.0 首个版本以来每一次集成相关 API、Props、类型和构建产物的变更。本文以该文档为主线,梳理其组织规范、逐版本的关键演进(尤其是 Unreleased 中新增的交互控制、视口控制与工具系统重构,以及 0.18.0 的 ESM 化改造),并结合仓库内源码给出每条变更对应的实现位置,帮助嵌入该库的宿主应用开发者准确评估升级影响、完成破坏性变更迁移。

一、变更日志的组织规范与版本结构

CHANGELOG 文件头部以 HTML 注释形式声明了维护规范:每条变更必须归入 Features(新功能)、Fixes(缺陷修复)、Chore(非 src 文件变更,如 package.json)、Refactor(重构)四类之一并附 PR 链接,且最新变更追加在对应小节的顶部。

从整份文档的结构看,它形成了两条平行的信息流:

  1. Excalidraw API 小节 —— 影响宿主集成的变更(新增/重命名/移除的 props、命令式 API、类型、构建产物)。0.8.0 起文档明确标注“These section lists the updates which may affect your integration, so it is recommended to go through this when upgrading the version”,即升级版本时必读。
  2. Excalidraw Library 小节 —— 每次版本后固定以 “This section lists the updates made to the excalidraw library and will not affect the integration” 开头,记录只影响编辑器本身功能(如激光笔、elbow 箭头、帧、协作光标)而不影响嵌入方式的变更。

版本时间线(按文档记载)为:0.1.0 首个发布 → 0.4.x/0.5.0 → 0.6.0 → 0.7.0 → 0.8.0 → 0.9.0/0.10.0/0.11.0/0.12.0(后三者指向外部 release notes)→ 0.13.0 → 0.14.0/0.14.1/0.14.2 → 0.15.0/0.15.1/0.15.2 → 0.16.0/0.16.1 → 0.17.0/0.17.3 → 0.18.0 (2025-03-11) → 顶部 Unreleased(面向下一个版本的 API 变更,按日期标注,如 2026-07-14、2026-07-05、2026-07-03)。

二、Unreleased:面向受控宿主的新 API

CHANGELOG 顶部的 Unreleased 部分是目前信息密度最高的段落,它整体描述了一次“让宿主应用完全掌控编辑器交互、视口与工具”的 API 升级。以下逐项展开,并标注仓库内的实现落点。

2.1 interaction 与 ui 属性:把编辑器变成“只读画布”或“半交互画布”

新增 interaction 属性,类型签名为:

boolean | {
  enabled?: {
    navigation?: boolean;
    links?: boolean;
    embeds?: boolean;
    interactiveContent?: boolean;
    browserZoom?: boolean;
  }
}
  • 设为 false 时编辑器对用户完全惰性:不能平移、缩放、选择、编辑,键盘快捷键、剪贴板、拖放全部失效;且隐含只读模式(appState.viewModeEnabled 报告为 true 且不可解除)。此时组件不再捕获 wheel/touch 事件,页面可以在组件上方自然滚动(浏览器自身缩放默认仍被阻止)。
  • enabled 下的各字段是“选择性例外”:
    • navigation:允许以只读模式的方式平移(指针拖拽、滚轮、PageUp/PageDown,Shift 横向)与缩放(ctrl/cmd+滚轮、双指捏合、画布缩放与适配缩放快捷键 ctrl/cmd +/-/0、shift+1/2/3),并且遵守 appState.scrollConstraints,可与视口锁定组合用于受限查看器;
    • links:渲染链接图标并保持带链接元素可点击(点击元素任意位置打开链接);不允许时链接图标根本不渲染;
    • embeds:让 embeddable/iframe 元素保持 hover 与点击交互;
    • interactiveContent:附加简写,同时开启 linksembeds(及未来新增的交互内容类型);
    • browserZoom:在编辑器上方保留浏览器自身缩放手势(当 navigation 已开启时无意义,因为缩放输入已被编辑器接管)。

场景仍照常渲染,并可通过命令式 API(updateScenesetViewport 等)完全受控,适合实时预览、缩略图和标注叠层等场景。

同时新增 ui 属性(boolean | { enabled: { zoom?: boolean; scrollBackToContent?: boolean } }):设为 false 时不渲染 Excalidraw 默认 UI(工具栏、默认菜单与页脚控件、侧边栏、画布弹出层、右键菜单);对象形式在隐藏默认 UI 的同时可选保留缩放控件或“滚动回内容”按钮。画布内容(元素、文本编辑、帧名、嵌入)照常渲染,除非同时设置 interaction={false} 否则编辑器仍可交互;宿主 children 照常挂载,宿主自供 UI(包括 Excalidraw 导出的 <MainMenu><Footer> 等组件)继续渲染——隐藏自身展示组件是宿主的职责。

源码侧可以确认这些属性已落在组件的 Props 类型定义中:packages/excalidraw/types.ts#L876-L920interactionuiactiveTool 三个属性相邻声明,说明它们属于同一次“宿主控制面”扩展。

2.2 activeTool 受控属性与非交互模式下的用户驱动工具

  • 新增 activeTool 属性({ type: ToolType } | { type: "custom"; customType: string }),用于强制指定当前激活工具(受控)。设置期间,用户与 API 发起的工具切换一律被忽略:setActiveTool 对不匹配的目标直接拒绝并打印 console 警告;未强制的工具栏按钮渲染为禁用态;工具锁定切换(Q)失效;内部流程重置工具时(如场景加载 restore())编辑器会自动回弹到强制工具。强制工具表现得如同处于锁定态——使用后不会退回选择工具、绘制出的元素不会被自动选中——同时不会修改 appState.activeTool.locked,用户已持久化的锁工具偏好不受影响。移除该属性即可把工具控制权交还编辑器(当前工具保持激活)。
  • 强制工具必须“可激活”才生效:不能因 UIOptions.tools 被禁用;在非交互模式下还需通过 interaction.enabled.tools 放行,否则编辑器停留在 selection 工具,待其可激活后再套用强制工具。image 不能被强制(其激活会打开文件选择器)。
  • 新增 interaction.enabled.tools{ laser?: boolean; custom?: boolean }):非交互编辑器中列出的工具仍保持用户驱动——当其为主激活工具时指针输入继续驱动该工具。laser 启用后指针笔画绘制激光轨迹,且指针位置继续通过 onPointerUpdate 广播,因此协作方可以看到演讲者的激光笔与光标,而演讲者其余编辑器保持惰性;custom 启用后自定义工具继续接收 onPointerDown/onPointerUp(激活它们需 locked: true,否则首次指针交互后会退回选择工具而失效)。
  • 开启某个工具不等于开启用户驱动的工具切换:键盘仍然禁用,工具切换依然是宿主驱动(ExcalidrawAPI.setActiveTool)。该机制与 enabled.navigation 组合时,启用的工具赢得主指针拖拽,而滚轮输入(与中键拖拽)仍然平移/缩放;主激活工具启用时编辑器会消费画布上的 touch 输入(笔画不会滚动页面)。
  • 不变式维护:非交互模式下 setActiveTool 只激活经 interaction.enabled.tools 放行的工具,激活其他工具会被拒绝并警告(此前即使工具无法使用也会切换成功,例如只更新了画布光标)。工具状态本身也维护同样不变式——当激活工具失去例外资格(enabled.tools 变化、或会话中途变为完全非交互)时,工具重置为默认 selection,避免演讲者交接后其激光笔状态经 onChange/协作指针负载泄漏。

典型的组合用法:演示者强制 laser,观众用 selection + interaction={false}

2.3 工具系统与工具栏重构(含破坏性变更)

工具栏 UI 从隐藏的 radio/checkbox input 重写为基于中央工具注册表组合出的真实 <button> 元素,且所有工具激活统一走单一 setActiveTool 代码路径。破坏性变更:

  • ExcalidrawAPI.setActiveTool(tool, keepSelection?) 的第二个参数由布尔值改为选项对象:setActiveTool(tool, { keepSelection?: boolean; toggle?: boolean = false })。hand 与 eraser 现在是切换型工具:激活时记录上一个激活工具,再次激活(含通过 setActiveTool(..., { toggle: true }))即切回——重复调用会交替切换而非空操作;默认 toggle: false 保证幂等激活。
  • 工具按钮渲染为带 aria-pressed 的语义化 <button>,不再是 <label> 包裹的隐藏 <input type="radio">/<input type="checkbox">。如果你基于 DOM 做样式或查询:.ToolIcon_type_radio.ToolIcon_type_checkbox.ToolIcon--selected 类名不复存在,选中态改由 .ToolIcon--checked 类与 aria-pressed 属性表达;<Sidebar.Trigger>(含默认 Library 触发器)也改为渲染 <button>
  • 移除被 setActiveTool 漏斗与工具注册表取代的内部 action:toggleHandTooltoggleEraserTooltoggleLassoToolsetFrameAsActiveToolsetEmbeddableAsActiveTool(这些 ActionName 不再存在)。

行为变化:H(手)与 E(橡皮)快捷键现在可靠地切换回上一个激活工具(此前在不同平台/输入路径下不一致,且只读模式下 hand 不可用);hand 或 eraser 激活时按 Escape 同样返回上一工具;重复点击其工具栏按钮是空操作。在所有平台,点击已激活的 selection 工具都会切换到 lasso(此前只有 iOS 因受控 radio 特性生效)。工具字母快捷键对 CapsLock 不敏感,Shift+字母 永不切换工具,Ctrl+E 不再切换橡皮。切换工具不再产生 undo 历史条目。命令面板的工具命令改由工具注册表派生:重复的 "Hand" 条目消失,新增 "Web Embed","Lasso" 恒在列表中。触控笔模式在任意工具按钮上的首次笔触即激活(此前会延迟且偶发丢失)。

对应修复:移动端工具栏弹出层中当前选中项在点按后保持高亮;工具高亮不再与实际激活工具失步(受控 radio 的 DOM 失步问题)。工具注册表与 action 漏斗的实现可参见 packages/excalidraw/actions/register.tspackages/excalidraw/actions/manager.tsxpackages/excalidraw/components/ToolIcon.scss 所在的工具组件目录。

2.4 视口控制与滚动/缩放锁定:setViewport

ExcalidrawAPI.scrollToContent 被重建为 ExcalidrawAPI.setViewport()——统一的视口导航 API,支持视口适配(fit: "scale-down" | "contain" | "none")、把滚动/缩放锁定到目标(带橡皮筋式过度滚动)、围绕已渲染编辑器 UI 适配,以及场景加载时的视口初始化。

破坏性变更

// before
excalidrawAPI.scrollToContent(element, {
  fitToViewport: true,
  animate: true,
});
// after
excalidrawAPI.setViewport({ target: element, fit: "contain" });
  • ExcalidrawAPI.scrollToContent(target?, opts?)ExcalidrawAPI.setViewport(opts | null) 取代。
  • target 变为必传,不再默认为整个场景——要复现旧默认需显式传 target: excalidrawAPI.getSceneElements()。除元素/元素 id/element-link URL 外,现在也接受 Bounds{ x, y, width?, height? } 矩形(缺省维度默认为视口尺寸)。目标中已删除或非场景内的元素会被过滤并打印 console 警告。
  • fitToContent/fitToViewport 分别被 fit: "scale-down" / "contain" 取代,默认 "scale-down"(放大到目标恰好适配,最大 100%)。旧默认值取决于目标类型:字符串目标(元素 id/element-link)默认为 fit-to-content——与新默认一致——而元素目标或省略目标时默认按当前缩放重新居中而不改变缩放;要用旧行为请传 fit: "none"
  • animate/durationanimation: boolean | { duration?: number } 取代;导航现在默认始终带动画(此前仅字符串目标动画,元素目标除非 animate: true 都是跳变)——animation: false 可瞬时跳变。
  • canvasOffsetsoffsets 取代,且额外支持 UI 派生偏移(见下)。
  • viewportZoomFactorminZoommaxZoom 不再受支持。
  • zoomToFitBounds 改收 fit: "scale-down" | "contain" | "none" 替代 fitToViewport/viewportZoomFactor,且默认不再把结果缩放向下取整到 10% 步进——传 steppedZoom: true 可保留旧的取整行为。

新增能力

  • setViewport({ ..., lock }) 把视口锁定到解析后的目标:lock.scroll 约束平移范围在目标盒内,lock.zoom 使解析出的缩放成为最小缩放(继续放大允许)。动画导航进入锁定视口期间,用户的平移、缩放、fit 与页面导航输入会被消费而不改变或打断过渡动画;目标锁在动画稳定后才与最终视口一起安装。更新的 setViewport(...) 会取代进行中的过渡并从当前渲染视口出发。setViewport(null) 取消待处理过渡并清除任何待处理/激活的锁而不导航;不带 lock 的导航同样会取代既有锁。
  • lock.overscroll 为锁增加橡皮筋:平移可越过约束给定距离(视口像素,与缩放无关),待滚轮输入稳定或活跃指针/多指手势释放后弹回。默认 trueDEFAULT_OVERSCROLL(150px,从包根导出,实现位于 packages/excalidraw/viewport.ts#L40);传数字可定制,false/0 为刚性锁。缩放本身对锁仍是硬夹取——沿其边缘滑动而非过度滚动——且在弹回动画期间保持可用,不打断动画也不丢弃已有的屏幕空间过度量。
  • 锁激活期间,缩放适配快捷键(shift+1/2/3)作用于锁定盒而非场景元素(在无选择时),从而把视口恢复到锁的静息状态;重置缩放(ctrl/cmd+0)重置到锁的最小缩放而非 100%。所有 fit 结果都会重新对锁夹取。
  • 新增 <Excalidraw initialState={{ viewport }} />,用于场景加载时一次性初始化视口(并可安装锁),接受与 setViewport 相同除 animation 外的选项,且优先于 initialData.scrollToContent
  • offsets 选项(setViewportinitialState.viewport 均支持)按边内缩可用视口,避免目标被适配到覆盖 UI 之下。它组合静态像素边与 ui: true | ViewportOffsetsOptions——后者测量当前已渲染的编辑器 UI(工具栏、样式面板、侧边栏等),支持可配置 padding、按边覆盖,以及为当前隐藏的面板预留空间(reserve: { stylesPanel?: boolean; sidebar?: boolean })。同一测量也通过新的 ExcalidrawAPI.getViewportOffsets(opts?) 暴露;渲染在编辑器容器内的自定义 UI 可用 data-viewport-ui="top" | "bottom" | "side" 属性声明自己参与测量。

视口逻辑的源码实现集中在 packages/excalidraw/components/App.viewport.tssetViewport 定义于该文件 L634 起,包括目标解析与“已删除元素被过滤”的 console 警告)以及共享常量所在的 packages/excalidraw/viewport.ts;锁与 scrollConstraints 的组合行为还有测试佐证,见 packages/excalidraw/tests/scrollConstraints.test.tsx

2.5 其他破坏性变更:UIAppState 瘦身与主题委托

  • UIAppState 不再包含 zoomshouldCacheIgnoreZoom,以及随指针移动或动画帧更新的画布交互瞬态 snapLinesoriginSnapOffsetsuggestedBindingframeToHighlightelementsToHighlight。这些值不再驱动编辑器 UI 重渲染,因此接收 UIAppState 的 UI 渲染 props(renderCustomStats、自定义 <Footer/> 内容等)将拿不到它们;若读自完整 AppState 则会渲染过期值。正确做法是改用 useExcalidrawStateValue 订阅——组件只在选择值变化时重渲染:
import { useExcalidrawStateValue } from "@excalidraw/excalidraw";

const ZoomReadout = () => {
  // `undefined` on initial render, before the editor API initializes
  const zoomValue = useExcalidrawStateValue(
    (appState) => appState.zoom.value,
  );

  if (zoomValue == null) {
    return null;
  }

  return <div>{(zoomValue * 100).toFixed(0)}%</div>;
};

在 React 组件之外,也可以用 ExcalidrawAPI.onStateChange((appState) => appState.zoom.value, (zoomValue) => {...}) 订阅,或继续沿用 onScrollChange(scrollX, scrollY, zoom) prop。

  • 默认 UI 发起的主题切换现在委托给 <Excalidraw onThemeChange={(theme) => ...} />(若提供);未提供 onThemeChange 时仍回退到更新内部编辑器状态。具体影响:
    • MainMenu.DefaultItems.ToggleTheme 不再接受条目级 onSelect 回调;需要控制明暗/系统主题的应用应改为向 <Excalidraw />onThemeChange
    • 带系统主题的 MainMenu.DefaultItems.ToggleTheme 现用 allowSystemTheme 配合 theme={Theme | "system"},后者仅用于渲染选中值;常规明暗切换项应传 allowSystemTheme={false}
    • CommandPalette.defaultItems.toggleTheme 被移除,默认主题命令改由命令面板自身在 UIOptions.canvasActions.toggleTheme 启用时渲染。
    • UIOptions.canvasActions.toggleTheme 仍控制默认主题 UI 的可用性;为 null 时,若 props.theme 缺省或提供了 props.onThemeChange 则默认 true,否则默认禁用。
  • excalidrawAPI prop 更名为 onExcalidrawAPI:现在在挂载时调用(而非构造期间),卸载时以 null 再调用一次。CHANGELOG 提示该 API 未来可能整体移除(可用 onMountonUnmount 管理 ExcalidrawAPI 对象,例如缓存到全局状态)。

2.6 其他新特性:生命周期、事件、导出与状态订阅

  • 新增 ExcalidrawAPI.isDestroyed 标志:编辑器卸载后置 true。对已销毁实例调用任何 get* 方法、onStateChangeonEvent 会在开发环境抛错、生产环境 console.error。虽然卸载时 ExcalidrawAPI 会被重置为 null,仍建议调用前检查 isDestroyed 以防细微竞态。
  • 新增 onMountonInitializeonUnmount props:onMount 在编辑器根挂载后接收 { excalidrawAPI, container }onInitialize 在初始场景加载后触发;onUnmount 在卸载前触发。这些 prop 的接入可见 packages/excalidraw/index.tsx#L77-L80#L219-L222 向内部组件的透传。
  • 同一组事件也可通过命令式 api.onEvent(...) 访问:
<Excalidraw
  onExcalidrawAPI={(api) => {
    api.onEvent("editor:mount", ({ excalidrawAPI, container }) => {
      console.log(container);
    });

    api.onEvent("editor:initialize").then((readyApi) => {
      readyApi.setViewport({
        target: readyApi.getSceneElements(),
        animation: false,
      });
    });
  }}
/>

CHANGELOG 明确预告:未来版本中 excalidrawAPI.on* 系列订阅大多(甚至全部)将被移除,取而代之的是 excalidrawAPI.onEvent(name)。此外新增仅能通过 api.onEvent("editor:unmount") 访问的 "editor:unmount" 生命周期事件。

  • 从包中导出 <ExcalidrawAPIProvider/>useExcalidrawAPI()useExcalidrawStateValue(prop | props | selectorFunction)useOnExcalidrawStateChange(prop | props | selectorFunction, callback)。命令式 API 同时新增 onStateChange(prop | props | selectorFunction, callback?)onEvent(name, callback)。用法示例:
<ExcalidrawAPIProvider>
  <Excalidraw />
  <Logger />
</ExcalidrawAPIProvider>;

function Logger() {
  // initially null before the ExcalidrawAPIProvider initializes after
  // <Excalidraw/> renders
  // When <Excalidraw/> unmounts, is reset back to null
  const api = useExcalidrawAPI();

  useExcalidrawStateValue("viewModeEnabled", (viewModeEnabled) => {
    console.log("view mode changed:", viewModeEnabled);
  });

  React.useEffect(() => {
    if (api) {
      console.log("editor instance id:", api.id);
    }
  }, [api]);

  return null;
}

这些 hooks 的实现位于 packages/excalidraw/hooks/useAppStateValue.ts,并在 packages/excalidraw/index.tsxpackages/excalidraw/types.ts 中导出/声明。

  • 新增 onExport,允许宿主在异步工作完成前延迟 JSON 导出。处理函数接收导出数据与 AbortSignal,可返回 Promise 或 yield 进度更新的 async generator,驱动内建 toast UI:
<Excalidraw
  onExport={async function* (_type, { files }, { signal }) {
    yield { type: "progress", message: "Waiting for images..." };

    await waitForImagesToLoad(files, signal);

    if (signal.aborted) {
      return;
    }

    yield { type: "progress", message: "Export ready", progress: 1 };
  }}
/>

2.7 Unreleased 中 Library 侧的破坏性变更

  • updateActiveTool(来自 @excalidraw/common):data 参数中的 lastActiveToolBeforeEraser 更名为 lastActiveTool(它本就以此名存储于 appState.activeTool,且不再专属于橡皮)。
  • getClosestElementBounds 不再从 @excalidraw/element 导出。改用 getCommonBounds / getElementBounds,或如确需相同行为则自行基于 getElementBounds 重新实现最近目标距离检查。

三、0.18.0 (2025-03-11):ESM 化与一批功能里程碑

0.18.0 的亮点(Highlights)列出了该版本的功能主线:命令面板(command palette)、多人协作撤销/重做、可编辑元素 stats、文本元素自动换行、更多字体的字体选择器、中日韩字体、SVG 导出的字体子集化、elbow 箭头、流程图、场景搜索、图片裁剪、元素链接。

3.1 破坏性变更:从 UMD 转向 ES modules

包从 UMD 迁移到 ESM 打包格式。@excalidraw/excalidraw 的新 dist 目录只包含打包后的源码文件,依赖均可 tree-shake。文档给出(简化后的)产物结构:

@excalidraw/excalidraw/
├── dist/
│   ├── dev/
│   │   ├── fonts/
│   │   ├── index.css
│   │   ├── index.js
│   │   ├── index.js.map
│   ├── prod/
│   │   ├── fonts/
│   │   ├── index.css
│   │   ├── index.js
│   └── types/

(文档注明该结构省略了可懒加载模块——包括以 ES 模块形式处理的语言包——以及开发包的 source maps。)

迁移要点:

  • 环境要求:你的 JS 环境必须支持 ES modules;可能需要在 package.json 中定义 "type": "module" 或使用 <script type="module" />
  • TypeScript"moduleResolution": "node""node10" 不再可用(它们不支持 package.json"exports" 字段),应改用 "bundler""node16""nodenext"
  • ESM 严格解析:若使用 Webpack 等要求导入路径完全指定的打包器,需显式关闭该特性——例如 Webpack 中把 resolve.fullySpecified 设为 false。因此 CRA 在不 eject 或使用变通方案(如 craco)的情况下不再可用。
  • 导入方式变化

使用打包器(Vite、Next.js 等)时:

// excalidraw library with public API
import * as excalidrawLib from "@excalidraw/excalidraw";
// excalidraw react component
import { Excalidraw } from "@excalidraw/excalidraw";
// excalidraw styles, usually auto-processed by the build tool (i.e. vite, next, etc.)
import "@excalidraw/excalidraw/index.css";
// excalidraw types (optional)
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";

无打包器的纯浏览器环境(import map 用于去重 react 与 react-dom 版本):

<!-- Environment: browser with a script tag and no bundler -->

<!-- excalidraw styles -->
<link
  rel="stylesheet"
  href="https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/dev/index.css"
/>
<!-- import maps used for deduplicating react & react-dom versions -->
<script type="importmap">
  {
    "imports": {
      "react": "https://esm.sh/react@19.0.0",
      "react/jsx-runtime": "https://esm.sh/react@19.0.0/jsx-runtime",
      "react-dom": "https://esm.sh/react-dom@19.0.0"
    }
  }
</script>
<script type="module">
  import React from "https://esm.sh/react@19.0.0";
  import ReactDOM from "https://esm.sh/react-dom@19.0.0";
  import * as ExcalidrawLib from "https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/dev/index.js?external=react,react-dom";
</script>

(上述脚本引的是 0.18.0 的 esm.sh CDN 地址,属于文档给出的示例引用方式;版本升级后需同步替换。)

  • excalidraw-assets / excalidraw-assets-dev 目录废弃:这两个曾承载语言包与字体的目录不再使用。
    • 语言包:不再作为静态 .json 资产,而是经 esbuild 直接转译为 ES 模块 .js。注意某些构建工具(如 Vite)可能需要把 build target 设为 es2022,以支持“任意模块命名空间标识符名”(如 export { english as "en-us" }):
// vite.config.js
optimizeDeps: {
  esbuildOptions: {
    // Bumping to 2022 due to "Arbitrary module namespace identifier names" not being
    // supported in Vite's default browser target
    target: "es2022",
    // Tree shaking is optional, but recommended
    treeShaking: true,
  },
}
  • 字体:默认从 CDN 自动加载。自托管时,需把 node_modules/@excalidraw/excalidraw/dist/prod/fonts 的内容复制到你的静态资源路径(如项目 public/ 目录),并把 window.EXCALIDRAW_ASSET_PATH 设置为相同路径(根目录即 /):
<script>window.EXCALIDRAW_ASSET_PATH = "/";</script>

或自托管 CDN 时:

<script>
  window.EXCALIDRAW_ASSET_PATH = "https://cdn.domain.com/subpath/";
</script>

或在 Next.js 中按 location.origin 动态设置:

<Script id="load-env-variables" strategy="beforeInteractive">
  {`window["EXCALIDRAW_ASSET_PATH"] = location.origin;`} // or use just "/"!
</Script>
  • updateScenecommitToHistorycaptureUpdate 取代(多人协作撤销/重做计划的一部分,随新增 Store 组件引入):
// before
updateScene({ elements, appState, commitToHistory: true }); // A
updateScene({ elements, appState, commitToHistory: false }); // B

// after
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
updateScene({
  elements,
  appState,
  captureUpdate: CaptureUpdateAction.IMMEDIATELY,
}); // A
updateScene({
  elements,
  appState,
  captureUpdate: CaptureUpdateAction.NEVER,
}); // B

文档强调部分更新不被 store/history 观察——例如 collaborators 对象或 AppState 中未被观察的部分(非 ObservedAppState)——无论 captureUpdate 取值都不会进入本地撤销/重做栈。三种语义的对照表:

撤销行为 commitToHistory(旧) captureUpdate(新) 说明
立即可撤销 true CaptureUpdateAction.IMMEDIATELY 用于应被捕获的更新,绝大多数本地更新;这些更新立即进入本地撤销/重做栈
最终可撤销 false(默认) CaptureUpdateAction.EVENTUALLY(默认) 用于不应立即捕获的更新——通常是某些异步多步过程的例外;否则它们都会被下一次 IMMEDIATELY 捕获(由下一次 updateScene 或编辑器内部触发)。这些更新最终进入本地撤销/重做栈
永不撤销 无对应值 CaptureUpdateAction.NEVER 新增:此前无等价物。推荐用于永不记录的更新,如远端更新或场景初始化

CaptureUpdateAction 在仓库中的使用可与文档相互印证:如 packages/excalidraw/actions/actionAlign.tsx 中各类对齐更新传 CaptureUpdateAction.IMMEDIATELY,而 packages/excalidraw/actions/actionAddToLibrary.tsCaptureUpdateAction.EVENTUALLY——与上表“本地编辑立即捕获、辅助更新延迟捕获”的语义一致。

  • 其他破坏性/结构性变更
    • ExcalidrawTextElement.baseline 被移除,替换为基于字体度量(font metrics)的垂直偏移计算,在每次文本元素重渲染时执行;若使用自定义字体,需扩展 FONT_METRICS 对象。
    • ExcalidrawEmbeddableElement.validated 被移除并移入私有编辑器状态(除非你曾读该属性,否则基本不受影响);内嵌 URL 的校验仍在内部进行,公开 prop validateEmbeddable 继续生效。
    • Stats 容器 CSS 变化:若使用 renderCustomStats,可能需要调整样式以保持原布局。
    • <DefaultSidebar /> 的触发器现在总是与宿主应用的触发器合并,经 <DefaultSidebar.Triggers/> 渲染;<DefaultSidebar.Triggers/> 不再接受 children 以外的任何 props。

3.2 0.18.0 功能要点

除 Highlights 所列主线外,0.18.0 与集成相关的其他特性(择要):convertToExcalidrawElements 在创建 frame 时优先使用用户给定的坐标与尺寸;props.initialData 可以是返回 ExcalidrawInitialDataState(或其 Promise)的函数;MainMenu.DefaultItems.ToggleTheme 支持 onSelect(theme) 回调与可选 allowSystemThemeuseHandleLibrary 新增 opts.adapter(新的库初始化/持久化推荐模式)与 opts.migrationAdapter(跨存储迁移,如 LocalStorage 迁到 IndexedDB),同时软弃用 opts.getInitialLibraryItems;新增 onPointerUp prop;暴露 getVisibleSceneBounds 辅助函数;window.EXCALIDRAW_ASSET_PATH 扩展为接受 string[](多个 base URL 回退);支持自定义文本度量提供器;新增 props.onDuplicategridSize 与启用状态分离并支持自定义 gridStep;构建侧升级 Vite 5.4.x、Vitest 2.x 并升级 React 19。修复列表覆盖 elbow 箭头路由/正交性、frame 绑定、图片导出保留 alpha 通道、SVG 归一化、文本换行布局抖动等数百项细节。

四、历史版本演进脉络:集成 API 如何一步步长成今天的样子

以下按 CHANGELOG 的记载归纳各版本对宿主集成有实质影响的变化,可作为升级时的“考古地图”。

4.1 早期(0.1.0 – 0.7.0):确立宿主集成基础

  • 0.1.0@excalidraw/excalidraw 首个发布。
  • 0.2.0:导出若干供宿主通信的 Extra API;移除语言选择器并新增 langCoderenderFooter(i18n 交由宿主负责,BREAKING);新增 exportToBackend prop 供宿主实现可分享链接。0.2.1 起 CSS 与 JS 一起打包,宿主无需单独导入 css。
  • 0.3.0zenModeEnabled/gridModeEnabled/viewModeEnabled 受控 props;getAppState 暴露于 ref;允许宿主为协作者传 coloruserState
  • 0.3.1:支持 Excalidraw 置于可滚动容器内。0.4.0:暴露 window.EXCALIDRAW_ASSET_PATH(默认从 unpkg 的当前版本 dist 加载资产,文件名带 hash 便于缓存更新);initialData.scrollToCenter 控制挂载时是否滚动居中;导出 restore/restoreAppState/restoreElements0.4.2/0.4.3:包裹层改为 position relative、滚动防抖降至 100ms、CSS 变量作用域化避免与宿主冲突。0.5.0name prop(完全受控)、theme prop(完全受控)、libraryReturnUrl、canvas/svg/blob 导出 API;BREAKING:offsetLeft/offsetTop 移除(改用 ref 的 setCanvasOffsets)、initialData.scrollToCenter/setScrollToContent 更名、appState.appearance 更名 appState.theme(含 Appearance_darktheme--dark 类名变更)。0.6.0UIOptions prop 定制画布动作(背景色选择器、清空、导出、加载、保存等);width/height props 移除(BREAKING,组件改为占满容器 100%,容器需非零尺寸);导出 TypeScript 类型(@excalidraw/excalidraw/types);renderCustomStatssetToastMessage(ref);图片元素支持;BREAKING:script 标签嵌入文件名改为 excalidraw.production.min.js(dev 为 excalidraw.development.js)、setCanvasOffsets 更名 refresh0.7.0scrollToContent API(BREAKING:由 setScrollToContent 更名)支持单元素或不传(默认场景元素);库改为按实例隔离(BREAKING:宿主负责通过 onLibraryChange 持久化并经 initialData.libraryItems 导入);键盘事件默认绑定组件而非 document(BREAKING:handleKeyboardGlobally 恢复旧行为);detectScrollonPaste prop;类型更名 DataStateExportedDataStateLibraryDataExportedLibraryData

4.2 0.8.0 – 0.15.2:命令式 API 与组件化 UI 成型

  • 0.8.0updateScene 支持更新任意 appState 属性(此前仅 viewBackgroundColor);serializeAsJSON 助手;renderTopRightUI prop;加密盾牌图标从包中移除(改由 renderFooter 渲染,appState 一并传入该 prop)。
  • 0.13.0restoreElements() 新增可选参数控制是否重算文本尺寸(默认 true,协作等紧循环场景可关闭);renderSidebar prop 渲染自定义侧边栏;toggleMenu prop;theme 半受控;元素 customDataexportPaddingexportToCanvas/exportToBlob,默认 10)。BREAKING:UIOptions.canvasActions.theme 更名 toggleThemesetToastMessage 更名 setToast(签名更新,可传 duration/closable/message)。
  • 0.14.0:欢迎屏自定义;主菜单组件 API 暴露(MainMenu 及其定制);Footer 从 render prop 改为子组件(BREAKING:renderFooter 移除);<Excalidraw/> 顶层未知 children 直接渲染进容器;LiveCollaborationTrigger 组件暴露(取代 props.onCollabButtonClick,BREAKING)。元素 schema:currentItemStrokeSharpness/currentItemLinearStrokeSharpness 合并为 currentItemRoundnessstrokeSharpnessroundness
  • 0.14.1/0.14.2:按钮 overflow 修复(LiveCollaborationTrigger 协作者计数 CSS);欢迎屏不再默认渲染(UIOptions.welcomeScreen 弃用,需自行渲染);MainMenu.Item/MainMenu.ItemLink 支持 onSelect(event)event.preventDefault() 可阻止菜单关闭)。
  • 0.15.0scrollToContent 新增 opts(视口适配与动画滚动);useI18n() hook(返回 t()langCode);restoreElements/restore 的可选参数改为 opts{ refreshDimensions?: boolean, repairBindings?: boolean })。BREAKING:restoreElements 的位置参数 refreshDimensions 移除。

4.3 0.16.0 – 0.17.3:侧边栏、API 现代化与 Preact

  • 0.16.0:程序化创建容器、线性元素、文本容器、带标签箭头与箭头绑定;Web-Embeds(iframe 元素别名)与 props.validateEmbeddablescrollToContent 支持 opts.fitToViewport/opts.viewportZoomFactor;元素 link URL 严格净化。侧边栏 tab 化(BREAKING 较多)props.renderSidebar 移除改为 children 渲染;appState.isSidebarDocked 更名 appState.defaultSidebarDockedPreference(语义收窄为默认侧边栏,自定义侧边栏需自行管理 docked 状态);Sidebarprops.dockable 移除(以 props.onDock() + props.docked 表达可停靠);Sidebar.Header 不再默认渲染;props.onClose 更名 props.onStateChangerestore()/restoreAppState() 无论 docked 状态都保留 appState.openSidebar。同版本还引入 DefaultSidebar 组件(自定义默认侧边栏、加 tab)、帧渲染定制、色板重设计、协作对话框重设计等 Library 侧功能。
  • 0.17.0BREAKING:Ref 支持移除,集成改用 excalidrawAPI prop 访问 API;API 中的 readyreadyPromise 停用移除;useDevice hook 返回值区分 editorviewport 断点。功能面:可禁用 image 工具(UIOptions.tools = { image: false },同时禁用图片插入但保留 .excalidraw 文件导入);excalidrawAPI prop;导出 getCommonBoundselementsOverlappingBBoxisElementInsideBBoxelementPartiallyOverlapsWithOrContainsBBox 等边界工具;transform API 默认重新生成 id 并平移绑定 0.5px 避免重叠;onChange/onPointerDown/onPointerUp API 订阅者;props.locked 支持 setActiveToolMainMenu.Item 新增 selected prop。构建侧:Preact 支持——宿主需把 process.env.IS_PREACT 设为 true,Vite 等构建器需在 define 中显式提供该变量。
  • 0.17.3:修复 customData 在转换为 ExcalidrawElement 时丢失的问题;修复 v0.17.0 中断掉的 UMD 浏览器构建,并再次强调 Vite 下需 "process.env.IS_PREACT": JSON.stringify("true")(字符串而非布尔);禁用箭头标签的边界缓存修复错误渲染。

4.4 0.9.0 / 0.10.0 / 0.11.0 / 0.12.0

这四个版本在 CHANGELOG 中仅留版本头并指向外部 release notes(0.9.0 2021-07-10、0.10.0 2021-10-13、0.11.0 2022-02-17、0.12.0 2022-07-07),细节未在本文件中展开——阅读升级历史时这是唯一的“信息缺口”,需要留意。

五、从文档到仓库:继续深入的入口

六、总结

@excalidraw/excalidraw 的 CHANGELOG 呈现出一条清晰的演进路线:早期版本解决“能不能嵌进宿主应用”的问题(类型导出、CSS 打包、资产路径、实例隔离、键盘事件作用域);中期版本把 UI 拆成可替换的组件 API(MainMenu、Footer、Sidebar、WelcomeScreen)并持续收窄 render prop、转向 children 模式;0.17/0.18 完成从 ref 到 excalidrawAPI prop、从 commitToHistorycaptureUpdate 的现代化;当前 Unreleased 段落则把“宿主控制面”推到极致——interaction/ui/activeTool 三个 props 与 setViewport 锁定模型共同覆盖了实时预览、演示模式、受控编辑器等场景,同时用 onEvent/ExcalidrawAPIProvider/useExcalidrawStateValue 预告了事件与状态订阅 API 的下一轮收敛方向。对正在嵌入 Excalidraw 的开发者而言,这份文档既是升级前的核对清单(每个 BREAKING CHANGE 都给出了 before/after 与替代方案),也是理解该库“编辑器作为受控组件”设计哲学的最佳入口。

登录后查看全文
热门项目推荐
相关项目推荐