Tychlog

后MVC时代的前端架构

component

很多人觉得,前后端的差异主要是分别承载了数据和样式,功能和皮肤。前端就是视觉方面的,后端是实质性的。追溯到很多年前,确实是这样的,所谓的前端只是由于后端MVC中的View过于复杂,为了提升用户体验,提高加载速度,以及降低服务器压力,所衍生出的一些优化技术。

前端框架演进

最初前端没有架构,也不需要。但随着UI交互的复杂度激增,我们发现API提供的数据仍然需要进行处理,再进行渲染,而分离这些需要处理的数据和视觉渲染部分后又需要一层进行控制,自然而然,将后端的MVC照搬了过来。但其实人们并没有发现,并不是我们把MVC搬到了前端,而是把后端的V搬到前端之后又分成了MVC,这个问题暂且搁下,后面会解释。

接着前端又再一次的变得复杂,尤其是不同交互对于同一资源的操作,导致过程化的控制器过于臃肿,而不堪重负,MVVM应运而生。人们都以为,MVVM就是把C变成了ViewModel,但是ViewModel是一个实体,如何控制呢?真正用会MVVM的开发者都会发现,MVVM提倡的是定义而不是控制,定义M和V的关系,什么样的Model应该呈现什么样的View,然后一切自然而然的随着用户的行为去改变。

其实原先的C被更高度的抽象了,变成了框架的一部分,读取M和V的关系,并监听他们,在一方改变的时候根据关系修改另一方,达到双向绑定。而ViewModel其实是为了描述这一关系而抽象出来的Model,因为它是相对于Model更偏向View的,所以叫ViewModel。接着出现的一系列MVWhatever很好的说明了ViewModel并不是C变的,人们以为既然C可以变成VM,那也可以变成别的。殊不知,ViewModel也是一种Model,它是由View分裂出来的,而View只能分裂出Model和View,不会出现别的。

样式和数据的区别

React横空出世,却没有人能说清楚它到底是MV什么,甚至许多人搞不清M在哪,最后干脆说React就是个View。那么React到底是什么架构呢?先别急,我想先讲一个故事:

有一个语言学者提出一个观点 - 法国人的数学很烂,为什么呢?因为法语中的quatre-vingt(80)的意思是 4个20,居然都不会把4乘以20计算出来,是不是数学很差?

第一次听到这个故事,我感到很好笑,难道汉语就不是这样了吗?我们叫做“八十”,其实还是8个10,和4个20有什么区别,还不是都没有计算。后来再一想,不仅不是,而且法国人数学应该很好,因为汉语遵循了现代通用的10进制,而法语是20进制和10进制混合的,所以他们大脑可以自然的映射10进制和20进制,就好比是左撇子为了迎合右撇子的世界而变得思维敏捷一样的道理。

讲到这里,可能你会觉得莫名其妙,这跟前端有什么关系。其实我只是想说明一个道理,我们经常会被一瞬间的思考误导,比如这个故事中,人们会把习惯的“八十”看做是一个独立的东西,而嘲笑法国人他们的“quatre-vingt”,其实我们只是习惯了这种计算,或者说在大脑中建立了映射,就忽略了计算,而将其看做一个整体。由此说明,我们在看到一个信息的时候是会进行无意识计算的,从而你会认为你看到的就是那个东西本身,并不需要计算。我们认为“八十”是一个对象,而不是表达式,但事实是没有符号可以表达80这个数,所以用‘八个十’的表达式来表示,只不过我们太熟练所以自动计算了。

抛弃固有思维,现在假设@为81进制数的最大数,既80,再假设有一种操作符可以把十进制变成81进制数,这一过程是不是数据的计算?但如果有一种字体格式可以把连在一起的8和0两个数字变成@的样子,这算不算样式的改变?也许你会觉得通过操作符是在计算机中当做了@,而字体只是长得像,但计算机真正认识的只有可能是二进制binary,所以整个过程是不是也可以看做都是样式?还有人会说,数据无论变成什么样式它本身都不会变,而样式会变。但如果显示器有色差呢,你看到的蓝色就真的是蓝色吗?样式没变,你看到的仍然会变。

其实数据和样式本质没有区别,区别只在于程度,从计算机到人类的理解程度。差异来自于我们自己,我们把难以理解的叫数据,容易理解的叫样式,计算时间长的叫数据,计算时间短的叫样式。一个矩形你知道是样式,一个长xx,宽xx,边框为xx的东西你会以为是数据,但对于程序来说其实是一样的。处理一下,得到另一种形式的等同的东西,这就是一个不断翻译的过程,从二进制翻译到人类语言,甚至到非语言的一种印象,比如视觉,语音,甚至意识。无论是什么,都是让人类更容易理解。

Component架构

耗费了这么大的篇幅说明样式和数据没有区别的目的其实是为了解释React的架构。前面说到的MVC和MVVM其实都是对于View的一种演进,因为所谓View才是最复杂的,为什么说所谓呢,基于前面的结论,View就是Model,但它是人们难以理解的部分,所以没有被抽象为Model,而是直接显示出来让用户自己去读。为了便于理解,想象一种极端的情况,Model只有二进制形式,直接显示给了用户,理论上来说,用户是可以看懂的,只是难度高了一点点。而从View抽象成Model的过程,其实就是程序将二进制翻译出来的过程。而框架的所做的改进就是翻译程度的提升,让用户更容易的读取。

回到React身上,它的架构就十分清晰了,前面的MVC是把后端的View分离出了一部分Model,而MVVM,是把MVC的V又分离了一部分Model出来。React所做的犹如它的版本号一般,直接起飞,每个Component其实都是View分离出来的Model,理论上来说,你能抽象出无限层Component,这个极限上,View已经简单的没有意义了。而实际来说,你可以视项目情况而定,把View抽象到某个程度后扔给用户自己阅读。而由于分成无数层,M到V的过程也变得简单,不再需要控制,因为复杂的计算已经被分解成了极简单的计算分散到每一层中了,甚至有时候仅仅是Component为传入的props加上一些字面量,然后传入另一个Component,或者是分解或组合成Object再向下传递。一旦真的理解了View即是Model的思想,你就会发现,React似乎什么也没做,其实却把什么都做了,而且非常简单。

但Component这种架构也有其问题所在,那就是太过于松散,对架构设计的要求比较高。一旦你并非基于由机器到人类的理解程度来抽象分层你的Component,其复用性和扩展性就会大大降低。

前端面向对象

前面说到了View和Model没有本质上的区别,那么前端架构和后端架构为什么会有区别呢?原因很简单,后端可以把未翻译完的数据丢给前端,但前端不能随随便便丢给用户,所以前端变成了多层MV,而不是后端简单的分为了一层MV。前端面向对象的设计也更加的困难,尤其是拥有多年后端开发经验的开发人员,更容易误导自己,因为在后端翻译完成的东西,在前端就变成了最原始的东西。

举个例子,就拿最常见的电商说事吧,设计一个商品页面的Component架构。在拥有所谓数据的时候,它是某一个商品的页面,比如一件印着国旗的T恤。但显然我们的代码库在运行之前是没有这个从数据库传过来的数据的,所以我们没法把它抽象成一个叫做NationalFlagTShirtPage的Component的。退而求其次,在失去后端数据之后,它应该是一个衣服类的商品页面,比如有一些尺码对照和试衣功能,所以可以有一个叫做ClothesShowcase的Component。说到后端数据的这一层,并不是为了搞笑,它反映出了一个问题。其实后端对于前端,就是上一层的Component。同理,在下一层的Component中,我们也应该忽视这一层传入到下一层的数据,因为它不应该有这个数据。

比如ClothesPage还应该包含一个显示图片的区域,和一个显示尺码信息的区域。这时,很多人会在ClothesShowcase的里面再放一个ClothesImage和ClothesInformation,但是在上一层作为代码一部分的’Clothes’,在这一层应该已经被忽略,我们应该放入ProductImage和SizeInformation。只有当ClothesPage调用SizeInformation并传入’S’,’M’,’L’之类衣服尺码作为参数的时候,它才是衣服尺寸,而它自身应该仅仅是尺寸信息的Component。我们会发现,上一层的代码(字面量’L’等),变成了下一层的数据,如此一直下去,所有的特性都会变成数据,而代码,可能仅仅是一些最基础的元素,比如按钮,方框之类的,甚至是HTML本身。

如果一直使用对于后端来说的数据层面的’clothes’的话,就只能一次把ClothesPage这个Component写好,而没法继续抽象了。而其子Component可能仅仅是由于过于臃肿而强行分割的patial了。这样做除了代码短一些之外,完全不具有复用性和扩展性,因为ClothesPage下的所有Component都只能为Clothes服务了。而其他每个概念都必须把这一切重复一遍。

如果走入另一种极端呢,抽象出一种万用Component,只要传入一个大而全的object,就可以渲染出任意的页面。这个时候虽然复用性有了,但是你会发现你什么也没做,因为这个Component就是HTML的另一层封装,那些传入的参数包含了所有数据,你需要把这些不同的数据同化。

归根结底,Component架构的精髓在于多层,按照人类理解程度分层。否则永远无法分清楚什么是数据,什么是样式,因为它们只会在某一层中有划分。混乱的分层只会导致架构回归到传统的MVC两层结构中去。也因此,前端面向对象必须基于一层,脱离了某一层而论对象或类,都是可笑的,一个类,到了下一层可能就是一个实例。