「可視化搭建系統(tǒng)」——從設(shè)計(jì)到架構(gòu),探索前端的領(lǐng)域和意義

阿里巴巴集團(tuán)前端委員會主席 @圓心 對前端未來期許有四點(diǎn):搭建服務(wù)呢堰, Serverless抄瑟,智能化,IDE枉疼。仔細(xì)想想皮假,一個(gè)「可視化搭建系統(tǒng)」的想象空間,正能完美命中這些方面骂维。前端的邊界在哪里惹资,對于業(yè)務(wù)的價(jià)值又在哪里,我們不妨靜下來航闺,一起從「可視化搭建系統(tǒng)」的角度來思考褪测。

—— 有人說前端「可視化搭建系統(tǒng)」說到底只是重復(fù)造輪子產(chǎn)生的玩具猴誊;有人說前端「可視化搭建系統(tǒng)」本質(zhì)是組件枚舉,毫無意義侮措。片面的認(rèn)知必有其產(chǎn)生道理懈叹,但我們不妨從更高的角度出發(fā),并真切落地實(shí)踐分扎,也許你會發(fā)現(xiàn):作為 FEer项阴,我們能做的事情也許更多。

頁面搭建技術(shù)流派概覽和彩蛋放送

據(jù)我觀察“幾乎每一個(gè)前端團(tuán)隊(duì)笆包,都會有一個(gè)頁面搭建系統(tǒng)”环揽。頁面搭建技術(shù)是一個(gè)老生常談的話題,可這個(gè)話題伴隨著前端技術(shù)的發(fā)展庵佣,歷久彌新歉胶。究其原因,包括但不限于:

  • 運(yùn)營活動頁面對于產(chǎn)品業(yè)務(wù)至關(guān)重要巴粪,是吸引流量通今、提高留存的關(guān)鍵手段
  • 高頻且重復(fù)度較高的活動頁面開發(fā),對于前端意味著大量的時(shí)間和人力成本消耗

在此背景下肛根,快速頁面搭建技術(shù)就顯得尤為重要辫塌。

由于每個(gè)產(chǎn)品業(yè)務(wù)的特點(diǎn)、運(yùn)營需求和設(shè)計(jì)規(guī)范不盡相同派哲,因此頁面搭建平臺就出現(xiàn)了“百花齊放臼氨,百家爭鳴”的局面。我們在“閉門造車”的同時(shí)芭届,博覽眾家之長储矩,對比歸納,持續(xù)優(yōu)化褂乍。為此持隧,我們分析了社區(qū)上幾乎所有開源產(chǎn)品和方案,包括但不限于:

相關(guān)技術(shù)分析文章:

其特點(diǎn)和技術(shù)方向可以各有特點(diǎn)褥实,但總體可以歸納為以下圖示:

技術(shù)方向

按照目標(biāo)受眾呀狼,可區(qū)分:

受眾

我們也從海量優(yōu)秀方案中總結(jié)出解決這一類運(yùn)營需求的通用手段:將復(fù)雜頁面的搭建抽象成結(jié)構(gòu)化數(shù)據(jù),由結(jié)構(gòu)數(shù)據(jù)驅(qū)動組件/模版的拼裝性锭。簡單的這樣一句話很好理解赠潦,按照這樣的想法也能構(gòu)建出一個(gè)可用的平臺,但能否更進(jìn)一步草冈,想在技術(shù)和業(yè)務(wù)上突破瓶頸她奥,還需要打通更多環(huán)節(jié):

  • 結(jié)構(gòu)化數(shù)據(jù)如何設(shè)計(jì)才能兼顧優(yōu)雅和高性能瓮增,且天然支持活動編輯時(shí)的“時(shí)光旅行 Redo/Undo”功能
  • 如何平衡頁面的自由發(fā)揮度和規(guī)范統(tǒng)一度
  • 如何突破原始模版引擎,借力框架(React哩俭、Vue 等)組件化思想绷跑,并做到 framework free
  • 如何優(yōu)雅實(shí)現(xiàn)專題模版功能,一鍵導(dǎo)入功能以及插拔式編輯
  • 如何貼合自身業(yè)務(wù)特點(diǎn)凡资,平衡實(shí)用性砸捏、適用性和可擴(kuò)展性
  • 如何不斷持續(xù)迭代,以適應(yīng)新的需求發(fā)展
  • 如何借助社區(qū)的力量隙赁,做大做強(qiáng)
  • 如何最大化發(fā)揮可配置垦藏,如何最大化方便接入方擴(kuò)展
  • 如何避免組件枚舉堆積的混亂

業(yè)界已有方案中,有的較好地解決了這些關(guān)鍵點(diǎn)中一個(gè)或多個(gè)問題伞访,有的更像是一個(gè)練手的玩具掂骏。請讀者繼續(xù)閱讀,接下來我將介紹「結(jié)合編輯器技術(shù)的頁面搭建平臺」思路厚掷,整體如下圖:

新思路

當(dāng)編輯器技術(shù)遇見頁面搭建需求

讓我們先回到一個(gè)寬泛而有趣的問題上:“前端開發(fā)的難點(diǎn)到底在什么地方?”弟灼。

在這個(gè)問題下,舊有 @于江水 提到兩個(gè)點(diǎn):

  • 業(yè)務(wù)邏輯很復(fù)雜而且多變
  • 垂直領(lǐng)域解決方案并不簡單

這里對其答案進(jìn)行簡單搬運(yùn)和擴(kuò)展冒黑,原答案可參考:于江水的回答田绑。
順著這個(gè)思路我們來分析,前面提到的運(yùn)營活動頁面——單純開發(fā)這些頁面難度其實(shí)不高抡爹。但是對于前端團(tuán)隊(duì)來說掩驱,如果高頻多變的運(yùn)營需求在短時(shí)間內(nèi)集中爆發(fā),那么就成了一個(gè)系統(tǒng)性的問題了豁延。比如極端情況:對于淘寶雙十一昙篙、京東大促,簡單地堆人堆時(shí)間也只是杯水車薪诱咏。于是誕生了頁面搭建平臺。

這樣一個(gè)平臺涉及到的技術(shù)點(diǎn)是網(wǎng)狀的:比如涉及到開發(fā)工具鏈缴挖、數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)袋狞、渲染器和交互設(shè)計(jì)、數(shù)據(jù)源導(dǎo)入映屋、頁面編譯構(gòu)建苟鸯、頁面生成、代碼發(fā)布棚点、活動發(fā)布早处、版本管理、在線運(yùn)營管理瘫析、權(quán)限管理砌梆、可視化“所見即所得”實(shí)現(xiàn)默责、后端存儲、CDN 同步咸包、數(shù)據(jù)打點(diǎn)和統(tǒng)計(jì)桃序、數(shù)據(jù)分析等。后續(xù)結(jié)合平臺化能力烂瘫,也會涉及到組件市場的設(shè)計(jì)媒熊,甚至 serverless,no/low code 技術(shù)坟比。

而作為垂直領(lǐng)域一個(gè)不可忽視的方向——編輯器開發(fā)芦鳍,技術(shù)難度只會更高:除了編輯器本身的各種功能實(shí)現(xiàn)外,還需要兼顧兼容性葛账,更要適應(yīng)業(yè)務(wù)需求柠衅。同時(shí),編輯器就是生產(chǎn)工具注竿,任何一個(gè)中后臺系統(tǒng)似乎都必不可少茄茁,需求市場上,不管是石墨文檔巩割、釘釘文檔裙顽、頭條飛書等都有著廣泛而強(qiáng)烈的需求。該領(lǐng)域值得深耕而優(yōu)秀開發(fā)專家卻鳳毛麟角宣谈。

為了解決「可視化搭建系統(tǒng)」愈犹,我們嘗試把一個(gè)上述「復(fù)雜的業(yè)務(wù)平臺」和「垂直領(lǐng)域的富文本開發(fā)」這兩大難題結(jié)合起來,打造一個(gè)功能強(qiáng)大的編輯器闻丑,同時(shí)完成頁面搭建平臺的工作——這聽上去雖然是“難上加難”漩怎,但似乎兩大方向的融合是一種美妙的思路和創(chuàng)新。

具體來說嗦嗡,編輯器除了支持傳統(tǒng)富文本功能以外勋锤,需要加入對業(yè)務(wù)功能區(qū)塊的支持,這時(shí)候在數(shù)據(jù)結(jié)構(gòu)上侥祭,選用 JSON base 的存儲方式:傳統(tǒng)富文本區(qū)塊以 JSON 字段存儲富文本內(nèi)容叁执,其它復(fù)合型自定義業(yè)務(wù)區(qū)塊存儲為 JSON 對象結(jié)構(gòu)。在此基礎(chǔ)上矮冬,我們實(shí)現(xiàn)對該 JSON 對象結(jié)構(gòu)的解析谈宛,實(shí)現(xiàn)編輯器內(nèi)“所見即所得”。

這里單獨(dú)說一下富文本之外的“復(fù)合型自定義業(yè)務(wù)區(qū)塊”胎署。我們知道最終搭建出來的頁面將會充滿各種 Sku 商品吆录、自定義組件、用戶卡片等區(qū)塊琼牧,最終這些內(nèi)容的輸出需要被 C 端渲染器所理解恢筝、所解析哀卫。

我們來結(jié)合下圖,進(jìn)一步說明:

編輯器區(qū)塊

區(qū)塊 1 是傳統(tǒng)富文本內(nèi)容滋恬,區(qū)塊 2 是一個(gè)復(fù)合型自定義業(yè)務(wù)區(qū)塊——Sku 卡片聊训,區(qū)塊 3 是另一個(gè)復(fù)合型自定義業(yè)務(wù)區(qū)塊——用戶卡片。這樣一來編輯器不再是一個(gè)單一的富文本編輯器恢氯,而是最終輸出內(nèi)容為復(fù)雜 JSON 類型的多功能編輯器带斑。

不同業(yè)務(wù)場景、特點(diǎn)勋拟,需要完全不同的前端解決方案勋磕,在開發(fā)這些垂直解決方案的時(shí)候,業(yè)務(wù)分析敢靡、技術(shù)選型挂滓、架構(gòu)設(shè)計(jì)、開發(fā)落地是非常難的啸胧。接下來赶站,就讓我們一步步探索,一步步實(shí)現(xiàn)一個(gè)基于并兼顧編輯器技術(shù)的多功能的頁面搭建平臺纺念。

靈活強(qiáng)大的 Markdown 編輯器和頁面搭建創(chuàng)新嘗試

我相信現(xiàn)如今沒有程序員不知道 Markdown贝椿,它對程序員或者所有互聯(lián)網(wǎng)從業(yè)人員來說都非常友好。簡單說陷谱,Markdown 是一種輕量級標(biāo)記語言烙博,它允許我們使用易讀易寫的純文本格式編寫文檔。現(xiàn)如今許多網(wǎng)站都廣泛使用 Markdown 來撰寫幫助文檔或是用它來在社區(qū)上發(fā)表消息烟逊。比如:GitHub渣窜、Wikipedia、簡書宪躯、reddit 等乔宿。

除了易于編寫,Markdown 的可擴(kuò)展性和可轉(zhuǎn)換性也是它收到追捧的重要原因访雪。也正因?yàn)槿绱擞璨覀兂跗诘倪\(yùn)營活動頁面搭建就是基于 Markdown 編輯器實(shí)施的。具體流程如圖:

Markdown 編輯器粗略流程

當(dāng)然這只是一個(gè)非常粗略簡易版的流程示意圖冬阳,接下來我將分:

  • Markdown 擴(kuò)展和自定義解析器
  • 完善使用體驗(yàn),打造頁面生成能力

兩個(gè)方面進(jìn)行詳細(xì)解釋党饮。

Markdown 擴(kuò)展和自定義解析器

Markdown 原本使用場景是面向文檔和寫作肝陪,它支持的標(biāo)記和語法并不能滿足所有場景需求。因此社區(qū)上存在不少 Markdown 解析器刑顺,其目的是對 Markdown 源內(nèi)容進(jìn)行解析和擴(kuò)展氯窍。在眾多解析器當(dāng)中,最出名的就是 marked.js 了。這里簡單對 marked.js 這個(gè)庫原理進(jìn)行分析凉驻,將會有助于理解后續(xù)我們的實(shí)現(xiàn)方案系羞。

說起解析,其實(shí)就是經(jīng)典的“編譯原理”套路政供。套用在 marked.js 上播聪,如下圖:

marked.js 原理

工作機(jī)制很簡單,marked.js 接受輸入源文本字符串后布隔,創(chuàng)建詞法解析器實(shí)例:

const lexer = new marked.Lexer()

詞法解析器實(shí)例 lexer 的使命是將輸入源進(jìn)行分詞离陶,解析出 tokens:

const tokens = lexer.lex(content)

如何理解分詞生成的 tokens 呢?其實(shí) tokens 就是 AST 對象(或直接把它理解成 json 數(shù)據(jù)衅檀,它是樹形結(jié)構(gòu)招刨,表達(dá)出 Markdown 中段落,塊引用哀军,列表沉眶,標(biāo)題,規(guī)則和代碼塊等信息)杉适。

接下來谎倔,marked.js 實(shí)例化一個(gè)解析器:

const parser = new marked.Parser()

該解析器 parser 接收 tokens,根據(jù) tokens 生成 html 富文本:

const html = parser.parse(tokens)

當(dāng)然淘衙,這只是很粗略的流程传藏,但細(xì)心的讀者可以窺出端倪:如果想擴(kuò)展 Markdown 語法:我們可以修改 lexer 生成 tokens 的函數(shù),目的是加入我們的自定義 Markdown 語法解析成新類型 token 的能力彤守;同時(shí)修改 parser 解析函數(shù)毯侦,根據(jù)新 token 類型,生成我們預(yù)期結(jié)果具垫。這里我不在深入贅述這個(gè)過程侈离,事實(shí)上,我們采用的方案也沒有 fork 去修改 marked.js 代碼筝蚕,而是自己基于 marked.js卦碾,封裝了更上層的解析器。

完善使用體驗(yàn) 打造頁面生成能力

由上可知起宽,我們的頁面搭建需求主要集中在插入各種組件卡片洲胖,插入帶鏈接 banner 圖片等復(fù)合型自定義業(yè)務(wù)區(qū)塊。這每一個(gè)需求都應(yīng)該對應(yīng)一個(gè) Markdown 的新語法規(guī)則坯沪。

比如绿映,輸入:

<SkuCell>live@12345@rondStyle</SkuCell>

則表示頁面中插入一個(gè) id 為 12345 的 Sku 卡片。

如果讓運(yùn)營同學(xué)手動輸入上述語法內(nèi)容無疑是痛苦且不可接受的。因此我們設(shè)計(jì)了 Markdown 編輯器的按鈕:「添加 Sku Cell」叉弦,點(diǎn)擊按鈕之后丐一,會彈出表單對話框,由運(yùn)營輸入 Sku 類型和 id 淹冰,即可自動在 Markdown 編輯器中光標(biāo)所在位置插入一行內(nèi)容:

<SkuCell>live@12345@rondStyle</SkuCell>

這樣的設(shè)計(jì)方便運(yùn)營使用和記憶库车。因此對于使用者來說,只需要了解基本的 Markdown 語法樱拴,而不需要再去記牢和手動輸入新型語法柠衍。

為了滿足“所見即所得”需求,我們需要在運(yùn)營鍵入內(nèi)容時(shí)疹鳄,同時(shí)進(jìn)行對輸入源的解析拧略。解析的過程需要逐行進(jìn)行:

  • 如果解析當(dāng)前行內(nèi)容符合 Markdown 原始語法,則用 marked.js 進(jìn)行解析瘪弓,得到解析出來的富文本結(jié)果垫蛆,推入結(jié)果數(shù)據(jù)棧(這里的數(shù)據(jù)棧是一個(gè) result 數(shù)組)
  • 如果解析當(dāng)前行內(nèi)容符合新擴(kuò)展的 Markdown 語法,則使用自己的解析器函數(shù)(暫且命名為 feParse)對該行進(jìn)行解析(解析器函數(shù)實(shí)現(xiàn)是一個(gè)簡易的編譯分詞過程
  • feParse 函數(shù)接收擴(kuò)展新語法內(nèi)容腺怯,對于不同表意方式使用不同的 helper 處理袱饭,比如處理 <SkuCell>live@12345@rondStyle</SkuCell> 將會被 skuCellHelper 函數(shù)處理
  • skuCellHelper 函數(shù)解析內(nèi)容,分析得到分詞結(jié)果(標(biāo)記為 formData):
type: 'live',
sku_id: 12345,
style: 'rondStyle'
  • 根據(jù)上面分詞結(jié)果呛占,請求后端接口虑乖,獲取該 Sku 對應(yīng)的數(shù)據(jù),比如該 id 為 12345 的 live 數(shù)據(jù)(標(biāo)記為 liveData):
author: 'live 作者名',
id: 12345,
created_date: '2019 10-12 20:34',
description: 'live 介紹',
duration: '20mins',
// ...
  • 根據(jù)以上兩種數(shù)據(jù):formData 和 liveData晾虑,利用 React 服務(wù)端渲染能力疹味,獲得該 Sku 組件對應(yīng)的富文本 skuRichText:
const skuRichText = ReactDOMServer.renderToString(<SkuCell data={... formData, ... liveData} />)
  • 將 skuRichText 推入結(jié)果數(shù)據(jù)棧 result

最終我們逐行解析的結(jié)果產(chǎn)出為:

result = [
    '第一行富文本內(nèi)容',
    '第二行 Sku 卡片對應(yīng)的富文本內(nèi)容'帜篇,
    // ...
]

合并 result 內(nèi)容糙捺,渲染出富文本,顯示在頁面右側(cè)笙隙,實(shí)現(xiàn)所見即所得效果洪灯。

總結(jié)一下實(shí)現(xiàn)“所見即所得效果”的要點(diǎn)為:

  • 自定義 Markdown 語法解析器
  • 利用 React 服務(wù)端渲染能力得到特殊組件的富文本內(nèi)容

需要指出的是,在實(shí)際實(shí)施當(dāng)中:運(yùn)營在編輯器中竟痰,保存并提交給后端的數(shù)據(jù)區(qū)別于上述 result签钩,它也是一個(gè)數(shù)組:submitData,用來表示運(yùn)營輸入的內(nèi)容坏快。對于原始 Markdown 語法铅檩,我們直接使用其對應(yīng)的富文本內(nèi)容;對于新的擴(kuò)充語法莽鸿,我們并沒有使用其對應(yīng)的富文本內(nèi)容柠并,而是使用了上述 formData 的數(shù)據(jù)結(jié)構(gòu),最終提交類似內(nèi)容:

submitData = [
    {
        type: 'richText',
        content: '<p>XXXX</p>'
    },
    {
        type: 'sku',
        content: {
            type: 'live',
            sku_id: 12345,
            style: 'rondStyle'
        }
    }臼予,
    // ...
]

這樣的考慮是為了 C 端用戶在請求頁面時(shí),能夠獲得最新的實(shí)時(shí) Sku 數(shù)據(jù)啃沪。如何理解實(shí)時(shí) Sku 數(shù)據(jù)呢粘拾?在運(yùn)營編輯頁面時(shí),假設(shè)插入一條 Sku 的標(biāo)題信息為“標(biāo)題一”创千。再一天后缰雇,該 Sku 的標(biāo)題信息變成了“標(biāo)題二”。如果我們保存并使用了運(yùn)營編輯時(shí)使用的富文本信息追驴,那么 C 端頁面一定是“標(biāo)題一”械哟,而不是最新的“標(biāo)題二”。因此我們只提交該 Sku 的 id殿雪。當(dāng)有 C 端用戶請求頁面時(shí)暇咆,由后端通過 RPC/Http 調(diào)用,獲取最新的數(shù)據(jù)丙曙,并由組件在服務(wù)端渲染出內(nèi)容爸业,最終返回給前端。

整個(gè)流程如下:

Markdown 編輯器粗略流程

到此為止亏镰,我們實(shí)現(xiàn)了一款基于 Markdown扯旷,利用 Markdown 語法靈活性,擴(kuò)展而成的編輯器索抓。這個(gè)編輯器中內(nèi)置了諸如「插入 Sku 卡片」钧忽、「插入 Banner 圖」等一系列的業(yè)務(wù)功能。

基于這套思想逼肯,我們完成了幫助運(yùn)營快速搭建活動頁面的復(fù)合型編輯器和頁面生成器耸黑,它的優(yōu)點(diǎn)非常明顯:

  • 輸入即所見,所見即所得
  • 支持靈活擴(kuò)展汉矿,可以基于解析器支持所有類型的語法和任意組件
  • 運(yùn)營只需要熟悉基本的 Markdown 語法即可崎坊,擴(kuò)展語法由點(diǎn)按按鈕完成

最終效果圖:

Markdown 編輯器效果

技術(shù)方案都是在不斷演化推進(jìn)當(dāng)中發(fā)展并完善的。在該平臺運(yùn)行半年多之后洲拇,我們大膽進(jìn)行了創(chuàng)新優(yōu)化奈揍,并最終用更高效的方案實(shí)現(xiàn)了全面替換。感興趣的讀者請繼續(xù)閱讀赋续。

不止是富文本編輯器

上面我們提到了已有復(fù)合型編輯器即頁面生成器的優(yōu)點(diǎn)男翰,經(jīng)過半年多的線上服務(wù)后,我們再去深入分析一下它的缺點(diǎn):

  • 編輯器內(nèi) Markdown 語法內(nèi)容纽乱,對于運(yùn)營仍然較為晦澀難懂
  • 運(yùn)營還是需要一定的學(xué)習(xí)和使用成本
  • 依賴實(shí)時(shí)解析和渲染的“所見即所得”
  • 對于每一種新的組件蛾绎,都要創(chuàng)建一種新的 Markdown 語法

這些缺點(diǎn)很好理解,這里著重講一下“所見即所得”。上面我們提到“所見即所得”租冠,實(shí)際依賴了實(shí)時(shí)解析內(nèi)容源為全量富文本鹏倘,并實(shí)時(shí)渲染富文本的能力。雖然滿足了需求顽爹,但是這樣的做法性能成本較高纤泵,即便加上常用的“防抖和截流”手段,對于瀏覽器的壓力仍然不小镜粤。能不能像“積木系統(tǒng)”捏题、“拖拽搭建頁面系統(tǒng)”一樣,直接在“畫布”上修改肉渴,做到更加真實(shí)的“所見即所得”呢公荧?

“拖拽系統(tǒng)”優(yōu)缺點(diǎn)鮮明。
首先同规,以大量 H5 生成工具為代表的拖拽系統(tǒng)雖然看上去功能強(qiáng)大循狰,但是本質(zhì)上卻是依靠組件的堆積和無窮盡的配置擴(kuò)展,最終產(chǎn)出的數(shù)據(jù)形態(tài)和功能野蠻生長下去捻浦,比較容易出現(xiàn)“失控”的局面晤揣,而逐漸被邊緣化。
這里的失控既指運(yùn)營側(cè)朱灿、產(chǎn)品設(shè)計(jì)側(cè)沒有統(tǒng)一約束昧识,也包含了代碼膨脹后的維護(hù)角度的失控。另一方面盗扒,從最終結(jié)果上看跪楞,拖拽系統(tǒng)將頁面的拼接轉(zhuǎn)嫁到運(yùn)營身上,這些“搬磚”的工作量對于運(yùn)營其實(shí)也并不算小侣灶,同時(shí)它缺少“規(guī)范化”的強(qiáng)制約束甸祭,不利于視覺設(shè)計(jì)的統(tǒng)一,運(yùn)營同學(xué)“自我發(fā)揮”反倒不一定完全是好事褥影。退一步來說池户,社區(qū)上已經(jīng)存在不少可用的拖拽系統(tǒng),重復(fù)造輪子也毫無意義凡怎。

結(jié)合我們的需求特點(diǎn):頁面區(qū)塊和設(shè)計(jì)樣式固定校焦、組件形態(tài)固定、頁面排版固定统倒、重文字和圖片內(nèi)容寨典、頁面交互并不復(fù)雜,我們認(rèn)為房匆,多功能富文本編輯器將會是一個(gè)值得深入試水的方向耸成。

傳統(tǒng)的富文本編輯器就是一個(gè)強(qiáng)大的“超級文字加工廠”报亩,類似我們常用的 word,運(yùn)營可以在其上“肆意揮灑”井氢。如何在富文本編輯器上弦追,加入設(shè)計(jì)規(guī)范,并實(shí)現(xiàn)業(yè)務(wù)組件添加呢毙沾?

首先骗卜,富文本編輯器是前端一個(gè)非常值得深入研究的重要方向,社區(qū)上各類開源富文本編輯器也不在少數(shù)左胞,但是從時(shí)間和開發(fā)成本的角度來看,我們既不想重新實(shí)現(xiàn)一個(gè)融入了自己業(yè)務(wù)的增強(qiáng)型富文本編輯器举户;又不想做各種魔改已有方案烤宙。

無法找到一個(gè)合適的解決方案,還是讓我們先從需求角度分析:

  • 新型多功能富文本編輯器俭嘁,需要支持歷史上的 Markdown 語法數(shù)據(jù)躺枕,否則會出現(xiàn)歷史數(shù)據(jù)不兼容的線上問題
  • 新型多功能富文本編輯器,不僅為頁面生成器服務(wù)供填,也要能夠支持多類型橫向業(yè)務(wù)以及純富文本編輯器業(yè)務(wù)
  • 新型多功能富文本編輯器拐云,要支持所有富文本的特性,包括復(fù)制粘貼內(nèi)容等
  • 新型多功能富文本編輯器近她,要支持插入自定義組件和區(qū)塊叉瘩,比如 Sku 卡片等
  • 新型多功能富文本編輯器,應(yīng)該插件化粘捎,可插拔
  • 新型多功能富文本編輯器薇缅,要做到完全的所見即所得
  • 新型多功能富文本編輯器,要支持模版形式快速搭建頁面
  • 新型多功能富文本編輯器攒磨,要接入格式自動規(guī)范機(jī)制泳桦,自動實(shí)現(xiàn)標(biāo)點(diǎn)擠壓、統(tǒng)一排版等功能

綜上需求和設(shè)計(jì)方案娩缰,我們選用了 Draft.js 作為這套多功能編輯器的底層框架灸撰,一句話足以總結(jié)做出該選擇的原因:Draft.js 實(shí)際上并不是一個(gè)富文本編輯器,它其實(shí)是一個(gè)用于構(gòu)建富文本內(nèi)容和富文本編輯器的基礎(chǔ)設(shè)施拼坎。做個(gè)比喻:如果把富文本內(nèi)容比作一幅畫浮毯,Draft.js 只提供了畫紙和畫筆,至于怎么畫演痒,開發(fā)者享有很大的自由 ——(出自文章:Draft.js 在知乎的實(shí)踐)亲轨。

這正符合我們的需要:我們不要一個(gè)完整的解決方案,而需要一個(gè)舞臺鸟顺。至于如何解析內(nèi)容惦蚊,如何渲染內(nèi)容器虾,如何生成數(shù)據(jù),應(yīng)該全部由開發(fā)者把控蹦锋。事實(shí)證明兆沙,這樣的創(chuàng)新設(shè)計(jì)對于頁面搭建生成器以及傳統(tǒng)編輯業(yè)務(wù)場景非常貼合,我們最終實(shí)現(xiàn)了目前服務(wù)于后臺系統(tǒng)的強(qiáng)大多功能編輯器 —— Versatile Editor莉掂。

Versatile 譯為“多才多藝的葛圃;有多種技能的;多面手的憎妙;多用途的库正,多功能的”。目前 Versatile Editor 已經(jīng)全面接管了所有后臺系統(tǒng)編輯需求厘唾。它的技術(shù)設(shè)計(jì)和體系也非常清晰褥符。下面我們主要從

  • 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
  • 插件體系設(shè)計(jì)
  • 多數(shù)據(jù)源支持
  • 使用體驗(yàn)設(shè)計(jì)
  • 頁面模版支持
  • 其他細(xì)節(jié)

六個(gè)方面進(jìn)行分析。

別具匠心的數(shù)據(jù)結(jié)構(gòu)

數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)思想是:使用結(jié)果數(shù)據(jù)棧(數(shù)組)存儲每一個(gè) Draft.js 編輯器塊級內(nèi)容抚垃,數(shù)據(jù)每一項(xiàng)都順序?qū)?yīng)每一個(gè)塊元素喷楣。這些塊元素分為兩大類:純富文本內(nèi)容和純自定義組件內(nèi)容。對于純富文本內(nèi)容鹤树,我們重新實(shí)現(xiàn)了將 Draft.js 的不可變數(shù)據(jù)結(jié)構(gòu)解析轉(zhuǎn)換為富文本的工具函數(shù) draftToHtml铣焊;對于純自定義組件,我們只提取出組件最小還原數(shù)據(jù)(比如 Sku Cell 組件的 sku id 等信息)罕伯。

運(yùn)營在編輯器側(cè)提交流程如下圖:

提交流程

具體說明一下圖中的核心 contentState曲伊。contentState 是 ContentState 類型的對象,它規(guī)定了如何存儲具體的富文本內(nèi)容捣炬,包括文字熊昌、塊級元素、行內(nèi)樣式湿酸、元數(shù)據(jù)等婿屹。

這里需要注意的一點(diǎn)是:在輸出數(shù)據(jù)上,我們至少提交兩種數(shù)據(jù)給后端存儲:

  • rawContent
  • renderTreeData

其中 rawContent 是根據(jù)不可變數(shù)據(jù) contentState 進(jìn)行序列化后的結(jié)果推溃,rawContent 可以通過數(shù)據(jù)表示出當(dāng)前編輯器內(nèi)所有內(nèi)容昂利。我們提交 rawContent 的目的是用于編輯還原。當(dāng)運(yùn)營再次打開編輯器時(shí)铁坎,編輯器可以根據(jù) rawContent 迅速渲染出上一次提交的所有內(nèi)容蜂奸,以供編輯。

而 renderTreeData 是經(jīng)過計(jì)算并處理后提交的數(shù)據(jù)硬萍,它的目的是存儲到數(shù)據(jù)庫中扩所,用于后端返回給 C 端頁面,C 端頁面最終根據(jù) renderTreeData 由渲染器渲染出完整的活動運(yùn)營頁面朴乖。由上圖可知祖屏,renderTreeData 的生成助赞,我們開發(fā)了 RenderTreeGenerator 的實(shí)例上 generate 方法:

new RenderTreeGenerator(
  contentState,
  getToHtmlOptions(contentState, this.props.editorConfig),
  this.customBlockModules
).generate()

如圖:

RenderTreeGenerator1
RenderTreeGenerator2

RenderTreeGenerator 接受 Draft.js 的不可變數(shù)據(jù)類型 contentState 作為第一個(gè)參數(shù),自定義配置項(xiàng)作為第二個(gè)參數(shù)袁勺,React 組件集合 this.customBlockModules 作為第三個(gè)參數(shù)雹食。this.customBlockModules 是一個(gè)數(shù)組,包含了所有自定義區(qū)塊 React 組件名期丰,在自定義區(qū)塊類型命中該數(shù)組時(shí)群叶,需要啟動自定義區(qū)塊,并生成結(jié)構(gòu)化數(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 類型街立,如果當(dāng)前類型在 this.customBlockModules 當(dāng)中,則 export 出渲染數(shù)據(jù)以及當(dāng)前 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 當(dāng)中埠通,仍按照富文本導(dǎo)出
          processRichText()
        }
      } else {
        processRichText()
      }
    }

    // 其他美化或清理工作几晤,比如連續(xù)富文本區(qū)塊的合并

    return this.finalOutput
}

這里不同于前期 Markdown 編輯器的關(guān)鍵點(diǎn)主要有兩處:

  • 我們監(jiān)聽編輯器區(qū)塊的 onBlur 事件,在此事件觸發(fā)時(shí)植阴,開始生成結(jié)果數(shù)據(jù)
  • “所見即所得”——不再需要在手動實(shí)時(shí)解析渲染實(shí)現(xiàn)。因?yàn)?Draft.js 是一個(gè)基于 React 的編輯器圾浅,我們可以直接在編輯器中渲染出一個(gè) React 組件

如下圖:

展示富文本編輯器

以上兩個(gè)特征也正是基于 Draft.js 的多功能編輯器優(yōu)于 Markdown 編輯器的關(guān)鍵點(diǎn)掠手。

可插拔、可移植的插件化和組件化設(shè)計(jì)

多功能編輯器的多功能不是說說而已狸捕,為了支持海量功能需求喷鸽,且考慮到方便第三方功能擴(kuò)展,我們設(shè)計(jì)了良好的編輯器插件體系灸拍。目前項(xiàng)目中使用了 11 個(gè)插件做祝,它們涵蓋了:插入代碼、插入公式鸡岗、插入鏈接混槐、插入引用、插入視頻轩性、復(fù)制粘貼還原內(nèi)容声登、插入圖片、插入重點(diǎn)樣式揣苏、插入注解等悯嗓。項(xiàng)目還沉淀出來海量業(yè)務(wù)組件,包括:頁面喵點(diǎn)組件卸察、Banner 圖組件脯厨、Sku 卡片組件、各類按鈕組件坑质、滾動列表組件合武、圖片畫廊組件等临梗。所有的組件和插件原則上都是可以面向社區(qū)、面向第三方使用的眯杏,同時(shí)后續(xù)計(jì)劃只需要一個(gè) NPM 包即可接入一個(gè)新的功能或新的自定義組件類型夜焦。**這也為后續(xù)的組件市場設(shè)計(jì)、no/low code 設(shè)計(jì)打下了基礎(chǔ)岂贩。

在編輯器初始化時(shí)茫经,我們注冊并實(shí)例化各種插件以及自定義組件萎津。因?yàn)槲覀兌喙δ芫庉嬈鞯睦砟罹桶私Y(jié)構(gòu)化和數(shù)據(jù)化卸伞,所有的這些插件和組件都可以依賴 decorator 進(jìn)行解析锉屈,這也就意味著:從另外一處編輯器實(shí)例中復(fù)制任何內(nèi)容(包括自定義組件)到當(dāng)前編輯器,都可以直接還原數(shù)據(jù)颈渊,無縫完美支持組件的復(fù)制粘貼功能遂黍。

多數(shù)據(jù)源支持

任何一項(xiàng)技術(shù)創(chuàng)新和更迭,都要考慮歷史包袱和歷史債務(wù)的解決雾家。多功能編輯器也不例外,前面提到芯咧,歷史編輯內(nèi)容是使用 Markdown 格式的。以運(yùn)營頁面生成器場景為例竹揍,歷史活動頁面 A 對應(yīng)的后端存儲數(shù)據(jù)是 Markdown 字符串敬飒。我們在使用新的多功能編輯器替換舊的 Markdown 編輯器后芬位,如果運(yùn)營同學(xué)想再次編輯活動頁面 A无拗,新的多功能編輯器上自然就要兼容歷史內(nèi)容晶衷。

為此我們的方案是:在編輯器中接收到數(shù)據(jù)源后,如果嗅探為歷史 Markdown 格式晌纫,那么先利用 marked.js 將此 Markdown 格式內(nèi)容轉(zhuǎn)換為富文本內(nèi)容,再根據(jù)富文本內(nèi)容轉(zhuǎn)換為 Draft.js 支持的不可變數(shù)據(jù)結(jié)構(gòu)锹漱。

總結(jié)一下,對于編輯器初始化時(shí)的數(shù)據(jù)源(rawContent)處理流程如下圖:

數(shù)據(jù)源解析

對于編輯器獲取的數(shù)據(jù) rawContent哥牍,我們使用 isDraftJson 工具函數(shù)判斷該 rawContent 是否可以被多功能編輯器以 Draft.js 支持的數(shù)據(jù)解析:如果可以喝检,則證明 rawContent 為由新的多功能編輯器提交的數(shù)據(jù)撼泛,可以直接使用并恢復(fù)出編輯器內(nèi)容挠说。如果 isDraftJson(rawContent) 判別為 false愿题,那么就表示無法被 Draft.js 解析,需要兼容歷史 Markdown 語法潘酗,由 marked.js 解析出富文本后再交給 Draft.js 處理,由富文本生成 Draft.js 的不可變數(shù)據(jù)仔夺;如果解析都失敗,則直接將 rawContent 視為 textarea 內(nèi)容缸兔,直接填入到編輯器當(dāng)中。

圖中并未畫出如果 rawContent 為空(或不存在)時(shí)的處理方式惰蜜。實(shí)際上,如果 rawContent 為空蝎抽,我們使用 ContentState.createFromText('') 方法生成一個(gè)初始化為空內(nèi)容的不可變數(shù)據(jù)路克。

實(shí)際過程由于歷史包袱原因樟结,對于多數(shù)據(jù)源的支持實(shí)現(xiàn)更為復(fù)雜精算,這過于特殊,我們不再展開灰羽。

持續(xù)打磨使用體驗(yàn)

編輯器一個(gè)非常重要的話題就是體驗(yàn)。相信很多人都經(jīng)歷過編輯器的體驗(yàn)之殤:“輸入卡頓廉嚼、詭異的光標(biāo)位置”等,但這里我認(rèn)為沒有必要分析傳統(tǒng)編輯器的體驗(yàn)優(yōu)化話題怠噪,更有意義的是從我們特有的多功能編輯器特點(diǎn)入手,聊一聊用戶體驗(yàn)傍念。

舉一個(gè)例子:按照 Draft.js 的設(shè)計(jì)葛闷,每一個(gè)區(qū)塊之間上下都會有個(gè)空行。如圖:

空行

這樣會導(dǎo)致提交編輯器內(nèi)容時(shí)双藕,生成的自定義區(qū)塊數(shù)據(jù)前后會包含了兩個(gè)空區(qū)塊數(shù)據(jù),最終導(dǎo)致渲染出的頁面也會包含兩個(gè)空白行忧陪,直接影響頁面設(shè)計(jì)效果。社區(qū)上關(guān)于這個(gè)設(shè)計(jì)的 issue 討論不少赤嚼,比如 Empty line on adding atomic block

事實(shí)上更卒,這是為了靈活地在自定義區(qū)塊前后添加或刪除內(nèi)容。設(shè)想蹂空,如果我們連續(xù)添加了三個(gè)自定義區(qū)塊——Sku 卡片 A,Sku 卡片 B上枕,Sku 卡片 C。如果 A辨萍,B,C 之間沒有空行锈玉,那么我們?nèi)绾卧诳ㄆ?A 和卡片 B 之間插入一個(gè)新的卡片 D 呢?如果 ABC 卡片彼此之間保持一個(gè)空行拉背,那么使用者可以用光標(biāo)定位到 AB 之間的空行,再插入卡片 D椅棺。這就是自定義區(qū)塊前后自動存在空行的意義。

有的開發(fā)者可能會想:我們可以保持這個(gè)空行的存在两疚,在最終生成的數(shù)據(jù)時(shí),自動將空行刪除不就可以了嗎诱渤?事實(shí)上,拿到 Draft.js 編輯器的數(shù)據(jù)時(shí),我們無法判斷是用戶自主回車創(chuàng)建的預(yù)期中的空行鞋吉,還是自定義區(qū)塊自帶的前后空行,因此無法直接在結(jié)果數(shù)據(jù)上粗暴地移除空行谓着。

為了達(dá)到更好的使用體驗(yàn):我們開發(fā)的 FocusPlugin 插件,優(yōu)雅地解決了問題:依然是每一個(gè)自定義區(qū)塊前后不保留空行治筒,但是利用 FocusPlugin 插件,使得每一個(gè)自定義區(qū)塊都可以被點(diǎn)擊選中舷蒲,或者用鍵盤上下鍵遍歷選中耸袜,選中之后可以直接摁下回車鍵,添加空行牲平,甚至可以摁下 delete 鍵堤框,刪除該區(qū)塊纵柿。如圖:當(dāng)自定義區(qū)塊被選中時(shí):

選中狀態(tài)

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

編輯狀態(tài) 1
編輯狀態(tài) 2

基于 FocusPlugin 插件蟋软,以修改當(dāng)前 Sku 卡片 id 為例嗽桩,id 進(jìn)行修改后,發(fā)送獲取新的 id 的數(shù)據(jù)碌冶,并在數(shù)據(jù)成功獲取后調(diào)用 modifyAtomicBlock(entityKey, data) 方法,觸發(fā) replaceEntityData(editorState, entityKey, data) 方法進(jìn)行編輯器不可變數(shù)據(jù)的更新,并由 handleEditorStateChange 方法一并更新狀態(tài)拒逮,最終反應(yīng)在編輯器視圖中。

這一編輯發(fā)生過程總結(jié)圖為:

編輯能力流程

使用體驗(yàn)確實(shí)不是一蹴而就的的事情臀规,這是一個(gè)需要持續(xù)迭代優(yōu)化的過程滩援。經(jīng)過不斷地打磨塔嬉,Versatile Editor 最終趨于穩(wěn)定。目前 Versatile Editor 已經(jīng)支持了數(shù)百量級的頁面搭建谨究,以知乎投放的頁面為例,包括但不限于:

頁面模版支持

Daft.js 編輯器內(nèi)容是完全基于數(shù)據(jù)狀態(tài)的纪吮,它使用了不可變數(shù)據(jù)庫進(jìn)行數(shù)據(jù)的更新操作俩檬,秉承純函數(shù)式更新,因而天然對于“時(shí)光旅行(Undo/Redo)”的特性能夠良好支持碾盟。另一方面,一切皆數(shù)據(jù)也讓我們實(shí)現(xiàn)“頁面模版”功能非常簡單而巧妙冰肴。

我們可以將所有模版拆分為幾個(gè)大的自定義區(qū)塊,并創(chuàng)建這個(gè)活動模版所對應(yīng)的數(shù)據(jù):比如對于模版 A:頭部為一個(gè)頭圖 Banner联逻,我們可以編輯器中創(chuàng)建一個(gè)由占位圖表示的 Banner 圖片检痰;第二區(qū)塊為電子書榜單 Top10包归,即可在編輯器中創(chuàng)建一個(gè) Ranking 組件铅歼,并由任意占位 10 個(gè)電子書數(shù)據(jù)填充公壤,以此類推。提交數(shù)據(jù)之后椎椰,即可獲得描述這個(gè)頁面模版的數(shù)據(jù)厦幅。

當(dāng)運(yùn)營在創(chuàng)建頁面慨飘,并選擇使用「排行榜模版 A」時(shí),我們就用已經(jīng)提前預(yù)制的數(shù)據(jù)作為 rawContent 進(jìn)行編輯器初始化休弃。得到模版后吞歼,運(yùn)營即可添加修改玫芦,快速完成模版頁面創(chuàng)建。

整體流程如下:

活動模版流程

其他細(xì)節(jié)

到此為止桥帆,我們介紹了社區(qū)方案和我們自己持續(xù)迭代的方案。其中還有一些小的細(xì)節(jié)在這里簡要帶過老虫,主要包括:預(yù)覽、排版忽刽、安全性夺欲、配置系統(tǒng)幾個(gè)方面說明。

“所見即所得”使得運(yùn)營編輯活動效率大幅提高些阅,但是在編輯器提交發(fā)布和推廣之前,還是需要一個(gè)完整的可預(yù)覽頁面地址供進(jìn)一步回歸市埋。由于這些推廣頁面都是面向移動端,因此我們在這個(gè)多功能編輯器兼頁面生成器的產(chǎn)品設(shè)計(jì)上抒倚,預(yù)留有頁面發(fā)布地址和二維碼生成功能坷澡,進(jìn)一步優(yōu)化運(yùn)營使用體驗(yàn)。如圖:

鏈接和二維碼

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

插件化自動排版

一個(gè)頁面往往無法只由編輯器生成狰住,可能還包括配置內(nèi)容齿梁。這些配置需求我們用進(jìn)入編輯器之前的表單來承載,表單填寫完畢勺择,生成基礎(chǔ)配置數(shù)據(jù)后,再進(jìn)入編輯器進(jìn)行創(chuàng)作省核。表單是頁面中數(shù)據(jù)交互的基本形式,對于非開發(fā)人員使用也沒有使用門檻邓深,但是切記不可將表單設(shè)計(jì)的過于復(fù)雜笔刹。同時(shí)要注意芥备,編輯系統(tǒng)和配置系統(tǒng)需要解偶的原則舌菜。

前面提到編輯器就是生產(chǎn)工具,編輯器的效能就意味著生成效率袱瓮。一旦編輯器出現(xiàn)線上問題爱咬,那么就會直接影響正常的生產(chǎn)活動。因此精拟,為了保障編輯器的安全性和強(qiáng)健性虱歪,我們加入了測試環(huán)節(jié)栅表。主要包括:單元測試,UI 測試怪瓶。單元測試主要驗(yàn)證關(guān)鍵函數(shù)和方法的正確性,比如上面提到的 autoFormat 方法洗贰,各種插件的輸入和輸出正確性校驗(yàn),數(shù)據(jù)修改的工具方法校驗(yàn)等宣增;UI 測試主要依靠 Enzyme矛缨,來保證關(guān)鍵交互的正常運(yùn)行。

最后箕昭,其他涉及點(diǎn)比如:一鍵換膚淤齐、字?jǐn)?shù)統(tǒng)計(jì)等由于篇幅原因镇饺,這里都不在詳述。

富文本編輯器是一個(gè)深坑笔诵,Draft.js 雖然背靠 Facebook 團(tuán)隊(duì)授翻,但也一直在深坑中掙扎藤为,我們此間開發(fā)過程確實(shí)是一部血淚史夺刑,但我們團(tuán)隊(duì)也在此方向積累了豐富的經(jīng)驗(yàn),后續(xù)技術(shù)細(xì)節(jié)也會一一進(jìn)行分享遍愿,請持續(xù)關(guān)注訂閱。

總結(jié)

我一直在思考桅咆,什么樣的文章能夠給讀者帶來真正的思考和啟迪坞笙。一方面入木三分講解語言特性和設(shè)計(jì)刽脖,深入技術(shù)細(xì)節(jié),庖丁解牛般的分析是我們所需要的,這類文章需要靠代碼說話却邓;另一方面,總結(jié)梳理技術(shù)趨勢简十,從更高的角度敘述方案的落地和演進(jìn)撬腾,更是對大局觀和格局的培養(yǎng),這對于團(tuán)隊(duì)的技術(shù)規(guī)劃和舵向同樣至關(guān)重要民傻。

這篇文章粗淺總結(jié)了業(yè)界在「可視化頁面搭建」技術(shù)探索的方方面面,并整理了各種相關(guān)技術(shù)博客和分析文章漓踢。我們還介紹了編輯器技術(shù)和編輯器技術(shù)所能給「可視化頁面搭建」帶來的破局和創(chuàng)新。在此基礎(chǔ)上奴迅,我們更是從一個(gè)自研的公司級「可視化頁面搭建系統(tǒng)」入手挺据,從探索階段到成熟階段的演進(jìn)歷史進(jìn)行了介紹。

事實(shí)上扁耐,「可視化頁面搭建系統(tǒng)」的話題還遠(yuǎn)為結(jié)束:我們正在此方向上探索更多可能,「微組件/微前端」占哟,「頁面歸因能力」、「no/low code 技術(shù)」榨乎、「自定義組件埋點(diǎn)以及 A/B 流量能力」瘫筐、「運(yùn)行時(shí)的組件構(gòu)建和渲染方案」,甚至「Serveless」策肝、「云端 IDE」等隐绵。后續(xù)我們將會繼續(xù)產(chǎn)出相關(guān)文章拙毫,請讀者持續(xù)關(guān)注:技術(shù)博客,我們也在廣泛求賢缀蹄。

回到文章開篇所提到的那個(gè)問題上:“前端開發(fā)的難點(diǎn)到底在什么地方?”,我想已有答案的開發(fā)者將持續(xù)優(yōu)化答案蛀醉,仍然未知的開發(fā)者很快將會找到自己的答案衅码。

Happy coding!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末逝段,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子夭谤,更是在濱河造成了極大的恐慌巫糙,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件参淹,死亡現(xiàn)場離奇詭異,居然都是意外死亡浙值,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門烟勋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來筐付,“玉大人,你說我怎么就攤上這事瓦戚。” “怎么了畜疾?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長啡捶。 經(jīng)常有香客問我,道長徒溪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮揍拆,結(jié)果婚禮上茶凳,老公的妹妹穿的比我還像新娘嫂拴。我一直安慰自己贮喧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布辩恼。 她就那樣靜靜地躺著谓形,像睡著了一般。 火紅的嫁衣襯著肌膚如雪寒跳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天米辐,我揣著相機(jī)與錄音,去河邊找鬼书释。 笑死,一個(gè)胖子當(dāng)著我的面吹牛爆惧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播肴捉,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼齿穗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起跺株,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤脖卖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后畦木,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛆封,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年勾栗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片围俘。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖绣夺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情陶耍,我是刑警寧澤她混,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站坤按,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏臭脓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一砚作、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧葫录,春花似錦、人聲如沸骇扇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽熬苍。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間梦裂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工年柠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人答憔。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓掀抹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親傲武。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345