[譯] 爲數字優先新聞編輯室開發文本編輯器

內觀一個你可能認爲理所固然的技術內部運做

若是你和美國的大多數人同樣,幾乎天天都會使用某個文本編輯器。不管是基本的 Apple Notes,仍是像 Google Docs、Microsoft Word 或 Mediumz 等更高級的東西,咱們的文本編輯器都容許咱們記錄和呈現咱們重要的想法和信息,使咱們可以以最吸引人的方式講述故事。前端

可是你可能沒有想過這些文本編輯器的後臺運做原理。每次你按下某個鍵時,可能會執行數百行的代碼來在頁面上呈現你想要的字符。看似很小的操做,例如拖動選擇文本中的幾段文字或將文本轉換爲標題,這實際上會觸發程序系統底層的大量變化。node

雖然你可能無需考慮爲這些複雜的文本編輯操做提供動力的代碼,但我在紐約時報的團隊確不斷在思考它。咱們的主要任務是爲新聞工做室建立一個高度定製的報道編輯器。除了輸入和呈現內容的基礎功能以外,這個新的報道編輯器須要將 Google Docs 的高級特性與 Medium 的直觀設計重點結合起來,而且添加新聞室工做流程獨有的許多功能特性。android

多年以來,紐約時代報新聞編輯室使用了一個傳統的自制文本編輯器,它並無知足其衆多需求。雖然咱們的舊版編輯器很是適合新聞編輯室的生產工做流程,但它的用戶界面還有許多不足:它嚴重的分隔了工做流程,將報道的不一樣部分(例如文本、照片、社交媒體和文案編輯)分離成應用程序的徹底不一樣的部分。所以,要在這個較老的編輯器中生成一片文章須要瀏覽一系列冗長的、非直觀的,而且視覺上沒有吸引力的標籤。ios

除了使用戶的工做流程碎片化以外,傳統的編輯器在工程方面也形成很大的痛苦。它依賴於直接操做 DOM 來在編輯器中呈現全部內容,例如添加各類 HTML 標記以表示已刪除文本,新文本和註釋之間的區別。這意味着其餘團隊的工程師必須在文章發佈並呈現到網站以前對文章進行大量嚴格的標記清理,將會是一個耗時而且容易出錯的過程。git

隨着新聞編輯室的發展,咱們設想了一個新的報道編輯器,它能夠直觀的將報道的不一樣組成部分內聯,這樣記者和編輯均可以在發佈前準確的看到報道的樣子。另外,理想狀況下,新的方法在其代碼實現中更加直觀和靈活,避免了舊版編輯器的許多問題。github

考慮到這兩個目標,個人團隊開始開發這個新型文本編輯器,並將其命名爲 Oak。通過大量研究和數月的原型設計,咱們選擇在 ProseMirror 的基礎上開發它。ProseMirror 是一個用於構建富文本編輯器的強大開源 JavaScript 工具包,它採用了和咱們舊版編輯器徹底不一樣的方法,使用它本身的非 HTML 樹形結構 來表示文檔,該結構由段落、標題、列表和鏈接等來描述文本的構成。數據庫

與咱們舊版的編輯器所不一樣的是,基於 ProseMirror 開發的文本編輯器的輸出能夠最終能夠呈現爲 DOM 樹、Markdown 文本或任何其餘能夠表達其編碼概念的其餘格式,使它很是通用而且解決許多咱們在舊版文本編輯器上遇到的問題。後端

那麼 ProseMirror 到底是如何工做的呢?讓咱們趕快深刻它背後的技術。bash

一切都是節點

ProseMirror 將其主要元素 — 段落、標題、列表、圖片等 — 構造爲節點。許多節點均可以具備子節點,例如 heading_basic 節點能夠具備包括 heading1bylinetimestampimage 等子節點。這構成了我上面所提到的屬性結構。前端框架

這種樹狀結構有趣的例外在於段落節點編纂文本的方式。考慮由如下句子組成的段落,「This is strong text with emphasis」。

DOM 會將該句子編成樹,以下所示:

句子的傳統 DOM 表示 — 其標籤以嵌套的樹狀方式工做。來源:ProseMirror

可是,在 ProseMirror 中,段落的內容表示爲一個扁平的內聯元素序列,每一個元素都有本身的樣式

ProseMirror 如何構造相同的句子。來源:ProseMirror

扁平化的段落結構有一個有點:ProseMirror 依據其數字位置來追蹤每一個節點。由於 ProseMirror 將上面示例中的斜體和粗體字 "emphasis" 識別爲其本身的獨立節點,因此它能夠將節點的位置表示爲簡單的字符偏移,而不是將其視爲文檔樹中的位置。例如,文本編輯器能夠知道 "emphasis" 一詞從文檔的 63 位開始。這使得選擇、查找和使用更加容易。

全部的這些節點 — 段落、標題、圖像等 — 具備它們相關聯的某些特徵,包括大小、佔位符和可拖動性。在某些特定節點(如圖像或視頻),它們還必須包括 ID 以便媒體文件可以在較大的 CMS 環境中被找到。Oak 是如何知道全部這些節點功能的呢?

爲了告訴 Oak 特定節點是怎麼樣的,咱們使用「節點規範」來建立它,它是一個定義了文本編輯器須要理解並正確使用節點的自定義方法或行爲的類。接着咱們定義一個適用於編輯器中全部節點的 schema,而且代表了每一個節點在整個文檔中可以被容許放置的位置。(例如,咱們不但願用戶在頁眉中放置嵌入式推文,所以咱們在模式中禁止它。)在 schema 中咱們列出了全部在 Oak 環境中存在的節點以及他們之間的關聯方式。

export function nytBodySchemaSpec() {
  const schemaSpec = {
    nodes: {
      doc: new DocSpec({ content: 'block+', marks: '_' }),
      paragraph: new ParagraphSpec({ content: 'inline*', group:  'block', marks: '_' }),
      heading1: new Heading1Spec({ content: 'inline*', group: 'block', marks: 'comment' }),
      blockquote: new BlockquoteSpec({ content: 'inline*', group: 'block', marks: '_' }),
      summary: new SummarySpec({ content: 'inline*', group: 'block', marks: 'comment' }),
      header_timestamp: new HeaderTimestampSpec({ group: 'header-child-block', marks: 'comment' }),
      ...
    },
    marks: 
      link: new LinkSpec(),
      em: new EmSpec(),
      strong: new StrongSpec(),
      comment: new CommentMarkSpec(),
    },
  };
}
複製代碼

使用Oak環境中存在的全部節點的列表以及它們彼此之間的關係,ProseMirror 能夠在任什麼時候間點建立文檔模型。此模型是一個對象,與最頂層插圖中示例採用 Oak 編輯的文章旁邊顯示的 JOSN 結構很是類似。當用戶編輯文章時,該對象將不斷被包含編輯內容的新對象替換,以確保 ProseMirror 始終知道文檔包含的節點信息來在頁面上呈現內容。

說到這裏,每當 ProseMirror 知道節點在文檔樹中如何組合以後,它又是如何那些節點是什麼樣子又或如何實際在頁面上顯示它們?要將 ProseMirror 的狀態映射到 DOM,每一個節點都有一個開箱即用的簡易方法 toDOM() 用來將節點轉化爲基本的 DOM 標籤。例如,Paragraph 節點的 toDOM() 方法會將它轉化爲 <p> 標籤,而 Image 節點會被轉化爲 <img> 標籤。可是因爲 Oak 須要自定義節點來作一些特殊的事務,咱們的團隊利用 ProseMirror 的 NodeView 功能來設計一個用來以特殊方式渲染節點的自定義 React 組件。

(注意:ProseMirror 與框架無關,NodeView 可使用任何前端框架建立。咱們的團隊使用 React)

跟蹤文本樣式

若是建立的節點具備經過 ProseMirror 從其 NodeView 獲取的特定視覺外觀,那麼其餘用戶添加的樣式(例如粗體和斜體)改如何生效?這裏就是 marks 標記的用處,或許你已經在上面的構架代碼塊中注意到它。

咱們聲明瞭 schema 中的全部節點以後,緊接着定義每一個節點容許具備的 marks 類型。在 Oak 中咱們爲一些節點支持某些 marks,而另外一些節點卻不支持。例如,咱們在小標題節點中容許斜體和超連接,但在大型標題節點中都不容許。對給定節點的 marks 將會保存在 ProseMirror 的當前文檔狀態中。咱們也使用 marks 用於實現自定義批註功能,這將在下文介紹。

編輯功能的幕後工做原理?

爲了在任何給定時間呈現文檔的準確版本並跟蹤版本歷史記錄,咱們記錄用戶更改文檔的幾乎全部操做很是重要。例如,按下 「s」 或者回車鍵,又或插入一張圖片。ProseMirror 將每個這些微小的變化稱爲一個 step

爲了確保 app 的全部部分同步並顯示最新數據,文檔的 state 是不可變的。這就意味着經過簡單地編輯現有數據對象,不會發生對 state 的更新。ProseMirror 接受舊對象,並將其與 step 對象合併以達到一個全新狀態。(對於一些熟悉Flux概念的人來講,這可能很熟悉。)

此流程能夠鼓勵更加清晰的代碼同時也可以留下更新的痕跡,從而實現一些編輯器包括版本比較在內的重要功能。咱們在 Redux store 中追蹤這些 steps 以及它們的順序,從而使用戶可以在版本之間隨意切換,輕鬆實現回滾或前滾更改,並查看不一樣用戶所作的編輯:

咱們的版本比較功能依賴於仔細跟蹤在不可變的 Redux state 下的每一個事務。

咱們開發的一些炫酷的功能

ProseMirror 是有意模塊化和可模塊化的,這意味着實現其餘功能須要大量自定義定製。這對咱們來講再好不過了,由於咱們的目標就是開發一個知足新聞編輯室特殊需求的文本編輯器。咱們團隊開發的一些最有趣的功能包括:

跟蹤變化

就像上面展現的同樣,咱們的「跟蹤變化」功能能夠說是 Oak 最早進最重要的功能。因爲新聞編輯室的文章涉及記者和其餘各類編輯之間的複雜流程,所以可以跟蹤不一樣用戶對文檔所作的更改以及什麼時候更改是很是重要的。此功能很大程度上依賴對每一個事務的仔細跟蹤,並將它們每個存入數據庫中。而後在文檔中用綠色來標記新增的內容,紅色來標記刪除的內容。

自定義標題

Oka 的目標之一是成爲一個以設計爲中心的文本編輯器,讓記者和編輯可以以最適合任何給定故事的方式呈現視覺新聞。爲此,咱們建立了自定義標題節點,其中包括了水平和垂直的全屏圖像。Oak 中的這些標題是有着特殊 NodeViews 和 schemas 的節點來容許它們包含署名、時間戳、圖像和其餘嵌套的節點。對於用戶而言,所編輯時的標題是在面向讀者的網站上發表的文章的標題的寫照,使記者和編輯儘量接近地表示文章在實際紐約時報網站上發佈時的樣子。

一些 Oak 的標題選項。從左到右:基本標題,水平全屏標題,垂直全屏標題。

批註功能

評註是新聞編輯工做流程的重要組成部分。編輯須要與記者交流,提出問題並給出建議。在咱們舊版編輯器中,用戶被迫將他們的批註與文章文本一塊兒直接放入文檔中,常常會使文章看起來很是雜亂而且容易被遺漏。對於 Oak,咱們團隊開發了一個複雜的 ProseMirror 插件可以將批註在文章右側顯示。在底層,批註實際上使一種 mark,它使文本的附註像粗體、斜體、或者超連接同樣,區別僅僅在於展示的樣式。

在Oak中,批註是一種 mark,不過顯示在相關文本或節點的右側。


自從它的構思以來,Oak已經走過了漫長的道路,咱們很高興能爲開始從舊版編輯器轉換的新聞工做室繼續開發新功能。咱們計劃開始開發協同編輯功能,可以容許多個用戶同時編輯文章,這將從根本上改善記者和編輯的合做方式。

文本編輯器的複雜程度比許多人所知道的都要高。我爲可以成爲 Oak 團隊的一員來開發這樣的工具感到榮幸。做爲做者,我以爲這個編輯器很是有趣,而且它對世界上最大和最有影響力的新聞編輯室之一的運做也很是重要。感謝個人經理 Tessa Ann Taylor 和 Joe Hart,以及在我來到這以前已經在 Oak 工做的咱們團隊:Thomas Rhiel、Jeff Sisson、Will Dunning、Matthew Stake、Matthew Berkowitz、Dylan Nelson、Shilpa Kumar、Shayni Sood 以及 Robinson Deckert。我很幸運能有這麼棒的隊友讓 Oak 這一魔術編輯器誕生。謝謝。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索