TinyMCE 核心函数库 @ephox/katamari 实战指南:数据结构与高阶函数详解
【免费下载链接】tinymceThe world's #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 the
apipackage.(不要使用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 与 Result
Optional:None 或 Some(x)
Optional<T>表示一个"可能存在也可能不存在"的值,要么是Some<T>(有值),要么是None(无值)。它与null/undefined的对比在 Optional.ts 的源码注释中有明确阐述:
- 没有
Optional的??空值合并运算符,但换来一整套 helper 函数; - 支持嵌套:
Optional内部的值本身仍可以是 nullable 或另一个Optional; - 不存在关闭"严格可选检查"的开关(不像严格空值检查那样可以关闭)。
构造与判断:
const a = Optional.some(42); // 有值 const b = Optional.none<number>(); // 无值 a.isSome(); // true b.isNone(); // true源码实现上的两个值得注意的细节:
- 单例优化:
Optional内部用私有tag: boolean+value?: T表示,且none()复用一个静态单例对象singletonNone(源码注释称 "every instance of Optional.none is identical, so just reuse the same object"),避免创建无数个无意义的空对象; - catamorphism 核心:
fold(onNone, onSome)可用来实现该类型上几乎所有其他方法(源码注释明确说明其构成 catamorphism)。
常用方法一览:
| 方法 | 语义 | 示例 |
|---|---|---|
map(f) | 有值则转换,无值则保持 None(Functor) | Optional.some(2).map(x => x * 3)→some(6) |
bind(f) | 有值则用返回 Optional 的函数继续(Monad) | Optional.some(2).bind(x => x > 1 ? Optional.some('ok') : Optional.none()) |
exists(p)/forall(p) | 存在/全部满足谓词;None 时分别返回 false/true | |
filter(p) | 值不满足谓词则变为 None | |
getOr(v)/getOrThunk(f) | 无值时取默认值(thunk 版延迟计算) | |
or(o)/orThunk(f) | 无值时用另一个 Optional 替代 | |
getOrDie(msg?) | 无值直接抛异常,仅建议测试中用 | |
from(x) | 把 nullable/undefined 输入转成 Optional(静态方法) | Optional.from(null)→none() |
getOrNull()/getOrUndefined() | 转回 nullable/undefined | |
each(f) | 有值才执行副作用 | |
toArray() | 转为 0 或 1 个元素的数组 | |
toString() | 输出some(...)或none(),便于调试 |
Result:Error 或 Value
Result<T, E>表示"一个值,或者一个错误"。与异常机制对比,源码注释指出了其类型安全优势:
- 每个函数的签名都明确声明是否返回
Result(类似受检异常 checked exceptions); - 错误的类型
E可由 TypeScript 静态检查; - 无法"忘记"处理错误——要拿到值就必须处理
Result。
Result与Optional的取舍原则(源码注释原文要点):二者都能类型安全地存储"可能不存在的数据",区别在于数据不存在时发生了什么——Optional什么都不存,Result存一个错误。所以:没有数据只是"没有"而非问题时用Optional;没有数据意味着出错、后续可能要处理错误时用Result。
构造方式(见 Result.ts):
const 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 与 LazyValue
Future:异步值的抽象
Future<T>是对"将来才有的值"的抽象,本质是基于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 的值,再执行第二个 Future |
toLazy() | 转为 LazyValue |
toCached() | 缓存 Promise 结果,多次get只执行一次底层任务 |
toPromise() | 转回原生 Promise |
get(cb) | 用回调方式取到值 |
源码中的errorReporter是一个很典型的防御技巧:它不在 Promise 内部直接 throw(会被 Promise "黑洞"吞掉),而是通过setTimeout(..., 0)逃逸出 Promise 执行栈再抛出,保证错误可见。
FutureResult:Result 与 Future 的组合
FutureResult<A, E>在 FutureResult.ts 中定义为extends Future<Result<A, E>>,即"将来会到达的 Result"。它在Future基础上补充了:
bindFuture(f):值到达后执行返回Future<Result<B, E>>的函数;bindResult(f):值到达后执行返回Result<B, E>的函数;mapResult(f)/mapError(f):分别转换成功值与错误;withTimeout(timeout, errorThunk):超时后产出指定错误;toCached():缓存版本。
其构造器包括nu、value、error、fromResult、fromFuture、fromPromise等,覆盖了从 Promise、Future、Result 各种来源构造的组合场景。这是 TinyMCE 中大量异步插件逻辑(如加载、远程请求、保存)的底层抽象。
LazyValue:只计算一次的异步值
LazyValue<T>是"只会计算一次"的异步值(见 LazyValue.ts)。其内部机制:
- 用
Optional.none<T>()作为尚未就绪的缓存槽; - 构造时立即调用
baseFn(set)开始计算; - 所有
get(cb)注册的回调被收集到callbacks数组,值一旦set就一次性派发并清空; isReady()判断是否已就绪;- 回调通过
setTimeout(..., 0)异步触发,避免同步重入问题。
由于值只计算一次且结果被缓存,LazyValue非常适合"昂贵但只需一次的异步初始化"场景。Future.toLazy()可与之互转。
五、可变数据结构:Cell 与 Singleton
Cell:最简可变容器
Cell<T>的实现非常朴素(见 Cell.ts):用闭包变量保存值,暴露get()与set(v)两个方法。它相当于一个类型安全的"可变盒子",常用来在纯函数式风格中容纳少量状态。
const counter = Cell(0); counter.set(counter.get() + 1); counter.get(); // 1Singleton:可变的 Optional 数据
Singleton在 Singleton.ts 中提供了比Cell更丰富的"单槽可变状态",内部以Cell(Optional.none<T>())实现,统一提供clear()、isSet()、get(): Optional<T>、set(v)四个操作(set与clear前都会先触发 revoke 钩子)。它针对不同场景派生了多种变体:
| 变体 | 说明 |
|---|---|
singleton(doRevoke) | 基础版,可在替换/清除时执行 revoke 钩子 |
repeatable(delay) | 内部保存一个setIntervalid,set(fn)以固定间隔重复执行,clear()清除定时器 |
destroyable() | 要求存的值有destroy()方法,替换/清除时自动调用 |
unbindable() | 要求存的值有unbind()方法,替换/清除时自动调用 |
api() | 在 destroyable 基础上增加run(fn),对当前值执行动作 |
value() | 在 singleton 基础上增加on(fn),对当前值执行动作 |
这套设计在 TinyMCE 中广泛用于管理"全局唯一实例"(如唯一的事件绑定、唯一的编辑器实例级资源),替换旧值前自动释放旧资源,避免泄漏。
六、代数数据类型:Adt
Adt是在 JavaScript 中对 代数数据类型(Algebraic Data Type) 的近似实现,基于 Church Encoding(邱奇编码)方法,源码注释对此有明确说明,并建议"语法与用法请看测试代码"。
核心 API 是Adt.generate(cases),其中cases是形如[{ CaseName: ['arg1', 'arg2'] }]的数组,每个元素恰好一个键(构造器名),值为该构造器的参数名列表。生成过程会做一系列运行时校验(见 Adt.ts):
cases必须是数组且至少一个 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 与 Merger
Arr:数组操作函数集
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 | 查找,返回Optional |
findUntil(xs, pred, until) | 找到或遇到终止条件即停 |
findMap(xs, f) | 依次应用返回 Optional 的函数,取第一个 Some |
contains(xs, x)/indexOf(xs, x) | 包含判断 / 索引(Optional) |
foldl(xs, f, acc)/foldr(xs, f, acc) | 左折叠 / 右折叠 |
flatten(xss)/bind(xs, f) | 展平 / map 后展平(flatMap) |
groupBy(xs, f) | 按派生键连续分组(类 HaskellgroupBy,顺序保留) |
range(n, f) | 生成[f(0), ..., f(n-1)] |
chunk(xs, size) | 按固定大小切块 |
get(xs, i)/head(xs)/last(xs) | 安全取元素,返回 Optional |
sort(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-check
README 指出 katamari 使用bedrock(@ephox/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-manual:bedrock -d src/test/ts,手动/可见浏览器模式运行测试;build:tsc,类型检查并编译;lint:eslint --max-warnings=0 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 world's #1 JavaScript library for rich text editing. Available for React, Vue and Angular项目地址: https://gitcode.com/gh_mirrors/ti/tinymce
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考