基本思路
#1. 为什么要做组件化?
无论前端也好,后端也好,都是整个软件体系的一部分。软件产品也是产品,它的研发过程也必然是有其目的。绝大多数软件产品是追逐利润的,在产品目标确定的情况下,成本有两个途径来优化:减少部署成本,提高开发效率。
减少部署成本的方面,业界研究得非常多,比如近几年很流行的“去IOE”,就是很典型的,从一些费用较高的高性能产品迁移到开源的易替换的产品集群,又比如使用Linux + Mono来部署.net应用,避开Windows Server的费用。
提高开发效率这方面,业界研究得更多,主要途径有两点:加快开发速度,减少变更代价。怎样才能加快开发速度呢?如果我们的开发不是重新造轮子,而是每一次做新产品都可以利用已有的东西,那就会好很多。怎样才能减少变更代价呢?如果我们能够理清模块之间的关系,合理分层,每次变更只需要修改其中某个部分,甚至不需要修改代码,仅仅是改变配置就可以,那就更好了。
我们先不看软件行业,来看一下制造行业,比如汽车制造业,他们是怎么造汽车的呢?造汽车之前,先设计,把整个汽车分解为不同部件,比如轮子,引擎,车门,座椅等等,分别生产,最后再组装,所以它的制造过程可以较快。如果一辆汽车轮胎被扎破了,需要送去维修,维修的人也没有在每个地方都修一下,而是只把轮胎拆下来修修就好了,这个轮胎要是实在坏得厉害,就干脆换上个新的,整个过程不需要很多时间。
席德梅尔出过一款很不错的游戏,叫做《文明》(Civilization),在第三代里面,有一项科技研究成功之后,会让工人工作效率加倍,这项科技的名字就叫做:可替换部件(Replacement Parts)。所以,软件行业也应当引入可替换的部件,一般称为组件。
#2. 早期的前端怎么做组件化的?
在服务端,我们有很多组件化的途径,像J2EE的Beans就是一种。组件建造完成之后,需要引入一些机制来让它们可配置,比如说,工作流引擎,规则引擎,这些引擎用配置的方式组织最基础的组件,把它们串联为业务流程。不管使用什么技术、什么语言,服务端的组件化思路基本没有本质差别,大家是有共识的,具体会有服务、流程、规则、模型等几个层次。
早期展示层基本以静态为主,服务端把界面生成好,浏览器去拿来展示,所以这个时期,有代码控制的东西几乎全在服务端,有分层的,也有不分的。如果做了分层,大致结构就是下图这样:
![9331891b98441d2824564545f662d756.png](https://i-blog.csdnimg.cn/blog_migrate/345a7da539e21fca7270a93b2bb1aeb0.png)
这个图里,JSP(或者其他什么P,为了举例方便,本文中相关的服务端技术都用Java系的来表示)响应浏览器端的请求,把HTML生成出来,跟相关的JavaScript和CSS一起拿出去展示。注意这里的关键,浏览器端对界面的形态和相关业务逻辑基本都没有控制权,属于别人给什么就展示什么,想要什么要先提申请的尴尬局面。
这个时期的Web开发,前端的逻辑是基本可忽略的,所以前端组件化方式大同小异,无论是ASP还是JSP还是其他什么P,都可以自定义标签,把HTML代码和行间逻辑打包成一个标签,然后使用者直接放置在想要的地方,就可以了。
在这一时代,所谓的组件化,基本都是taglib这样的思路,把某一块界面包括它的业务逻辑一起打成一个端到端的组件,整个非常独立,直接一大块从界面到逻辑都有,而且逻辑基本上都是在服务端控制,大致结构如下图所示。
![2f8db1bc72a511a2a4779e9f5782bd48.png](https://i-blog.csdnimg.cn/blog_migrate/62852caf9b8e5bf7bcbfe6f12c0a5c56.jpeg)
#3. SPA时代,出现了新问题
自从Web2.0逐渐流行,Web前端已经不再是纯展示了,它逐渐把以前在C/S里面做的一些东西做到B/S里面来,比如说Google和微软的在线Office,这种复杂度的Web应用如果还用传统那种方式做组件化,很显然是行不通的。
我们看看之前这种组件化的方式,本质是什么?是展现层跟业务逻辑层的隔离,后端在处理业务逻辑,前端纯展现。如果现在还这么划分,就变成了前端有界面和逻辑,后端也有逻辑,这就比较乱了。我们知道,纯逻辑的分层组件化还是比较容易的,任何逻辑如果跟展现混起来,就比较麻烦了,所以我们要把分层的点往前推,推到也能把单独的展现层剥离出来。
如下图所示,因为实际上HTML、CSS、JavaScript这些都逐渐静态化,所以不再需要把它们放在应用服务器上了,我们可以把它们放在专门的高性能静态服务器上,再进一步发展,就可以是CDN(Content Delivery Network,内容分发网络)。前端跟后端的通信,基本都是通过AJAX来,也会有一些其他的比如WebSocket之类,总之尽量少刷新了。
![6aedcdcd076506451adcc99ba21656f7.png](https://i-blog.csdnimg.cn/blog_migrate/0045e0665cb8e7c5fa908db073450a10.jpeg)
在这张图里面可以看到,真正的前端已经形成了,它跟应用服务器之间形成了天然的隔离,所以也能够很独立地进行一些发展演进。
现在很多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。如下图所示:
![e58c96bdd032adeb949131c663aaccbd.png](https://i-blog.csdnimg.cn/blog_migrate/a5a8a8e1b32a6896eec84afc9760bc1d.jpeg)
在SPA时代,这已经不合适了,所以浏览器端形成了自己的MVC等层次,这里的V已经变成客户端渲染了,通常会使用一些客户端的HTML模版去实现,而模型和控制器,也相应地在浏览器端形成了。
![25219977e3a8d236e5efa308c9f1fdbb.png](https://i-blog.csdnimg.cn/blog_migrate/4464b6e66091da52bfbb1084c64be70c.jpeg)
我们有很多这个层面的框架,比如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。
![004eceb43a444571232fe0b7f53d0833.png](https://i-blog.csdnimg.cn/blog_migrate/43a19afcd7d538c64a403cdfbf32af70.jpeg)
如上图所示,总的原则是先分层次,层内再作切分。这么做的话,不再存在之前那种端到端组件了,使用起来没有原先那么方便,但在另外很多方面比较好。
6.3. CSS的组件化
这方面,业界也有很多探索,比如LESS,SASS,Stylus等。为什么CSS也要做组件化呢?传统的CSS是一种扁平的文本结构,变更成本较高,比如说想要把结构从松散改紧凑,需要改动很多。如果把实际使用的CSS只当作输出结果,而另外有一种适合变更的方式当作中间过程,这就好多了。比如说,我们把一些东西定义成变量,每个细节元素使用这些变量,当需要整体变更的时候,只需修改这些变量然后重新生成一下就可以了。
以上,我们讨论了大致的Web前端开发的组件化思路,后续将阐述组件化之后的协作过程和管控机制。