读懂 preact-moveable 的 CHANGELOG:发布历史、关键修复与版本演进深度解析

原创2026-10-09 16:26:021,750 阅读
文章标签:前端UI组件

读懂 preact-moveable 的 CHANGELOG:发布历史、关键修复与版本演进深度解析

preact-moveable 是 Moveable 生态中面向 Preact 框架的官方组件封装,提供 Draggable、Resizable、Scalable、Rotatable、Warpable、Pinchable、Groupable、Snappable 等完整的元素操控能力。本文以 packages/preact-moveable/CHANGELOG.md 为主线,逐条拆解从 0.31.4 到 0.55.0 的全部发布记录:哪些修复影响 iframe 与 Shadow DOM 场景、哪些改动决定 npm 发布产物的完整性、工具链如何从 rollup 演进到 vite,并结合 源码 与 package.json 给出可验证的实现依据。读完本文,你既能掌握该组件的版本演进脉络,也能据此制定自己的升级与选型策略。

preact-moveable 是什么:一个薄封装、两种分发、三层关系

在阅读 CHANGELOG 之前,先理解这个包的本质。从源码看,preact-moveable 并不是独立重写一套拖拽引擎,而是一个围绕 react-moveable 核心引擎的 Preact 适配层:

对应到 package.json,其依赖只有两个:react-moveable ~0.56.0(核心引擎)与 preact-css-styled ^1.1.9(CSS 注入方案);构建产物则通过 main(dist/moveable.cjs.js)与 module(dist/moveable.esm.js)同时提供 CJS 与 ESM 两种分发。

理解了“薄封装”这个定位,CHANGELOG 里的大量条目就顺理成章:绝大多数版本号的跳动,本质上是跟随 react-moveable 核心引擎同步发布的节奏,而真正属于 preact-moveable 自身的变更,集中在 CSS 注入位置、npm 发布文件清单、构建工具链这几个点上。

CHANGELOG 的信息结构与书写规范

该 CHANGELOG 遵循 Conventional Commits 约定(文件首部即声明“See Conventional Commits for commit guidelines”),并采用 lerna 单仓库(monorepo)的发布方式——仓库根目录的 lerna.json 与 packages 目录 下并列的 react-moveable、preact-moveable、vue-moveable、svelte-moveable 等子包印证了这一点。

其条目类型按图标划分,共出现四类:

类型 图标 出现版本示例 含义
新功能 :rocket: New Features 0.48.0 新增能力(如 croact compat)
Bug 修复 :bug: Bug Fix 0.50.0、0.49.7、0.49.4、0.38.0、0.33.2、0.43.1 修复具体缺陷,常带 issue 编号
文档 :memo: Documentation 0.46.3、0.46.2、0.42.0 README、storybook 链接等文档修正
重构 :house: Code Refactoring 0.51.1、0.31.9 工程结构或构建方式调整
其他 :mega: Other 绝大多数版本 以 publish packages / update packages 为主

其中 :mega: Other 下的 publish packages 条目,是 lerna 在 monorepo 中同步发布所有子包时自动生成的记录,并不代表本包有实质性代码改动;而 update packages / update packages versions 则代表跟随核心引擎或其他依赖的版本刷新。分辨这两类条目,是高效阅读本文件的关键技巧。

版本发布时间线总览

CHANGELOG 覆盖了 2022-04 至 2023-12 期间约 55 个版本(0.31.4 → 0.55.0)。下表按时间倒序汇总全部版本及其关键变更,方便快速检索:

版本 日期 关键变更
0.55.0 2023-12-03 常规发布
0.54.0 2023-10-28 常规发布
0.53.2 2023-09-19 常规发布
0.53.1 2023-07-11 常规发布
0.53.0 2023-07-09 常规发布
0.52.2 2023-07-04 常规发布
0.52.1 2023-07-02 常规发布(两次)
0.51.1 2023-06-28 重构:change rollup to vite;fix update modules
0.51.0 2023-06-25 常规发布
0.50.0 2023-06-04 修复:fix ownerDocument css #932;support iframe context #932;update packages
0.49.7 2023-05-16 修复:fix css-styled version #916
0.49.6 2023-05-15 修复:update croact, css-styled version #916
0.49.5 2023-05-09 常规发布
0.49.4 2023-05-08 修复:fix files field #910 #909 #907
0.49.0 2023-05-01 常规发布
0.48.1 2023-04-16 常规发布
0.48.0 2023-04-13 新功能:add croact;add croact compat
0.47.0 2023-03-26 常规发布
0.46.3 2023-03-21 文档:fix README
0.46.2 2023-03-14 文档:fix story url;update packages
0.46.1 2023-03-10 常规发布
0.46.0 2023-03-08 常规发布
0.45.1 2023-01-29 常规发布(两次)
0.44.1 2022-12-16 常规发布
0.44.0 2022-12-10 常规发布
0.43.1 2022-12-05 修复:move react-moveable path
0.43.0 2022-12-03 常规发布
0.42.1 2022-11-28 常规发布
0.42.0 2022-11-27 文档:fix README;常规发布(两次)
0.41.0 2022-11-13 常规发布
0.40.1 2022-11-06 常规发布
0.40.0 2022-11-04 常规发布;update packages #760
0.39.0 2022-10-17 常规发布(两次);update packages
0.38.4 2022-10-10 常规发布(两次)
0.38.1 2022-09-19 常规发布
0.38.0 2022-09-15 修复:fix shadow dom css #729;update packages
0.37.4 2022-08-24 常规发布
0.37.3 2022-08-23 常规发布
0.37.1 2022-08-12 常规发布
0.37.0 2022-08-06 常规发布
0.36.3 2022-08-03 常规发布
0.36.2 2022-08-01 常规发布(三次)
0.35.0 2022-07-25 常规发布
0.34.0 2022-07-21 常规发布
0.33.2 2022-07-17 修复:fix files field in package.json #700
0.33.0 2022-07-17 常规发布
0.32.0 2022-06-09 常规发布
0.31.9 2022-06-07 重构:use yarn workspace;update demo configuration
0.31.8 2022-05-01 update packages versions
0.31.6 2022-04-27 update packages versions
0.31.5 2022-04-27 update packages versions
0.31.4 2022-04-26 update packages versions;upgrade storybook and typescript

可见该包的发布节奏相当密集,平均每 2~4 周一个版本,其中真正的代码级变更集中在若干修复与两处工程化重构上,下面逐一深入。

关键 Bug 修复逐项解析

CHANGELOG 中的 Bug Fix 条目都携带了 issue 编号,是理解组件边界与常见踩坑场景的第一手资料。

iframe 上下文与 ownerDocument CSS(0.50.0,issue #932)

0.50.0 同时记录了两条修复:fix ownerDocument css #932 与 support iframe context #932。这是同一问题的一体两面:当 Moveable 的目标元素位于 iframe 内部时,组件渲染出的控制点、指南线等 UI 也需要注入对应 iframe 的 document(即 ownerDocument)中,否则样式与交互会出现错乱。

从包结构看,这一修复与 CSS 注入机制直接相关:preact-moveable 的依赖 preact-css-styled 负责将样式注入文档,而 ownerDocument 的正确获取决定了注入目标是顶层文档还是 iframe 的内嵌文档。可以推断,修复的核心是让样式注入逻辑依据目标元素所在的文档上下文动态选择注入位置。这条记录的实际价值在于:如果你的项目把编辑器目标放在 iframe 中(例如富文本编辑器、预览画布场景),应使用 ≥ 0.50.0 的版本。

Shadow DOM CSS 修复(0.38.0,issue #729)

0.38.0 的 fix shadow dom css #729 与上面的 iframe 修复属于同一主题的另一个变体:当目标元素位于 Shadow DOM 内部时,Moveable 控制 UI 的样式注入同样需要定位到正确的 Shadow Root。这条记录同样指向 CSS 注入的目标节点解析逻辑,说明该组件对“目标元素所在文档/根节点”的适配是逐步完善的:先是 Shadow DOM(0.38.0),后是 iframe(0.50.0)。在 Web Components + Shadow DOM 架构中使用本组件时,应关注此修复版本。

package.json files 字段修复(0.33.2 #700 与 0.49.4 #910 #909 #907)

这是最能体现“发布工程细节”的两条修复。files 字段决定了发布到 npm 的压缩包中实际包含哪些文件。0.33.2 修复 fix files field in package.json #700,0.49.4 再次修复 fix files field #910 #909 #907——后者一次关联了三个 issue,说明该问题在真实使用中反复触发。

当前 package.json 中的 files 配置为:

"files": [
    "./*",
    "src/*",
    "dist/*",
    "declaration/*"
]

对照构建脚本 npm run build(rollup -c && npm run declaration && print-sizes ./dist)可以还原问题背景:构建流程会产出 dist/(rollup 打包结果)与 declaration/(tsc 生成的类型声明),如果 files 清单漏掉这两者,用户安装后将拿不到编译产物或 .d.ts 类型,直接表现为“装上了但 import 报错/无类型提示”。这提醒使用方:升级后如遇模块缺失或类型丢失,可先核对发布包的 files 清单是否与构建输出一致。

css-styled 与 croact 依赖版本对齐(0.49.6 / 0.49.7,issue #916)

0.49.6 记录 update croact, css-styled version #916,0.49.7 紧接着记录 fix css-styled version #916。这是依赖版本对齐问题:preact-css-styled 的版本未与核心引擎期望的版本同步,导致样式行为不一致。修复方式是更新依赖声明,使封装层与核心引擎的 CSS 注入依赖保持同一版本线。这也解释了 package.json 中为何将 preact-css-styled 显式列为直接依赖——它是该封装层特有的、与 React 侧 react-css-styled 对应的别名依赖(见 rollup.config.js 中的 aliasModules: {"react-css-styled": "preact-css-styled"})。

构建产物路径调整(0.43.1)

0.43.1 的 move react-moveable path 是对核心引擎依赖路径的修正。结合 Moveable.ts 的 import Moveable from "react-moveable" 与 package.json 中 react-moveable ~0.56.0 的依赖声明,可以推断该修复与打包时正确解析 react-moveable 的入口路径(CJS/ESM 切换后路径变化)有关。对使用者而言,这条记录没有直接 API 影响,更多是打包层面的内部修正。

架构与工程化演进

CHANGELOG 记录了三次值得关注的工程化变迁,它们共同塑造了当前仓库快照中的项目形态。

从 rollup 到 vite(0.51.1)

0.51.1 的重构条目明确写着 change rollup to vite。不过从当前仓库快照看,package.json 的发布构建仍以 rollup 为主(build 脚本使用 rollup -c,配合 rollup.config.js 中的 @daybrush/builder 生成 ESM 与 CJS 双产物),start 脚本同样使用 rollup -c rollup.config.demo.js -w 起 watch 模式。这说明 0.51.1 的 vite 化迁移主要落在开发/演示侧的构建链路(demo 与 storybook),发布链路保持了 rollup 的稳定输出。可以得出的结论是:该版本后开发体验更接近现代 vite 生态,但消费端使用的 dist/moveable.esm.js / dist/moveable.cjs.js 产物形态并未改变。

yarn workspace 化(0.31.9)

0.31.9 的 use yarn workspace 与仓库根目录的 yarn.lock 相互印证:Moveable 单仓库在 2022 年 6 月切换到 yarn workspace 管理多包依赖。结合 lerna 的使用,这套组合(lerna 负责版本与发布、yarn workspace 负责依赖链接)正是 CHANGELOG 中大量“publish packages 批量同步版本”条目的来源。

croact compat 支持(0.48.0)

0.48.0 是唯一标注 :rocket: New Features 的版本:add croact 与 add croact compat。croact 是 Moveable 生态内部的另一个轻量组件方案,仓库中对应独立的 packages/croact-moveable 子包。这条变更意味着 preact-moveable 开始支持在 croact 兼容模式下运行,扩大了组件的宿主框架覆盖面。

Storybook 与 TypeScript 升级(0.31.4)

0.31.4 记录 upgrade storybook and typescript,是时间线最早的可辨识工程变更,与此后各版本的 update packages versions 一起,构成了依赖持续滚动的基线。

快速上手:完整能力配置与事件体系

了解版本脉络后,实际使用时可参考 README.md 的完整示例。安装只需:

$ npm i preact-moveable

核心用法是渲染一个 <Moveable> 组件,通过布尔开关启用各项能力,并在对应事件回调中把计算结果写回目标元素(组件自身不直接修改目标 DOM,而是把 transform、width、height 等值交给你的回调,这是它与“黑盒拖拽库”的最大区别):

import Moveable from "preact-moveable";

render() {
    return (
        <Moveable
            target={document.querySelector(".target")}
            container={null}
            origin={true}

            /* Resize, Scale event edges */
            edge={false}

            /* draggable */
            draggable={true}
            throttleDrag={0}
            onDragStart={({ target, clientX, clientY }) => {
                console.log("onDragStart", target);
            }}
            onDrag={({
                target,
                beforeDelta, beforeDist,
                left, top,
                right, bottom,
                delta, dist,
                transform,
                clientX, clientY,
            }: OnDrag) => {
                console.log("onDrag left, top", left, top);
                // target!.style.left = `${left}px`;
                // target!.style.top = `${top}px`;
                console.log("onDrag translate", dist);
                target!.style.transform = transform;
            }}
            onDragEnd={({ target, isDrag, clientX, clientY }) => {
                console.log("onDragEnd", target, isDrag);
            }}

            /* When resize or scale, keeps a ratio of the width, height. */
            keepRatio={true}

            /* resizable */
            /* Only one of resizable, scalable, warpable can be used. */
            resizable={true}
            throttleResize={0}
            onResizeStart={({ target , clientX, clientY}) => {
                console.log("onResizeStart", target);
            }}
            onResize={({
                target, width, height,
                dist, delta,
                clientX, clientY,
            }: OnResize) => {
                console.log("onResize", target);
                delta[0] && (target!.style.width = `${width}px`);
                delta[1] && (target!.style.height = `${height}px`);
            }}
            onResizeEnd={({ target, isDrag, clientX, clientY }) => {
                console.log("onResizeEnd", target, isDrag);
            }}

            /* scalable */
            /* Only one of resizable, scalable, warpable can be used. */
            scalable={true}
            throttleScale={0}
            onScaleStart={({ target, clientX, clientY }) => {
                console.log("onScaleStart", target);
            }}
            onScale={({
                target, scale, dist, delta, transform,
                clientX, clientY,
            }: OnScale) => {
                console.log("onScale scale", scale);
                target!.style.transform = transform;
            }}
            onScaleEnd={({ target, isDrag, clientX, clientY }) => {
                console.log("onScaleEnd", target, isDrag);
            }}

            /* rotatable */
            rotatable={true}
            throttleRotate={0}
            onRotateStart={({ target, clientX, clientY }) => {
                console.log("onRotateStart", target);
            }}
            onRotate={({
                target,
                delta, dist,
                transform,
                clientX, clientY,
            }: onRotate) => {
                console.log("onRotate", dist);
                target!.style.transform = transform;
            }}
            onRotateEnd={({ target, isDrag, clientX, clientY }) => {
                console.log("onRotateEnd", target, isDrag);
            }}

            /* warpable */
            /* Only one of resizable, scalable, warpable can be used. */
            warpable={true}
            onWarpStart={({ target, clientX, clientY }) => {
                console.log("onWarpStart", target);
            }}
            onWarp={({
                target,
                clientX, clientY,
                delta, dist,
                multiply, transform,
            }) => {
                console.log("onWarp", target);
                // target.style.transform = transform;
                this.matrix = multiply(this.matrix, delta);
                target.style.transform = `matrix3d(${this.matrix.join(",")})`;
            }}
            onWarpEnd={({ target, isDrag, clientX, clientY }) => {
                console.log("onWarpEnd", target, isDrag);
            }}

            // Enabling pinchable lets you use events that
            // can be used in draggable, resizable, scalable, and rotatable.
            pinchable={true}
            onPinchStart={({ target, clientX, clientY, datas }) => {
                // pinchStart event occur before dragStart, rotateStart, scaleStart, resizeStart
                console.log("onPinchStart");
            }}
            onPinch={({ target, clientX, clientY, datas }) => {
                // pinch event occur before drag, rotate, scale, resize
                console.log("onPinch");
            }}
            onPinchEnd={({ isDrag, target, clientX, clientY, datas }) => {
                // pinchEnd event occur before dragEnd, rotateEnd, scaleEnd, resizeEnd
                console.log("onPinchEnd");
            }}
        />
    );
}

几点实操要点:

  • resizable、scalable、warpable 三者互斥,只能启用其一,因为它们的底层都依赖变换矩阵的计算结果;
  • pinchable 不是独立的能力,而是触屏场景下把 draggable、resizable、scalable、rotatable 聚合到一起的入口,其事件按“先 pinch、后具体操作”的顺序触发;
  • keepRatio 用于 resize/scale 时锁定宽高比;
  • 各 throttle* 参数(throttleDrag、throttleResize、throttleScale、throttleRotate)控制事件触发的节流阈值,默认 0 表示不节流。

分组能力:多目标 Groupable 使用指南

当需要对多个目标做统一操控时,groupable.md 提供了完整的 Group 用法。要点如下:

  • 将 target 设为元素数组(如 document.querySelectorAll(".target"));
  • 事件名在原有基础上追加 Group 后缀:onDragGroupStart / onDragGroup / onDragGroupEnd,resizable、scalable、rotatable、pinchable 同理;
  • 每个 Group 事件的 events 数组内含每个子目标的独立事件对象,可逐个回写样式。

核心示例(多目标拖拽):

import Moveable from "preact-moveable";

render() {
    return (
        <Moveable
            /* multiple targets */
            target={[].slice.call(document.querySelectorAll(".target"))}
            container={null}
            origin={true}

            /* Resize event edges */
            edge={false}

            /* draggable */
            draggable={true}
            throttleDrag={0}
            onDragGroupStart={({ targets }) => {
                console.log("onDragGroupStart", targets);
            }}
            onDragGroup={({ targets, events }) => {
                console.log("onDragGroup", targets);

                events.forEach(ev => {
                    // drag event
                    console.log("onDrag left, top", ev.left, ev.top);
                    // ev.target!.style.left = `${ev.left}px`;
                    // ev.target!.style.top = `${ev.top}px`;
                    console.log("onDrag translate", ev.dist);
                    ev.target!.style.transform = ev.transform;
                });
            }}
            onDragGroupEnd={({ targets, isDrag, clientX, clientY }) => {
                console.log("onDragGroupEnd", targets, isDrag);
            }}
        />
    );
}

分组 resize 的场景,需要注意方向计算:onResizeGroup 中每个子事件的 offset 需根据 direction(方向向量)取反后叠加 ev.drag.beforeDist 得到最终位移,即“负方向才产生左/上偏移”:

onResizeGroup={({ targets, direction }) => {
    console.log("onResizeGroup", targets);

    events.forEach(ev => {
        const offset = [
            direction[0] < 0 ? -ev.delta[0] : 0,
            direction[1] < 0 ? -ev.delta[1] : 0,
        ];
        // ev.drag is a drag event that occurs when the group resize.
        const left = offset[0] + ev.drag.beforeDist[0];
        const top = offset[1] + ev.drag.beforeDist[1];
        const width = ev.width;
        const top = ev.top;
    });
}}

分组 scale 与 rotate 的通用模式是:遍历 events,对每个子目标读取 ev.drag.beforeDist(组变换引起的位移)、ev.scale(缩放比)或 ev.beforeDist(旋转角),再写入各自的 style。分组能力底层由 packages/helper/src/GroupManager.ts 等辅助模块提供矩阵与坐标计算支撑,这些细节与组事件中暴露的 ev.drag 子事件结构相互印证。

结合 CHANGELOG 制定升级策略

将本文的版本脉络转化为可执行的升级建议:

  1. 版本节奏认知:preact-moveable 的常规版本跟随 react-moveable 核心同步发布,publish packages 类条目无需特别关注;真正需要评估的是带 :bug:、:rocket:、:house: 图标的条目。

  2. 按使用场景划定最低版本:

    • 涉及 iframe 内嵌目标 → 需要 ≥ 0.50.0(#932);
    • 涉及 Shadow DOM / Web Components → 需要 ≥ 0.38.0(#729);
    • 依赖类型声明与完整发布产物 → 需要 ≥ 0.49.4(#910 #909 #907),并建议核对安装包的 files 清单;
    • 使用 croact 兼容模式 → 需要 ≥ 0.48.0。
  3. 验证路径:升级后建议对照 README.md 的完整示例跑一遍全部能力(drag / resize / scale / rotate / warp / pinch / group),重点检查样式注入位置(iframe、Shadow DOM)与类型提示是否正常。若使用仓库源码调试,可运行 npm start 打开 demo/index.html 观察 demo 构建;发布产物则可执行 npm run build 验证 rollup 输出与声明文件生成。

  4. 依赖联动:由于该包直接依赖 react-moveable ~0.56.0 与 preact-css-styled,升级时需一并关注这两个依赖的版本区间,避免出现 0.49.6/0.49.7 所修复的“css-styled 版本未对齐”类问题。

总结

从 0.31.4 到 0.55.0,preact-moveable 的 CHANGELOG 完整记录了一个“薄封装组件”的演进路径:绝大部分条目是跟随核心引擎的同步发布,而属于自身的关键变更高度集中在三个维度——CSS 注入目标的场景适配(iframe #932、Shadow DOM #729)、npm 发布产物的完整性(files 字段 #700 / #910 #909 #907)、以及构建与依赖工程化(rollup→vite、yarn workspace、croact compat)。理解这些条目的真实含义,不仅有助于判断“该不该升级、升到哪个版本”,更能帮助你在 iframe、Shadow DOM、Web Components 等复杂宿主场景下做出正确的架构选择。

登录后查看全文
moveable