组件:编辑世界的火种(Allspark)

1,998 阅读8分钟

console.info

我是小斑,一个富文本编辑器,今天聊聊咱这编辑器的基本组成:组件!在我的世界里:Everything is a Component. 大到一篇文章,小到一个字符,都是一个组件。


思考

3 个月前,阿飞也就是我的创造者,写下第一行代码前,思考过这样一个问题:如何描述一篇文章?或者说:是什么组成了一篇文章?对于这个问题,大家都有自己的答案:文字、段落、图片、标题、表格、列表等等。但软件开发需要严谨的逻辑,继续往下思考,就会引出一些问题:

  1. 文字是段落、标题的组成,而不是文章的组成;
  2. 列表、表格确实是文章的组成,但组成它们的又是什么?段落?标题?那列表嵌套列表该如何表示?
  3. 关于图片:表情包之类和文字并排放置的图片,和占用一阵行的图片,明显不是同一类;
  4. 标题和段落看似两种类型,但除了基础样式不同外,它们行为却又是相同的;
  5. ···

在软件开发上,有一准则:

任何一个复杂的问题,都可以拆解为一个个小而简单的组成。

因此,在写下小斑的第一行代码前,阿飞带着这些问题,经历整理归类,相似内容抽象提取后,最终得出以下类型关系图,并由此得出结论:文章也是一个组件,由内容块(Block)组成。

image

虽说图片中有如此多的组件,按是否抽象,可分为两个阵营,抽象组件与具象组件。

那就先聊聊抽象组件。


抽象组件

这么多的组件中,近一半是抽象组件,由于具象组件是抽象组件的具象化呈现,充分了解抽象组件后,具象组件的含义就可以轻松理解了。

Component

Component 组件是所有组件的基石,就像变形金刚里的火种,是所有组件最基本的构成,Component 赋予了组件以下能力:

  1. 管理组件的样式信息;
  2. 管理组件内容变更时的历史栈;

同时,作为所有组件的基类,Component 还规定了组件必须实现的方法,或是必须明确的属性:

  1. 确定组件类型,type 属性;
  2. 实现 render 方法,规定组件该如何渲染自己;

具体实现:Component

Inline

Inline 代表一个行内的块,是光标可以操作的最小部分:字符、表情图片、公式(实现中)都派生于 Inline 类。

Inline 类管理了行内块与内容块(Block)之间的联系。

具体实现:Inline

Block

Block 代表内容块,是组成集合的最小单位。一篇文章就是一个 Block 的集合,列表、表格也是,同时列表、表格又是 Block 的派生类,那么列表嵌套列表这种结构就能轻松的表示了。

那内容块(Block)需要实现哪些基础操作呢?

  1. 管理父组件信息;
  2. 实现不同内容块(Block)之间的互相转换;
  3. 与父组件之间的互动:增删改查等行为;
  4. 与兄弟组件的互动,比如:将自己合并到前一组件(在组件最前端触发删除),或接收后一组件;

依据分工的不同,Block 组件又可以派生出 3 类组件:PlainTextCollectionMedia

具体实现:Block

PlainText

PlainText 为纯文本组件,其内容为纯字符,表现与代码编辑器一致,派生出 Code 组件。

具体实现:PlainText

Media

Media 为多媒体组件,具象组件,可生成图片、视频、音频的内容块,实现了 Block 规定的所有方法。

具体实现:Media

Collection

Collection 代表集合,为一系列组件的容器,控制其子组件的呈现效果。

容器组件的主要工作就是对子组件的增删改查。

依据子组件的类型,集合组件可以拆分为 Inline 的集合(ContentCollection),与 Block 的集合(StructureCollection)。

具体实现:Collection

ContentCollection

ContentCollectionInline 组件的集合,包含一连串文字、表情图片、行内公式。

根据使用场景的不同,派生出标题、段落、表格项组件。

具体实现:ContentCollection

StructureCollection

StructureCollectionBlock 组件的集合,通过 StructureCollectionBlock 的配合、嵌套,就可以完整的表现出一篇文章、列表等。

根据使用的场景,派生出列表、表格、文章等组件。

具体实现:StructureCollection


具象组件

经过一步步的抽象,抽象组件派生出的具象组件已不需编写太多的代码去实现相应的功能,但却有一个最重要的方法,必须得自己实现:render

render 函数在 Component 组件下定义,是组件对外呈现的途径。

那如何进行 render 呢?简单的生成 Html ?小斑的目标可是生成任意环境下的文章,包括但不限于 MarkdownHtml , 但 render 方法只有一个,如何生成多变的内容呢?

内容生成器,就该登场啦!


内容生成器

道家哲学有一句话说的好:以不变应万变!

对应到小斑的世界里,不变的是文章的结构,变的是生成的内容,那如何以不变的内容,去生成不同的内容呢?

既然组件对渲染不同的结果无能为力,那何不把渲染这个任务外包给专业的团队呢?

查看以下代码:

class XXX extends Component {
  render() {
    return getContentBuilder().buildArticle(
      this.id,
      this.conent, // 代表组件的内容
      this.decorate.getStyle(), // 组件的样式信息
      this.decorate.getData() // 组件所携带的信息
    );
  }
}

通过 getContentBuilder 获取生成器,告诉生成器来个 Article ,然后把自己所持有的属性,内容一通扔给生成器,生成什么我不管,大手一挥,躺下喝茶!

**ps:**为什么不把组件给扔给生成器呢?大家都知道 JS 是一门高度动态的语言,只要获取了原对象,就可以对这个对象胡作非为,为了保证组件不被修改,为了维护爱和正义,把生成组件所必须的内容交给生成器就可以啦!天下太平 ~

这里大家可能会问:为什么要通过 getContentBuilder 方法获取生成器,生成器作为参数直接传入不可以吗?

可以当然是可以的,但试想,如果我需要渲染一篇文章,实际调用 render 的是 Article 组件,其他组件通过层层递归的方式调用 render 函数,当然将生成器依次传入每个组件中可以解决这个问题,但是耦合性太高,不利于开发与维护,因此为了将组件的内部逻辑与渲染行为彻底的封开,把获取生成器这个动作再次外包出去,实在是香的不行!

因此这里其实有两个外包(代理)动作:

  1. 将组件的渲染动作外包给生成器,也就是 buildArticle 的部分;
  2. 将获取生成器的动作外包给一个叫 getContentBuilder 方法;

既然这里用了双重外包,这么复杂的概念,好处在哪呢?且听我细细道来:

  1. 将组件的内部逻辑与渲染严格分开,文章的结构归结构,呈现归呈现,如果渲染有问题,那直接去生成器中找问题即可;
  2. 相同的代码,却能产生不同的结果,因为 getContentBuilder 是一个方法,它可以返回不同的生成器;
  3. 模式固定,代码简洁,且高度解耦。
  4. 生成器也是一个类,开发可以通过类的继承,重载等方式,修改组件的呈现,而无需关注组件的内部逻辑;

几个已经实现的生成器:

  1. ContentBuilder :用于生成可编辑的 Html 结构,该类为编辑器的核心类,是 Html 可编辑的核心;
  2. HtmlBuilder :用于生成静态 Html 文本,生成的文本不可编辑,纯文本的 Html
  3. MarkdownBuilder :用于生成静态 Markdown 文本;
  4. BaseBuilder :生成器抽象类,任一生成器必须继承并实现该类下所有的方法;

最后

关于组件的部分,大致就是这样,太细反而会照成理解上的困难,今天就到这儿啦。组件到底是如何组成,如何渲染,其实并不是最重要,因为阿飞已经都弄好啦,最重要的是在文中提到关于软件开发的两点,:

  1. 任何一个复杂的问题,都可以拆解为一个个小而简单的组成;
  2. 如何以不变应万变最佳?代理永远是最佳的答案!

小斑的强大也正是因巧妙的使用这两点哦!相信大家如果能彻底 get 到这两点的精髓,处理日常问题也会更得心应手哦 ~

好啦,小斑课堂到这结束,希望大家多多使用斑码编辑器哦!爱你们哦~

我是小斑,我为自己带盐!