WYSISYN編輯器 Prosemirror 入門

爲何選擇prosemirror

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

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

至於爲何要踩這個天坑,是公司想要一個所見即所得的markdown編輯器,不須要markdown源碼,要有用markdown語法同樣的輸入規則,最後還須要輸出markdown文檔做爲存儲,在次之上還須要一些制定的需求。這就要求這個選型應該是一個靈活,可配置模塊化編輯器框架,而不是一個開箱便可用的一個應用node

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

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

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

如下的內容,幾乎是官網的文檔,經過本身理解和簡化寫下來的,有興趣的能夠去官網瞭解更加詳細的內容。npm

prosemirror簡介

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

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

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

相關文章
相關標籤/搜索