網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
簡(jiǎn)介
在Fiberplane,我們最近遇到了一個(gè)有趣的挑戰(zhàn):我們正在使用的富文本編輯器庫(kù)已經(jīng)過(guò)時(shí)了。我們?cè)?jīng)使用Slate.js——一個(gè)很好的編輯器——但是當(dāng)我們?yōu)閰f(xié)作編輯實(shí)現(xiàn)我們自己的富文本基元時(shí),我們發(fā)現(xiàn)我們自己的基元和Slate的數(shù)據(jù)模型之間的脫節(jié)是一個(gè)阻礙因素。所以我們開(kāi)始思考——如果我們建立自己的富文本編輯器(RTE, Rich Text Editor)會(huì)怎樣?
從一個(gè)非常高層次的角度來(lái)看,一個(gè)富文本編輯器是由兩個(gè)部分組成的。
- 一個(gè)數(shù)據(jù)模型和對(duì)其進(jìn)行操作的核心邏輯。
- 一個(gè)渲染上述數(shù)據(jù)模型的狀態(tài)并處理用戶(hù)互動(dòng)的視圖。
我們?cè)谝晥D中使用了Slate,但結(jié)果是它也拉入了自己的數(shù)據(jù)模型。如果我們可以直接在React中實(shí)現(xiàn)視圖,我們可以大大簡(jiǎn)化我們的堆棧,并完全控制它的每個(gè)方面。缺點(diǎn)是什么?RTEs因?yàn)樾枰С謴?fù)雜的用戶(hù)交互而臭名昭著,而現(xiàn)在我們需要自己處理每一個(gè)交互。
在這篇文章中,我們將討論我們所面臨的挑戰(zhàn)以及我們?nèi)绾谓鉀Q這些問(wèn)題。
數(shù)據(jù)模型
我們的產(chǎn)品是一個(gè)協(xié)作式的筆記本編輯器。筆記本是一個(gè)基于塊的編輯器,由不同類(lèi)型的單元組成,從文本單元到圖片和圖表。因此,我們確定了一個(gè)數(shù)據(jù)模型,它既有利于我們的協(xié)作功能,也有利于為我們?cè)趩卧駜?nèi)使用的任何富文本字段提供動(dòng)力的RTE。在這篇文章中,我們將重點(diǎn)討論TextCell
。
struct TextCell { pub id: String, pub content: String, pub formatting: Option<Formatting>, }
這里的content
只是純文本內(nèi)容,而formatting
是將純文本變成富文本的東西。"多汁"的部分都在格式化類(lèi)型里面。
type Formatting = Vec<AnnotationWithOffset>; ? struct AnnotationWithOffset { annotation: Annotation, offset: u32, } ? enum Annotation { StartBold, EndBold, StartItalics, EndItalics, StartLink { url: String }, EndLink, /* more like these... */ }
正如你所看到的,這只不過(guò)是一個(gè)注釋列表,它定義了要應(yīng)用的格式化類(lèi)型和它開(kāi)始的偏移量。我們有意不選擇類(lèi)似于HTML的樹(shù)狀結(jié)構(gòu),因?yàn)楦袷交秶梢灾丿B,這將導(dǎo)致復(fù)雜的樹(shù)狀操作。此外,每個(gè)注釋只有一個(gè)偏移量的簡(jiǎn)單性使我們很容易實(shí)現(xiàn)我們用于協(xié)作的操作轉(zhuǎn)換(OT)算法。
核心邏輯
隨著數(shù)據(jù)模型的出現(xiàn),也帶來(lái)了與之互動(dòng)的代碼。當(dāng)你在一個(gè)單元格中打字時(shí),我們?cè)谀睦锊迦胄麓虻淖址窟@如何影響content
和相關(guān)的formatting
?如果你在一個(gè)選擇上切換格式,應(yīng)該發(fā)生什么?如果你將一個(gè)單元格從中間分割開(kāi)來(lái),又該怎么辦?所有這些以及更多都在Rust的核心邏輯中實(shí)現(xiàn)。
你要知道,無(wú)論如何我們都需要這些邏輯,因?yàn)槲覀兊腛T算法也需要它。但現(xiàn)在我們也能用同樣的原語(yǔ)來(lái)驅(qū)動(dòng)我們的編輯器。
為了使這個(gè)邏輯易于測(cè)試,它被實(shí)現(xiàn)為純函數(shù),我們?cè)赥ypeScript的Redux reducer中調(diào)用。我們創(chuàng)建了fp-bindgen來(lái)生成Rust代碼和調(diào)用它的TypeScript代碼之間的綁定關(guān)系。
為了適應(yīng)RTE(當(dāng)我們還在使用Slate時(shí)還不需要),我們不得不自己引入一段邏輯,就是光標(biāo)管理。例如,當(dāng)用戶(hù)按下左方向鍵時(shí),我們分派一個(gè)MoveCursor
動(dòng)作,其有效載荷如下。
struct MoveCursorPayload { pub delta: i32, pub extend_selection: bool, pub unit: CursorUnit, }
delta
指定光標(biāo)是向前還是向后移動(dòng),通過(guò)指定一個(gè)1
或-1
的值。extend_selection
屬性是在用戶(hù)按住Shift
鍵時(shí)使用的,用來(lái)擴(kuò)展當(dāng)前的選擇,或者在還沒(méi)有選擇的情況下創(chuàng)建一個(gè)。這個(gè)unit
決定了我們是按Unicode字母群(用戶(hù)通常稱(chēng)之為 "字符")還是按單詞移動(dòng)光標(biāo),用于用戶(hù)按住Ctrl
/?
鍵時(shí)。然后,我們的Rust還原器會(huì)處理這些動(dòng)作,并處理所有的邊緣情況,包括確保光標(biāo)不會(huì)出現(xiàn)在@
的中間。
視圖
在我們RTE的大部分開(kāi)發(fā)過(guò)程中,我們的編輯器甚至不是一個(gè)編輯器。至少?gòu)臑g覽器的角度來(lái)看不是。這是因?yàn)闉g覽器通常只識(shí)別兩種類(lèi)型的編輯器:純文本編輯器,如<input>
和<textarea>
元素,以及使用一種叫做contenteditable
的屬性創(chuàng)建的自由格式編輯器。我們的編輯器兩者都不是。
我們?cè)谧罱K版本中仍然使用contenteditable
屬性,因?yàn)槲覀兒芸鞎?huì)討論一些實(shí)際的影響,但我們有意識(shí)地決定盡可能少地依賴(lài)它。這對(duì)我們最初構(gòu)建RTE的方式產(chǎn)生了深遠(yuǎn)的影響,你將在本節(jié)中看到。
如果我們最初的版本根本沒(méi)有使用contenteditable
,那么我們?cè)趺茨軌騽?chuàng)建一個(gè)富文本編輯器?從用戶(hù)的角度來(lái)看,RTE只不過(guò)是一個(gè)看起來(lái)像文本字段的東西,有一個(gè)光標(biāo),允許他們輸入任何他們喜歡的內(nèi)容。
所以我們創(chuàng)建了一個(gè)普通的React組件,并根據(jù)單元格的content
和formatting
生成了富文本內(nèi)容,然后使用React.createElement()
插入實(shí)際的元素,這些元素只是一個(gè)應(yīng)用了樣式的<span>
元素的平面列表(偶爾會(huì)有<a>
元素灑在鏈接上)。然后,我們添加了必要的事件處理程序來(lái)捕捉用戶(hù)的互動(dòng),這又將再次調(diào)用數(shù)據(jù)模型上的適當(dāng)邏輯。
那么用戶(hù)的光標(biāo)呢?只是另一個(gè)我們自己插入的小React組件。我們會(huì)在useLayoutEffect()
鉤子中測(cè)量它需要的位置,然后根據(jù)這個(gè)來(lái)定位它。
所以......很簡(jiǎn)單,很容易,對(duì)嗎?好吧,我們現(xiàn)在需要處理的大量的交互使這成為一個(gè)重大的挑戰(zhàn)。例如,讓我們?cè)倏匆幌鹿鈽?biāo)導(dǎo)航。上一節(jié)中的例子顯示了如何向左和向右移動(dòng)光標(biāo)。但是如果用戶(hù)按了向下的箭頭,他們的光標(biāo)最終會(huì)在哪兩個(gè)字符之間呢?這不是一個(gè)簡(jiǎn)單的問(wèn)題,因?yàn)楸3止鈽?biāo)的垂直位置需要測(cè)量上面那一行的字符的位置。但你如何定義什么是 "上面那一行"?無(wú)論是content
還是formatting
都不包含這些信息。然后記住我們還必須支持選擇。還有鼠標(biāo)互動(dòng)...
這當(dāng)然會(huì)讓人感到不知所措,在開(kāi)發(fā)過(guò)程中,可能很難保持對(duì)哪些工作和哪些不工作的概述。而這正是我們覺(jué)得最初沒(méi)有contenteditable
的工作很好的原因。我們自己做所有的事情,使我們非常清楚自己的位置。任何不工作的交互都是我們?nèi)匀恍枰獙?shí)現(xiàn)的。沒(méi)有什么會(huì)意外地工作,因?yàn)闉g覽器為我們解決了這個(gè)問(wèn)題--瀏覽器在這里處于次要地位。
當(dāng)然,對(duì)于最終的版本,很難繞過(guò)使用contenteditable
。這是因?yàn)槿绻麤](méi)有它,瀏覽器擴(kuò)展將無(wú)法識(shí)別你的編輯器。而移動(dòng)瀏覽器甚至?xí)B固地拒絕調(diào)出屏幕鍵盤(pán)......
手動(dòng)差異化
所以我們確實(shí)需要contenteditable
,但是還有一個(gè)問(wèn)題。React不支持對(duì)已啟用contenteditable
的元素的內(nèi)容進(jìn)行修補(bǔ)。這是有原因的:contenteditable
基本上是告訴瀏覽器去玩吧。這就像一個(gè)沒(méi)有規(guī)則的操場(chǎng)。
React并不喜歡這樣。它依靠虛擬DOM來(lái)決定它需要如何更新實(shí)際的DOM,但當(dāng)瀏覽器可以在它不知情的情況下把地毯從它下面拉出來(lái)并更新實(shí)際的DOM時(shí),這種方法就陷入了困境。這也是我們一開(kāi)始就避免的原因。為了在更新我們的數(shù)據(jù)模型時(shí)能夠保留用戶(hù)的意圖(OT算法的一個(gè)重要方面),最好是了解導(dǎo)致任何變化的互動(dòng)。但是,如果你試圖理解瀏覽器對(duì)DOM在內(nèi)容可編輯元素中的變化,你最多只能是猜測(cè)。
所以我們借鑒了React的玩法,實(shí)現(xiàn)我們自己的差異算法。但我們不是針對(duì)虛擬DOM進(jìn)行差分,而是在useLayoutEffect()
鉤子函數(shù)中針對(duì)真實(shí)DOM進(jìn)行差分和修補(bǔ)。這相對(duì)簡(jiǎn)單,因?yàn)槲覀兊挠美浅?zhuān)業(yè),而且它還有一個(gè)好處,如果真實(shí)DOM中發(fā)生任何意外(可能是由于瀏覽器擴(kuò)展),我們的算法將簡(jiǎn)單地將視圖恢復(fù)到我們基于數(shù)據(jù)模型的預(yù)期。
雜項(xiàng)
上述所有內(nèi)容可能會(huì)讓你對(duì)編輯器的工作原理有一個(gè)較高的認(rèn)識(shí),但魔鬼是在細(xì)節(jié)中的。下面是我們需要解決的一些小問(wèn)題。
- 支持Unicode。每個(gè)人都喜歡的標(biāo)準(zhǔn),但在工作中卻很麻煩。幸運(yùn)的是,Rust有優(yōu)秀的unicode_segmentation板塊,對(duì)我們幫助很大。這幫助我們解決了一些問(wèn)題,比如按字進(jìn)行光標(biāo)導(dǎo)航,以及確保光標(biāo)能正確地跳過(guò)字母群。
-
光標(biāo)定位是很棘手的,但我們發(fā)現(xiàn)最好的方法是使用瀏覽器的
Selection
對(duì)象,并通過(guò)這種方式設(shè)置一個(gè)(透明的)本地光標(biāo)。然后我們使用getBoundingClientRect()
來(lái)測(cè)量瀏覽器渲染光標(biāo)的位置,然后我們就可以在那里定位我們自己的光標(biāo)。
- 組合事件被瀏覽器用來(lái)組成帶有重音的字符和處理拼音等輸入。不要忘記處理這些。
總結(jié)
創(chuàng)建你自己的富文本編輯器是一項(xiàng)艱巨的任務(wù),但只要有正確的架構(gòu)和良好的規(guī)劃,它肯定是可以做到的。如果你發(fā)現(xiàn)自己處于必須選擇或開(kāi)發(fā)一個(gè)富文本編輯器的位置,我們希望你能發(fā)現(xiàn)這篇文章的有用信息。
注:特別感謝技術(shù)指導(dǎo)dazhao(趙達(dá))對(duì)本文翻譯的審閱指正。
作者:Arend van Beelen
原文鏈接:Creating a Rich Text Editor using Rust and React
原文鏈接:https://juejin.cn/post/7094630970636107789
相關(guān)推薦
- 2022-12-23 Android中Intent與Bundle的使用詳解_Android
- 2023-04-04 Golang利用casbin實(shí)現(xiàn)權(quán)限驗(yàn)證詳解_Golang
- 2022-12-05 Python中WebService客戶(hù)端接口調(diào)用及身份驗(yàn)證的問(wèn)題_python
- 2022-11-07 Python根據(jù)字典值對(duì)字典進(jìn)行排序的三種方法實(shí)例_python
- 2022-03-29 Android頂部標(biāo)題欄的布局設(shè)計(jì)_Android
- 2022-04-16 Android中RecyclerView實(shí)現(xiàn)商品分類(lèi)功能_Android
- 2022-06-17 一文輕松了解ASP.NET與ASP.NET?Core多環(huán)境配置對(duì)比_實(shí)用技巧
- 2022-09-14 如何使用R語(yǔ)言做邏輯回歸詳解_R語(yǔ)言
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門(mén)
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支