web前端的弱架构导致的代码污染

就要开始新的项目了,全站的前端架构也要开始构思,得益于之前做雅虎关系时的一些或成功或失败的实践总结,还是应当从架构的层面着手,去解决一些前端团队开发的问题。如今市面上有着各种各样的js库和框架,但库和框架还是有很大区别的,首先,“库”是widget的一个集合,特点是上手容易使用简便,参照例子只大概只需要引用一个script标签到页面中,再加上一些简单的类似a.start(config)的启动代码就可以了,而框架则是网站的树状结构的抽象,包括模块关系,功能之间的依赖,入口的先后顺序,以及性能的hack等等,幸运的是阿里还是很明智的选择了yui作为其前端架构的底层,只是由于 yui2强大丰富的widget库,使得多数人认为yui2更是一个库而非框架,而yui2的yuiloader也仅仅是一个可选的模块,这样就更加淡化了yui2在框架层面灵活强大的表现力,个人认为如果网站没有以yuiloader搭建起来的模块树做支撑,yui2的作用无疑是一把牛刀只用作杀鸡而已。在yui3中,这种情况有了根本改变,框架层面的模块化代替了简单的库,这对于团队开发的大型网站是很有启发的。

当然,在做前端开发的时候,使用loader必然要多写几行代码,多几次封装,又麻烦又不方便,这样做到底好在什么地方呢。这个话题大概说来话长,这里只从网站规模和项目特点的方面来作简单说明:

像taobao这种大型的网站,前端开发占有不小的比重,这类网站一般会按照大类划分成不同的产品,每个产品相当于一个独立完整的网站,只有整体样式风格和组件会和主战保持一致。

那么从web前端角度讲,这类网站大致包含四部分内容:

  • 网站下辖站点所需的全局变量和函数,包括所有子站共通的部分
  • 子站外框(公共头尾)
  • 网页主体内容
  • 组件

根据三者的特点,各自的重用情况为:

  • 全站的全局变量和函数:几乎每个页面都要用到
  • 子站外框:几乎每个子站的网页都保持一致
  • 网页的主体内容,不可重用
  • 组件:重用率高,但是被有选择的调用,只在必要的时候才会引用组件

从需求的角度上讲,这类网站的特点是更新频繁,从工程的角度讲,代码的维护成本大于甚至远远大于代码开发成本。也就是说项目随着时间的推移,修改功能的项目数量要远远大于新功能的开发,到项目的中后期,工程师的大部分时间不是写新代码,而是花大量的经历去读旧代码,或者进行性能优化,或者针对新需求进行改进。不管是性能改进还是需求改进,都将增加代码的熵,代码污染也会越来越严重,维护成本也会急剧变大,这让项目后期开发工程师苦不堪言,有一部分原因是项目前期文档不全,缺少写代码的必要的约束和约定,再者是注释不够及时和规范,导致代码可读性差,三是工程师水平参差不齐,有些人在修改代码过程中会无意识的破坏原有代码的结构,四是时间压力导致代码质量下降,五是框架结构的不合理。

完善文档和规范、增加新人培训力度可以以解决一部分问题,但这往往和制度和执行力度有关。从项目生命周期来看,项目分为叠代和快速开发,快速开发是开发时间短,不需要和其他项目并行,及时开发及时测试及时打包上线,和其他项目发生冲突的可能性很小,简单迭代的开发模式是基于分支和主干的多线程的并行开发。在此基础上的前端开发也是保持和后段开发同步的迭代。迭代的优点是项目并行和减少冲突的发生。从后端开发的角度看,经常多人开发同一个页面,只是每个工程师负责的模块不同而已,一般情况下页面中只有 include的入口,然后调用各自工程师的代码,这样在页面中的代码污染就比较低,但是随着页面复杂度的增加,后段代码会越来越多的直接出现在页面当中,比如使用更多的echo代替模版,html代码中嵌套更多的if判断和for循环。这样会增加代码污染的程度。从前端开发来看,也会经常出现多人合作开发同一个页面,也是每人负责不同的功能模块,这里有两种情况,1是多人同时开发不同的应用模块,两者没有依赖关系,2是多人分别开发框架和应用,两者之间有依赖关系,应用依赖框架。而两者之间的耦合应当仅仅以入口调用作为接口,因此前端框架应当考虑到这种情况,即多人开发的前端功能代码应当尽可能地分离。

在js代码的写法上,自然推荐将代码都封装到页面的js文件中,在page中只留出入口。然而实际情况并非如此,在缺少框架约束的情况下,多数人不会选择去在读懂之前的烂代码的基础上再添加自己的代码,通常会直接用最简单粗暴的用script标签引一个js,紧跟着开一段script 代码块写启动入口,甚至于连script标签都烂的写,直接在原先代码块中直接开辟一段区域自己加代码。这种做法应当如何看待呢?从库的角度讲,库中给了很多模块的实例,使用模块的方式大都也是手写一个script标签引一个js文件,然后再页面中添加启动入口。那么当多人开发同一个页面中的不同功能的时候就会这样做:

首先,a在页面中实现了一个功能,大致是这个样子:

a.start();

b也实现了另外一个功能,大致是这个样子:

b.start();

b在coding的时候多数情况不会去review页面代码,或者说review了页面的烂代码也看不懂,再或者说两个人并行开发,并不知晓对方的实现。这样就会导致base.js的引用重复。

另外一种情况是,如果一个例子比较复杂,需要引用多个js文件,比如:

m.start();

那么a.js和b.js的引用顺序则需要特别注意,但在实际开发中,并不能保证每个人都能细心的将js文件的顺序排好,如果排乱了顺序则需要比较多的调试成本。所以由此看出,纯粹基于“库”模式的开发会带来很多隐患,这里只提到了开发过程中常遇到的问题,另外,这种手写js标签引用js文件的方式也是缺乏语义并牺牲了性能(每个script src占用一个http请求?),同时,inline script block散步在页面的各个角落,尽管每个script block都被匿名函数闭包起来,开发速度很快,但这种代码毫无结构性可言,对于其他人也是晦涩难懂,等到项目中后期的维护成本会很高,所以说换作谁都不愿意去接手维护一个如此coding的项目。因此,可以认为这种coding方式所导致的代码污染会大大降低生产率(这里的生产率包括开发和维护两部分)和页面性能。

而yui3强制使用Ylaoder,从结构上一定程度的保证代码质量和可读性。按照yui3的思路,项目的前期应当花大精力去做模块的依赖,在此基础上开发出丰富的widget,使用过程中指需要load模块名字,而不必去关系模块需要依赖那些js,以及这些js的先后顺序是什么。另外,通过简单hack,loader可以将yui3以及所有项目js都combo在一起(taobao assetcdn现在不提供这种功能,不过很快就会有了),在多的模块都合并到一个js http请求中,外加一个种子,一个页面的js http请求顶多只有两个。

那么是否可以说,象taobao这种复杂多变(需求变更频繁)的大网站就可以用yui3来架构,以避免深度的代码污染了呢?非也,yui3在设计上并不能满足类似taobao的这种门户。上文提到了网站的四个组成部分,全局、外框、主内容区和组件,他们之间的关系应当是什么样的呢?显然,页面rander的时候,应当首先加载全局变量,然后初始化load页面所属于的子站的公共头和公共尾,之后再执行页面逻辑,因此网站三个层次的逻辑关系很明确,所以三者之间必然会发生耦合,框架则需要将这三个层次之间的耦合降到最低,因为,实际情况可能是由很多人同时开发框架功能、模块、页面主逻辑和底层库的维护,因此就需要每个部分的负责团队的代码相互隔离,以保证并行开发中不发生冲突,鉴于此,纯粹基于yui3的代码结构有三种:

1,通过YUI().use()的嵌套将层次之间的逻辑关系表达清楚,比如,a的代码属于框架,b的代码属于应用,c的代码属于子应用:

YUI().use(*,function(){
    //a的代码
    YUI().use(*,function(){
        //b的代码
        YUI().use(*,function(){
            //c的代码
        });
    });
});

三个闭包的嵌套,这段代码层次感比较清晰,把a的代码看作框架,b的代码看作应用,c的代码看作子应用,调用关系一目了然,即框架执行完毕后加载并执行应用的代码,应用的代码加载并执行完后执行子应用的代码。

但三段代码非常紧密的耦合在一块,在团队并行开发中将会经常发生代码冲突。如果代码一次编写不再修改则这种写法并无大碍,往往代码需要经过反复修改,因此这种显示依赖的编码风格无法避免代码污染的发生。而且最可能b部分的代码是污染最严重的,因为应用可能包含多个子应用,比如产品的列表页御览页和详情页可能都包含相同的右侧广告位,这样b的代码中就可能包含有平级的多个YUI().use()。

另外一点,每个YUI().use相当于一个loader,每次loader的加载都会将本闭包内需要的模块所对应的js文件加载进页面中,这样如果三次loader的加载有重复的情况则无法去重,造成的不必要的http请求将会增加一倍以上,严重影响性能。

2,一个闭包,各自入口

YUI().use(/*a,b,c的modules*/,function(){
    a.start();
    b.start();
    c.start();
});

这种写法比较精炼,调用顺序一目了然,同样,若代码一次编写不再修改则这是一种不错的写法,考虑到代码的频繁修改,这种方法仍有三个缺点,1,a,b,c都各自需要引用各自的modules,各自的模块是作为参数写在use的非倒数第一个参数中的,谁也不清楚哪些是a的模块,哪些是b的模块,哪些是c的模块,这种歧异代码是多人协作项目中必须要避免的。2,a,b,c的代码之间看不出依赖关系,而实际上,框架、应用和子应用之间的调用应当是按顺序,而在代码维护过程中,a,b,c的调用顺序是可能被修改的。3,三个人写的代码仍然在一起,不敢保证每次修改都会将模块封装致页面中,一旦有人开了先例图省事将模块代码直接写在了这个闭包中,以后的修改将会一发不可收拾,导致每次需求变更都会更加污染代码。这种情况在雅虎关系的开发中频繁发生,并最终导致代码极其臃肿,后期即便开发一个类似tabnew的小功能都要耗费半个月时间。

3,各自闭包,互不干扰

YUI.use(*,function(Y){
    //a的代码
});

YUI.use(*,function(Y){
    //b的代码
});

YUI.use(*,function(Y){
    //c的代码
});

三个人写的代码完全分开,不存在相互干扰的问题,再者,即便某个人的代码写的乱七八糟,也不会影响其它人的代码的健康。但缺点有2个,1,象上一节说的代码实际上有依赖关系,这里看不出依赖关系,2,同样的性能问题。

因此如上所述诸多因为框架约束不足带来的代码污染和维护成本增高的现象,应当通过更加强壮的框架来解决。目前正在开发中的cubee框架将会很大程度解决上述问题的发生。

嗯?什么是cubee,cubee有什么用?首先可以确定,cubee是框架,非库,初衷是解决的是团队项目中的代码污染和降低高维护成本,关于cubee的设计详情以后再分享吧。

posted at