基本思路
1. 为什么要做组件化?
无论前端也好,后端也好,都是整个软件体系的一部分。软件产品也是产品,它的研发过程也必然是有其目的。绝大多数软件产品是追逐利润的,在产品目标确定的情况下,成本有两个途径来优化:减少部署成本,提高开发效率。
减少部署成本的方面,业界研究得非常多,比如近几年很流行的“去IOE”,就是很典型的,从一些费用较高的高性能产品迁移到开源的易替换的产品集群,又比如使用Linux + Mono来部署.net应用,避开Windows Server的费用。
提高开发效率这方面,业界研究得更多,主要途径有两点:加快开发速度,减少变更代价。怎样才能加快开发速度呢?如果我们的开发不是重新造轮子,而是每一次做新产品都可以利用已有的东西,那就会好很多。怎样才能减少变更代价呢?如果我们能够理清模块之间的关系,合理分层,每次变更只需要修改其中某个部分,甚至不需要修改代码,仅仅是改变配置就可以,那就更好了。
我们先不看软件行业,来看一下制造行业,比如汽车制造业,他们是怎么造汽车的呢?造汽车之前,先设计,把整个汽车分解为不同部件,比如轮子,引擎,车门,座椅等等,分别生产,最后再组装,所以它的制造过程可以较快。如果一辆汽车轮胎被扎破了,需要送去维修,维修的人也没有在每个地方都修一下,而是只把轮胎拆下来修修就好了,这个轮胎要是实在坏得厉害,就干脆换上个新的,整个过程不需要很多时间。
席德梅尔出过一款很不错的游戏,叫做《文明》(Civilization),在第三代里面,有一项科技研究成功之后,会让工人工作效率加倍,这项科技的名字就叫做:可替换部件(Replacement Parts)。所以,软件行业也应当引入可替换的部件,一般称为组件。
2. 早期的前端怎么做组件化的?
在服务端,我们有很多组件化的途径,像J2EE的Beans就是一种。组件建造完成之后,需要引入一些机制来让它们可配置,比如说,工作流引擎,规则引擎,这些引擎用配置的方式组织最基础的组件,把它们串联为业务流程。不管使用什么技术、什么语言,服务端的组件化思路基本没有本质差别,大家是有共识的,具体会有服务、流程、规则、模型等几个层次。
早期展示层基本以静态为主,服务端把界面生成好,浏览器去拿来展示,所以这个时期,有代码控制的东西几乎全在服务端,有分层的,也有不分的。如果做了分层,大致结构就是下图这样:
这个图里,JSP(或者其他什么P,为了举例方便,本文中相关的服务端技术都用Java系的来表示)响应浏览器端的请求,把HTML生成出来,跟相关的JavaScript和CSS一起拿出去展示。注意这里的关键,浏览器端对界面的形态和相关业务逻辑基本都没有控制权,属于别人给什么就展示什么,想要什么要先提申请的尴尬局面。
这个时期的Web开发,前端的逻辑是基本可忽略的,所以前端组件化方式大同小异,无论是ASP还是JSP还是其他什么P,都可以自定义标签,把HTML代码和行间逻辑打包成一个标签,然后使用者直接放置在想要的地方,就可以了。
在这一时代,所谓的组件化,基本都是taglib这样的思路,把某一块界面包括它的业务逻辑一起打成一个端到端的组件,整个非常独立,直接一大块从界面到逻辑都有,而且逻辑基本上都是在服务端控制,大致结构如下图所示。
3. SPA时代,出现了新问题
自从Web2.0逐渐流行,Web前端已经不再是纯展示了,它逐渐把以前在C/S里面做的一些东西做到B/S里面来,比如说Google和微软的在线Office,这种复杂度的Web应用如果还用传统那种方式做组件化,很显然是行不通的。
我们看看之前这种组件化的方式,本质是什么?是展现层跟业务逻辑层的隔离,后端在处理业务逻辑,前端纯展现。如果现在还这么划分,就变成了前端有界面和逻辑,后端也有逻辑,这就比较乱了。我们知道,纯逻辑的分层组件化还是比较容易的,任何逻辑如果跟展现混起来,就比较麻烦了,所以我们要把分层的点往前推,推到也能把单独的展现层剥离出来。
如下图所示,因为实际上HTML、CSS、JavaScript这些都逐渐静态化,所以不再需要把它们放在应用服务器上了,我们可以把它们放在专门的高性能静态服务器上,再进一步发展,就可以是CDN(Content Delivery Network,内容分发网络)。前端跟后端的通信,基本都是通过AJAX来,也会有一些其他的比如WebSocket之类,总之尽量少刷新了。
在这张图里面可以看到,真正的前端已经形成了,它跟应用服务器之间形成了天然的隔离,所以也能够很独立地进行一些发展演进。
现在很多Web程序在往SPA(单页面程序,Single Page Application)的方向发展,这类系统通常比较类似传统的C/S程序,交互过程比较复杂,因此它的开发过程也会遇到一些困难。
那为什么大家要做SPA呢?它有很多明显的好处,最核心的优势就是高效。这个高效体现在两个方面:一是对于用户来说,这种方式做出来的东西体验较好,类似传统桌面程序,对于那些需要频繁操作的行业用户,有很大优势。二是运行的效率较高,之前集成一些菜单功能,可能要用iframe的方式引入,但每个iframe要独立引入一些公共文件,服务器文件传输的压力较大,还要初始化自己的一套内存环境,比较浪费,互相之间也不太方便通信,一般要通过postMessage之类的方式去交互。
有了SPA之后,比如一块界面,就可以是一个HTML片段,用AJAX去加载过来处理之后放到界面上。如果有逻辑的JavaScript代码,也可以用require之类的异步加载机制去运行时加载,整体的思路是比较好的。
很多人说,就以这样的需求,用jQuery再加一个异步js加载框架,不是很足够了吗?这两个东西用得好的话,也是能够解决一些问题的,但它们处理的并不是最关键的事情。在Web体系中,展现层是很天然的,因为就是HTML和CSS,如果只从文件隔离的角度,也可以做出一种划分的方式,逻辑放在单独的js文件里,html内部尽量不写js,这就是之前比较主流的前端代码划分方式。
刚才我们提到,SPA开发的过程中会遇到一些困难,这些困难是因为复杂度大为提升,导致了一些问题,有人把这些困难归结为纯界面的复杂度,比如说,控件更复杂了之类,没有这么简单。问题在于什么呢?我打个比方:我们在电脑上开两个资源管理器窗口,浏览到同一个目录,在一个目录里把某个文件删了,你猜猜另外一个里面会不会刷新?
毫无疑问,也会刷新,但是你看看你用的Web页面,如果把整个复杂系统整合成单页的,能保证对一个数据的更新就实时反馈到所有用它的地方吗?怎么做,是不是很头疼?代码组织的复杂度大为提高,所以需要做一些架构方面的提升。
4. 架构的变更
提到架构,我们通常会往设计模式上想。在著名的《设计模式》一书中,刚开始就讲了一种典型的处理客户端开发的场景,那就是MVC。
传统的MVC理念我们并不陌生,因为有Struts,所以在Web领域也有比较经典的MVC架构,这里面的V,就负责了整个前端的渲染,而且是服务端的渲染,也就是输出HTML。如下图所示:
在SPA时代,这已经不合适了,所以浏览器端形成了自己的MVC等层次,这里的V已经变成客户端渲染了,通常会使用一些客户端的HTML模版去实现,而模型和控制器,也相应地在浏览器端形成了。
我们有很多这个层面的框架,比如Backbone,Knockout,Avalon,Angular等,采用了不同的设计思想,有的是MVC,有的是MVP,有的是MVVM,各有其特点。
以Angular为例,它推荐使用双向绑定去实现视图和模型的关联,这么一来,如果不同视图绑定在同一模型上,就解决了刚才所说的问题。而模型本身也通过某种机制,跟其他的逻辑模块进行协作。
这种方式就是依赖注入。依赖注入的核心理念就是通过配置来实例化所依赖的组件。使用这种模式来设计软件架构,会牺牲一些性能,在跟踪调试的便利性等方面也会有所损失,但换来的是无与伦比的松耦合和可替代性。
比如说,这些组件就可以单独测试,然后在用的时候随手引入,毫无压力。对于从事某一领域的企业来说,光这一条就足以吸引他在上面大量投入,把所有不常变动领域模型的业务代码都用此类办法维护起来,这是一种财富。
5. MV*框架的基本原理
如果我们来设计Angular这么一个前端框架,应当如何入手呢?很显然,逻辑的控制必须使用JavaScript,一个框架,最本质的事情在于它的逻辑处理方式。
我们的界面为什么可以多姿多彩?因为有HTML和CSS,注意到这两种东西都是配置式的写法,参照后端的依赖注入,如果把这两者视为跟Spring框架中一些XML等同的配置文件,思路就豁然开朗了。
与后端不同的是,充当前端逻辑工具的JavaScript不能做入口,必须挂在HTML里才能运行,所以出现了一个怪异的状况:逻辑要先挂在配置文件(HTML)上,先由另外的容器(浏览器或者Hybird的壳)把配置文件加载起来,然后才能从某个入口开始执行逻辑。好消息是,过了这一步,逻辑层就开始大放异彩了。
从这个时候开始,框架就启动了,它要做哪些事情呢?
初始化自身(bootstrap)
异步加载可能尚未引入的JavaScript代码(require)
解析定义在HTML上的规则(template parser)
实例化模型(scope)
创建模型和DOM的关联关系(binding, injection)
这些是主线流程,还有一些支线,比如:
解析url的search字符串,恢复状态(route)
加载HTML部件模板(template url)
部件模板和模型的关联(binding)
6. 如何做组件化
6.1. HTML的组件化
SPA的一个典型特征就是部分加载,界面的部件化也是其中比较重要的一环。界面片段在动态请求得到之后,借助模版引擎之类的技术,经过某种转换,放置到主界面相应的地方。所以,从这个角度来看,HTML的组件化非常容易理解,那就是界面的片段化和模板化。
6.2. JavaScript的组件化
JavaScript这个部分有好几个发展阶段。
早期的共享文件,把公共功能的代码提出出来,多个页面共用
动态引用,消灭全局变量
在某些框架上进一步划分,比如Angular里面又分为provider,service,factory,controller
JavaScript组件化的目标是什么呢,是清晰的职责,松耦合,便于单元测试和重复利用。这里的松耦合不仅体现在js代码之间,也体现在js跟DOM之间的关系,所以像Angular这样的框架会有directive的概念,把DOM操作限制到这类代码中,其他任何js代码不操作DOM。
如上图所示,总的原则是先分层次,层内再作切分。这么做的话,不再存在之前那种端到端组件了,使用起来没有原先那么方便,但在另外很多方面比较好。
6.3. CSS的组件化
这方面,业界也有很多探索,比如LESS,SASS,Stylus等。为什么CSS也要做组件化呢?传统的CSS是一种扁平的文本结构,变更成本较高,比如说想要把结构从松散改紧凑,需要改动很多。如果把实际使用的CSS只当作输出结果,而另外有一种适合变更的方式当作中间过程,这就好多了。比如说,我们把一些东西定义成变量,每个细节元素使用这些变量,当需要整体变更的时候,只需修改这些变量然后重新生成一下就可以了。
以上,我们讨论了大致的Web前端开发的组件化思路,后续将阐述组件化之后的协作过程和管控机制。
管控平台
1. HTML片段
我们为什么要管理HTML片段?因为有界面要用它们,当这些片段多了之后,需要有个地方来管理起来,可以检索、预览它们,还能看到大致描述。
这应该是整个环节中一个相对很简单的东西,照理说,有目录结构,然后剩下的就是单个的HTML片段文件了,这就可以解决存储和检索的问题了,但我们还要考虑更多。
已有的HTML片段,如何被使用呢?这肯定是一种类似include的方式,通过某种特殊标签(不管是前端还是后端的方式)把这些片段引用进来,这时候就有了第一个问题:
假设有界面A和界面B同时引用了片段C,在某个开发人员修改片段C内容的时候,他如何得知将会影响到界面A和B呢?一个比较勉强的方式是全项目查找,但这在很多情况下是不够的。
如果我们的HTML片段是作为独立的公共库存在的,它已经不能通过项目内查找去解决这一问题了,因为不管A还是B,只要他不处于片段C的项目空间,就无从追寻。
这时候很多人会问两个问题:
跨项目的界面片段重用,意义在哪里?
如果我们的产品是针对一个小领域,它的复杂度根本不需要划分多个项目部分来协作完成。设想场景是面对很大的行业,各项目都是子产品,将来可能是其中若干个联合部署,这时候,保持其中的一致性是非常重要的。比如我们有个基本配置界面,在多个子产品中都要用,如果各自开发一个,其操作风格很可能就是不一致的,给人的印象就是不专业。所以会需要把常见的界面片段都归集起来,供业务方挑选使用。
修改C,只提供说明,但是不通知A和B,不实时更新他们的版本,然后自行决定怎样升级,如何?
这会有一个问题,每次有小功能升级的时候,代码是最容易同步合并的,所以才会有“持续集成”这个概念,如果是一直伴随升级,总要比隔一个大阶段才升级好,升级成本应尽量分摊到平时,就像农妇养小猪,小猪每天长一点,每天都抱来抱去,不觉得吃力,即使长大了也还能抱得动。
现在问题就很明确了,一定要有一种方式来把这个依赖关系管理起来,很显然,已有的版本库是肯定管不了这些的,所以只能在外围做一些处理。
我们建立一个管理平台,除了管理实体文件的版本,还管它们之间的关系。具体这个关系如何收集整理,有两种方式:手动配置,代码分析。
手动配置是比较土的方式,开发人员每提交一个文件,就去这系统上手动配置它的依赖关系。代码分析的话,要在每次提交文件的时候解析文件的包含规则,找出确切的文件。这两者各有利弊,前者比较笨,但容易做,后者对代码格式的要求比较高,要考虑的情况较多。
我们的界面往往不是那么简单,HTML片段也可能有层次的,举例来说:
界面A里面包含了片段B,但是片段B自身又包含了片段C,所以这个依赖关系也是有层级的,需要在设计的时候一并考虑。
2. JavaScript模块
JavaScript代码的管理,比HTML片段的状况好一些,因为业界很多这方面的解决方案。但它们还是没有解决当依赖项产生变更的时候反向通知的问题。
所以我们还是得像HTML片段一样,把它们的依赖关系都管理到平台里。于是,每个JavaScript模块都显式配置了自己所依赖的其他模块,通过这种单向关系,形成了一套完整的视图。
在JavaScript模块的代码实现中,我们是不提倡直接写依赖关系的。很多通用规范,比如AMD,往往建议我们这样写模块:
新闻热点
疑难解答