乐趣区

关于javascript:可视化搭建系统从设计到架构探索前端的领域和意义

阿里巴巴团体前端委员会主席 @圆心 对前端将来期许有四点:搭建服务,Serverless,智能化,IDE。认真想想,一个「可视化搭建零碎」的设想空间,正能完满命中这些方面。前端的边界在哪里,对于业务的价值又在哪里,咱们无妨静下来,一起从「可视化搭建零碎」的角度来思考。

—— 有人说前端「可视化搭建零碎」说到底只是反复造轮子产生的玩具;有人说前端「可视化搭建零碎」实质是组件枚举,毫无意义。全面的认知必有其产生情理,但咱们无妨从更高的角度登程,并真切落地实际,兴许你会发现:作为 FEer,咱们能做的事件兴许更多。

页面搭建技术流派概览和彩蛋放送

据我察看“简直每一个前端团队,都会有一个页面搭建零碎”。页面搭建技术是一个陈词滥调的话题,可这个话题随同着前端技术的倒退,历久弥新。究其原因,包含但不限于:

  • 经营流动页面对于产品业务至关重要,是吸引流量、进步留存的要害伎俩
  • 高频且反复度较高的流动页面开发,对于前端意味着大量的工夫和人力老本耗费

在此背景下,疾速页面搭建技术就显得尤为重要。

因为每个产品业务的特点、经营需要和设计规范不尽相同,因而页面搭建平台就呈现了“百花齐放,百家争鸣”的场面。咱们在“闭门造车”的同时,博览众家之长,比照演绎,继续优化。为此,咱们剖析了社区上简直所有开源产品和计划,包含但不限于:

  • 百度 H5
  • iH5
  • 转转魔方平台
  • 百度外卖页面配置平台:Blocks
  • 携程乐高零碎
  • 人人贷流动经营平台
  • 新版微信编辑器
  • 鲁班 H5
  • 阿里云凤蝶
  • MAKA
  • 码良平台
  • grapes
  • 可视化布局 bootcss
  • 民间计划:pipeline-editor
  • 一个国外的民间计划:vvvebjs

相干技术剖析文章:

  • 页面可视化搭建工具前生今世
  • 页面可视化搭建工具技术要点
  • QQ 会员流动经营平台演变和技术实际——高效流动经营
  • 积木零碎,将经营零碎做到极致
  • 流动经营自动化平台实际
  • 可视化搭建前端工程 – 阿里飞冰理解一下
  • 飞冰对于流动引擎的可借鉴之处
  • 前端工程实际之可视化搭建零碎
  • 如何设计高扩大的在线网页制作平台
  • 鲁班 H5 作者:@小小鲁班
  • 厌倦了写流动页?快来撸一个页面生成器吧!

其特点和技术方向能够各有特点,但总体能够演绎为以下图示:

依照指标受众,可辨别:

咱们也从海量优良计划中总结出解决这一类经营需要的 通用伎俩:将简单页面的搭建形象成结构化数据,由构造数据驱动组件 / 模版的拼装。简略的这样一句话很好了解,依照这样的想法也能构建出一个可用的平台,但是否更进一步,想在技术和业务上冲破瓶颈,还须要买通更多环节:

  • 结构化数据如何设计能力兼顾优雅和高性能,且人造反对流动编辑时的“时光旅行 Redo/Undo”性能
  • 如何均衡页面的自由发挥度和标准对立度
  • 如何冲破原始模版引擎,借力框架(React、Vue 等)组件化思维,并做到 framework free
  • 如何优雅实现专题模版性能,一键导入性能以及插拔式编辑
  • 如何贴合本身业务特点,均衡实用性、适用性和可扩展性
  • 如何一直继续迭代,以适应新的需要倒退
  • 如何借助社区的力量,做大做强
  • 如何最大化施展可配置,如何最大化不便接入方扩大
  • 如何防止组件枚举沉积的凌乱

业界已有计划中,有的较好地解决了这些关键点中一个或多个问题,有的更像是一个练手的玩具。请读者持续浏览,接下来我将介绍「联合编辑器技术的页面搭建平台」思路,整体如下图:

当编辑器技术遇见页面搭建需要

让咱们先回到一个宽泛而乏味的问题上:“前端开发的难点到底在什么中央?”。

在这个问题下,旧有 @于江水 提到两个点:

  • 业务逻辑很简单而且多变
  • 垂直畛域解决方案并不简略

这里对其答案进行简略搬运和扩大,原答案可参考:于江水的答复。
顺着这个思路咱们来剖析,后面提到的经营流动页面——单纯开发这些页面难度其实不高。然而对于前端团队来说,如果高频多变的经营需要在短时间内集中暴发,那么就成了一个系统性的问题了。比方极其状况:对于淘宝双十一、京东大促,简略地堆人堆工夫也只是无济于事。于是诞生了页面搭建平台。

这样一个平台波及到的技术点是 网状的:比方波及到开发工具链、数据结构设计、渲染器和交互设计、数据源导入、页面编译构建、页面生成、代码公布、流动公布、版本治理、在线经营治理、权限治理、可视化“所见即所得”实现、后端存储、CDN 同步、数据打点和统计、数据分析等。后续联合平台化能力,也会波及到组件市场的设计,甚至 serverless,no/low code 技术。

而作为垂直畛域一个不可漠视的方向——编辑器开发,技术难度只会更高:除了编辑器自身的各种性能实现外,还须要兼顾兼容性,更要适应业务需要。同时,编辑器就是生产工具,任何一个中后盾零碎仿佛都必不可少,需要市场上,不论是石墨文档、钉钉文档、头条飞书等都有着宽泛而强烈的需要。该畛域值得深耕而优良开发专家却百里挑一。

为了解决「可视化搭建零碎」,咱们尝试把一个上述「简单的业务平台」和「垂直畛域的富文本开发」这两大难题联合起来,打造一个功能强大的编辑器,同时实现页面搭建平台的工作——这听下来尽管是“难上加难”,但仿佛两大方向的交融是一种美好的思路和翻新。

具体来说,编辑器除了反对传统富文本性能以外,须要退出对业务性能区块的反对,这时候在数据结构上,选用 JSON base 的存储形式:传统富文本区块以 JSON 字段存储富文本内容,其它复合型自定义业务区块存储为 JSON 对象构造。在此基础上,咱们实现对该 JSON 对象构造的解析,实现编辑器内“所见即所得”。

这里独自说一下富文本之外的“复合型自定义业务区块”。咱们晓得最终搭建进去的页面将会充斥各种 Sku 商品、自定义组件、用户卡片等区块,最终这些内容的输入须要被 C 端渲染器所了解、所解析。

咱们来联合下图,进一步阐明:

区块 1 是传统富文本内容,区块 2 是一个复合型自定义业务区块——Sku 卡片,区块 3 是另一个复合型自定义业务区块——用户卡片。这样一来编辑器不再是一个繁多的富文本编辑器,而是最终输入内容为简单 JSON 类型的多功能编辑器。

不同业务场景、特点,须要齐全不同的前端解决方案,在开发这些垂直解决方案的时候,业务剖析、技术选型、架构设计、开发落地是十分难的。接下来,就让咱们一步步摸索,一步步实现一个基于并兼顾编辑器技术的多功能的页面搭建平台。

灵便弱小的 Markdown 编辑器和页面搭建翻新尝试

我置信现如今没有程序员不晓得 Markdown,它对程序员或者所有互联网从业人员来说都十分敌对。简略说,Markdown 是一种轻量级标记语言,它容许咱们应用易读易写的纯文本格式编写文档。现如今许多网站都宽泛应用 Markdown 来撰写帮忙文档或是用它来在社区上发表音讯。比方:GitHub、Wikipedia、简书、reddit 等。

除了易于编写,Markdown 的 可扩展性和可转换性 也是它收到追捧的重要起因。也正因为如此,咱们初期的经营流动页面搭建就是基于 Markdown 编辑器施行的。具体流程如图:

当然这只是一个十分粗略简易版的流程示意图,接下来我将分:

  • Markdown 扩大和自定义解析器
  • 欠缺应用体验,打造页面生成能力

两个方面进行具体解释。

Markdown 扩大和自定义解析器

Markdown 本来应用场景是面向文档和写作,它反对的标记和语法并不能满足所有场景需要。因而社区上存在不少 Markdown 解析器,其目标是对 Markdown 源内容进行解析和扩大。在泛滥解析器当中,最闻名的就是 marked.js 了。这里简略对 marked.js 这个库原理进行剖析,将会有助于了解后续咱们的实现计划。

说起解析,其实就是经典的“编译原理”套路。套用在 marked.js 上,如下图:

工作机制很简略,marked.js 承受输出源文本字符串后,创立词法解析器实例:

const lexer = new marked.Lexer()

词法解析器实例 lexer 的使命是将输出源进行分词,解析出 tokens:

const tokens = lexer.lex(content)

如何了解分词生成的 tokens 呢?其实 tokens 就是 AST 对象(或间接把它了解成 json 数据,它是树形构造,表白出 Markdown 中段落,块援用,列表,题目,规定和代码块等信息)。

接下来,marked.js 实例化一个解析器:

const parser = new marked.Parser()

该解析器 parser 接管 tokens,依据 tokens 生成 html 富文本:

const html = parser.parse(tokens)

当然,这只是很粗略的流程,但仔细的读者能够窥出端倪:如果想扩大 Markdown 语法:咱们能够批改 lexer 生成 tokens 的函数,目标是退出咱们的自定义 Markdown 语法解析成新类型 token 的能力;同时批改 parser 解析函数,依据新 token 类型,生成咱们预期后果。这里我不在深刻赘述这个过程,事实上,咱们采纳的计划也没有 fork 去批改 marked.js 代码,而是本人基于 marked.js,封装了更下层的解析器。

欠缺应用体验 打造页面生成能力

由上可知,咱们的页面搭建需要次要集中在插入各种组件卡片,插入带链接 banner 图片等复合型自定义业务区块。这每一个需要都应该对应一个 Markdown 的新语法规定。

比方,输出:

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

则示意页面中插入一个 id 为 12345 的 Sku 卡片。

如果让经营同学手动输出上述语法内容无疑是苦楚且不可承受的。因而咱们设计了 Markdown 编辑器的按钮:「增加 Sku Cell」,点击按钮之后,会弹出表单对话框,由经营输出 Sku 类型和 id,即可主动在 Markdown 编辑器中光标所在位置插入一行内容:

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

这样的设计不便经营应用和记忆。因而对于使用者来说,只须要理解根本的 Markdown 语法,而不须要再去记牢和手动输出新型语法。

为了满足“所见即所得”需要,咱们须要在经营键入内容时,同时进行对输出源的解析。解析的过程须要逐行进行:

  • 如果解析以后行内容合乎 Markdown 原始语法,则用 marked.js 进行解析,失去解析进去的富文本后果,推入后果数据栈(这里的数据栈是一个 result 数组)
  • 如果解析以后行内容合乎新扩大的 Markdown 语法,则应用本人的解析器函数(暂且命名为 feParse)对该行进行解析(解析器函数实现是一个繁难的编译分词过程
  • feParse 函数接管扩大新语法内容,对于不同表意形式应用不同的 helper 解决,比方解决 <SkuCell>live@12345@rondStyle</SkuCell> 将会被 skuCellHelper 函数解决
  • skuCellHelper 函数解析内容,剖析失去分词后果(标记为 formData):
type: 'live',
sku_id: 12345,
style: 'rondStyle'
  • 依据下面分词后果,申请后端接口,获取该 Sku 对应的数据,比方该 id 为 12345 的 live 数据(标记为 liveData):
author: 'live 作者名',
id: 12345,
created_date: '2019 10-12 20:34',
description: 'live 介绍',
duration: '20mins',
// ...
  • 依据以上两种数据:formData 和 liveData,利用 React 服务端渲染能力,取得该 Sku 组件对应的富文本 skuRichText:
const skuRichText = ReactDOMServer.renderToString(<SkuCell data={... formData, ... liveData} />)
  • 将 skuRichText 推入后果数据栈 result

最终咱们逐行解析的后果产出为:

result = [
    '第一行富文本内容','第二行 Sku 卡片对应的富文本内容',// ...
]

合并 result 内容,渲染出富文本,显示在页面右侧,实现所见即所得成果。

总结一下实现“所见即所得成果”的要点为:

  • 自定义 Markdown 语法解析器
  • 利用 React 服务端渲染能力失去非凡组件的富文本内容

须要指出的是,在理论施行当中:经营在编辑器中,保留并提交给后端的数据区别于上述 result,它也是一个数组:submitData,用来示意经营输出的内容。对于原始 Markdown 语法,咱们间接应用其对应的富文本内容;对于新的裁减语法,咱们并没有应用其对应的富文本内容,而是应用了上述 formData 的数据结构,最终提交相似内容:

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

这样的思考是为了 C 端用户在申请页面时,可能取得最新的实时 Sku 数据。如何了解实时 Sku 数据呢?在经营编辑页面时,假如插入一条 Sku 的题目信息为“题目一”。再一天后,该 Sku 的题目信息变成了“题目二”。如果咱们保留并应用了经营编辑时应用的富文本信息,那么 C 端页面肯定是“题目一”,而不是最新的“题目二”。因而咱们只提交该 Sku 的 id。当有 C 端用户申请页面时,由后端通过 RPC/Http 调用,获取最新的数据,并由组件在服务端渲染出内容,最终返回给前端。

整个流程如下:

到此为止,咱们实现了一款基于 Markdown,利用 Markdown 语法灵活性,扩大而成的编辑器。这个编辑器中内置了诸如「插入 Sku 卡片」、「插入 Banner 图」等一系列的业务性能。

基于这套思维,咱们实现了帮忙经营疾速搭建流动页面的复合型编辑器和页面生成器,它的长处非常明显:

  • 输出即所见,所见即所得
  • 反对灵便扩大,能够基于解析器反对所有类型的语法和任意组件
  • 经营只须要相熟根本的 Markdown 语法即可,扩大语法由点按按钮实现

最终效果图:

技术计划都是在一直演变推动当中倒退并欠缺的。在该平台运行半年多之后,咱们大胆进行了翻新优化,并最终用更高效的计划实现了全面替换。感兴趣的读者请持续浏览。

不止是富文本编辑器

下面咱们提到了已有复合型编辑器即页面生成器的长处,通过半年多的线上服务后,咱们再去深入分析一下它的毛病:

  • 编辑器内 Markdown 语法内容,对于经营依然较为艰涩难懂
  • 经营还是须要肯定的学习和应用老本
  • 依赖实时解析和渲染的“所见即所得”
  • 对于每一种新的组件,都要创立一种新的 Markdown 语法

这些毛病很好了解,这里着重讲一下“所见即所得”。下面咱们提到“所见即所得”,理论依赖了实时解析内容源为全量富文本,并实时渲染富文本的能力。尽管满足了需要,然而这样的做法性能老本较高,即使加上罕用的“防抖和截流”伎俩,对于浏览器的压力依然不小。能不能像“积木零碎”、“拖拽搭建页面零碎”一样,间接在“画布”上批改,做到更加实在的“所见即所得”呢?

“拖拽零碎”优缺点显明。
首先,以大量 H5 生成工具为代表的拖拽零碎尽管看上去功能强大,然而实质上却是依附组件的沉积和无穷尽的配置扩大,最终产出的数据状态和性能横蛮成长上来,比拟容易呈现“失控”的场面,而逐步被边缘化。
这里的失控既指经营侧、产品设计侧没有对立束缚,也蕴含了代码收缩后的保护角度的失控。另一方面,从最终后果上看,拖拽零碎将页面的拼接转嫁到经营身上,这些“搬砖”的工作量对于经营其实也并不算小,同时它短少“规范化”的强制束缚,不利于视觉设计的对立,经营同学“自我施展”反倒不肯定齐全是坏事。退一步来说,社区上曾经存在不少可用的拖拽零碎,反复造轮子也毫无意义。

联合咱们的需要特点:页面区块和设计款式固定、组件状态固定、页面排版固定、重文字和图片内容、页面交互并不简单,咱们认为,多功能富文本编辑器将会是一个值得深刻试水的方向。

传统的富文本编辑器就是一个弱小的“超级文字加工厂”,相似咱们罕用的 word,经营能够在其上“肆意挥洒”。如何在富文本编辑器上,退出设计规范,并实现业务组件增加呢?

首先,富文本编辑器是前端一个十分值得深入研究的重要方向,社区上各类开源富文本编辑器也不在少数,然而从工夫和开发成本的角度来看,咱们既不想从新实现一个融入了本人业务的增强型富文本编辑器;又不想做各种魔改已有计划。

无奈找到一个适合的解决方案,还是让咱们先从需要角度剖析:

  • 新型多功能富文本编辑器,须要反对历史上的 Markdown 语法数据,否则会呈现历史数据不兼容的线上问题
  • 新型多功能富文本编辑器,不仅为页面生成器服务,也要可能反对多类型横向业务以及纯富文本编辑器业务
  • 新型多功能富文本编辑器,要反对所有富文本的个性,包含复制粘贴内容等
  • 新型多功能富文本编辑器,要反对插入自定义组件和区块,比方 Sku 卡片等
  • 新型多功能富文本编辑器,应该插件化,可插拔
  • 新型多功能富文本编辑器,要做到齐全的所见即所得
  • 新型多功能富文本编辑器,要反对模版模式疾速搭建页面
  • 新型多功能富文本编辑器,要接入格局主动标准机制,主动实现标点挤压、对立排版等性能

综上需要和设计方案,咱们选用了 Draft.js 作为这套多功能编辑器的底层框架,一句话足以总结做出该抉择的起因:Draft.js 实际上并不是一个富文本编辑器,它其实是一个用于构建富文本内容和富文本编辑器的基础设施。做个比喻:如果把富文本内容比作一幅画,Draft.js 只提供了画纸和画笔,至于怎么画,开发者享有很大的自在 ——(出自文章:Draft.js 在知乎的实际)。

这正合乎咱们的须要:咱们不要一个残缺的解决方案,而须要一个舞台。至于如何解析内容,如何渲染内容,如何生成数据,应该全副由开发者把控。事实证明,这样的翻新设计对于页面搭建生成器以及传统编辑业务场景十分贴合,咱们最终实现了目前服务于后盾零碎的弱小多功能编辑器 —— Versatile Editor。

Versatile 译为“多才多艺的;有多种技能的;多面手的;多用途的,多功能的”。目前 Versatile Editor 曾经全面接管了所有后盾零碎编辑需要。它的技术设计和体系也十分清晰。上面咱们次要从

  • 数据结构设计
  • 插件体系设计
  • 多数据源反对
  • 应用体验设计
  • 页面模版反对
  • 其余细节

六个方面进行剖析。

别具匠心的数据结构

数据结构的设计思维是:应用后果数据栈(数组)存储每一个 Draft.js 编辑器块级内容,数据每一项都程序对应每一个块元素。这些块元素分为两大类:纯富文本内容和纯自定义组件内容。对于纯富文本内容,咱们从新实现了将 Draft.js 的不可变数据结构解析转换为富文本的工具函数 draftToHtml;对于纯自定义组件,咱们只提取出组件最小还原数据(比方 Sku Cell 组件的 sku id 等信息)。

经营在编辑器侧提交流程如下图:

具体阐明一下图中的外围 contentState。contentState 是 ContentState 类型的对象,它规定了如何存储具体的富文本内容,包含文字、块级元素、行内款式、元数据等。

这里须要留神的一点是:在输入数据上,咱们至多提交两种数据给后端存储:

  • rawContent
  • renderTreeData

其中 rawContent 是依据不可变数据 contentState 进行序列化后的后果,rawContent 能够通过数据表示出以后编辑器内所有内容。咱们提交 rawContent 的目标是用于编辑还原。当经营再次关上编辑器时,编辑器能够依据 rawContent 迅速渲染出上一次提交的所有内容,以供编辑。

而 renderTreeData 是通过计算并解决后提交的数据,它的目标是存储到数据库中,用于后端返回给 C 端页面,C 端页面最终依据 renderTreeData 由渲染器渲染出残缺的流动经营页面。由上图可知,renderTreeData 的生成,咱们开发了 RenderTreeGenerator 的实例上 generate 办法:

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

如图:

RenderTreeGenerator 承受 Draft.js 的不可变数据类型 contentState 作为第一个参数,自定义配置项作为第二个参数,React 组件汇合 this.customBlockModules 作为第三个参数。this.customBlockModules 是一个数组,蕴含了所有自定义区块 React 组件名,在自定义区块类型命中该数组时,须要启动自定义区块,并生成结构化数据。

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 出渲染数据以及以后 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()
      }
    }

    // 其余丑化或清理工作,比方间断富文本区块的合并

    return this.finalOutput
}

这里不同于后期 Markdown 编辑器的关键点次要有两处:

  • 咱们监听编辑器区块的 onBlur 事件,在此事件触发时,开始生成后果数据
  • “所见即所得”——不再须要在手动实时解析渲染实现。因为 Draft.js 是一个基于 React 的编辑器,咱们能够间接在编辑器中渲染出一个 React 组件

如下图:

以上两个特色也正是基于 Draft.js 的多功能编辑器优于 Markdown 编辑器的关键点。

可插拔、可移植的插件化和组件化设计

多功能编辑器的多功能不是说说而已,为了反对海量性能需要,且思考到不便第三方性能扩大,咱们设计了良好的编辑器插件体系。目前我的项目中应用了 11 个插件,它们涵盖了:插入代码、插入公式、插入链接、插入援用、插入视频、复制粘贴还原内容、插入图片、插入重点款式、插入注解等。我的项目还积淀进去海量业务组件,包含:页面喵点组件、Banner 图组件、Sku 卡片组件、各类按钮组件、滚动列表组件、图片画廊组件等。所有的组件和插件原则上都是能够面向社区、面向第三方应用的,同时后续打算只须要一个 NPM 包即可接入一个新的性能或新的自定义组件类型。** 这也为后续的组件市场设计、no/low code 设计打下了根底。

在编辑器初始化时,咱们注册并实例化各种插件以及自定义组件。因为咱们多功能编辑器的理念就包含了结构化和数据化,所有的这些插件和组件都能够依赖 decorator 进行解析,这也就意味着:从另外一处编辑器实例中复制任何内容(包含自定义组件)到以后编辑器,都能够间接还原数据,无缝完满反对组件的复制粘贴性能。

多数据源反对

任何一项技术创新和更迭,都要思考历史包袱和历史债权的解决。多功能编辑器也不例外,后面提到,历史编辑内容是应用 Markdown 格局的。以经营页面生成器场景为例,历史流动页面 A 对应的后端存储数据是 Markdown 字符串。咱们在应用新的多功能编辑器替换旧的 Markdown 编辑器后,如果经营同学想再次编辑流动页面 A,新的多功能编辑器上天然就要兼容历史内容。

为此咱们的计划是:在编辑器中接管到数据源后,如果嗅探为历史 Markdown 格局,那么先利用 marked.js 将此 Markdown 格局内容转换为富文本内容,再依据富文本内容转换为 Draft.js 反对的不可变数据结构。

总结一下,对于编辑器初始化时的数据源(rawContent)解决流程如下图:

对于编辑器获取的数据 rawContent,咱们应用 isDraftJson 工具函数判断该 rawContent 是否能够被多功能编辑器以 Draft.js 反对的数据解析:如果能够,则证实 rawContent 为由新的多功能编辑器提交的数据,能够间接应用并复原出编辑器内容。如果 isDraftJson(rawContent) 判断为 false,那么就示意无奈被 Draft.js 解析,须要兼容历史 Markdown 语法,由 marked.js 解析出富文本后再交给 Draft.js 解决,由富文本生成 Draft.js 的不可变数据;如果解析都失败,则间接将 rawContent 视为 textarea 内容,间接填入到编辑器当中。

图中并未画出如果 rawContent 为空(或不存在)时的解决形式。实际上,如果 rawContent 为空,咱们应用 ContentState.createFromText(”) 办法生成一个初始化为空内容的不可变数据。

理论过程因为历史包袱起因,对于多数据源的反对实现更为简单,这过于非凡,咱们不再开展。

继续打磨应用体验

编辑器一个十分重要的话题就是体验。置信很多人都经验过编辑器的体验之殇:“输出卡顿、诡异的光标地位”等,但这里我认为没有必要剖析传统编辑器的体验优化话题,更有意义的是从咱们特有的多功能编辑器特点动手,聊一聊用户体验。

举一个例子:依照 Draft.js 的设计,每一个区块之间高低都会有个空行。如图:

这样会导致提交编辑器内容时,生成的自定义区块数据前后会蕴含了两个空区块数据,最终导致渲染出的页面也会蕴含两个空白行,间接影响页面设计成果。社区上对于这个设计的 issue 探讨不少,比方 Empty line on adding atomic block。

事实上,这是为了灵便地在自定义区块前后增加或删除内容。构想,如果咱们间断增加了三个自定义区块——Sku 卡片 A,Sku 卡片 B,Sku 卡片 C。如果 A,B,C 之间没有空行,那么咱们如何在卡片 A 和卡片 B 之间插入一个新的卡片 D 呢?如果 ABC 卡片彼此之间放弃一个空行,那么使用者能够用光标定位到 AB 之间的空行,再插入卡片 D。这就是自定义区块前后主动存在空行的意义。

有的开发者可能会想:咱们能够放弃这个空行的存在,在最终生成的数据时,主动将空行删除不就能够了吗?事实上,拿到 Draft.js 编辑器的数据时,咱们无奈判断是用户自主回车创立的预期中的空行,还是自定义区块自带的前后空行,因而无奈间接在后果数据上粗犷地移除空行。

为了达到更好的应用体验:咱们开发的 FocusPlugin 插件,优雅地解决了问题:仍然是每一个自定义区块前后不保留空行,然而利用 FocusPlugin 插件,使得每一个自定义区块都能够被点击选中,或者用键盘高低键遍历选中,选中之后能够间接摁下回车键,增加空行,甚至能够摁下 delete 键,删除该区块。如图:当自定义区块被选中时:

最终这套基于 FocusPlugin 插件的计划使得交互更加顺畅天然,达到了更好的成果。基于此,咱们能够十分顺利地实现自定义区块的更改:比方以后选中区块为一个 id 是 1234 的 Sku 卡片,如果经营须要替换为 id 是 5678 的 Sku 卡片,只须要抉择以后区块,选中之后在右侧呈现的编辑区中更改 id 内容,确定后即实现替换,如图所示:

基于 FocusPlugin 插件,以批改以后 Sku 卡片 id 为例,id 进行批改后,发送获取新的 id 的数据,并在数据胜利获取后调用 modifyAtomicBlock(entityKey, data) 办法,触发 replaceEntityData(editorState, entityKey, data) 办法进行编辑器不可变数据的更新,并由 handleEditorStateChange 办法一并更新状态,最终反馈在编辑器视图中。

这一编辑产生过程总结图为:

应用体验的确不是欲速不达的的事件,这是一个须要继续迭代优化的过程。通过一直地打磨,Versatile Editor 最终趋于稳定。目前 Versatile Editor 曾经反对了数百量级的页面搭建,以知乎投放的页面为例,包含但不限于:

  • 高考直击

)

  • 抗击疫情——致敬奔赴一线的逆行者
  • 爱要「盐」选
  • 朗朗流动页面——每个爱音乐的孩子必听的巨匠课

等高流量内容。

页面模版反对

Daft.js 编辑器内容是齐全基于数据状态的,它应用了不可变数据库进行数据的更新操作,秉承纯函数式更新,因此人造对于“时光旅行(Undo/Redo)”的个性可能良好反对。另一方面,所有皆数据也让咱们实现“页面模版”性能非常简单而奇妙。

咱们能够将所有模版拆分为几个大的自定义区块,并创立这个流动模版所对应的数据:比方对于模版 A:头部为一个头图 Banner,咱们能够编辑器中创立一个由占位图示意的 Banner 图片;第二区块为电子书榜单 Top10,即可在编辑器中创立一个 Ranking 组件,并由任意占位 10 个电子书数据填充,以此类推。提交数据之后,即可取得形容这个页面模版的数据。

当经营在创立页面,并抉择应用「排行榜模版 A」时,咱们就用曾经提前预制的数据作为 rawContent 进行编辑器初始化。失去模版后,经营即可增加批改,疾速实现模版页面创立。

整体流程如下:

其余细节

到此为止,咱们介绍了社区计划和咱们本人继续迭代的计划。其中还有一些小的细节在这里简要带过,次要包含:预览、排版、安全性、配置零碎几个方面阐明。

“所见即所得”使得经营编辑流动效率大幅提高,然而在编辑器提交公布和推广之前,还是须要一个残缺的可预览页面地址供进一步回归。因为这些推广页面都是面向挪动端,因而咱们在这个多功能编辑器兼页面生成器的产品设计上,预留有页面公布地址和二维码生成性能,进一步优化经营应用体验。如图:

另一方面,咱们对于页面文字的编审有着严格的要求,比方:不能应用中文引号,须要应用「」;英文和数字与其余汉字之间须要预留一个空格;甚至标点的地位也有严格标准,须要实现传统相似“标点悬挂、标点挤眼”等一系列排版需要。因而,该多功能编辑器兼页面生成器配置了可插拔的主动排版能力,次要实现主动排版标准的审校和修改,如图:

一个页面往往无奈只由编辑器生成,可能还包含配置内容。这些配置需要咱们用进入编辑器之前的表单来承载,表单填写结束,生成根底配置数据后,再进入编辑器进行创作。表单是页面中数据交互的根本模式,对于非开发人员应用也没有应用门槛,然而切记不可将表单设计的过于简单。同时要留神,编辑零碎和配置零碎须要解偶的准则。

后面提到编辑器就是生产工具,编辑器的效力就意味着生成效率。一旦编辑器呈现线上问题,那么就会间接影响失常的生产流动。因而,为了保障编辑器的安全性和健壮性,咱们退出了测试环节。次要包含:单元测试,UI 测试。单元测试次要验证要害函数和办法的正确性,比方下面提到的 autoFormat 办法,各种插件的输出和输入正确性校验,数据批改的工具办法校验等;UI 测试次要依附 Enzyme,来保障要害交互的失常运行。

最初,其余波及点比方:一键换肤、字数统计等因为篇幅起因,这里都不在详述。

富文本编辑器是一个深坑,Draft.js 尽管背靠 Facebook 团队,但也始终在深坑中挣扎,咱们此间开发过程的确是一部血泪史,但咱们团队也在此方向积攒了丰盛的教训,后续技术细节也会一一进行分享,请继续关注订阅。

总结

我始终在思考,什么样的文章可能给读者带来真正的思考和启迪。一方面入木三分解说语言个性和设计,深刻技术细节,庖丁解牛般的剖析是咱们所须要的,这类文章须要靠代码谈话;另一方面,总结梳理技术趋势,从更高的角度叙述计划的落地和演进,更是对大局观和格局的造就,这对于团队的技术布局和舵向同样至关重要。

这篇文章浅显总结了业界在「可视化页面搭建」技术摸索的方方面面,并整顿了各种相干技术博客和剖析文章。咱们还介绍了编辑器技术和编辑器技术所能给「可视化页面搭建」带来的破局和翻新。在此基础上,咱们更是 从一个自研的公司级「可视化页面搭建零碎」动手,从摸索阶段到成熟阶段的演进历史进行了介绍。

事实上,「可视化页面搭建零碎」的话题还远为完结:咱们正在此方向上摸索更多可能,「微组件 / 微前端」,「页面归因能力」、「no/low code 技术」、「自定义组件埋点以及 A/B 流量能力」、「运行时的组件构建和渲染计划」,甚至「Serveless」、「云端 IDE」等。后续咱们将会持续产出相干文章,请读者继续关注:技术博客,咱们也在 宽泛求贤。

回到文章开篇所提到的那个问题上:“前端开发的难点到底在什么中央?”,我想已有答案的开发者将继续优化答案,依然未知的开发者很快将会找到本人的答案。

Happy coding!

退出移动版