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

stutter發表於2018-10-31

為數字優先新聞編輯室開發文字編輯器

內觀一個你可能認為理所當然的技術內部運作

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

Aaron Krolik / 紐約時報的插圖

如果你和美國的大多數人一樣,幾乎每天都會使用某個文字編輯器。無論是基本的 Apple Notes,還是像 Google Docs、Microsoft Word 或 Mediumz 等更高階的東西,我們的文字編輯器都允許我們記錄和呈現我們重要的想法和資訊,使我們能夠以最吸引人的方式講述故事。

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

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

多年以來,紐約時代報新聞編輯室使用了一個傳統的自制文字編輯器,它並沒有滿足其眾多需求。雖然我們的舊版編輯器非常適合新聞編輯室的生產工作流程,但它的使用者介面還有許多不足:它嚴重的分隔了工作流程,將報導的不同部分(例如文字、照片、社交媒體和文案編輯)分離成應用程式的完全不同的部分。因此,要在這個較老的編輯器中生成一片文章需要瀏覽一系列冗長的、非直觀的,並且視覺上沒有吸引力的標籤。

除了使使用者的工作流程碎片化之外,傳統的編輯器在工程方面也造成很大的痛苦。它依賴於直接操作 DOM 來在編輯器中呈現所有內容,例如新增各種 HTML 標記以表示已刪除文字,新文字和註釋之間的區別。這意味著其他團隊的工程師必須在文章釋出並呈現到網站之前對文章進行大量嚴格的標記清理,將會是一個耗時並且容易出錯的過程。

隨著新聞編輯室的發展,我們設想了一個新的報導編輯器,它可以直觀的將報導的不同組成部分內聯,這樣記者和編輯都可以在釋出前準確的看到報導的樣子。另外,理想情況下,新的方法在其程式碼實現中更加直觀和靈活,避免了舊版編輯器的許多問題。

考慮到這兩個目標,我的團隊開始開發這個新型文字編輯器,並將其命名為 Oak。經過大量研究和數月的原型設計,我們選擇在 ProseMirror 的基礎上開發它。ProseMirror 是一個用於構建富文字編輯器的強大開源 JavaScript 工具包,它採用了和我們舊版編輯器完全不同的方法,使用它自己的非 HTML 樹形結構 來表示文件,該結構由段落、標題、列表和連線等來描述文字的構成。

與我們舊版的編輯器所不同的是,基於 ProseMirror 開發的文字編輯器的輸出可以最終可以呈現為 DOM 樹、Markdown 文字或任何其他可以表達其編碼概念的其他格式,使它非常通用並且解決許多我們在舊版文字編輯器上遇到的問題。

那麼 ProseMirror 究竟是如何工作的呢?讓我們趕快深入它背後的技術。

一切都是節點

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” 或者Enter鍵,又或插入一張圖片。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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章