我们来分析一下究竟哪些因素让前端开发这么困扰。
先看看界面部分吧。
#1. 命令式还是声明式
毫无疑问,就写界面来说,声明式的代码编写效率远高于命令式:
<Panel title="Test"> <Button label="Click me"/> </Panel> Panel p = new Panel(); p.title = "Test"; Button b = new Button(); b.label = "Click me"; p.add(b);
第一种容易写,容易理解。
#2. 控件标签集
不管你的软件面向什么行业,至少都要一些控件,或者是基本的表单输入,或者是复杂的比如树形表格,里面还可以跨行跨列渲染的。
如果我们有一套映射到控件的标签,那么写代码是肯定会简单很多的,比如说,在HTML里面没有原生的Panel,那么,刚才第一段代码可能就要变成:
<div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Simple HTML Loader</h3> </div> <div class="panel-body"> <button>Click me</button> </div> </div>
我们为了使得界面代码编写更高效,毫无疑问会倾向于把这么一堆东西简化成一个Panel标签,这样就会逐步建立一套面向自己行业的标签集。
#3. 带逻辑的控件
刚才这个例子为什么简单呢,因为它只是一个普通容器,静态的,不带逻辑,所以即使你用什么静态模板也能解决问题。如果复杂一点,是一个TabNavigator,就要考虑切换的事件,再复杂一些是个树形表格,那就更麻烦了。
我们来看jQuery提供的插件方式实现TabNaviator:
<div id="tabs"> <ul> <li><a href="#tabs-1">Nunc tincidunt</a></li> <li><a href="#tabs-2">Proin dolor</a></li> <li><a href="#tabs-3">Aenean lacinia</a></li> </ul> <div id="tabs-1"> </div> <div id="tabs-2"> </div> <div id="tabs-3"> </div> </div> <script> $(function() { $( "#tabs" ).tabs(); }); </script>
从我个人的角度看,这种代码很愚蠢。蠢在何处呢?HTML这类声明式的界面描述语言,写起来本来应当直观一些的,但是被这么一搞,又往命令式的方向去了。而且两种东西混杂,声明和渲染居然分了两处,又增加了维护的成本。
难道就没有别的办法来解决这个问题吗?
我们看看其他语言和框架,比如Flex和Silverlight。
<mx:TabNavigator id="tn" width="100%" height="100%"> <!-- Define each panel using a VBox container. --> <mx:VBox label="Panel 1"> <mx:Label text="TabNavigator container panel 1"/> </mx:VBox> <mx:VBox label="Panel 2"> <mx:Label text="TabNavigator container panel 2"/> </mx:VBox> <mx:VBox label="Panel 3"> <mx:Label text="TabNavigator container panel 3"/> </mx:VBox> </mx:TabNavigator>
上面这段是Flex里面的TabNavigator,在这个链接底部有运行结果:TabNavigator
为什么它可以看不到逻辑的代码,但是又确实能有动作呢,因为它的实现类是mx.containers.TabNavigator,在这个代码里,可以自己手动去处理一切内部实现,但是暴露给业务开发人员的就是这么简单的标签。
我们看看在HTML和JS这个体系里用什么办法去解决。不要提JSF这类服务端技术,因为它的思路也是不好的,展示代码的生成和渲染都不在一个地方,会有很多问题。
#4. Polymer与Angular
早期IE里有HTC,也就是HTML Components,因为别的浏览器厂商不喜欢,所以快要消亡了。在W3C新的HTML规范里,有一个Web Components,参见这里:Introduction to Web Components
这个东西跟HTC的思想本出同源,它引入了Custom Elements和Shadow DOM这两个概念,也就是说,我可以自定义一个标签,然后在内部随便怎么折腾,用这个标签的人可以很方便。
很美好,是不是,但是只适用于比较新的浏览器,基于这个理念架构的框架Polymer的目标也只是支持一些比较新的浏览器。Polymer
那么怎么办呢?我们还有Angular,它也可以自定义标签,然后用directive的方式写内部实现。
<tabs> <pane title="Localization"> </pane> <pane title="Pluralization"> </pane> </tabs>
<script id="components.js"> angular.module('components', []) .directive('tabs', function() { return { restrict: 'E', transclude: true, scope: {}, controller: function($scope, $element) { var panes = $scope.panes = []; $scope.select = function(pane) { angular.forEach(panes, function(pane) { pane.selected = false; }); pane.selected = true; } this.addPane = function(pane) { if (panes.length == 0) $scope.select(pane); panes.push(pane); } }, template: '<div class="tabbable">' + '<ul class="nav nav-tabs">' + '<li ng-repeat="pane in panes" ng-class="{active:pane.selected}">'+ '<a href="" ng-click="select(pane)">{{pane.title}}</a>' + '</li>' + '</ul>' + '<div class="tab-content" ng-transclude></div>' + '</div>', replace: true }; }) .directive('pane', function() { return { require: '^tabs', restrict: 'E', transclude: true, scope: { title: '@' }, link: function(scope, element, attrs, tabsCtrl) { tabsCtrl.addPane(scope); }, template: '<div class="tab-pane" ng-class="{active: selected}" ng-transclude>' + '</div>', replace: true }; }) </script>
这么一来,也就有些接近我们的目标了,看到现在,我们还记得目标是什么吗?是尽可能精简的面向领域的容器和控件标签集,有了这个,写界面代码才能更简单。
#5. 为什么HTML默认标签集这么小
事情结束了吗?没有呢。我们的HTML体系为什么标签集这么小?因为他要解决的是通用领域的东西,怎样才能通用呢?要的是尽可能无歧义。
怎样的东西会没有歧义?那就是它的含义尽可能少,比如说单行文本输入框,总没人对它有歧义吧,它无非就是可以设置最大最小长度,是否只读,是否禁用,最多通过某种规则来限制输入字符,最多最多,也就这些可做的了,大家都认同。
Button就不同了,一开始他是<input type="button" value="Click"/>,后来大家想要各种各样的button,于是开放了<button></button>这样的标签,可以在里面写各种HTML,我记得当时很多人在中间加上下和左右两层marquee,简直玩坏了。
现在HTML里面又有了数字输入,日期时间输入这样的东西,数字的没什么疑问,就是最大最小值,步进值等等,日期时间这个就复杂了,它怎么做,都有人不满意。有人要日期排左边,有人要时间排上面,有人只要年和月,有人只要分和秒。有人要点空白表示选中,有人要双击日期表示选中,还有人想用农历、波斯历、尼泊尔历,简直没完了,还不如不做,谁要谁自己做……
所以,面向各领域的人们,自己动手,丰衣足食吧。
#6. 界面修饰
好了,控件集的问题解决了,我们来看看界面的修饰。
你们发现没有,不管用什么非HTML的标签体系,可能写代码会很快,但是有时候要修饰界面,比如只是调整一下所有容器的边距,某些按钮的圆角之类,就会生不如死。
这时候你会发现,HTML里面的CSS真是神器,什么都能干,而且是面向切面的,只要你的HTML结构是良好的,完全不需要调整这个层面的代码。为什么其他体系的CSS没有这么强呢?比如说Flex也可以写CSS,QT也可以写CSS。
因为CSS的部分实在是太复杂了,复杂到整个浏览器里面绝大部分的代码都在处理这方面的东西,像Google的Chrome团队有1000多人,别的体系没法有这么大投入,只能看着羡慕。
上次看到一个问题,近30年来软件开发体系有哪些本质的改进?我觉得CSS真的可以入选,这是一个把结构和展现完全分离的典范,并且实现得很好。
我们的前端开发一般都是面向某个领域的,不管什么领域,CSS方向都可以有一个很独立的规划,因为它可以不影响界面的结构。所以这个方面,其实不太会对前端开发造成太多压力,压力只集中在维护CSS的人群身上。
好了,上面扯了那么多,其实到现在还在界面的层次,一直没有去谈到真正的逻辑。那么,最让我们困扰的部分是哪里呢?
#7. 模块化和加载
Web前端开发有个最苦闷的事情就是选型,因为HTML这个体系很开放,提供的默认能力又不是很足够,如果要做复杂交互的东西,会需要很多额外的工作。有各种框架从各种角度来解决问题,但怎么把这些东西整合到正好符合自己的需要,是一个很花精力的事情,很多时候恨不得自己把全部轮子都造一遍。
真正的开发工作中,跨浏览器,踩各种坑应该是最烦闷的事,其他部分,如果有做好自己领域里标签的定义,或者不用标签用其他方式,应该不算特别困难。
有人说JavaScript语言本身比较松散,所以写业务逻辑比较头疼,这不算大问题。基于B/S的开发,有一个大坑是你在运行的时候要先把代码加载过来,然后才能跑。你看那些C/S软件,有这困扰吗?再看看后端程序员,谁还要关心自己的代码执行之前要做的事情?
所以后端程序员写前端代码,都情不自禁地会引入一大堆库。我们形象一点来描述一下这个过程:
嗯,大家都用jQuery,我也引入,抄了两段代码发现真不错。咦,我要个树控件,网上逛了一圈,拿了个zTree回来。再埋头苦干半个小时,缺数据表格控件,于是过了一会,jQuery UI被整体引入了。再埋头苦干,上网乱点了点,浏览器跳出个广告,一看叫做Kendo UI,看看发现不错,引进来再说,用里面的某个控件。又过了一阵,听说最近Angular很火啊,看了看例子,表单功能怎么那么强,我也要用!捣鼓捣鼓又加进去了。项目里又要用图表库,看了半天眼睛都花了,百度的ECharts不错哦,引进来。哎呀我界面怎么那么丑,人家的怎么那么清爽,查看源码,一看,Bootstrap,去官网一看,真乃神器,不用简直对不起自己。
没多久之后,这个界面已经融合了各种主流框架,代码写法五花八门,依赖了几M的JS库,更要命的是里面某些JS有冲突,某些样式也互相覆盖,快疯了。
这里有哪些问题呢?
- JS代码要先加载到界面才能执行,而这么几M的代码加载过来就要好久了,然后每个框架还要把自己初始化,又耗不少时间,半分钟之后自己写的JS才开始执行,用户等得都快怀孕了。
- 不管是JS还是CSS,都应当控制基准的代码,这件事的主要意义是避免冲突,因为整个体系都比较松散,如果不加控制,就会造成冲突。即使在服务端写Java,也有类签名一致性之类的问题,所以这个部分必须要重视。
刚才这两点,第二点暂时不是我们要探讨的范围,第一点,引出的话题就是异步加载,这是一个可以展开说很多的话题,也不再说了。异步加载和缓存是面对复杂场景必做的优化措施。
但是这个里面规范就有好几种,具体实现方式就更多了。ES6的module也许可以解决这个问题。harmony:modules [ES Wiki]
#8. 逻辑的分层
网站型和应用型Web程序对分层的需求是不一样的。网站型的逻辑大部分都在处理UI,而应用型可能有很多业务逻辑,这部分需要更好的组织,以便复用,或者即使我们的目标不包括复用,为了这个代码的可维护性,也需要有比较好的组织方式。
本质上这些组织方式与传统的客户端软件开发没什么不同,主要要做的无非就是UI层的隔离,或者模板化,或者别的什么方式。纯逻辑的代码大家都会写,但这个逻辑怎么跟界面产生关系,这是个问题。
有些框架通过在HTML元素上设置额外属性,然后启动的时候读取,在框架内部做一些相关的事情,比如Angular、Avalon和Knockout。有的框架在视图层中让开发人员手动去处理界面,就像未引入框架的那样,比如Backbone,两者是各有利弊的。
前面这种,一般功能是会很强大,但是它自身所做的东西必须足够多,多得帮你做掉绝大部分本来该自己做的事,你才会特别爽。所以,用这类框架来做表单型应用的时候,是会非常舒服的,因为这些需求他做框架的时候能预见,所以比如校验、联动、存取之类的都会处理掉。假如你要做一个绘图类应用,这就麻烦了,不管你是用Canvas还是SVG,它所能帮到的都不多。这时候,后面这类可能反而适合一些。
这些数据分层框架的原理是什么呢?是要做一层表单与数据的对应关系,所以他要检测数据的变动,比如一个Object,它某个值变更了,要去把对应的界面更改之类。这里面也有很多的坑,可以一步一步踩过来。。。
到现在,我大致可以回答你的问题,什么情况下前端开发会比较轻松呢?
- 针对自己领域的界面标签库比较完善,或者易于扩展
- 样式容易调整,并且独立于界面元素
- 逻辑模块化,层次分明,在某种统一规范上存在大量可用库
咦,我这三点好像在说微软的WPF体系吗?