TinyMCE 核心函数库 ephox/katamari 实战指南数据结构与高阶函数详解【免费下载链接】tinymceThe worlds #1 JavaScript library for rich text editing. Available for React, Vue and Angular项目地址: https://gitcode.com/gh_mirrors/ti/tinymce本文基于 TinyMCE 开源仓库中的 modules/katamari/README.md 及其源码编写。ephox/katamari是 TinyMCE 编辑器家族包括 core、themes/silver、各 plugins 以及 alloy、agar 等兄弟模块共同依赖的基础 TypeScript 库它只提供数据结构与可复用高阶函数的纯模块集合不捆绑任何命令。读完本文你将掌握Optional/Result/Future/Cell/Adt等核心类型的设计思想与 API 用法学会用函数式风格编写更健壮的编辑器业务代码并了解如何在本仓库中运行 katamari 的原子测试。一、katamari 是什么按照 README 的官方定位katamari是各种数据结构与可复用高阶函数的集合a collection of various data structures and reusable higher-order functions。它有三个显著特征纯模块集合不捆绑任何 CLI 命令不提供可执行入口只导出可引用的模块通用基础层整个 tinymce 仓库中的核心编辑器、主题与插件均建立在它之上函数式取向大量 API 借鉴 Haskell 等函数式语言的类型类思想Functor、Monad、Traversable 等源码注释中对此直言不讳。在 package.json 中该包描述为 Basic data type library当前版本为11.0.0许可证为GPL-2.0-or-later唯一运行时依赖是ephox/dispute用于相等性比较等。二、安装与使用约定安装katamari 以 npm 包ephox/katamari形式发布安装命令为npm install ephox/katamari在 TinyMCE 仓库内部各模块直接通过 npm workspace 依赖它例如源码中的import { Optional } from ephox/katamari。使用约定只用 api 包README 给出了一个非常重要且易被忽视的约定Note, refrain from using any modules that are not in theapipackage.不要使用api包之外的任何模块。也就是说虽然源码目录下还有async/如AsyncValues.ts、str/如StrAppend.ts、StringParts.ts、util/如BagUtils.ts、IdUtils.ts等子目录这些属于内部实现细节对外使用一律应通过api目录下的模块。api 包的实际导出清单见 Main.ts它统一导出了Adt、Arr、Cell、Fun、Future、FutureResult、LazyValue、Merger、Obj、Optional、Result、Singleton、Strings、Thunk、Type等全部公共 API。这一点与包配置互相印证package.json中main/module/types及exports入口全部指向api/Main源码入口为./src/main/ts/ephox/katamari/api/Main.ts。三、可选数据结构Optional 与 ResultOptionalNone 或 Some(x)OptionalT表示一个可能存在也可能不存在的值要么是SomeT有值要么是None无值。它与null/undefined的对比在 Optional.ts 的源码注释中有明确阐述没有Optional的??空值合并运算符但换来一整套 helper 函数支持嵌套Optional内部的值本身仍可以是 nullable 或另一个Optional不存在关闭严格可选检查的开关不像严格空值检查那样可以关闭。构造与判断const a Optional.some(42); // 有值 const b Optional.nonenumber(); // 无值 a.isSome(); // true b.isNone(); // true源码实现上的两个值得注意的细节单例优化Optional内部用私有tag: booleanvalue?: T表示且none()复用一个静态单例对象singletonNone源码注释称 every instance of Optional.none is identical, so just reuse the same object避免创建无数个无意义的空对象catamorphism 核心fold(onNone, onSome)可用来实现该类型上几乎所有其他方法源码注释明确说明其构成 catamorphism。常用方法一览方法语义示例map(f)有值则转换无值则保持 NoneFunctorOptional.some(2).map(x x * 3)→some(6)bind(f)有值则用返回 Optional 的函数继续MonadOptional.some(2).bind(x x 1 ? Optional.some(ok) : Optional.none())exists(p)/forall(p)存在/全部满足谓词None 时分别返回 false/truefilter(p)值不满足谓词则变为 NonegetOr(v)/getOrThunk(f)无值时取默认值thunk 版延迟计算or(o)/orThunk(f)无值时用另一个 Optional 替代getOrDie(msg?)无值直接抛异常仅建议测试中用from(x)把 nullable/undefined 输入转成 Optional静态方法Optional.from(null)→none()getOrNull()/getOrUndefined()转回 nullable/undefinedeach(f)有值才执行副作用toArray()转为 0 或 1 个元素的数组toString()输出some(...)或none()便于调试ResultError 或 ValueResultT, E表示一个值或者一个错误。与异常机制对比源码注释指出了其类型安全优势每个函数的签名都明确声明是否返回Result类似受检异常 checked exceptions错误的类型E可由 TypeScript 静态检查无法忘记处理错误——要拿到值就必须处理Result。Result与Optional的取舍原则源码注释原文要点二者都能类型安全地存储可能不存在的数据区别在于数据不存在时发生了什么——Optional什么都不存Result存一个错误。所以没有数据只是没有而非问题时用Optional没有数据意味着出错、后续可能要处理错误时用Result。构造方式见 Result.tsconst r1 Result.value(42); // 成功值 const r2 Result.error(boom); // 错误 const r3 Result.fromOption(Optional.none(), empty); // 从 Optional 转换None 时得到错误Result的方法与Optional高度对称fold(onError, onValue)、isValue()/isError()、map/mapError、bind、exists/forall、getOr/or/getOrThunk/orThunk/getOrDie、each以及特有的toOptional()丢弃错误转为Optional。值得注意的是Result采用对象字面量实现并刻意在对象上附带tag/inner调试信息源码注释说明不放进公开类型签名是为了防止生产代码误用同时方便 console 调试。四、异步数据结构Future、FutureResult 与 LazyValueFuture异步值的抽象FutureT是对将来才有的值的抽象本质是基于Promise的再封装见 Future.ts。构造方式Future.nu(baseFn)通过回调式 completer 构造内部转成new Promise(baseFn)Future.pure(a)立即完成的 Future。核心方法方法语义map(f)值到达后转换内部run().then(fab)bind(f)值到达后用返回 Future 的函数继续anonBind(futureB)忽略本 Future 的值再执行第二个 FuturetoLazy()转为 LazyValuetoCached()缓存 Promise 结果多次get只执行一次底层任务toPromise()转回原生 Promiseget(cb)用回调方式取到值源码中的errorReporter是一个很典型的防御技巧它不在 Promise 内部直接 throw会被 Promise 黑洞吞掉而是通过setTimeout(..., 0)逃逸出 Promise 执行栈再抛出保证错误可见。FutureResultResult 与 Future 的组合FutureResultA, E在 FutureResult.ts 中定义为extends FutureResultA, E即将来会到达的 Result。它在Future基础上补充了bindFuture(f)值到达后执行返回FutureResultB, E的函数bindResult(f)值到达后执行返回ResultB, E的函数mapResult(f)/mapError(f)分别转换成功值与错误withTimeout(timeout, errorThunk)超时后产出指定错误toCached()缓存版本。其构造器包括nu、value、error、fromResult、fromFuture、fromPromise等覆盖了从 Promise、Future、Result 各种来源构造的组合场景。这是 TinyMCE 中大量异步插件逻辑如加载、远程请求、保存的底层抽象。LazyValue只计算一次的异步值LazyValueT是只会计算一次的异步值见 LazyValue.ts。其内部机制用Optional.noneT()作为尚未就绪的缓存槽构造时立即调用baseFn(set)开始计算所有get(cb)注册的回调被收集到callbacks数组值一旦set就一次性派发并清空isReady()判断是否已就绪回调通过setTimeout(..., 0)异步触发避免同步重入问题。由于值只计算一次且结果被缓存LazyValue非常适合昂贵但只需一次的异步初始化场景。Future.toLazy()可与之互转。五、可变数据结构Cell 与 SingletonCell最简可变容器CellT的实现非常朴素见 Cell.ts用闭包变量保存值暴露get()与set(v)两个方法。它相当于一个类型安全的可变盒子常用来在纯函数式风格中容纳少量状态。const counter Cell(0); counter.set(counter.get() 1); counter.get(); // 1Singleton可变的 Optional 数据Singleton在 Singleton.ts 中提供了比Cell更丰富的单槽可变状态内部以Cell(Optional.noneT())实现统一提供clear()、isSet()、get(): OptionalT、set(v)四个操作set与clear前都会先触发 revoke 钩子。它针对不同场景派生了多种变体变体说明singleton(doRevoke)基础版可在替换/清除时执行 revoke 钩子repeatable(delay)内部保存一个setIntervalidset(fn)以固定间隔重复执行clear()清除定时器destroyable()要求存的值有destroy()方法替换/清除时自动调用unbindable()要求存的值有unbind()方法替换/清除时自动调用api()在 destroyable 基础上增加run(fn)对当前值执行动作value()在 singleton 基础上增加on(fn)对当前值执行动作这套设计在 TinyMCE 中广泛用于管理全局唯一实例如唯一的事件绑定、唯一的编辑器实例级资源替换旧值前自动释放旧资源避免泄漏。六、代数数据类型AdtAdt是在 JavaScript 中对 代数数据类型Algebraic Data Type 的近似实现基于 Church Encoding邱奇编码方法源码注释对此有明确说明并建议语法与用法请看测试代码。核心 API 是Adt.generate(cases)其中cases是形如[{ CaseName: [arg1, arg2] }]的数组每个元素恰好一个键构造器名值为该构造器的参数名列表。生成过程会做一系列运行时校验见 Adt.tscases必须是数组且至少一个 case每个 case 恰好只有一个名字one and only one name per case不允许重复的构造器名不允许名为cata的构造器保留字每个 case 的参数必须是数组构造器被调用时参数个数必须与声明一致否则抛 Wrong number of arguments to case ...。生成出的每个构造器返回的对象带有fold(...caseHandlers)按构造器声明顺序匹配的 catamorphism参数个数必须等于 case 总数match(branches)按名字匹配的分支分发要求所有构造器都被覆盖Not all branches were specified when using match顺序无关log(label)仅用于调试向 console 输出构造器清单与当前参数。典型用法示意结合 Adt.ts 的 API 结构const Shape Adt.generate([ { circle: [radius] }, { rect: [width, height] } ]); const area (shape: Adt) shape.fold( (radius) Math.PI * radius * radius, // circle (width, height) width * height // rect );由于是基于 Church 编码的近似实现Adt为 TypeScript 世界带来了接近 sum type / 模式匹配的建模能力是 TinyMCE 内部表达一组互斥变体的核心工具。七、高阶函数集合Arr、Obj 与 MergerArr数组操作函数集Arr是一整套基于原生 for 循环手写的数组工具函数见 Arr.ts刻意不用Array.prototype.forEach/map/filter等高阶方法源码注释解释了原因手写循环可以做长度缓存、预分配数组new Array(len)后按索引赋值、避免 push 等方式的性能微优化并引用了 jsperf 基准。这也体现了 katamari 作为基础库对性能的极致追求。常用函数部分函数语义map(xs, f)/each(xs, f)/eachr(xs, f)映射 / 正序遍历 / 逆序遍历filter(xs, pred)/partition(xs, pred)过滤 / 按谓词拆分为{ pass, fail }find(xs, pred)/findIndex/findLast/findLastIndex查找返回OptionalfindUntil(xs, pred, until)找到或遇到终止条件即停findMap(xs, f)依次应用返回 Optional 的函数取第一个 Somecontains(xs, x)/indexOf(xs, x)包含判断 / 索引Optionalfoldl(xs, f, acc)/foldr(xs, f, acc)左折叠 / 右折叠flatten(xss)/bind(xs, f)展平 / map 后展平flatMapgroupBy(xs, f)按派生键连续分组类 HaskellgroupBy顺序保留range(n, f)生成[f(0), ..., f(n-1)]chunk(xs, size)按固定大小切块get(xs, i)/head(xs)/last(xs)安全取元素返回 Optionalsort(xs, cmp)/reverse(xs)排序返回副本/ 反转返回副本unique(xs, cmp?)去重可自定义比较器difference(a, b)差集mapToObject(xs, f)把数组映射为以元素为键的对象equal(a1, a2, eq?)基于ephox/dispute的相等性比较from(x)/pure(x)ArrayLike 转数组 / 单元素数组一个常见示例const users [ { name: a, admin: true }, { name: b, admin: false } ]; const admins Arr.filter(users, (u) u.admin); const firstName Arr.head(users).map((u) u.name); // Optional.some(a)Obj对象操作函数集Obj提供针对 JavaScript 普通对象的工具函数典型能力包括Obj.keys键数组Adt内部即用它、Obj.values、Obj.map、Obj.get(obj, key)返回Optional在 Obj.ts 中实现等同样遵循安全取值优先返回 Optional的风格。Merger对象合并函数集Merger专注于对象的合并见 Merger.ts典型如Merger.merge(...objs)、Merger.deepMerge(...objs)用于把默认配置与用户配置逐层合并——这是 TinyMCE 处理编辑器初始化配置defaults 与用户 options 的合并时频繁依赖的能力。八、完整 API 清单与更多工具除了上述主体Main.ts导出的公共 API 还包括README 之外的锦上添花Fun函数工具constant、identity、always、never、apply、die、noop、compose等Result内部大量使用Type运行时类型判断isArray、isFunction、isNonNullable、isString等Thunk惰性求值工具Thunk.constant等Strings/Num/Regex/Unicode/Id/Unique/Global/Namespace/Resolve/Zip/Jam/Contracts/HashMap/HashSet/StringMatch/Throttler节流器/Maybes/Optionals/Results/LazyValues/Futures/OptionalInstances/ResultInstances等。各模块源码均位于 modules/katamari/src/main/ts/ephox/katamari/api/读者可按需查阅api之外的async/、str/、util/目录属于内部实现不建议直接使用。九、测试bedrock fast-checkREADME 指出 katamari 使用bedrockephox/bedrock运行原子测试atomic tests测试主要用fast-check属性测试/Property-based Testing 框架编写。也就是说大量测试不是给定输入断言输出的示例式用例而是声明性质、由 fast-check 自动生成大量随机输入来验证不变量。运行测试在仓库根目录下进入模块目录执行bun run test该命令实际触发的是 package.json 中定义的test脚本bedrock-auto -b chrome-headless -d src/test/ts即用 headless Chrome 运行src/test/ts目录下的全部测试。相关的辅助脚本还包括test-manualbedrock -d src/test/ts手动/可见浏览器模式运行测试buildtsc类型检查并编译linteslint --max-warnings0 src/**/*.ts零警告严格 lint。测试源码位于 modules/katamari/src/test/ts仓库中共 104 个测试文件以 atomic 为主。例如Adt的用法细节README 建议直接查阅测试代码这是最权威的活文档。十、在 TinyMCE 中的实际地位katamari 是整个 tinymce monorepo 的公共基础依赖tinymce核心、themes/silver、各官方插件以及alloy、agar、bridge、mcagar等兄弟模块的package.json均声明依赖ephox/katamari。编辑器的选区处理、配置合并、异步插件流程、UI 状态管理背后都有本文所述类型的身影。理解 katamari等于拿到了阅读 TinyMCE 全部上层源码的钥匙。小结ephox/katamari以极小的 API 面覆盖了函数式编程中最常用的基础设施用Optional/Result消灭 null/异常带来的隐式风险用Future/LazyValue统一异步模型用Cell/Singleton管理受控状态用Adt在 TypeScript 中模拟代数数据类型再用Arr/Obj/Merger提供高性能的高阶函数工具。结合本仓库的 README、Main.ts 导出清单与 src/test/ts 测试代码你可以快速把这些模式应用到自己的编辑器插件与业务开发中。【免费下载链接】tinymceThe worlds #1 JavaScript library for rich text editing. Available for React, Vue and Angular项目地址: https://gitcode.com/gh_mirrors/ti/tinymce创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
