ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Draft.js 输入事件处理与移动端兼容性:从 2018 年周会记录看 contenteditable 编辑器的 IME 与 Android 挑战

Draft.js 输入事件处理与移动端兼容性:从 2018 年周会记录看 contenteditable 编辑器的 IME 与 Android 挑战 前端UI组件【免费下载链接】draft-jsA React framework for building text editors.项目地址https://gitcode.com/gh_mirrors/dr/draft-js点击查看免费下载Draft.js 作为 Facebook 开源的 React 富文本编辑器框架其核心难点从来不是渲染一段富文本而是在 contenteditable 这个天生不统一的浏览器原生环境里维持 DOM 与内部模型的一致。2018-02-23 周会记录 正是这种工程难点的真实切片Twitter 团队在移动端集成 Draft.js 时遭遇的 iOS 自动纠正autocorrect问题、Android 键盘完全不接受输入的问题以及 Chrome 65 中鼠标移动触发 composition 事件的反常行为共同指向了一个深水区——输入事件尤其是 IME 合成输入的处理。读完本文你将理解 Draft.js 的输入事件处理架构edit/composite 模式切换、composition 处理、DOM 观察器掌握这些浏览器兼容性问题的成因与仓库中的实际应对并为自研 contenteditable 编辑器的移动端适配提供可落地的借鉴。会议背景一份周会记录里的四个关键议题这份周会记录文档标题仍沿用上一周的Draft.js Weekly 02/08/18正文实际记录的是 02/23 的进展讨论的主题非常集中全部围绕输入与移动端展开议题要点文件体积追踪 demo原定于本周演示由 Alan Norbauer 提供的 PR对应 pull/1644 链接推迟到下周Twitter 团队集成进展正在把 Draft 集成进自家 composer移动端已有原型iOS 除自动纠正外基本可用Android 部分键盘完全无法接收用户输入重大问题Chrome 65 异常鼠标移动会触发 composition 事件见 issue 1657树形块数据结构tree block data下次会议再更新进展移动端支持讨论Sophie在 Android 上阻止/控制事件似乎不生效Isaac这或许正是方向——Android 与 Chrome 的事件系统不一致若 Twitter 团队愿意投入社区支持对 Draft.js 进行移动 Web 支持的重构这份记录没有大段代码但它提到的每一个问题都能在当前仓库的源码中找到对应的工程实体。下面逐一展开。Chrome 65 的 composition 异常Draft.js 如何管理 IME 合成输入问题本质鼠标移动为何会触发 composition 事件周会记录提到 Chrome 65 中出现鼠标移动触发 composition 事件的问题对应 issue 1657。在 contenteditable 编辑器中compositionstart/compositionend事件本应只与 IME 输入法合成如中文、日文、韩文输入相关但部分浏览器版本会在鼠标交互后误触发或延迟触发这些事件导致编辑器错误地进入合成模式或提前结束合成。这类问题对 Draft.js 这种状态驱动渲染的架构是致命的合成模式compositemode下编辑器会暂停常规的编辑处理交给专门的 composition handler一旦状态错乱光标会跳动、字符会重复或丢失。源码应对DraftEditorCompositionHandler 的三个防御机制Draft.js 将 IME 合成输入的全部逻辑封装在 src/component/handlers/composition/DraftEditorCompositionHandler.js 中它对浏览器异常做了三层防御1. 延迟解析RESOLVE_DELAY 20ms。onCompositionEnd不会立即处理合成结果而是设置 20ms 的延迟const RESOLVE_DELAY 20; // 允许 compositionstart 在 compositionend 后再次触发 onCompositionEnd(editor) { resolved false; stillComposing false; setTimeout(() { if (!resolved) { DraftEditorCompositionHandler.resolveComposition(editor); } }, RESOLVE_DELAY); }代码注释解释得很清楚浏览器在compositionend之后仍会把字符插入到活动元素中如果resolveComposition执行前又来了一个compositionstart合成会话就继续。这正好应对了事件时序异常——例如 Chrome 65 中鼠标移动引发的多余 composition 事件会被stillComposing/resolved标志过滤掉。2.resolved标志去重。某些 IME 界面如 Windows 8.1 上的阿拉伯语 Google 输入工具会多次触发compositionend重复处理同一个合成事件会破坏 DOM因此只处理第一次// Arabic Google Input Tools on Windows 8.1 fires compositionend three times. resolved false;3. 键盘事件的兜底解释。onKeyDown中如果keydown发生在compositionend之后、20ms 定时器到期之前例如韩语 2-Set 输入法中先按 option-E 再退格就立即resolveComposition并把按键交回常规编辑模式editor._onKeyDown(e)若仍处于合成中则拦截左右方向键的默认行为Safari 会用方向键提交合成阻止默认行为可避免光标跳动。onKeyPress则对回车键preventDefault防止 Firefox 在提交合成时插入多余的换行符。composite 模式与事件分发这些 handler 之所以存在是因为 Draft.js 把编辑器运行划分为多种模式见 src/component/handlers/DraftEditorModes.jsedit常规文本输入、compositeIME 合成输入、drag拖拽、cut原生剪切、render正常空模式。compositionstart触发时编辑器进入composite模式compositionend处理后调用editor.exitCurrentMode()退出——测试用例 src/component/handlers/composition/tests/DraftEditorCompostionHandler-test.js 的第一条用例正是验证这一状态迁移test(isInCompositionMode is properly updated on composition events, () { editOnCompositionStart(editor, {}); expect(editor.setMode).toHaveBeenLastCalledWith(composite); expect(editor._latestEditorState.isInCompositionMode()).toBe(true); compositionHandler.onCompositionEnd(editor); jest.runAllTimers(); expect(editor._latestEditorState.isInCompositionMode()).toBe(false); expect(editor.exitCurrentMode).toHaveBeenCalled(); });DOMObserver用 MutationObserver 兜住不靠谱的事件如果浏览器事件不可靠那就直接观察 DOM 本身。compositionstart时会启动 src/component/handlers/composition/DOMObserver.js它用MutationObserver监听 contenteditable 容器subtreecharacterDatachildList把合成期间的所有 DOM 变更收集到MapoffsetKey, textContent中。resolveComposition时调用stopAndFlushMutations()取走全部变更再用DraftModifier.replaceText逐 leaf 写回模型。该文件头部明确写着重度借鉴 ProseMirror 的 DOMObserver——这是业界处理浏览器事件不一致的经典策略。仓库甚至为 IE 做了特判USE_CHAR_DATA UserAgent.isBrowser(IE 11)因为 IE11 的 MutationObserver 有严重 bug退回到DOMCharacterDataModified事件并对 IE11 的合成回车做replace(\n, )清洗。测试 DraftEditorCompostionHandler-test.js 中还用jest.useFakeTimers()控制 20ms 定时器验证了单块单 mutation多块 mutation同一块多个 leaf 的 mutation三种场景包括把react bdraft/b graphql合成为reacta draftbb graphqlccc。Android 键盘的无声输入为什么 DOM diff 是唯一出路周会里被反复确认的事实这是整个周会记录里最沉重的一笔Android 上部分键盘完全不接受用户输入。要理解为什么难修需要回溯 02/08 会议中 Sophie 对架构的解释见 meta/meeting-notes/2018-02-08-weekly-meeting.mdDraft.js 通过监听每个事件 → 更新内部状态来维持 DOM 与编辑器状态的同步有些事件会preventDefault有些如拼写检查则放行原生行为Android 的问题在于它只为大量操作发送单个input事件不发送keypress、不发送beforeInput而且input事件没有 keycode唯一能推断用户到底做了什么的方式是查看 DOM 并与模型做 diff当时所有其他浏览器都会发送有用的事件所以这个 DOM diff 方案一直没实现——它是一个相当大的重构。02/23 会议中 Sophie 进一步补充在 Android 上阻止/控制事件似乎完全不生效。Isaac 的回应指出了更深层的事实——Android 和现在的 Chrome 都没有一致的事件系统因此与其逐事件打补丁不如支持一次面向移动 Web 的架构重构。这就是周会记录的最终共识如果 Twitter 团队愿意投入Draft.js 社区支持重新架构以更好地支持移动 Web。仓库中的证据editOnInput 里针对 Android 的注释当前仓库中 src/component/handlers/edit/editOnInput.js 的代码注释原封不动地保留着这一历史困境。它先解释了input事件的定位——非 IE 浏览器中 contenteditable 可靠触发、在变更发生后立即触发因此当 DOM 与模型不一致时多半是拼写检查/自动纠正改动了 DOM此时把 DOM 文本写回模型即可// The input event fires in contentEditable elements reliably for non-IE // browsers, immediately after changes occur to the editor DOM. ... when an // input change leads to a DOM/model mismatch, the change should be // due to a spellcheck change, and we can incorporate it into our model.而在domText modelText的分支里注释直接点名了 Android 的缺陷// This can be buggy for some Android keyboards because they dont fire // standard onkeydown/pressed events and only fired editOnInput // so domText is already changed by the browser and ends up being equal // to modelText unexpectedly. // Newest versions of Android support the dom-inputevent-inputtype // and we can use the inputType to properly apply the state changes.也就是说Android 键盘不发keydown、不发keypress只发input导致浏览器已经先把 DOM 改了、domText modelText意外成立常规的 diff 路径失效。仓库的应对是读取event.nativeEvent.inputTypeInput Events Level 2 标准草案对deleteContentBackward调用keyCommandPlainBackspace、对deleteWordForward走专门的删除路径——这正是新版本 Android 支持 inputType 后用它来正确应用状态变更的落地实现。值得一提的是editOnInput中合并 Chrome 分裂的文本节点的防御行首输入时 Chrome 会把文本节点一分为二需把兄弟节点合并回 anchor 节点——这类 DOM 细节的兼容处理正是 contenteditable 编辑器事件不可信、DOM 也不可信双重困境的写照。另一个视角iOS 自动纠正与 IE 的同类问题周会记录中 iOS 的问题是自动纠正除外基本 OK。自动纠正autocorrect和拼写检查一样属于浏览器原生修改 DOM、Draft.js 无法提前拦截的变更只能事后通过input事件 DOM/model diff 吸收这正是 editOnInput.js 注释里描述的发生在我们无法观察或解释之前的原生变更。若自动纠正破坏了光标位置editOnInput注释提到 Safari autocorrect 提供的 DOM selection 不准确因此不信任它、基于字符增量charDelta自行推算选区就会表现为打字错位。同一问题在桌面端也有同源变体02/08 会议中 Sophie 提到IE到 IE11Edge 正常存在 IME 问题行为与 Android 类似——不发所需事件Facebook 当时的对策是在 IE 上排除 CJK 语言的 Draft 输入。这解释了 editOnBeforeInput.js 中大量isIE特判IE 不发 input 事件React 也不做 polyfill因此要用setImmediate模拟输入后的处理流程也印证了周会中每个浏览器都在以自己的方式破坏输入事件的无奈。移动端支持的重构方向控制事件 vs. 重写事件层周会记录的核心讨论可以浓缩为一个架构抉择Sophie阻止/控制事件在 Android 上似乎不生效。 Isaac是的这可能就是方向——因为 Android 和现在的 Chrome 事件系统不一致。这里的关键洞察是事件系统不一致正在从 Android 蔓延到桌面 ChromeChrome 65 的 composition 异常正是桌面端的一个信号因此逐浏览器打补丁的模式已经不可持续。重构的方向在 editOnInput.js 中已有雏形是不再依赖keydown/keypress/beforeInput这些预知型事件而是让浏览器先改 DOM用 MutationObserver /input事件拿到变更结果将 DOM 文本与模型文本 diff用DraftModifier.replaceText等 API 把差异回写进 ContentState用inputTypeInput Events 标准区分删除、插入等语义弥补没有 keycode的信息缺失。这也正是 ProseMirror、Slate 等现代编辑器普遍采用的DOM 作为源真相、模型反推路线的先驱实践——DOMObserver.js 文件头部的 ProseMirror 借鉴声明就是佐证。后续进展2028 年 9 月的回望移动端重构最终是否落地可以从仓库中更晚的会议记录窥见在 meta/meeting-notes/2018-09-07-meeting.md 中Twitter 的 Yuze 汇报他们 fork 了 Draft 并做了增量修改新版本网站已使用 React Draft但**此前计划让移动端支持更健壮的工作目前不在近期路线图上**。这说明 02/23 周会提出的移动端重构共识最终演化为通过 fork 增量改进消化移动端问题的务实路径——这也从侧面说明为 contenteditable 编辑器重构事件层是一项需要长期投入的工程并非一次周会决策就能完成。对自研 contenteditable 编辑器的可操作启示把周会记录与源码对照可以沉淀出几条可复用的工程经验永远不要信任浏览器事件。键盘事件keydown/keypress/beforeInput在 Android、部分 IE、异常 Chrome 版本中会缺失或错乱。设计事件处理层时应把DOM 最终状态作为兜底真相来源事件只作为提示。IME 合成必须用延迟 去重模式。参照RESOLVE_DELAY 20ms与resolved/stillComposing标志合成结束后延迟处理、允许重新进入合成、对重复的compositionend去重可同时规避 Chrome 65 类异常与多触发型 IME。用 MutationObserver 做最后防线。当compositionend后的字符插入时序不可控时DOMObserver.js 展示的方案监听 subtree characterData childList按 offsetKey 聚合变更交给DraftModifier.replaceText统一回写可以保证模型最终收敛。拥抱 inputType 标准化。deleteContentBackward、deleteWordForward等inputType值见 editOnInput.js 的onInputType是 Android 新版本与未来浏览器补全事件信息缺失的主要手段应优先支持。为每个浏览器保留特判的合法通道。从 IE 的setImmediate模拟 input、到 IE11 的DOMCharacterDataModified回退、再到 Firefox 的 quickfind 字符拦截和/见 editOnBeforeInput.jsDraft.js 证明在 contenteditable 生态里分浏览器特判不是技术债而是必要的基础设施。结语一份 2018 年的周会记录之所以值得今天的技术人阅读是因为它记录了 contenteditable 编辑器最顽固的一类问题——输入事件在移动端与异常桌面浏览器中的不可靠——以及一个成熟框架应对它的真实过程以composite模式与DOMObserver守住 IME 合成、以input事件与 DOM/model diff 兜住拼写检查与自动纠正、以inputType弥补 Android 的信息缺失并最终意识到重构事件层才是根治之道。仓库中 src/component/handlers/composition/DraftEditorCompositionHandler.js、src/component/handlers/composition/DOMObserver.js、src/component/handlers/edit/editOnInput.js 等文件就是这段历史的活化石——对任何正在自研富文本编辑器、或正在为既有编辑器做移动端适配的团队它们都是比最佳实践博客更可信的参考样本。赞分享前端UI组件【免费下载链接】draft-jsA React framework for building text editors.项目地址https://gitcode.com/gh_mirrors/dr/draft-js点击查看免费下载相关推荐Unity游戏自动翻译终极指南XUnity.AutoTranslator完全教程Unity游戏自动翻译终极指南XUnity.AutoTranslator完全教程 想要畅玩外语Unity游戏却受限于语言障碍XUnity.AutoTrans前端UI组件Draft.js编辑器组件架构与事件处理机制Draft.js编辑器组件架构与事件处理机制 Draft.js是一个基于React的富文本编辑器框架其核心组件DraftEditor采用React类组件模式构前端UI组件Brevent高级配置指南优化微信、音乐播放器等常用应用的10个技巧Brevent高级配置指南优化微信、音乐播放器等常用应用的10个技巧 Brevent是一款无需Root的Android后台管理神器能够智能管理应用后台运行上一篇容器资源不失控nerdctl资源限制完全指南下一篇Mobile MCP下一代移动自动化测试的革命性解决方案彻底改变你的开发工作流创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表