有人说“前端开发”是IT界最容易被误解的岗位,这不是空穴来风。若是你还认为前端只是从美工那里拿到切图, JS和CSS一番乱炖,难搞的功能就去网上信手拈来,CtrlC + Ctrl V的话,那就正中了这份误解的下怀。通过十几年的发展,web前端早已脱离了原来边缘化的形态,扮演了移动互联网开发链条中最关键的角色,是应用或产品可否打动用户的踹门砖。那么什么是前端开发,其又包含了哪些内容?css
前端开发的定义html
从狭义的定义来看,“前端开发”是指围绕HTML、JavaScript、CSS这样一套体系的开发技术,它的运行宿主是浏览器。从广义的定义来看,其应该包括:前端
l 专门为手持终端设计的相似WML这样的类HTML语言,以及相似WMLScript的类JavaScript语言。jquery
l VML和SVG等基于XML的描述图形的语言。web
l 从属于XML体系的XML,XPath,DTD等技术。数据库
l 用于支撑后端的ASP,JSP,ASP.net,PHP,Nodejs等语言或者技术。编程
l 被第三方程序打包的一种相似浏览器的宿主环境,好比Adobe AIR和使用HyBird方式的一些开发技术,如PhoneGap。后端
l Adobe Flash,Flex,Microsoft Silverlight,Java Applet,JavaFx等RIA开发技术。数组
本文主要从“web前端”,也即狭义前端的角度出发,以人类科技进步划时代的方式,将前端开发划分为几个重要的时代,带领你们领略一下前端这十几年来的发展历程。浏览器
石器时代
最先期的Web界面基本都是在互联网上使用,人们浏览某些内容,填写几个表单而且提交。当时的界面以浏览为主,基本都是HTML代码,咱们来看一个最简单的HTML文件:
<html> <head> <title>测试一</title> </head> <body> <h1>主标题</h1> <p>段落内容</p> </body> </html> |
为了执行一些动做或进行必定的业务处理,有时候会穿插一些JavaScript,如做为客户端校验这样的基础功能。代码的组织比较简单,并且CSS的运用也是比较少的。譬如:下面这个文档将带有一段JavaScript代码,用于拼接两个输入框中的字符串,而且弹出窗口显示。
<html> <head> <title>测试二</title> </head> <body> <inputid="firstNameInput"type="text"/> <inputid="lastNameInput"type="text"/> <inputtype="button"onclick="greet()"/> <scriptlanguage="JavaScript"> function greet(){ var firstName = document.getElementById("firstNameInput").value; var lastName = document.getElementById("lastNameInput").value; alert("Hello, "+ firstName +"."+ lastName); } </script> </body> </html> |
因为静态界面不能实现保存数据等功能,出现了不少服务端技术,早期的有CGI(Common Gateway Interface,多数用C语言或者Perl实现的),ASP(使用VBScript或者JScript),JSP(使用Java),PHP等等,Python和Ruby等语言也常被用于这类用途。
有了这类技术,在HTML中就可使用表单的post功能提交数据了,好比:
<formmethod="post"action="username.asp"> <p>First Name: <inputtype="text"name="firstName"/></p> <p>Last Name: <inputtype="text"name="lastName"/></p> <inputtype="submit"value="Submit"/> </form> |
在这个阶段,因为客户端和服务端的职责未做明确的划分,好比生成一个字符串,能够由前端的JavaScript作,也能够由服务端语言作。因此一般在一个界面里,会有两种语言混杂在一块儿,用<%和%>标记的部分会在服务端执行,输出结果,甚至常常有把数据库链接的代码跟页面代码混杂在一块儿的状况,给维护带来了很大的问题。
<html> <body> <p>Hello world!</p> <p> <%response.write("Hello world from server!")%> </p> </body> </html> |
青铜时代
青铜时代的典型标志是出现了组件化的萌芽,着眼点主要在文件的划分上。后端组件化比较常见的作法是,把某一类后端功能单独作成片断,而后其余须要的地方来include进来,典型的有:ASP里面数据库链接的地方,把数据源链接的部分写成conn.asp,而后其余每一个须要操做数据库的asp文件包含它。浏览器端则一般针对JavaScript脚本文件,把某一类的Javascript代码写到单独的js文件中,界面根据须要,引用不一样的js文件;针对界面组件,则一般利用frameset和iframe这两个标签。某一大块有独立功能的界面写到一个HTML文件,而后在主界面里面把它看成一个frame来载入,通常的B/S系统集成菜单的方式都是这样的。是否是以为很熟悉?对的,如今大多公司的内部系统正是这个时代的产物。
此外,还出现了一些基于特定浏览器的客户端组件技术,好比IE浏览器的HTC(HTML Component)。这种技术最初是为了对已有的经常使用元素附加行为的,后来有些场合也用它来实现控件。微软ASP.NET的一些版本里,使用这种技术提供了树形列表,日历,选项卡等功能。HTC的优势是容许用户自行扩展HTML标签,能够在本身的命名空间里定义元素,而后,使用HTML,JavaScript和CSS来实现它的布局、行为和观感。这种技术由于是微软的私有技术,因此逐渐变得不那么流行。Firefox浏览器布其后尘,也推出过一种叫XUL的技术,也一样没有流行起来。
铁器时代
这个时代的彗星是Ajax的出现以及JS基础框架的兴起。
AJAX
AJAX实际上是一系列已有技术的组合,早在这个名词出现以前,这些技术的使用就已经比较普遍了,GMail由于恰当地应用了这些技术,得到了很好的用户体验。因为Ajax的出现,规模更大,效果更好的Web程序逐渐出现,在这些程序中,JavaScript代码的数量迅速增长。出于代码组织的须要,“JavaScript框架”这个概念逐步造成,当时的主流是Prototype和Mootools,二者各有千秋,提供了各自方式的面向对象组织思路。
JavaScript基础框架
Prototype框架主要是为JavaScript代码提供了一种组织方式,对一些原生的JavaScript类型提供了一些扩展,好比数组、字符串,又额外提供了一些实用的数据结构,如:枚举,Hash等,除此以外,还对dom操做,事件,表单和Ajax作了一些封装。
Mootools框架的思路跟Prototype很接近,它对JavaScript类型扩展的方式别具一格,因此在这类框架中,常常被称做“最优雅的”对象扩展体系。
从这两个框架的所提供的功能来看,它们的定位是核心库,在使用的时候通常须要配合一些外围的库来完成。
倚天不出,谁与争锋?除以上二者之外,还有YUI,jQuery等,JavaScript基础框架在这个时代算得上是百花齐放,可是时间已经证实,真正的王者是jQuery。
jQuery与其余的基础框架都有所不一样,它着眼于简化DOM相关的代码。例如:
l DOM的选取
jQuery提供了一系列选择器用于选取界面元素,在其余一些框架中也有相似功能,但通常没有它简洁而强大。
$("*")//选取全部元素 $("#lastname")//选取id为lastname的元素 $(".intro")//选取全部class="intro"的元素 $("p")//选取全部<p>元素 $(".intro.demo")//选取全部 class="intro"且class="demo"的元素 |
l 链式表达式
在jQuery中,可使用链式表达式来连续操做DOM,若是不使用链式表达式,可能须要这么写:
var neat = $("p.neat"); neat.addClass("ohmy"); neat.show("slow"); |
可是有了链式表达式,一行代码就能够搞定:
$("p.neat").addClass("ohmy").show("slow"); |
除此以外,jQuery还提供了一些动画方面的特效代码,也有大量的外围库,好比jQuery UI这样的控件库,jQuery mobile这样的移动开发库等。
农业时代
这个时代的标志性事件是模块加载规范(AMD以及CMD)的出现。
铁器时代出现的基础框架提供了代码的组织能力,可是未能提供代码的动态加载能力。动态加载JavaScript为何重要呢?由于随着Ajax的普及,jQuery等辅助库的出现,Web上能够作很复杂的功能,所以,单页面应用程序(SPA,Single Page Application)也逐渐多了起来。
单个的界面想要作不少功能,须要写的代码是会比较多的,可是,并不是全部的功能都须要在界面加载的时候就所有引入,若是可以在须要的时候才加载那些代码,就把加载的压力分担了,在这个背景下,出现了一些用于动态加载JavaScript的框架,也出现了一些定义这类可被动态加载代码的规范。
AMD
在这些框架里,知名度最高的是RequireJS,遵循了AMD(Asynchronous Module Definition)的规范。好比下面这段,定义了一个动态的匿名模块,它依赖math模块:
define(["math"],function(math){ return{ addTen :function(x){ return math.add(x,10); } }; }); |
假设上面的代码存放于adder.js中,当须要使用这个模块的时候,经过以下代码来引入adder:
<scriptsrc="require.js"></script> <script> require(["adder"],function(adder){ //使用这个adder }); </script> |
RequireJS除了提供异步加载方式,也可使用同步方式加载模块代码。AMD规范除了使用在前端浏览器环境中,也能够运行于NodeJS等服务端环境,可是NodeJS内置的模块机制是基于CMD规范定义的。
CMD
值得一提的是,在浏览器端,除了RequireJS之外,国内的牛人淘宝玉伯开发了SeaJS异步模块加载器,其遵循CMD规范,目前已经有超过300家大型web应用或站点采用,SeaJS一样简单易学:
// 全部模块都经过 define 来定义 define(function(require, exports, module){ // 经过 require 引入依赖 var $ =require('jquery'); var Spinning =require('./spinning');
// 经过 exports 对外提供接口 exports.doSomething =...
// 或者经过 module.exports 提供整个接口 module.exports =... }); |
工业时代
“这是一个最好的时代,也是一个最坏的时代。”前端自动化和MV*框架真正让前端迎来了春天,可是这个时代框架插件众多、体系繁复,让前端新手无所适从。在这个时代,Web端功能日益复杂,人们不得不开始考虑这样一些问题:
l 如何更好地模块化开发
l 业务数据如何组织
l 界面和业务数据之间经过何种方式进行交互
在这种背景下,前端MVC、MVP、MVVM框架如雨后春笋,咱们暂且把这些框架都统称为MV*框架。这些框架的出现,正是为了解决上述这些问题,具体的实现思路各有不一样,主流的有Backbone,AngularJS,Ember三大剑客,本文主要选用Backbone和AngularJS来说述如下场景。
数据模型
在这些MV*框架里,定义数据模型的方式与以往有些差别,主要在于数据的get和set更加有意义了,好比说,能够把某个实体的get和set绑定到RESTful的服务上,这样,对某个实体的读写能够更新到数据库中。另一个特色是,它们通常都提供一个事件,用于监控数据的变化,这个机制使得数据绑定成为可能。
在一些框架中,数据模型须要在原生的JavaScript类型上作一层封装,好比Backbone的方式是这样:
varTodo=Backbone.Model.extend({ // Default attributes for the todo item. defaults :function(){ return{ title :"empty todo...", order :Todos.nextOrder(), done:false }; },
// Ensure that each todo created has `title`. initialize :function(){ if(!this.get("title")){ this.set({ "title":this.defaults().title }); } },
// Toggle the 'done' state of this todo item. toggle :function(){ this.save({ done:!this.get("done") }); } }); |
上述例子中,defaults方法用于提供模型的默认值,initialize方法用于作一些初始化工做,这两个都是约定的方法,toggle是自定义的,用于保存todo的选中状态。
除了对象,Backbone也支持集合类型,集合类型在定义的时候要经过model属性指定其中的元素类型。
// The collection of todos is backed by *localStorage* instead of a remote server. varTodoList=Backbone.Collection.extend({ // Reference to this collection's model. model :Todo,
// Save all of the todo items under the '"todos-backbone"' namespace. localStorage :newBackbone.LocalStorage("todos-backbone"),
// Filter down the list of all todo items that are finished. done:function(){ returnthis.filter(function(todo){ return todo.get('done'); }); },
// Filter down the list to only todo items that are still not finished. remaining :function(){ returnthis.without.apply(this,this.done()); },
// We keep the Todos in sequential order, despite being saved by unordered //GUID in the database. This generates the next order number for new items. nextOrder :function(){ if(!this.length) return1; returnthis.last().get('order')+1; },
// Todos are sorted by their original insertion order. comparator :function(todo){ return todo.get('order'); } }); |
数据模型也能够包含一些方法,好比自身的校验,或者跟后端的通信、数据的存取等等,在上面两个例子中,也都有体现。
AngularJS的模型定义方式与Backbone不一样,能够不须要通过一层封装,直接使用原生的JavaScript简单数据、对象、数组,相对来讲比较简便。
控制器
在Backbone中,是没有独立的控制器的,它的一些控制的职责都放在了视图里,因此其实这是一种MVP(Model View Presentation)模式,而AngularJS有很清晰的控制器层。
仍是以这个todo为例,在AngularJS中,会有一些约定的注入,好比$scope,它是控制器、模型和视图之间的桥梁。在控制器定义的时候,将$scope做为参数,而后,就能够在控制器里面为它添加模型的支持。
functionTodoCtrl($scope){ $scope.todos =[{ text :'learn angular', done:true },{ text :'build an angular app', done:false }];
$scope.addTodo =function(){ $scope.todos.push({ text : $scope.todoText, done:false }); $scope.todoText =''; };
$scope.remaining =function(){ var count =0; angular.forEach($scope.todos,function(todo){ count += todo.done?0:1; }); return count; };
$scope.archive =function(){ var oldTodos = $scope.todos; $scope.todos =[]; angular.forEach(oldTodos,function(todo){ if(!todo.done) $scope.todos.push(todo); }); }; } |
本例中为$scope添加了todos这个数组,addTodo,remaining和archive三个方法,而后,能够在视图中对他们进行绑定。
视图
在这些主流的MV*框架中,通常都提供了定义视图的功能。在Backbone中,是这样定义视图的:
// The DOM element for a todo item... varTodoView=Backbone.View.extend({ //... is a list tag. tagName :"li",
// Cache the template function for a single item. template: _.template($('#item-template').html()),
// The DOM events specific to an item. events :{ "click .toggle":"toggleDone", "dblclick .view":"edit", "click a.destroy":"clear", "keypress .edit":"updateOnEnter", "blur .edit":"close" },
// The TodoView listens for changes to its model, re-rendering. Since there's // a one-to-one correspondence between a **Todo** and a **TodoView** in this // app, we set a direct reference on the model for convenience. initialize :function(){ this.listenTo(this.model,'change',this.render); this.listenTo(this.model,'destroy',this.remove); },
// Re-render the titles of the todo item. render :function(){ this.$el.html(this.template(this.model.toJSON())); this.$el.toggleClass('done',this.model.get('done')); this.input =this.$('.edit'); returnthis; },
//......
// Remove the item, destroy the model. clear :function(){ this.model.destroy(); } }); |
上面这个例子是一个典型的“部件”视图,它对于界面上的已有元素没有依赖。也有那么一些视图,须要依赖于界面上的已有元素,好比下面这个,它经过el属性,指定了HTML中id为todoapp的元素,而且还在initialize方法中引用了另一些元素,一般,须要直接放置到界面的顶层试图会采用这种方式,而“部件”视图通常由主视图来建立、布局。
// Our overall **AppView** is the top-level piece of UI. varAppView=Backbone.View.extend({ // Instead of generating a new element, bind to the existing skeleton of // the App already present in the HTML. el : $("#todoapp"),
// Our template for the line of statistics at the bottom of the app. statsTemplate : _.template($('#stats-template').html()),
// Delegated events for creating new items, and clearing completed ones. events :{ "keypress #new-todo":"createOnEnter", "click #clear-completed":"clearCompleted", "click #toggle-all":"toggleAllComplete" },
// At initialization we bind to the relevant events on the `Todos` // collection, when items are added or changed. Kick things off by // loading any preexisting todos that might be saved in *localStorage*. initialize :function(){ this.input =this.$("#new-todo"); this.allCheckbox =this.$("#toggle-all")[0];
this.listenTo(Todos,'add',this.addOne); this.listenTo(Todos,'reset',this.addAll); this.listenTo(Todos,'all',this.render);
this.footer =this.$('footer'); this.main = $('#main');
Todos.fetch(); },
// Re-rendering the App just means refreshing the statistics -- the rest // of the app doesn't change. render :function(){ vardone=Todos.done().length; var remaining =Todos.remaining().length;
if(Todos.length){ this.main.show(); this.footer.show(); this.footer.html(this.statsTemplate({ done:done, remaining : remaining })); }else{ this.main.hide(); this.footer.hide(); }
this.allCheckbox.checked=!remaining; },
//...... }); |
对于AngularJS来讲,基本不须要有额外的视图定义,它采用的是直接定义在HTML上的方式,好比:
<divng-controller="TodoCtrl"> <span>{{remaining()}} of {{todos.length}} remaining</span> <ahref=""ng-click="archive()">archive</a> <ulclass="unstyled"> <ling-repeat="todo in todos"> <inputtype="checkbox"ng-model="todo.done"> <spanclass="done-{{todo.done}}">{{todo.text}}</span> </li> </ul> <formng-submit="addTodo()"> <inputtype="text"ng-model="todoText"size="30" placeholder="add new todo here"> <inputclass="btn-primary"type="submit"value="add"> </form> </div> |
在这个例子中,使用ng-controller注入了一个TodoCtrl的实例,而后,在TodoCtrl的$scope中附加的那些变量和方法均可以直接访问了。注意到其中的ng-repeat部分,它遍历了todos数组,而后使用其中的单个todo对象建立了一些HTML元素,把相应的值填到里面。这种作法和ng-model同样,都创造了双向绑定,即:
l 改变模型能够随时反映到界面上
l 在界面上作的操做(输入,选择等等)能够实时反映到模型里。
并且,这种绑定都会自动忽略其中可能由于空数据而引发的异常状况。
模板
模板是这个时代一种很典型的解决方案。咱们经常有这样的场景:在一个界面上重复展现相似的DOM片断,例如微博。以传统的开发方式,也能够轻松实现出来,好比:
var feedsDiv = $("#feedsDiv");
for(var i =0; i <5; i++){ var feedDiv = $("<div class='post'></div>");
var authorDiv = $("<div class='author'></div>"); var authorLink = $("<a></a>") .attr("href","/user.html?user='"+"Test"+"'") .html("@"+"Test") .appendTo(authorDiv); authorDiv.appendTo(feedDiv);
var contentDiv = $("<div></div>") .html("Hello, world!") .appendTo(feedDiv); var dateDiv = $("<div></div>") .html("发布日期:"+newDate().toString()) .appendTo(feedDiv);
feedDiv.appendTo(feedsDiv); } |
可是使用模板技术,这一切能够更加优雅,以经常使用的模板框架UnderScore为例,实现这段功能的代码为:
var templateStr ='<div class="post">' +'<div class="author">' +'<a href="/user.html?user={{creatorName}}">@{{creatorName}}</a>' +'</div>' +'<div>{{content}}</div>' +'<div>{{postedDate}}</div>' +'</div>'; vartemplate= _.template(templateStr); template({ createName :"Xufei", content:"Hello, world", postedDate:newDate().toString() }); |
也能够这么定义:
<scripttype="text/template"id="feedTemplate"> <% _.each(feeds,function(item){%> <div class="post"> <div class="author"> <a href="/user.html?user=<%= item.creatorName %>">@<%= item.creatorName %></a> </div> <div><%= item.content %></div> <div><%= item.postedData %></div> </div> <%});%> </script>
<script> $('#feedsDiv').html( _.template($('#feedTemplate').html(), feeds)); </script> |
除此以外,UnderScore还提供了一些很方便的集合操做,使得模板的使用更加方便。若是你打算使用BackBone框架,而且须要用到模板功能,那么UnderScore是一个很好的选择,固然,也能够选用其它的模板库,好比Mustache等等。
若是使用AngularJS,能够不须要额外的模板库,它自身就提供了相似的功能,好比上面这个例子能够改写成这样:
<divclass="post"ng-repeat="post in feeds"> <divclass="author"> <ang-href="/user.html?user={{post.creatorName}}">@{{post.creatorName}}</a> </div> <div>{{post.content}}</div> <div> 发布日期:{{post.postedTime | date:'medium'}} </div> </div> |
主流的模板技术都提供了一些特定的语法,有些功能很强。值得注意的是,他们虽然与JSP之类的代码写法相似甚至相同,但原理差异很大,这些模板框架都是在浏览器端执行的,不依赖任何服务端技术,即便界面文件是.html也能够,而传统好比JSP模板是须要后端支持的,执行时间是在服务端。
路由
一般路由是定义在后端的,可是在这类MV*框架的帮助下,路由能够由前端来解析执行。好比下面这个Backbone的路由示例:
varWorkspace=Backbone.Router.extend({ routes:{ "help":"help",// #help "search/:query":"search",// #search/kiwis "search/:query/p:page":"search"// #search/kiwis/p7 },
help:function(){ ... },
search:function(query, page){ ... } }); |
在上述例子中,定义了一些路由的映射关系,那么,在实际访问的时候,若是在地址栏输入"#search/obama/p2",就会匹配到"search/:query/p:page"这条路由,而后,把"obama"和"2"看成参数,传递给search方法。
AngularJS中定义路由的方式有些区别,它使用一个$routeProvider来提供路由的存取,每个when表达式配置一条路由信息,otherwise配置默认路由,在配置路由的时候,能够指定一个额外的控制器,用于控制这条路由对应的html界面:
app.config(['$routeProvider', function($routeProvider){ $routeProvider.when('/phones',{ templateUrl :'partials/phone-list.html', controller :PhoneListCtrl }).when('/phones/:phoneId',{ templateUrl :'partials/phone-detail.html', controller :PhoneDetailCtrl }).otherwise({ redirectTo :'/phones' }); }]); |
注意,在AngularJS中,路由的template并不是一个完整的html文件,而是其中的一段,文件的头尾均可以不要,也能够不要那些包含的外部样式和JavaScript文件,这些在主界面中载入就能够了。
自定义组件
用过XAML或者MXML的人必定会对其中的可扩充标签印象深入,对于前端开发人员而言,基于标签的组件定义方式必定是优于其余任何方式的,看下面这段HTML:
<div> <inputtype="text"value="hello, world"/> <button>test</button> </div> |
即便是刚刚接触这种东西的新手,也可以理解它的意思,而且可以照着作出相似的东西,若是使用传统的面向对象语言去描述界面,效率远远没有这么高,这就是在界面开发领域,声明式编程比命令式编程适合的最重要缘由。
可是,HTML的标签是有限的,若是咱们须要的功能不在其中,怎么办?在开发过程当中,咱们可能须要一个选项卡的功能,可是,HTML里面不提供选项卡标签,因此,通常来讲,会使用一些li元素和div的组合,加上一些css,来实现选项卡的效果,也有的框架使用JavaScript来完成这些功能。总的来讲,这些代码都不够简洁直观。
若是可以有一种技术,可以提供相似这样的方式,该多么好呢?
<tabs> <tabname="Tab 1">content 1</tab> <tabname="Tab 2">content 2</tab> </tabs> |
在AngularJS的首页,能够看到这么一个区块“Create Components”,在它的演示代码里,可以看到相似的一段:
<tabs> <panetitle="Localization"> ... </pane> <panetitle="Pluralization"> ... </pane> </tabs> |
那么,它是怎么作到的呢?秘密在这里:
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 }; }) |
这段代码里,定义了tabs和pane两个标签,而且限定了pane标签不能脱离tabs而单独存在,tabs的controller定义了它的行为,二者的template定义了实际生成的html,经过这种方式,开发者能够扩展出本身须要的新元素,对于使用者而言,这不会增长任何额外的负担。