阿里巴巴集團前端委員會主席 @圓心 對前端未來期許有四點:搭建服務, Serverless,智能化,IDE。仔細想想,一個「可視化搭建系統(tǒng)」的想象空間,正能完美命中這些方面。前端的邊界在哪里,對于業(yè)務的價值又在哪里,我們不妨靜下來,一起從「可視化搭建系統(tǒng)」的角度來思考。
—— 有人說前端「可視化搭建系統(tǒng)」說到底只是重復造輪子產(chǎn)生的玩具;有人說前端「可視化搭建系統(tǒng)」本質是組件枚舉,毫無意義。片面的認知必有其產(chǎn)生道理,但我們不妨從更高的角度出發(fā),并真切落地實踐,也許你會發(fā)現(xiàn):作為 FEer,我們能做的事情也許更多。
頁面搭建技術流派概覽和彩蛋放送
據(jù)我觀察“幾乎每一個前端團隊,都會有一個頁面搭建系統(tǒng)”。頁面搭建技術是一個老生常談的話題,可這個話題伴隨著前端技術的發(fā)展,歷久彌新。究其原因,包括但不限于:
- 運營活動頁面對于產(chǎn)品業(yè)務至關重要,是吸引流量、提高留存的關鍵手段
- 高頻且重復度較高的活動頁面開發(fā),對于前端意味著大量的時間和人力成本消耗
在此背景下,快速頁面搭建技術就顯得尤為重要。
由于每個產(chǎn)品業(yè)務的特點、運營需求和設計規(guī)范不盡相同,因此頁面搭建平臺就出現(xiàn)了“百花齊放,百家爭鳴”的局面。我們在“閉門造車”的同時,博覽眾家之長,對比歸納,持續(xù)優(yōu)化。為此,我們分析了社區(qū)上幾乎所有開源產(chǎn)品和方案,包括但不限于:
- 百度 H5
- iH5
- 轉轉魔方平臺
- 百度外賣頁面配置平臺:Blocks
- 攜程樂高系統(tǒng)
- 人人貸活動運營平臺
- 新版微信編輯器
- 魯班H5
- 阿里云鳳蝶
- MAKA
- 碼良平臺
- grapes
- 可視化布局 bootcss
- 民間方案:pipeline-editor
- 一個國外的民間方案:vvvebjs
相關技術分析文章:
- 頁面可視化搭建工具前生今世
- 頁面可視化搭建工具技術要點
- QQ會員活動運營平臺演變和技術實踐——高效活動運營
- 積木系統(tǒng),將運營系統(tǒng)做到極致
- 活動運營自動化平臺實踐
- 可視化搭建前端工程 - 阿里飛冰了解一下
- 飛冰對于活動引擎的可借鑒之處
- 前端工程實踐之可視化搭建系統(tǒng)
- 如何設計高擴展的在線網(wǎng)頁制作平臺
- 魯班H5作者:@小小魯班
- 厭倦了寫活動頁?快來擼一個頁面生成器吧!
其特點和技術方向可以各有特點,但總體可以歸納為以下圖示:

按照目標受眾,可區(qū)分:

我們也從海量優(yōu)秀方案中總結出解決這一類運營需求的通用手段:將復雜頁面的搭建抽象成結構化數(shù)據(jù),由結構數(shù)據(jù)驅動組件/模版的拼裝。簡單的這樣一句話很好理解,按照這樣的想法也能構建出一個可用的平臺,但能否更進一步,想在技術和業(yè)務上突破瓶頸,還需要打通更多環(huán)節(jié):
- 結構化數(shù)據(jù)如何設計才能兼顧優(yōu)雅和高性能,且天然支持活動編輯時的“時光旅行 Redo/Undo”功能
- 如何平衡頁面的自由發(fā)揮度和規(guī)范統(tǒng)一度
- 如何突破原始模版引擎,借力框架(React、Vue 等)組件化思想,并做到 framework free
- 如何優(yōu)雅實現(xiàn)專題模版功能,一鍵導入功能以及插拔式編輯
- 如何貼合自身業(yè)務特點,平衡實用性、適用性和可擴展性
- 如何不斷持續(xù)迭代,以適應新的需求發(fā)展
- 如何借助社區(qū)的力量,做大做強
- 如何最大化發(fā)揮可配置,如何最大化方便接入方擴展
- 如何避免組件枚舉堆積的混亂
業(yè)界已有方案中,有的較好地解決了這些關鍵點中一個或多個問題,有的更像是一個練手的玩具。請讀者繼續(xù)閱讀,接下來我將介紹「結合編輯器技術的頁面搭建平臺」思路,整體如下圖:

當編輯器技術遇見頁面搭建需求
讓我們先回到一個寬泛而有趣的問題上:“前端開發(fā)的難點到底在什么地方?”。
在這個問題下,舊有 @于江水 提到兩個點:
- 業(yè)務邏輯很復雜而且多變
- 垂直領域解決方案并不簡單
這里對其答案進行簡單搬運和擴展,原答案可參考:于江水的回答。
順著這個思路我們來分析,前面提到的運營活動頁面——單純開發(fā)這些頁面難度其實不高。但是對于前端團隊來說,如果高頻多變的運營需求在短時間內集中爆發(fā),那么就成了一個系統(tǒng)性的問題了。比如極端情況:對于淘寶雙十一、京東大促,簡單地堆人堆時間也只是杯水車薪。于是誕生了頁面搭建平臺。
這樣一個平臺涉及到的技術點是網(wǎng)狀的:比如涉及到開發(fā)工具鏈、數(shù)據(jù)結構設計、渲染器和交互設計、數(shù)據(jù)源導入、頁面編譯構建、頁面生成、代碼發(fā)布、活動發(fā)布、版本管理、在線運營管理、權限管理、可視化“所見即所得”實現(xiàn)、后端存儲、CDN 同步、數(shù)據(jù)打點和統(tǒng)計、數(shù)據(jù)分析等。后續(xù)結合平臺化能力,也會涉及到組件市場的設計,甚至 serverless,no/low code 技術。
而作為垂直領域一個不可忽視的方向——編輯器開發(fā),技術難度只會更高:除了編輯器本身的各種功能實現(xiàn)外,還需要兼顧兼容性,更要適應業(yè)務需求。同時,編輯器就是生產(chǎn)工具,任何一個中后臺系統(tǒng)似乎都必不可少,需求市場上,不管是石墨文檔、釘釘文檔、頭條飛書等都有著廣泛而強烈的需求。該領域值得深耕而優(yōu)秀開發(fā)專家卻鳳毛麟角。
為了解決「可視化搭建系統(tǒng)」,我們嘗試把一個上述「復雜的業(yè)務平臺」和「垂直領域的富文本開發(fā)」這兩大難題結合起來,打造一個功能強大的編輯器,同時完成頁面搭建平臺的工作——這聽上去雖然是“難上加難”,但似乎兩大方向的融合是一種美妙的思路和創(chuàng)新。
具體來說,編輯器除了支持傳統(tǒng)富文本功能以外,需要加入對業(yè)務功能區(qū)塊的支持,這時候在數(shù)據(jù)結構上,選用 JSON base 的存儲方式:傳統(tǒng)富文本區(qū)塊以 JSON 字段存儲富文本內容,其它復合型自定義業(yè)務區(qū)塊存儲為 JSON 對象結構。在此基礎上,我們實現(xiàn)對該 JSON 對象結構的解析,實現(xiàn)編輯器內“所見即所得”。
這里單獨說一下富文本之外的“復合型自定義業(yè)務區(qū)塊”。我們知道最終搭建出來的頁面將會充滿各種 Sku 商品、自定義組件、用戶卡片等區(qū)塊,最終這些內容的輸出需要被 C 端渲染器所理解、所解析。
我們來結合下圖,進一步說明:

區(qū)塊 1 是傳統(tǒng)富文本內容,區(qū)塊 2 是一個復合型自定義業(yè)務區(qū)塊——Sku 卡片,區(qū)塊 3 是另一個復合型自定義業(yè)務區(qū)塊——用戶卡片。這樣一來編輯器不再是一個單一的富文本編輯器,而是最終輸出內容為復雜 JSON 類型的多功能編輯器。
不同業(yè)務場景、特點,需要完全不同的前端解決方案,在開發(fā)這些垂直解決方案的時候,業(yè)務分析、技術選型、架構設計、開發(fā)落地是非常難的。接下來,就讓我們一步步探索,一步步實現(xiàn)一個基于并兼顧編輯器技術的多功能的頁面搭建平臺。
靈活強大的 Markdown 編輯器和頁面搭建創(chuàng)新嘗試
我相信現(xiàn)如今沒有程序員不知道 Markdown,它對程序員或者所有互聯(lián)網(wǎng)從業(yè)人員來說都非常友好。簡單說,Markdown 是一種輕量級標記語言,它允許我們使用易讀易寫的純文本格式編寫文檔。現(xiàn)如今許多網(wǎng)站都廣泛使用 Markdown 來撰寫幫助文檔或是用它來在社區(qū)上發(fā)表消息。比如:GitHub、Wikipedia、簡書、reddit 等。
除了易于編寫,Markdown 的可擴展性和可轉換性也是它收到追捧的重要原因。也正因為如此,我們初期的運營活動頁面搭建就是基于 Markdown 編輯器實施的。具體流程如圖:

當然這只是一個非常粗略簡易版的流程示意圖,接下來我將分:
- Markdown 擴展和自定義解析器
- 完善使用體驗,打造頁面生成能力
兩個方面進行詳細解釋。
Markdown 擴展和自定義解析器
Markdown 原本使用場景是面向文檔和寫作,它支持的標記和語法并不能滿足所有場景需求。因此社區(qū)上存在不少 Markdown 解析器,其目的是對 Markdown 源內容進行解析和擴展。在眾多解析器當中,最出名的就是 marked.js 了。這里簡單對 marked.js 這個庫原理進行分析,將會有助于理解后續(xù)我們的實現(xiàn)方案。
說起解析,其實就是經(jīng)典的“編譯原理”套路。套用在 marked.js 上,如下圖:

工作機制很簡單,marked.js 接受輸入源文本字符串后,創(chuàng)建詞法解析器實例:
const lexer = new marked.Lexer()
詞法解析器實例 lexer 的使命是將輸入源進行分詞,解析出 tokens:
const tokens = lexer.lex(content)
如何理解分詞生成的 tokens 呢?其實 tokens 就是 AST 對象(或直接把它理解成 json 數(shù)據(jù),它是樹形結構,表達出 Markdown 中段落,塊引用,列表,標題,規(guī)則和代碼塊等信息)。
接下來,marked.js 實例化一個解析器:
const parser = new marked.Parser()
該解析器 parser 接收 tokens,根據(jù) tokens 生成 html 富文本:
const html = parser.parse(tokens)
當然,這只是很粗略的流程,但細心的讀者可以窺出端倪:如果想擴展 Markdown 語法:我們可以修改 lexer 生成 tokens 的函數(shù),目的是加入我們的自定義 Markdown 語法解析成新類型 token 的能力;同時修改 parser 解析函數(shù),根據(jù)新 token 類型,生成我們預期結果。這里我不在深入贅述這個過程,事實上,我們采用的方案也沒有 fork 去修改 marked.js 代碼,而是自己基于 marked.js,封裝了更上層的解析器。
完善使用體驗 打造頁面生成能力
由上可知,我們的頁面搭建需求主要集中在插入各種組件卡片,插入帶鏈接 banner 圖片等復合型自定義業(yè)務區(qū)塊。這每一個需求都應該對應一個 Markdown 的新語法規(guī)則。
比如,輸入:
<SkuCell>live@12345@rondStyle</SkuCell>
則表示頁面中插入一個 id 為 12345 的 Sku 卡片。
如果讓運營同學手動輸入上述語法內容無疑是痛苦且不可接受的。因此我們設計了 Markdown 編輯器的按鈕:「添加 Sku Cell」,點擊按鈕之后,會彈出表單對話框,由運營輸入 Sku 類型和 id ,即可自動在 Markdown 編輯器中光標所在位置插入一行內容:
<SkuCell>live@12345@rondStyle</SkuCell>
這樣的設計方便運營使用和記憶。因此對于使用者來說,只需要了解基本的 Markdown 語法,而不需要再去記牢和手動輸入新型語法。
為了滿足“所見即所得”需求,我們需要在運營鍵入內容時,同時進行對輸入源的解析。解析的過程需要逐行進行:
- 如果解析當前行內容符合 Markdown 原始語法,則用 marked.js 進行解析,得到解析出來的富文本結果,推入結果數(shù)據(jù)棧(這里的數(shù)據(jù)棧是一個 result 數(shù)組)
- 如果解析當前行內容符合新擴展的 Markdown 語法,則使用自己的解析器函數(shù)(暫且命名為 feParse)對該行進行解析(解析器函數(shù)實現(xiàn)是一個簡易的編譯分詞過程)
- feParse 函數(shù)接收擴展新語法內容,對于不同表意方式使用不同的 helper 處理,比如處理
<SkuCell>live@12345@rondStyle</SkuCell>將會被 skuCellHelper 函數(shù)處理 - skuCellHelper 函數(shù)解析內容,分析得到分詞結果(標記為 formData):
type: 'live',
sku_id: 12345,
style: 'rondStyle'
- 根據(jù)上面分詞結果,請求后端接口,獲取該 Sku 對應的數(shù)據(jù),比如該 id 為 12345 的 live 數(shù)據(jù)(標記為 liveData):
author: 'live 作者名',
id: 12345,
created_date: '2019 10-12 20:34',
description: 'live 介紹',
duration: '20mins',
// ...
- 根據(jù)以上兩種數(shù)據(jù):formData 和 liveData,利用 React 服務端渲染能力,獲得該 Sku 組件對應的富文本 skuRichText:
const skuRichText = ReactDOMServer.renderToString(<SkuCell data={... formData, ... liveData} />)
- 將 skuRichText 推入結果數(shù)據(jù)棧 result
最終我們逐行解析的結果產(chǎn)出為:
result = [
'第一行富文本內容',
'第二行 Sku 卡片對應的富文本內容',
// ...
]
合并 result 內容,渲染出富文本,顯示在頁面右側,實現(xiàn)所見即所得效果。
總結一下實現(xiàn)“所見即所得效果”的要點為:
- 自定義 Markdown 語法解析器
- 利用 React 服務端渲染能力得到特殊組件的富文本內容
需要指出的是,在實際實施當中:運營在編輯器中,保存并提交給后端的數(shù)據(jù)區(qū)別于上述 result,它也是一個數(shù)組:submitData,用來表示運營輸入的內容。對于原始 Markdown 語法,我們直接使用其對應的富文本內容;對于新的擴充語法,我們并沒有使用其對應的富文本內容,而是使用了上述 formData 的數(shù)據(jù)結構,最終提交類似內容:
submitData = [
{
type: 'richText',
content: '<p>XXXX</p>'
},
{
type: 'sku',
content: {
type: 'live',
sku_id: 12345,
style: 'rondStyle'
}
},
// ...
]
這樣的考慮是為了 C 端用戶在請求頁面時,能夠獲得最新的實時 Sku 數(shù)據(jù)。如何理解實時 Sku 數(shù)據(jù)呢?在運營編輯頁面時,假設插入一條 Sku 的標題信息為“標題一”。再一天后,該 Sku 的標題信息變成了“標題二”。如果我們保存并使用了運營編輯時使用的富文本信息,那么 C 端頁面一定是“標題一”,而不是最新的“標題二”。因此我們只提交該 Sku 的 id。當有 C 端用戶請求頁面時,由后端通過 RPC/Http 調用,獲取最新的數(shù)據(jù),并由組件在服務端渲染出內容,最終返回給前端。
整個流程如下:

到此為止,我們實現(xiàn)了一款基于 Markdown,利用 Markdown 語法靈活性,擴展而成的編輯器。這個編輯器中內置了諸如「插入 Sku 卡片」、「插入 Banner 圖」等一系列的業(yè)務功能。
基于這套思想,我們完成了幫助運營快速搭建活動頁面的復合型編輯器和頁面生成器,它的優(yōu)點非常明顯:
- 輸入即所見,所見即所得
- 支持靈活擴展,可以基于解析器支持所有類型的語法和任意組件
- 運營只需要熟悉基本的 Markdown 語法即可,擴展語法由點按按鈕完成
最終效果圖:

技術方案都是在不斷演化推進當中發(fā)展并完善的。在該平臺運行半年多之后,我們大膽進行了創(chuàng)新優(yōu)化,并最終用更高效的方案實現(xiàn)了全面替換。感興趣的讀者請繼續(xù)閱讀。
不止是富文本編輯器
上面我們提到了已有復合型編輯器即頁面生成器的優(yōu)點,經(jīng)過半年多的線上服務后,我們再去深入分析一下它的缺點:
- 編輯器內 Markdown 語法內容,對于運營仍然較為晦澀難懂
- 運營還是需要一定的學習和使用成本
- 依賴實時解析和渲染的“所見即所得”
- 對于每一種新的組件,都要創(chuàng)建一種新的 Markdown 語法
這些缺點很好理解,這里著重講一下“所見即所得”。上面我們提到“所見即所得”,實際依賴了實時解析內容源為全量富文本,并實時渲染富文本的能力。雖然滿足了需求,但是這樣的做法性能成本較高,即便加上常用的“防抖和截流”手段,對于瀏覽器的壓力仍然不小。能不能像“積木系統(tǒng)”、“拖拽搭建頁面系統(tǒng)”一樣,直接在“畫布”上修改,做到更加真實的“所見即所得”呢?
“拖拽系統(tǒng)”優(yōu)缺點鮮明。
首先,以大量 H5 生成工具為代表的拖拽系統(tǒng)雖然看上去功能強大,但是本質上卻是依靠組件的堆積和無窮盡的配置擴展,最終產(chǎn)出的數(shù)據(jù)形態(tài)和功能野蠻生長下去,比較容易出現(xiàn)“失控”的局面,而逐漸被邊緣化。
這里的失控既指運營側、產(chǎn)品設計側沒有統(tǒng)一約束,也包含了代碼膨脹后的維護角度的失控。另一方面,從最終結果上看,拖拽系統(tǒng)將頁面的拼接轉嫁到運營身上,這些“搬磚”的工作量對于運營其實也并不算小,同時它缺少“規(guī)范化”的強制約束,不利于視覺設計的統(tǒng)一,運營同學“自我發(fā)揮”反倒不一定完全是好事。退一步來說,社區(qū)上已經(jīng)存在不少可用的拖拽系統(tǒng),重復造輪子也毫無意義。
結合我們的需求特點:頁面區(qū)塊和設計樣式固定、組件形態(tài)固定、頁面排版固定、重文字和圖片內容、頁面交互并不復雜,我們認為,多功能富文本編輯器將會是一個值得深入試水的方向。
傳統(tǒng)的富文本編輯器就是一個強大的“超級文字加工廠”,類似我們常用的 word,運營可以在其上“肆意揮灑”。如何在富文本編輯器上,加入設計規(guī)范,并實現(xiàn)業(yè)務組件添加呢?
首先,富文本編輯器是前端一個非常值得深入研究的重要方向,社區(qū)上各類開源富文本編輯器也不在少數(shù),但是從時間和開發(fā)成本的角度來看,我們既不想重新實現(xiàn)一個融入了自己業(yè)務的增強型富文本編輯器;又不想做各種魔改已有方案。
無法找到一個合適的解決方案,還是讓我們先從需求角度分析:
- 新型多功能富文本編輯器,需要支持歷史上的 Markdown 語法數(shù)據(jù),否則會出現(xiàn)歷史數(shù)據(jù)不兼容的線上問題
- 新型多功能富文本編輯器,不僅為頁面生成器服務,也要能夠支持多類型橫向業(yè)務以及純富文本編輯器業(yè)務
- 新型多功能富文本編輯器,要支持所有富文本的特性,包括復制粘貼內容等
- 新型多功能富文本編輯器,要支持插入自定義組件和區(qū)塊,比如 Sku 卡片等
- 新型多功能富文本編輯器,應該插件化,可插拔
- 新型多功能富文本編輯器,要做到完全的所見即所得
- 新型多功能富文本編輯器,要支持模版形式快速搭建頁面
- 新型多功能富文本編輯器,要接入格式自動規(guī)范機制,自動實現(xiàn)標點擠壓、統(tǒng)一排版等功能
綜上需求和設計方案,我們選用了 Draft.js 作為這套多功能編輯器的底層框架,一句話足以總結做出該選擇的原因:Draft.js 實際上并不是一個富文本編輯器,它其實是一個用于構建富文本內容和富文本編輯器的基礎設施。做個比喻:如果把富文本內容比作一幅畫,Draft.js 只提供了畫紙和畫筆,至于怎么畫,開發(fā)者享有很大的自由 ——(出自文章:Draft.js 在知乎的實踐)。
這正符合我們的需要:我們不要一個完整的解決方案,而需要一個舞臺。至于如何解析內容,如何渲染內容,如何生成數(shù)據(jù),應該全部由開發(fā)者把控。事實證明,這樣的創(chuàng)新設計對于頁面搭建生成器以及傳統(tǒng)編輯業(yè)務場景非常貼合,我們最終實現(xiàn)了目前服務于后臺系統(tǒng)的強大多功能編輯器 —— Versatile Editor。
Versatile 譯為“多才多藝的;有多種技能的;多面手的;多用途的,多功能的”。目前 Versatile Editor 已經(jīng)全面接管了所有后臺系統(tǒng)編輯需求。它的技術設計和體系也非常清晰。下面我們主要從
- 數(shù)據(jù)結構設計
- 插件體系設計
- 多數(shù)據(jù)源支持
- 使用體驗設計
- 頁面模版支持
- 其他細節(jié)
六個方面進行分析。
別具匠心的數(shù)據(jù)結構
數(shù)據(jù)結構的設計思想是:使用結果數(shù)據(jù)棧(數(shù)組)存儲每一個 Draft.js 編輯器塊級內容,數(shù)據(jù)每一項都順序對應每一個塊元素。這些塊元素分為兩大類:純富文本內容和純自定義組件內容。對于純富文本內容,我們重新實現(xiàn)了將 Draft.js 的不可變數(shù)據(jù)結構解析轉換為富文本的工具函數(shù) draftToHtml;對于純自定義組件,我們只提取出組件最小還原數(shù)據(jù)(比如 Sku Cell 組件的 sku id 等信息)。
運營在編輯器側提交流程如下圖:

具體說明一下圖中的核心 contentState。contentState 是 ContentState 類型的對象,它規(guī)定了如何存儲具體的富文本內容,包括文字、塊級元素、行內樣式、元數(shù)據(jù)等。
這里需要注意的一點是:在輸出數(shù)據(jù)上,我們至少提交兩種數(shù)據(jù)給后端存儲:
- rawContent
- renderTreeData
其中 rawContent 是根據(jù)不可變數(shù)據(jù) contentState 進行序列化后的結果,rawContent 可以通過數(shù)據(jù)表示出當前編輯器內所有內容。我們提交 rawContent 的目的是用于編輯還原。當運營再次打開編輯器時,編輯器可以根據(jù) rawContent 迅速渲染出上一次提交的所有內容,以供編輯。
而 renderTreeData 是經(jīng)過計算并處理后提交的數(shù)據(jù),它的目的是存儲到數(shù)據(jù)庫中,用于后端返回給 C 端頁面,C 端頁面最終根據(jù) renderTreeData 由渲染器渲染出完整的活動運營頁面。由上圖可知,renderTreeData 的生成,我們開發(fā)了 RenderTreeGenerator 的實例上 generate 方法:
new RenderTreeGenerator(
contentState,
getToHtmlOptions(contentState, this.props.editorConfig),
this.customBlockModules
).generate()
如圖:


RenderTreeGenerator 接受 Draft.js 的不可變數(shù)據(jù)類型 contentState 作為第一個參數(shù),自定義配置項作為第二個參數(shù),React 組件集合 this.customBlockModules 作為第三個參數(shù)。this.customBlockModules 是一個數(shù)組,包含了所有自定義區(qū)塊 React 組件名,在自定義區(qū)塊類型命中該數(shù)組時,需要啟動自定義區(qū)塊,并生成結構化數(shù)據(jù)。
generate 方法簡單偽代碼說明如下:
generate() {
this.output = []
this.blocks = this.contentState.getBlocksAsArray()
this.totalBlocks = this.blocks.length
this.currentBlock = 0
this.indentLevel = 0
this.wrapperTag = null
this.richTextArray = []
this.finalOutput = []
const processRichText = () => {
this.output.push({
type: 'RICHTEXT',
data: this.processRichText()
})
}
while (this.currentBlock < this.totalBlocks) {
const block = this.blocks[this.currentBlock]
let blockType = block.getType()
let type = blockType
// 對于 atomic 類型,如果當前類型在 this.customBlockModules 當中,則 export 出渲染數(shù)據(jù)以及當前 type
if (block.getEntityAt(0)) {
const entity = this.contentState.getEntity(block.getEntityAt(0))
type = entity.getType()
if (this.customBlockModules.has(type)) {
const entityData = entity.getData()
this.output.push({
type,
data: entityData
})
this.currentBlock += 1
} else {
// 不在 this.customBlockModules 當中,仍按照富文本導出
processRichText()
}
} else {
processRichText()
}
}
// 其他美化或清理工作,比如連續(xù)富文本區(qū)塊的合并
return this.finalOutput
}
這里不同于前期 Markdown 編輯器的關鍵點主要有兩處:
- 我們監(jiān)聽編輯器區(qū)塊的 onBlur 事件,在此事件觸發(fā)時,開始生成結果數(shù)據(jù)
- “所見即所得”——不再需要在手動實時解析渲染實現(xiàn)。因為 Draft.js 是一個基于 React 的編輯器,我們可以直接在編輯器中渲染出一個 React 組件
如下圖:

以上兩個特征也正是基于 Draft.js 的多功能編輯器優(yōu)于 Markdown 編輯器的關鍵點。
可插拔、可移植的插件化和組件化設計
多功能編輯器的多功能不是說說而已,為了支持海量功能需求,且考慮到方便第三方功能擴展,我們設計了良好的編輯器插件體系。目前項目中使用了 11 個插件,它們涵蓋了:插入代碼、插入公式、插入鏈接、插入引用、插入視頻、復制粘貼還原內容、插入圖片、插入重點樣式、插入注解等。項目還沉淀出來海量業(yè)務組件,包括:頁面喵點組件、Banner 圖組件、Sku 卡片組件、各類按鈕組件、滾動列表組件、圖片畫廊組件等。所有的組件和插件原則上都是可以面向社區(qū)、面向第三方使用的,同時后續(xù)計劃只需要一個 NPM 包即可接入一個新的功能或新的自定義組件類型。**這也為后續(xù)的組件市場設計、no/low code 設計打下了基礎。
在編輯器初始化時,我們注冊并實例化各種插件以及自定義組件。因為我們多功能編輯器的理念就包括了結構化和數(shù)據(jù)化,所有的這些插件和組件都可以依賴 decorator 進行解析,這也就意味著:從另外一處編輯器實例中復制任何內容(包括自定義組件)到當前編輯器,都可以直接還原數(shù)據(jù),無縫完美支持組件的復制粘貼功能。
多數(shù)據(jù)源支持
任何一項技術創(chuàng)新和更迭,都要考慮歷史包袱和歷史債務的解決。多功能編輯器也不例外,前面提到,歷史編輯內容是使用 Markdown 格式的。以運營頁面生成器場景為例,歷史活動頁面 A 對應的后端存儲數(shù)據(jù)是 Markdown 字符串。我們在使用新的多功能編輯器替換舊的 Markdown 編輯器后,如果運營同學想再次編輯活動頁面 A,新的多功能編輯器上自然就要兼容歷史內容。
為此我們的方案是:在編輯器中接收到數(shù)據(jù)源后,如果嗅探為歷史 Markdown 格式,那么先利用 marked.js 將此 Markdown 格式內容轉換為富文本內容,再根據(jù)富文本內容轉換為 Draft.js 支持的不可變數(shù)據(jù)結構。
總結一下,對于編輯器初始化時的數(shù)據(jù)源(rawContent)處理流程如下圖:

對于編輯器獲取的數(shù)據(jù) rawContent,我們使用 isDraftJson 工具函數(shù)判斷該 rawContent 是否可以被多功能編輯器以 Draft.js 支持的數(shù)據(jù)解析:如果可以,則證明 rawContent 為由新的多功能編輯器提交的數(shù)據(jù),可以直接使用并恢復出編輯器內容。如果 isDraftJson(rawContent) 判別為 false,那么就表示無法被 Draft.js 解析,需要兼容歷史 Markdown 語法,由 marked.js 解析出富文本后再交給 Draft.js 處理,由富文本生成 Draft.js 的不可變數(shù)據(jù);如果解析都失敗,則直接將 rawContent 視為 textarea 內容,直接填入到編輯器當中。
圖中并未畫出如果 rawContent 為空(或不存在)時的處理方式。實際上,如果 rawContent 為空,我們使用 ContentState.createFromText('') 方法生成一個初始化為空內容的不可變數(shù)據(jù)。
實際過程由于歷史包袱原因,對于多數(shù)據(jù)源的支持實現(xiàn)更為復雜,這過于特殊,我們不再展開。
持續(xù)打磨使用體驗
編輯器一個非常重要的話題就是體驗。相信很多人都經(jīng)歷過編輯器的體驗之殤:“輸入卡頓、詭異的光標位置”等,但這里我認為沒有必要分析傳統(tǒng)編輯器的體驗優(yōu)化話題,更有意義的是從我們特有的多功能編輯器特點入手,聊一聊用戶體驗。
舉一個例子:按照 Draft.js 的設計,每一個區(qū)塊之間上下都會有個空行。如圖:

這樣會導致提交編輯器內容時,生成的自定義區(qū)塊數(shù)據(jù)前后會包含了兩個空區(qū)塊數(shù)據(jù),最終導致渲染出的頁面也會包含兩個空白行,直接影響頁面設計效果。社區(qū)上關于這個設計的 issue 討論不少,比如 Empty line on adding atomic block。
事實上,這是為了靈活地在自定義區(qū)塊前后添加或刪除內容。設想,如果我們連續(xù)添加了三個自定義區(qū)塊——Sku 卡片 A,Sku 卡片 B,Sku 卡片 C。如果 A,B,C 之間沒有空行,那么我們如何在卡片 A 和卡片 B 之間插入一個新的卡片 D 呢?如果 ABC 卡片彼此之間保持一個空行,那么使用者可以用光標定位到 AB 之間的空行,再插入卡片 D。這就是自定義區(qū)塊前后自動存在空行的意義。
有的開發(fā)者可能會想:我們可以保持這個空行的存在,在最終生成的數(shù)據(jù)時,自動將空行刪除不就可以了嗎?事實上,拿到 Draft.js 編輯器的數(shù)據(jù)時,我們無法判斷是用戶自主回車創(chuàng)建的預期中的空行,還是自定義區(qū)塊自帶的前后空行,因此無法直接在結果數(shù)據(jù)上粗暴地移除空行。
為了達到更好的使用體驗:我們開發(fā)的 FocusPlugin 插件,優(yōu)雅地解決了問題:依然是每一個自定義區(qū)塊前后不保留空行,但是利用 FocusPlugin 插件,使得每一個自定義區(qū)塊都可以被點擊選中,或者用鍵盤上下鍵遍歷選中,選中之后可以直接摁下回車鍵,添加空行,甚至可以摁下 delete 鍵,刪除該區(qū)塊。如圖:當自定義區(qū)塊被選中時:

最終這套基于 FocusPlugin 插件的方案使得交互更加順暢自然,達到了更好的效果?;诖?,我們可以非常順利地完成自定義區(qū)塊的更改:比如當前選中區(qū)塊為一個 id 是 1234 的 Sku 卡片,如果運營需要替換為 id 是 5678 的 Sku 卡片,只需要選擇當前區(qū)塊,選中之后在右側出現(xiàn)的編輯區(qū)中更改 id 內容,確定后即完成替換,如圖所示:


基于 FocusPlugin 插件,以修改當前 Sku 卡片 id 為例,id 進行修改后,發(fā)送獲取新的 id 的數(shù)據(jù),并在數(shù)據(jù)成功獲取后調用 modifyAtomicBlock(entityKey, data) 方法,觸發(fā) replaceEntityData(editorState, entityKey, data) 方法進行編輯器不可變數(shù)據(jù)的更新,并由 handleEditorStateChange 方法一并更新狀態(tài),最終反應在編輯器視圖中。
這一編輯發(fā)生過程總結圖為:

使用體驗確實不是一蹴而就的的事情,這是一個需要持續(xù)迭代優(yōu)化的過程。經(jīng)過不斷地打磨,Versatile Editor 最終趨于穩(wěn)定。目前 Versatile Editor 已經(jīng)支持了數(shù)百量級的頁面搭建,以知乎投放的頁面為例,包括但不限于:
頁面模版支持
Daft.js 編輯器內容是完全基于數(shù)據(jù)狀態(tài)的,它使用了不可變數(shù)據(jù)庫進行數(shù)據(jù)的更新操作,秉承純函數(shù)式更新,因而天然對于“時光旅行(Undo/Redo)”的特性能夠良好支持。另一方面,一切皆數(shù)據(jù)也讓我們實現(xiàn)“頁面模版”功能非常簡單而巧妙。
我們可以將所有模版拆分為幾個大的自定義區(qū)塊,并創(chuàng)建這個活動模版所對應的數(shù)據(jù):比如對于模版 A:頭部為一個頭圖 Banner,我們可以編輯器中創(chuàng)建一個由占位圖表示的 Banner 圖片;第二區(qū)塊為電子書榜單 Top10,即可在編輯器中創(chuàng)建一個 Ranking 組件,并由任意占位 10 個電子書數(shù)據(jù)填充,以此類推。提交數(shù)據(jù)之后,即可獲得描述這個頁面模版的數(shù)據(jù)。
當運營在創(chuàng)建頁面,并選擇使用「排行榜模版 A」時,我們就用已經(jīng)提前預制的數(shù)據(jù)作為 rawContent 進行編輯器初始化。得到模版后,運營即可添加修改,快速完成模版頁面創(chuàng)建。
整體流程如下:

其他細節(jié)
到此為止,我們介紹了社區(qū)方案和我們自己持續(xù)迭代的方案。其中還有一些小的細節(jié)在這里簡要帶過,主要包括:預覽、排版、安全性、配置系統(tǒng)幾個方面說明。
“所見即所得”使得運營編輯活動效率大幅提高,但是在編輯器提交發(fā)布和推廣之前,還是需要一個完整的可預覽頁面地址供進一步回歸。由于這些推廣頁面都是面向移動端,因此我們在這個多功能編輯器兼頁面生成器的產(chǎn)品設計上,預留有頁面發(fā)布地址和二維碼生成功能,進一步優(yōu)化運營使用體驗。如圖:

另一方面,我們對于頁面文字的編審有著嚴格的要求,比如:不能使用中文引號,需要使用「」;英文和數(shù)字與其他漢字之間需要預留一個空格;甚至標點的位置也有嚴格規(guī)范,需要實現(xiàn)傳統(tǒng)類似“標點懸掛、標點擠眼”等一系列排版需求。因此,該多功能編輯器兼頁面生成器配置了可插拔的自動排版能力,主要完成自動排版規(guī)范的審校和修正,如圖:

一個頁面往往無法只由編輯器生成,可能還包括配置內容。這些配置需求我們用進入編輯器之前的表單來承載,表單填寫完畢,生成基礎配置數(shù)據(jù)后,再進入編輯器進行創(chuàng)作。表單是頁面中數(shù)據(jù)交互的基本形式,對于非開發(fā)人員使用也沒有使用門檻,但是切記不可將表單設計的過于復雜。同時要注意,編輯系統(tǒng)和配置系統(tǒng)需要解偶的原則。
前面提到編輯器就是生產(chǎn)工具,編輯器的效能就意味著生成效率。一旦編輯器出現(xiàn)線上問題,那么就會直接影響正常的生產(chǎn)活動。因此,為了保障編輯器的安全性和強健性,我們加入了測試環(huán)節(jié)。主要包括:單元測試,UI 測試。單元測試主要驗證關鍵函數(shù)和方法的正確性,比如上面提到的 autoFormat 方法,各種插件的輸入和輸出正確性校驗,數(shù)據(jù)修改的工具方法校驗等;UI 測試主要依靠 Enzyme,來保證關鍵交互的正常運行。
最后,其他涉及點比如:一鍵換膚、字數(shù)統(tǒng)計等由于篇幅原因,這里都不在詳述。
富文本編輯器是一個深坑,Draft.js 雖然背靠 Facebook 團隊,但也一直在深坑中掙扎,我們此間開發(fā)過程確實是一部血淚史,但我們團隊也在此方向積累了豐富的經(jīng)驗,后續(xù)技術細節(jié)也會一一進行分享,請持續(xù)關注訂閱。
總結
我一直在思考,什么樣的文章能夠給讀者帶來真正的思考和啟迪。一方面入木三分講解語言特性和設計,深入技術細節(jié),庖丁解牛般的分析是我們所需要的,這類文章需要靠代碼說話;另一方面,總結梳理技術趨勢,從更高的角度敘述方案的落地和演進,更是對大局觀和格局的培養(yǎng),這對于團隊的技術規(guī)劃和舵向同樣至關重要。
這篇文章粗淺總結了業(yè)界在「可視化頁面搭建」技術探索的方方面面,并整理了各種相關技術博客和分析文章。我們還介紹了編輯器技術和編輯器技術所能給「可視化頁面搭建」帶來的破局和創(chuàng)新。在此基礎上,我們更是從一個自研的公司級「可視化頁面搭建系統(tǒng)」入手,從探索階段到成熟階段的演進歷史進行了介紹。
事實上,「可視化頁面搭建系統(tǒng)」的話題還遠為結束:我們正在此方向上探索更多可能,「微組件/微前端」,「頁面歸因能力」、「no/low code 技術」、「自定義組件埋點以及 A/B 流量能力」、「運行時的組件構建和渲染方案」,甚至「Serveless」、「云端 IDE」等。后續(xù)我們將會繼續(xù)產(chǎn)出相關文章,請讀者持續(xù)關注:技術博客,我們也在廣泛求賢。
回到文章開篇所提到的那個問題上:“前端開發(fā)的難點到底在什么地方?”,我想已有答案的開發(fā)者將持續(xù)優(yōu)化答案,仍然未知的開發(fā)者很快將會找到自己的答案。
Happy coding!