Excalidraw 包演进全记录:从 @excalidraw/excalidraw CHANGELOG 解析集成 API 的演进与迁移路径
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 链接,且最新变更追加在对应小节的顶部。
从整份文档的结构看,它形成了两条平行的信息流:
- 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”,即升级版本时必读。
- 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:附加简写,同时开启links与embeds(及未来新增的交互内容类型);browserZoom:在编辑器上方保留浏览器自身缩放手势(当navigation已开启时无意义,因为缩放输入已被编辑器接管)。
场景仍照常渲染,并可通过命令式 API(updateScene、setViewport 等)完全受控,适合实时预览、缩略图和标注叠层等场景。
同时新增 ui 属性(boolean | { enabled: { zoom?: boolean; scrollBackToContent?: boolean } }):设为 false 时不渲染 Excalidraw 默认 UI(工具栏、默认菜单与页脚控件、侧边栏、画布弹出层、右键菜单);对象形式在隐藏默认 UI 的同时可选保留缩放控件或“滚动回内容”按钮。画布内容(元素、文本编辑、帧名、嵌入)照常渲染,除非同时设置 interaction={false} 否则编辑器仍可交互;宿主 children 照常挂载,宿主自供 UI(包括 Excalidraw 导出的 <MainMenu>、<Footer> 等组件)继续渲染——隐藏自身展示组件是宿主的职责。
源码侧可以确认这些属性已落在组件的 Props 类型定义中:packages/excalidraw/types.ts#L876-L920 中 interaction、ui、activeTool 三个属性相邻声明,说明它们属于同一次“宿主控制面”扩展。
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:toggleHandTool、toggleEraserTool、toggleLassoTool、setFrameAsActiveTool、setEmbeddableAsActiveTool(这些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.ts、packages/excalidraw/actions/manager.tsx 与 packages/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/duration被animation: boolean | { duration?: number }取代;导航现在默认始终带动画(此前仅字符串目标动画,元素目标除非animate: true都是跳变)——animation: false可瞬时跳变。canvasOffsets被offsets取代,且额外支持 UI 派生偏移(见下)。viewportZoomFactor、minZoom、maxZoom不再受支持。zoomToFitBounds改收fit: "scale-down" | "contain" | "none"替代fitToViewport/viewportZoomFactor,且默认不再把结果缩放向下取整到 10% 步进——传steppedZoom: true可保留旧的取整行为。
新增能力:
setViewport({ ..., lock })把视口锁定到解析后的目标:lock.scroll约束平移范围在目标盒内,lock.zoom使解析出的缩放成为最小缩放(继续放大允许)。动画导航进入锁定视口期间,用户的平移、缩放、fit 与页面导航输入会被消费而不改变或打断过渡动画;目标锁在动画稳定后才与最终视口一起安装。更新的setViewport(...)会取代进行中的过渡并从当前渲染视口出发。setViewport(null)取消待处理过渡并清除任何待处理/激活的锁而不导航;不带lock的导航同样会取代既有锁。lock.overscroll为锁增加橡皮筋:平移可越过约束给定距离(视口像素,与缩放无关),待滚轮输入稳定或活跃指针/多指手势释放后弹回。默认true即DEFAULT_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选项(setViewport与initialState.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.ts(setViewport 定义于该文件 L634 起,包括目标解析与“已删除元素被过滤”的 console 警告)以及共享常量所在的 packages/excalidraw/viewport.ts;锁与 scrollConstraints 的组合行为还有测试佐证,见 packages/excalidraw/tests/scrollConstraints.test.tsx。
2.5 其他破坏性变更:UIAppState 瘦身与主题委托
UIAppState不再包含zoom、shouldCacheIgnoreZoom,以及随指针移动或动画帧更新的画布交互瞬态snapLines、originSnapOffset、suggestedBinding、frameToHighlight、elementsToHighlight。这些值不再驱动编辑器 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,否则默认禁用。
excalidrawAPIprop 更名为onExcalidrawAPI:现在在挂载时调用(而非构造期间),卸载时以null再调用一次。CHANGELOG 提示该 API 未来可能整体移除(可用onMount与onUnmount管理ExcalidrawAPI对象,例如缓存到全局状态)。
2.6 其他新特性:生命周期、事件、导出与状态订阅
- 新增
ExcalidrawAPI.isDestroyed标志:编辑器卸载后置true。对已销毁实例调用任何get*方法、onStateChange或onEvent会在开发环境抛错、生产环境console.error。虽然卸载时ExcalidrawAPI会被重置为null,仍建议调用前检查isDestroyed以防细微竞态。 - 新增
onMount、onInitialize、onUnmountprops: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.tsx 与 packages/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>
updateScene的commitToHistory被captureUpdate取代(多人协作撤销/重做计划的一部分,随新增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.ts 传 CaptureUpdateAction.EVENTUALLY——与上表“本地编辑立即捕获、辅助更新延迟捕获”的语义一致。
- 其他破坏性/结构性变更:
ExcalidrawTextElement.baseline被移除,替换为基于字体度量(font metrics)的垂直偏移计算,在每次文本元素重渲染时执行;若使用自定义字体,需扩展FONT_METRICS对象。ExcalidrawEmbeddableElement.validated被移除并移入私有编辑器状态(除非你曾读该属性,否则基本不受影响);内嵌 URL 的校验仍在内部进行,公开 propvalidateEmbeddable继续生效。- 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) 回调与可选 allowSystemTheme;useHandleLibrary 新增 opts.adapter(新的库初始化/持久化推荐模式)与 opts.migrationAdapter(跨存储迁移,如 LocalStorage 迁到 IndexedDB),同时软弃用 opts.getInitialLibraryItems;新增 onPointerUp prop;暴露 getVisibleSceneBounds 辅助函数;window.EXCALIDRAW_ASSET_PATH 扩展为接受 string[](多个 base URL 回退);支持自定义文本度量提供器;新增 props.onDuplicate;gridSize 与启用状态分离并支持自定义 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;移除语言选择器并新增
langCode、renderFooter(i18n 交由宿主负责,BREAKING);新增exportToBackendprop 供宿主实现可分享链接。0.2.1 起 CSS 与 JS 一起打包,宿主无需单独导入 css。 - 0.3.0:
zenModeEnabled/gridModeEnabled/viewModeEnabled受控 props;getAppState暴露于 ref;允许宿主为协作者传color、userState。 - 0.3.1:支持 Excalidraw 置于可滚动容器内。0.4.0:暴露
window.EXCALIDRAW_ASSET_PATH(默认从 unpkg 的当前版本 dist 加载资产,文件名带 hash 便于缓存更新);initialData.scrollToCenter控制挂载时是否滚动居中;导出restore/restoreAppState/restoreElements。0.4.2/0.4.3:包裹层改为 position relative、滚动防抖降至 100ms、CSS 变量作用域化避免与宿主冲突。0.5.0:nameprop(完全受控)、themeprop(完全受控)、libraryReturnUrl、canvas/svg/blob 导出 API;BREAKING:offsetLeft/offsetTop移除(改用 ref 的setCanvasOffsets)、initialData.scrollToCenter/setScrollToContent更名、appState.appearance更名appState.theme(含Appearance_dark→theme--dark类名变更)。0.6.0:UIOptionsprop 定制画布动作(背景色选择器、清空、导出、加载、保存等);width/heightprops 移除(BREAKING,组件改为占满容器 100%,容器需非零尺寸);导出 TypeScript 类型(@excalidraw/excalidraw/types);renderCustomStats、setToastMessage(ref);图片元素支持;BREAKING:script 标签嵌入文件名改为excalidraw.production.min.js(dev 为excalidraw.development.js)、setCanvasOffsets更名refresh。0.7.0:scrollToContentAPI(BREAKING:由setScrollToContent更名)支持单元素或不传(默认场景元素);库改为按实例隔离(BREAKING:宿主负责通过onLibraryChange持久化并经initialData.libraryItems导入);键盘事件默认绑定组件而非 document(BREAKING:handleKeyboardGlobally恢复旧行为);detectScroll、onPasteprop;类型更名DataState→ExportedDataState、LibraryData→ExportedLibraryData。
4.2 0.8.0 – 0.15.2:命令式 API 与组件化 UI 成型
- 0.8.0:
updateScene支持更新任意appState属性(此前仅viewBackgroundColor);serializeAsJSON助手;renderTopRightUIprop;加密盾牌图标从包中移除(改由renderFooter渲染,appState一并传入该 prop)。 - 0.13.0:
restoreElements()新增可选参数控制是否重算文本尺寸(默认true,协作等紧循环场景可关闭);renderSidebarprop 渲染自定义侧边栏;toggleMenuprop;theme半受控;元素customData;exportPadding(exportToCanvas/exportToBlob,默认 10)。BREAKING:UIOptions.canvasActions.theme更名toggleTheme;setToastMessage更名setToast(签名更新,可传duration/closable/message)。 - 0.14.0:欢迎屏自定义;主菜单组件 API 暴露(
MainMenu及其定制);Footer从 render prop 改为子组件(BREAKING:renderFooter移除);<Excalidraw/>顶层未知 children 直接渲染进容器;LiveCollaborationTrigger组件暴露(取代props.onCollabButtonClick,BREAKING)。元素 schema:currentItemStrokeSharpness/currentItemLinearStrokeSharpness合并为currentItemRoundness,strokeSharpness→roundness。 - 0.14.1/0.14.2:按钮 overflow 修复(LiveCollaborationTrigger 协作者计数 CSS);欢迎屏不再默认渲染(
UIOptions.welcomeScreen弃用,需自行渲染);MainMenu.Item/MainMenu.ItemLink支持onSelect(event)(event.preventDefault()可阻止菜单关闭)。 - 0.15.0:
scrollToContent新增 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.validateEmbeddable;scrollToContent支持opts.fitToViewport/opts.viewportZoomFactor;元素linkURL 严格净化。侧边栏 tab 化(BREAKING 较多):props.renderSidebar移除改为 children 渲染;appState.isSidebarDocked更名appState.defaultSidebarDockedPreference(语义收窄为默认侧边栏,自定义侧边栏需自行管理 docked 状态);Sidebar的props.dockable移除(以props.onDock()+props.docked表达可停靠);Sidebar.Header不再默认渲染;props.onClose更名props.onStateChange;restore()/restoreAppState()无论 docked 状态都保留appState.openSidebar。同版本还引入DefaultSidebar组件(自定义默认侧边栏、加 tab)、帧渲染定制、色板重设计、协作对话框重设计等 Library 侧功能。 - 0.17.0:BREAKING:Ref 支持移除,集成改用
excalidrawAPIprop 访问 API;API 中的ready与readyPromise停用移除;useDevicehook 返回值区分editor与viewport断点。功能面:可禁用image工具(UIOptions.tools = { image: false },同时禁用图片插入但保留.excalidraw文件导入);excalidrawAPIprop;导出getCommonBounds、elementsOverlappingBBox、isElementInsideBBox、elementPartiallyOverlapsWithOrContainsBBox等边界工具;transform API 默认重新生成 id 并平移绑定 0.5px 避免重叠;onChange/onPointerDown/onPointerUpAPI 订阅者;props.locked支持setActiveTool;MainMenu.Item新增selectedprop。构建侧: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),细节未在本文件中展开——阅读升级历史时这是唯一的“信息缺口”,需要留意。
五、从文档到仓库:继续深入的入口
- 变更日志本体:packages/excalidraw/CHANGELOG.md —— 本文所有版本条目与 before/after 代码块的原始出处。
- 包入口与 API 导出(
onExcalidrawAPI/onMount/onInitialize/onUnmount的接收与透传、ExcalidrawAPIProvider与状态订阅 hooks 的导出):packages/excalidraw/index.tsx、packages/excalidraw/hooks/useAppStateValue.ts。 - 视口控制实现(
setViewport、目标解析与删除元素过滤警告、DEFAULT_OVERSCROLL常量):packages/excalidraw/components/App.viewport.ts、packages/excalidraw/viewport.ts、packages/excalidraw/tests/scrollConstraints.test.tsx。 captureUpdate的三种语义在 action 层的真实用法:packages/excalidraw/actions/actionAlign.tsx(IMMEDIATELY)、packages/excalidraw/actions/actionAddToLibrary.ts(EVENTUALLY)。- 工具注册表与 action 漏斗(工具系统重构的落点):packages/excalidraw/actions/register.ts、packages/excalidraw/actions/manager.tsx、packages/excalidraw/actions/shortcuts.ts。
- 仓库内可直接运行的集成示例:examples/with-nextjs/(Next.js 集成,含 excalidrawWrapper.tsx)与 examples/with-script-in-browser/(无打包器脚本加载),可对照 CHANGELOG 中“无 bundler(Browser)”的 ESM 导入方式验证环境配置。
六、总结
@excalidraw/excalidraw 的 CHANGELOG 呈现出一条清晰的演进路线:早期版本解决“能不能嵌进宿主应用”的问题(类型导出、CSS 打包、资产路径、实例隔离、键盘事件作用域);中期版本把 UI 拆成可替换的组件 API(MainMenu、Footer、Sidebar、WelcomeScreen)并持续收窄 render prop、转向 children 模式;0.17/0.18 完成从 ref 到 excalidrawAPI prop、从 commitToHistory 到 captureUpdate 的现代化;当前 Unreleased 段落则把“宿主控制面”推到极致——interaction/ui/activeTool 三个 props 与 setViewport 锁定模型共同覆盖了实时预览、演示模式、受控编辑器等场景,同时用 onEvent/ExcalidrawAPIProvider/useExcalidrawStateValue 预告了事件与状态订阅 API 的下一轮收敛方向。对正在嵌入 Excalidraw 的开发者而言,这份文档既是升级前的核对清单(每个 BREAKING CHANGE 都给出了 before/after 与替代方案),也是理解该库“编辑器作为受控组件”设计哲学的最佳入口。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00