WYSISYN編輯器 Prosemirror 入門

EasonYouHehe發表於2018-02-26

為什麼選擇prosemirror

編輯器一向是前端領域的一個難點,一款成熟的編輯器,需要涉及許多方面的東西。

到底有多少東西...這個可以看看掘金上一位大哥在知乎上的回答

至於為什麼要踩這個天坑,是公司想要一個所見即所得的markdown編輯器,不需要markdown原始碼,要有用markdown語法一樣的輸入規則,最後還需要輸出markdown文件作為儲存,在次之上還需要一些制定的需求。這就要求這個選型應該是一個靈活,可配置模組化編輯器框架,而不是一個開箱即可用的一個應用

在選型的時候,之前公司已經有人用prosemirror進行一些特殊編輯器的開發(然而那位同事在我沒來之前就走了),同時考慮的還有slate.js,上面那位大哥也有在掘金上釋出過一篇文章。那為什麼不選擇slate.js呢(另外還有個Draft.js沒有去了解過)。原因很簡單,就是因為我們的技術棧是Vue而不是Reactslate.js依賴於React作為檢視層,作為一個Vue應用,還是不想再專門引入一個React來為slate.js服務。

綜上的原因,就踩上了這個天坑。雖然我沒有用過slate.js,但是根據熱度以及在github上的star也好,活躍度也好,我覺得應該不會比slate.js小,但是它能產出的編輯器,不會比slate.js差。

但正因為活躍度等原因,你在谷歌或者百度上搜尋,是沒有關於prosemirror的任何中文資料的,我一度認為這個框架在國內就沒人用,直到有一天在discuss看到了上面說的那位大佬的頭像,我才知道原來國內還是有人用的。理所當然的,也不會有對應的中文文件,踩了坑也只能上discuss或者issue搜尋提問。但萬幸的是,作者非常熱心,幾乎每一個問題都會回答你,就算是非常入門級的問題,這一點在開發上幫了我很多忙。

以下的內容,幾乎是官網的文件,通過自己理解和簡化寫下來的,有興趣的可以去官網瞭解更加詳細的內容。

prosemirror簡介

如果你覺得prosemirror很陌生,那你也許聽過大名鼎鼎的codemirror。對,就是那個在瀏覽器上的程式碼編輯器,兩個是同個作者,一位非常有實力的德國人Marijn。上面說到的slate也是有些核心的概念例如schema是來自於prosemirror的。

prosemirror不是一個大而全的框架,甚至於你去npm上搜尋prosemirror壓根沒有這個包。

prosemirror由無數個小的模組組成,正如它官網上說的類似於樂高一樣堆疊成一個健壯編輯器

The core library is not an easy drop-in component—we are prioritizing modularity and customizeability over simplicity, with the hope that, in the future, people will distribute drop-in editors based on ProseMirror. As such, this is more of a lego set than a matchbox car.

它的核心庫有

  • prosemirror-model:定義編輯器的文件模型,用來描述編輯器內容的資料結構

  • prosemirror-state:提供描述編輯器整個狀態的資料結構,包括選擇,以及從一個狀態轉移到下一個狀態的事務處理系統。

  • prosemirror-view:實現一個使用者介面元件,該元件在瀏覽器中將給定的編輯器狀態顯示為可編輯元素,並處理使用者與該元素的互動。

  • prosemirror-transform:包含以可記錄和重放的方式修改文件的功能,這是state模組中事務的基礎,並使撤消歷史記錄和協作編輯成為可能。

看完這些描述是不是感覺很熟悉,一個非常像React的一組核心庫。他們構成了整個編輯器的基礎。當然,除了核心庫,還需要各種各樣的庫來實現快捷鍵prosemirror-commands、編輯歷史prosemirror-history等等。

實現一個小編輯器

這是一個功能非常有限的,只有一些基本的按鍵(例如enter換行bacakspace刪除)等,然後我們再加上一個ctrl-z撤回和ctrl-y重做。

一開始覺得是個小demo,就用了parcel打包,發現會報錯,第一次用parcel,不知道是我問題還是parcel問題。

import {EditorState} from "prosemirror-state"
import {EditorView} from "prosemirror-view"
// schama,校驗規則
import {schema} from "prosemirror-schema-basic"
// 歷史記錄以及撤回重做
import {undo, redo, history} from "prosemirror-history"
// 一個
import {keymap} from "prosemirror-keymap"
import {baseKeymap} from "prosemirror-commands"
// 
let content = document.getElementById("content")
// 生成一個state
let state = EditorState.create({
    doc: DOMParser.fromSchema(schema).parse(content),
    schema,
    plugins: [
        history(),
        keymap(baseKeymap),
        keymap({"Mod-z": undo, "Mod-y": redo})
    ]
    })
// 生成檢視
let view = new EditorView(document.getElementById('prosemirror'), {state})
複製程式碼

這段程式碼,把content的內容轉化為編輯器的初始文字,作為初始的編輯狀態。只能夠做簡單的編輯,例如刪除、撤回、換行等。

parser是什麼?

我們來看看上面那段程式碼做了什麼事情。首先,預定了一個conetentid的內容,這個在最後展示是不可見的,為的是把已有的html文件先存在dom裡。緊接著,通過DOMParse解析順著schema(下面會說這是什麼)這個html文字,獲得一個Node型別的物件,這個物件就可以傳入doc屬性作為一個初始的文字資料渲染成編輯器的可編輯文字。

這裡的DOMParse就是一個作為把DOM渲染成Node物件的一個解析器。除了DOMParse,還有一個解析器就是MarkdownParser,專門把markdown文件轉化為Node類資料。

那麼有解析器,就有對應的序列器,呼叫EditorState.JSON()可以把當前狀態的doc序列化成JSON格式,便於儲存。

schema是什麼?

schema是一套描述文件和Dom之間的關聯的一套轉化規則,如何把DOm轉化為Node或者說Node轉化為Dom,這是個關鍵,下面是一個基本的標題的schema

// heading的schema
heading: {
    // 可選的屬性
    attrs: {level: {default: 1}},
    // 節點內容的型別,是行還是塊
    content: "inline*",
    // 自身的型別,是行還是塊
    group: "block",
    // 解析Dom的規則以及屬性
    parseDOM: [{tag: "h1", attrs: {level: 1}},
                {tag: "h2", attrs: {level: 2}},
                {tag: "h3", attrs: {level: 3}},
                {tag: "h4", attrs: {level: 4}},
                {tag: "h5", attrs: {level: 5}},
                {tag: "h6", attrs: {level: 6}}],、
    // 生成Dom的規則
    toDOM(node) { return ["h" + node.attrs.level, 0] }
},
複製程式碼

這樣就是一個描述一個標題的文字規則,不過沒有這個文字規則,解析器或者序列器不知道如何去解析。任何一個在編輯器中出現的Dom以及任何一個需要轉化成Dom的節點型別,都需要有一個對應的schema否則無法編譯。

schema可以自行建立或者在現有的schema上進行新增。一個健壯的schema對每一個屬性的設定都有較高的要求,在這裡不舉例子了,免得帶偏,可以自行上官網學習。

Node是什麼?

Node類構成了Prosemirror文件的節點樹,它的子節點也是Node類。Node類並不能直接被改變,是一個持久的資料結構,類似於React中的state,需要通過apply一個transaction類才能夠改變doc的結構。而Node的結構又非常像Virtual Dom,都具有樹型和遞迴,通過例項解構來描述Dom,而且prosemirror也有自己一套高效的更新演算法來轉化NodeDom

Node的屬性非常多,比如在文件的位置、子節點的數量、節點大小、文字內容等等等等,在許多情況下,這些屬性都為實現某些特定的功能提供了非常大的幫助。

Transaction是什麼?

transaction是一個描述編輯器狀態改變的一個資料型別。在Prosemirror中,呼叫EditorView.updateState可以更新整個編輯器的狀態,就算是敲打一個空格,都必須要通過state進行更新。那麼,如果每次都用DOMParse建立新的Node來形成新的state,歷史記錄等東西必然不會保留,而且在Prosemirror中,到真正呼叫EditorState.apply的過程中,會經過很多的Plugins(如果有的話)去加工這個transaction,所以一定要經過EditorState.apply去應用一個transaction生成一個新的state,接著呼叫,才可以真正改變整個編輯器的狀態,並儲存好整個的狀態,在編輯的時候也是如此。我們可以先看看一個例子

let view = new EditorView(document.body, {
  state,
  // 這是一個鉤子函式,最後應用transaction的函式
  dispatchTransaction(transaction) {
    console.log("Document size went from", transaction.before.content.size,
                "to", transaction.doc.content.size)
    // 應用transaction,並生成一個新的state
    let newState = view.state.apply(transaction)
    // 更新state
    view.updateState(newState)
  }
})
複製程式碼

dispatchTransaction實際上是在呼叫EditorState.apply前的最後一個方法,這裡也可以不呼叫dispatchTransaction,預設進行了更新。在這裡的作用是,每次更新(不管是編輯還是插入刪除等操作)都會log一段文字,僅此而已。如果不進行apply和update的操作,將會報錯。可以通過Editor.tr獲取實時的transaction

keymap、歷史記錄

keymap是鍵盤輸入規則的外掛,history是歷史記錄的外掛,這個略過。

核心內容總結

到此為止,核心內容就已經介紹完畢,當然,核心內容只能作為對prosemirror的一個淺顯認知,好讓我們在後續的編輯器開發的時候,不會不明白它到底是怎麼的一個運作原理。

現在缺少的有一些輸入規則,有這些輸入規則,才能像寫markdown一樣實現WYSIWYN編輯器,還有頂部的操作欄等等。這些都是編輯器的一部分,不過因為不是核心庫,這裡就不講了。官方有一個example-setup一個設定樣例,官方同樣推薦通過這個樣例來改造成符合我們需求的設定

接下來,就讓我們偷懶地實現一個markdown的編輯器。例子同樣是來自於官網。

實現一個markdown編輯器

很簡單,只需要把parser換成defaultMarkdownParserplugins用預設的設定就可以了,然後再用prosemirror-example-setup的預設樣式,一個WYSIWYN編輯器就完成了。

class ProseMirrorView {
    constructor(target, content) {
        this.view = new EditorView(target, {
        state: EditorState.create({
            // 用預設的markdown parser解析markdown文件
            doc: defaultMarkdownParser.parse(content),
            // 設定樣例
            plugins: exampleSetup({schema})
            })
        })
    }
    // 暴露兩個常用方法,便於呼叫
    focus() { this.view.focus() }
    destroy() { this.view.destroy() }
}
new ProseMirrorView(document.getElementById('prosemirror'), '# hello')
複製程式碼

當然這只是一個非常簡單的markdown編輯器,官方給出的defaultMarkdownParser只是用的CommonMark標準,很多的常用markdown語法都沒有。我們可以從中進行非常多的自定義。

defaultMarkdownParser的markdown解析器是用markdown-it的,原理是解析成token後,通過schema再進行轉化。所以如果想要擴充markdown,需要懂得markdown-it或者其他的markdown解析器。

總結

本篇文章簡略地介紹了prosemirror的一些思想和核心內容,這只是涉及一些皮毛,並不是完全展現其魅力。在它的論壇上,有許多的開發者貢獻了許多令人拍案叫好的外掛或者成熟的編輯器,都非常值得去學習借鑑。希望能更加深入理解篇prosemirror

相關文章