引自:http://blog.jobbole.com/41988/
前端開發(fā)技術(shù),從狹義的定義來看,是指圍繞HTML、JavaScript、CSS這樣一套體系的開發(fā)技術(shù),它的運(yùn)行宿主是瀏覽器。從廣義的定義來看,包括了:
專門為手持終端設(shè)計(jì)的類似WML這樣的類HTML語言,類似WMLScript這樣的類JavaScript語言。
VML和SVG等基于XML的描述圖形的語言。
從屬于XML體系的XML,XPath,DTD等技術(shù)。
用于支撐后端的ASP,JSP,ASP.net,PHP,nodejs等語言或者技術(shù)。
被第三方程序打包的一種類似瀏覽器的宿主環(huán)境,比如Adobe AIR和使用HyBird方式的一些開發(fā)技術(shù),如PhoneGap(它使用Android中的WebView等技術(shù),讓開發(fā)人員使用傳統(tǒng)Web開發(fā)技術(shù)來開發(fā)本地應(yīng)用)
Adobe Flash,F(xiàn)lex,Microsoft Silverlight,Java Applet,JavaFx等RIA開發(fā)技術(shù)。
本文從狹義的前端定義出發(fā),探討一下這方面開發(fā)技術(shù)的發(fā)展過程。
從前端開發(fā)技術(shù)的發(fā)展來看,大致可以分為以下幾個(gè)階段:
一. 刀耕火種
1.靜態(tài)頁面
最早期的Web界面基本都是在互聯(lián)網(wǎng)上使用,人們?yōu)g覽某些內(nèi)容,填寫幾個(gè)表單,并且提交。當(dāng)時(shí)的界面以瀏覽為主,基本都是HTML代碼,有時(shí)候穿插一些JavaScript,作為客戶端校驗(yàn)這樣的基礎(chǔ)功能。代碼的組織比較簡單,而且CSS的運(yùn)用也是比較少的。
最簡單的是這樣一個(gè)文件:
<html>
<head>
<title>測(cè)試一</title>
</head>
<body>
<h1>主標(biāo)題</h1>
<p>段落內(nèi)容</p>
</body>
</html>
2.帶有簡單邏輯的界面
這個(gè)界面帶有一段JavaScript代碼,用于拼接兩個(gè)輸入框中的字符串,并且彈出窗口顯示。
<html>
<head>
<title>測(cè)試二</title>
</head>
<body>
<input id="firstNameInput" type="text" />
<input id="lastNameInput" type="text" />
<input type="button" onclick="greet()" />
<script language="JavaScript">
function greet() {
var firstName = document.getElementById("firstNameInput").value;
var lastName = document.getElementById("lastNameInput").value;
alert("Hello, " + firstName + "." + lastName);
}
</script>
</body>
</html>
3.結(jié)合了服務(wù)端技術(shù)的混合編程
由于靜態(tài)界面不能實(shí)現(xiàn)保存數(shù)據(jù)等功能,出現(xiàn)了很多服務(wù)端技術(shù),早期的有CGI(Common Gateway Interface,多數(shù)用C語言或者Perl實(shí)現(xiàn)的),ASP(使用VBScript或者JScript),JSP(使用Java),PHP等等,Python和Ruby等語言也常被用于這類用途。
有了這類技術(shù),在HTML中就可以使用表單的post功能提交數(shù)據(jù)了,比如:
<form method="post" action="username.asp">
<p>First Name: <input type="text" name="firstName" /></p>
<p>Last Name: <input type="text" name="lastName" /></p>
<input type="submit" value="Submit" />
</form>
在這個(gè)階段,由于客戶端和服務(wù)端的職責(zé)未作明確的劃分,比如生成一個(gè)字符串,可以由前端的JavaScript做,也可以由服務(wù)端語言做,所以通常在一個(gè)界面里,會(huì)有兩種語言混雜在一起,用<%和%>標(biāo)記的部分會(huì)在服務(wù)端執(zhí)行,輸出結(jié)果,甚至經(jīng)常有把數(shù)據(jù)庫連接的代碼跟頁面代碼混雜在一起的情況,給維護(hù)帶來較大的不便。
<html>
<body>
<p>Hello world!</p>
<p>
<%
response.write("Hello world from server!")
%>
</p>
</body>
</html>
4.組件化的萌芽
這個(gè)時(shí)代,也逐漸出現(xiàn)了組件化的萌芽。比較常見的有服務(wù)端的組件化,比如把某一類服務(wù)端功能單獨(dú)做成片段,然后其他需要的地方來include進(jìn)來,典型的有:ASP里面數(shù)據(jù)庫連接的地方,把數(shù)據(jù)源連接的部分寫成conn.asp,然后其他每個(gè)需要操作數(shù)據(jù)庫的asp文件包含它。
上面所說的是在服務(wù)端做的,瀏覽器端通常有針對(duì)JavaScript的,把某一類的Javascript代碼寫到單獨(dú)的js文件中,界面根據(jù)需要,引用不同的js文件。針對(duì)界面的組件方式,通常利用frameset和iframe這兩個(gè)標(biāo)簽。某一大塊有獨(dú)立功能的界面寫到一個(gè)html文件,然后在主界面里面把它當(dāng)作一個(gè)frame來載入,一般的B/S系統(tǒng)集成菜單的方式都是這樣的。
此外,還出現(xiàn)了一些基于特定瀏覽器的客戶端組件技術(shù),比如IE瀏覽器的HTC(HTML Component)。這種技術(shù)最初是為了對(duì)已有的常用元素附加行為的,后來有些場(chǎng)合也用它來實(shí)現(xiàn)控件。微軟ASP.net的一些版本里,使用這種技術(shù)提供了樹形列表,日歷,選項(xiàng)卡等功能。HTC的優(yōu)點(diǎn)是允許用戶自行擴(kuò)展HTML標(biāo)簽,可以在自己的命名空間里定義元素,然后,使用HTML,JavaScript和CSS來實(shí)現(xiàn)它的布局、行為和觀感。這種技術(shù)因?yàn)槭俏④浀乃接屑夹g(shù),所以逐漸變得不那么流行。
Firefox瀏覽器里面推出過一種叫XUL的技術(shù),也沒有流行起來。
二. 鐵器時(shí)代
這個(gè)時(shí)代的典型特征是Ajax的出現(xiàn)。
1.AJAX
AJAX其實(shí)是一系列已有技術(shù)的組合,早在這個(gè)名詞出現(xiàn)之前,這些技術(shù)的使用就已經(jīng)比較廣泛了,GMail因?yàn)榍‘?dāng)?shù)貞?yīng)用了這些技術(shù),獲得了很好的用戶體驗(yàn)。
由于Ajax的出現(xiàn),規(guī)模更大,效果更好的Web程序逐漸出現(xiàn),在這些程序中,JavaScript代碼的數(shù)量迅速增加。出于代碼組織的需要,“JavaScript框架”這個(gè)概念逐步形成,當(dāng)時(shí)的主流是prototype和mootools,這兩者各有千秋,提供了各自方式的面向?qū)ο蠼M織思路。
2.JavaScript基礎(chǔ)庫
Prototype框架主要是為JavaScript代碼提供了一種組織方式,對(duì)一些原生的JavaScript類型提供了一些擴(kuò)展,比如數(shù)組、字符串,又額外提供了一些實(shí)用的數(shù)據(jù)結(jié)構(gòu),如:枚舉,Hash等,除此之外,還對(duì)dom操作,事件,表單和Ajax做了一些封裝。
Mootools框架的思路跟Prototype很接近,它對(duì)JavaScript類型擴(kuò)展的方式別具一格,所以在這類框架中,經(jīng)常被稱作“最優(yōu)雅的”對(duì)象擴(kuò)展體系。
從這兩個(gè)框架的所提供的功能來看,它們的定位是核心庫,在使用的時(shí)候一般需要配合一些外圍的庫來完成。
jQuery與這兩者有所不同,它著眼于簡化DOM相關(guān)的代碼。 例如:
DOM的選擇
jQuery提供了一系列選擇器用于選取界面元素,在其他一些框架中也有類似功能,但是一般沒有它的簡潔、強(qiáng)大。
$("*") //選取所有元素
$("#lastname") //選取id為lastname的元素
$(".intro") //選取所有class="intro"的元素
$("p") //選取所有<p>元素
$(".intro.demo") //選取所有 class="intro"且class="demo"的元素
鏈?zhǔn)奖磉_(dá)式:
在jQuery中,可以使用鏈?zhǔn)奖磉_(dá)式來連續(xù)操作dom,比如下面這個(gè)例子:
如果不使用鏈?zhǔn)奖磉_(dá)式,可能我們需要這么寫:
var neat = $("p.neat");
neat.addClass("ohmy");
neat.show("slow");
但是有了鏈?zhǔn)奖磉_(dá)式,我們只需要這么一行代碼就可以完成這些:
$("p.neat").addClass("ohmy").show("slow");
除此之外,jQuery還提供了一些動(dòng)畫方面的特效代碼,也有大量的外圍庫,
比如jQuery UI這樣的控件庫,jQuery mobile這樣的移動(dòng)開發(fā)庫等等。
3.模塊代碼加載方式
以上這些框架提供了代碼的組織能力,但是未能提供代碼的動(dòng)態(tài)加載能力。動(dòng)態(tài)加載JavaScript為什么重要呢?因?yàn)殡S著Ajax的普及,jQuery等輔助庫的出現(xiàn),Web上可以做很復(fù)雜的功能,因此,單頁面應(yīng)用程序(SPA,Single Page Application)也逐漸多了起來。
單個(gè)的界面想要做很多功能,需要寫的代碼是會(huì)比較多的,但是,并非所有的功能都需要在界面加載的時(shí)候就全部引入,如果能夠在需要的時(shí)候才加載那些代碼,就把加載的壓力分擔(dān)了,在這個(gè)背景下,出現(xiàn)了一些用于動(dòng)態(tài)加載JavaScript的框架,也出現(xiàn)了一些定義這類可被動(dòng)態(tài)加載代碼的規(guī)范。
在這些框架里,知名度比較高的是RequireJS,它遵循一種稱為AMD(Asynchronous Module Definition)的規(guī)范。
比如下面這段,定義了一個(gè)動(dòng)態(tài)的匿名模塊,它依賴math模塊
define(["math"], function(math) {
return {
addTen : function(x) {
return math.add(x, 10);
}
};
});
假設(shè)上面的代碼存放于adder.js中,當(dāng)需要使用這個(gè)模塊的時(shí)候,通過如下代碼來引入adder:
<script src="require.js"></script>
<script>
require(["adder"], function(adder) {
//使用這個(gè)adder
});
</script>
RequireJS除了提供異步加載方式,也可以使用同步方式加載模塊代碼。AMD規(guī)范除了使用在前端瀏覽器環(huán)境中,也可以運(yùn)行于nodejs等服務(wù)端環(huán)境,nodejs的模塊就是基于這套規(guī)范定義的。(修訂,這里弄錯(cuò)了,nodejs是基于類似的CMD規(guī)范的)
三. 工業(yè)革命
這個(gè)時(shí)期,隨著Web端功能的日益復(fù)雜,人們開始考慮這樣一些問題:
如何更好地模塊化開發(fā)
業(yè)務(wù)數(shù)據(jù)如何組織
界面和業(yè)務(wù)數(shù)據(jù)之間通過何種方式進(jìn)行交互
在這種背景下,出現(xiàn)了一些前端MVC、MVP、MVVM框架,我們把這些框架統(tǒng)稱為MV*框架。這些框架的出現(xiàn),都是為了解決上面這些問題,具體的實(shí)現(xiàn)思路各有不同,主流的有Backbone,AngularJS,Ember,Spine等等,本文主要選用Backbone和AngularJS來講述以下場(chǎng)景。
1.數(shù)據(jù)模型
在這些框架里,定義數(shù)據(jù)模型的方式與以往有些差異,主要在于數(shù)據(jù)的get和set更加有意義了,比如說,可以把某個(gè)實(shí)體的get和set綁定到RESTful的服務(wù)上,這樣,對(duì)某個(gè)實(shí)體的讀寫可以更新到數(shù)據(jù)庫中。另外一個(gè)特點(diǎn)是,它們一般都提供一個(gè)事件,用于監(jiān)控?cái)?shù)據(jù)的變化,這個(gè)機(jī)制使得數(shù)據(jù)綁定成為可能。
在一些框架中,數(shù)據(jù)模型需要在原生的JavaScript類型上做一層封裝,比如Backbone的方式是這樣:
var Todo = 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方法用于提供模型的默認(rèn)值,initialize方法用于做一些初始化工作,這兩個(gè)都是約定的方法,toggle是自定義的,用于保存todo的選中狀態(tài)。
除了對(duì)象,Backbone也支持集合類型,集合類型在定義的時(shí)候要通過model屬性指定其中的元素類型。
// The collection of todos is backed by *localStorage* instead of a remote server.
var TodoList = Backbone.Collection.extend({
// Reference to this collection's model.
model : Todo,
// Save all of the todo items under the '"todos-backbone"' namespace.
localStorage : new Backbone.LocalStorage("todos-backbone"),
// Filter down the list of all todo items that are finished.
done : function() {
return this.filter(function(todo) {
return todo.get('done');
});
},
// Filter down the list to only todo items that are still not finished.
remaining : function() {
return this.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)
return 1;
return this.last().get('order') + 1;
},
// Todos are sorted by their original insertion order.
comparator : function(todo) {
return todo.get('order');
}
});
數(shù)據(jù)模型也可以包含一些方法,比如自身的校驗(yàn),或者跟后端的通訊、數(shù)據(jù)的存取等等,在上面兩個(gè)例子中,也都有體現(xiàn)。
AngularJS的模型定義方式與Backbone不同,可以不需要經(jīng)過一層封裝,直接使用原生的JavaScript簡單數(shù)據(jù)、對(duì)象、數(shù)組,相對(duì)來說比較簡便。
2.控制器
在Backbone中,是沒有獨(dú)立的控制器的,它的一些控制的職責(zé)都放在了視圖里,所以其實(shí)這是一種MVP(Model View Presentation)模式,而AngularJS有很清晰的控制器層。
還是以這個(gè)todo為例,在AngularJS中,會(huì)有一些約定的注入,比如$scope,它是控制器、模型和視圖之間的橋梁。在控制器定義的時(shí)候,將$scope作為參數(shù),然后,就可以在控制器里面為它添加模型的支持。
function TodoCtrl($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這個(gè)數(shù)組,addTodo,remaining和archive三個(gè)方法,然后,可以在視圖中對(duì)他們進(jìn)行綁定。
3.視圖
在這些主流的MV*框架中,一般都提供了定義視圖的功能。在Backbone中,是這樣定義視圖的:
// The DOM element for a todo item...
var TodoView = 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');
return this;
},
//......
// Remove the item, destroy the model.
clear : function() {
this.model.destroy();
}
});
上面這個(gè)例子是一個(gè)典型的“部件”視圖,它對(duì)于界面上的已有元素沒有依賴。也有那么一些視圖,需要依賴于界面上的已有元素,比如下面這個(gè),它通過el屬性,指定了HTML中id為todoapp的元素,并且還在initialize方法中引用了另外一些元素,通常,需要直接放置到界面的頂層試圖會(huì)采用這種方式,而“部件”視圖一般由主視圖來創(chuàng)建、布局。
// Our overall **AppView** is the top-level piece of UI.
var AppView = 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() {
var done = 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;
},
//......
});
對(duì)于AngularJS來說,基本不需要有額外的視圖定義,它采用的是直接定義在HTML上的方式,比如:
<div ng-controller="TodoCtrl">
<span>{{remaining()}} of {{todos.length}} remaining</span>
<a href="" ng-click="archive()">archive</a>
<ul class="unstyled">
<li ng-repeat="todo in todos">
<input type="checkbox" ng-model="todo.done">
<span class="done-{{todo.done}}">{{todo.text}}</span>
</li>
</ul>
<form ng-submit="addTodo()">
<input type="text" ng-model="todoText" size="30"
placeholder="add new todo here">
<input class="btn-primary" type="submit" value="add">
</form>
</div>
在這個(gè)例子中,使用ng-controller注入了一個(gè)TodoCtrl的實(shí)例,然后,在TodoCtrl的$scope中附加的那些變量和方法都可以直接訪問了。注意到其中的ng-repeat部分,它遍歷了todos數(shù)組,然后使用其中的單個(gè)todo對(duì)象創(chuàng)建了一些HTML元素,把相應(yīng)的值填到里面。這種做法和ng-model一樣,都創(chuàng)造了雙向綁定,即:
改變模型可以隨時(shí)反映到界面上
在界面上做的操作(輸入,選擇等等)可以實(shí)時(shí)反映到模型里。
而且,這種綁定都會(huì)自動(dòng)忽略其中可能因?yàn)榭諗?shù)據(jù)而引起的異常情況。
4.模板
模板是這個(gè)時(shí)期一種很典型的解決方案。我們常常有這樣的場(chǎng)景:在一個(gè)界面上重復(fù)展示類似的DOM片段,例如微博。以傳統(tǒng)的開發(fā)方式,也可以輕松實(shí)現(xiàn)出來,比如:
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("發(fā)布日期:" + new Date().toString())
.appendTo(feedDiv);
feedDiv.appendTo(feedsDiv);
}
但是使用模板技術(shù),這一切可以更加優(yōu)雅,以常用的模板框架UnderScore為例,實(shí)現(xiàn)這段功能的代碼為:
var templateStr = '<div class="post">'
+'<div class="author">'
+ '<a href="/user.html?user={{creatorName}}">@{{creatorName}}</a>'
+'</div>'
+'<div>{{content}}</div>'
+'<div>{{postedDate}}</div>'
+'</div>';
var template = _.template(templateStr);
template({
createName : "Xufei",
content: "Hello, world",
postedDate: new Date().toString()
});
也可以這么定義:
<script type="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是一個(gè)很好的選擇,當(dāng)然,也可以選用其它的模板庫,比如Mustache等等。
如果使用AngularJS,可以不需要額外的模板庫,它自身就提供了類似的功能,比如上面這個(gè)例子可以改寫成這樣:
<div class="post" ng-repeat="post in feeds">
<div class="author">
<a ng-href="/user.html?user={{post.creatorName}}">@{{post.creatorName}}</a>
</div>
<div>{{post.content}}</div>
<div>
發(fā)布日期:{{post.postedTime | date:'medium'}}
</div>
</div>
主流的模板技術(shù)都提供了一些特定的語法,有些功能很強(qiáng)。值得注意的是,他們雖然與JSP之類的代碼寫法類似甚至相同,但原理差別很大,這些模板框架都是在瀏覽器端執(zhí)行的,不依賴任何服務(wù)端技術(shù),即使界面文件是.html也可以,而傳統(tǒng)比如JSP模板是需要后端支持的,執(zhí)行時(shí)間是在服務(wù)端。
5.路由
通常路由是定義在后端的,但是在這類MV*框架的幫助下,路由可以由前端來解析執(zhí)行。比如下面這個(gè)Backbone的路由示例:
var Workspace = 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) {
...
}
});
在上述例子中,定義了一些路由的映射關(guān)系,那么,在實(shí)際訪問的時(shí)候,如果在地址欄輸入”#search/obama/p2″,就會(huì)匹配到”search/:query/p:page”這條路由,然后,把”obama”和”2″當(dāng)作參數(shù),傳遞給search方法。
AngularJS中定義路由的方式有些區(qū)別,它使用一個(gè)$routeProvider來提供路由的存取,每一個(gè)when表達(dá)式配置一條路由信息,otherwise配置默認(rèn)路由,在配置路由的時(shí)候,可以指定一個(gè)額外的控制器,用于控制這條路由對(duì)應(yīng)的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并非一個(gè)完整的html文件,而是其中的一段,文件的頭尾都可以不要,也可以不要那些包含的外部樣式和JavaScript文件,這些在主界面中載入就可以了。
6.自定義標(biāo)簽
用過XAML或者M(jìn)XML的人一定會(huì)對(duì)其中的可擴(kuò)充標(biāo)簽印象深刻,對(duì)于前端開發(fā)人員而言,基于標(biāo)簽的組件定義方式一定是優(yōu)于其他任何方式的,看下面這段HTML:
<div>
<input type="text" value="hello, world"/>
<button>test</button>
</div>
即使是剛剛接觸這種東西的新手,也能夠理解它的意思,并且能夠照著做出類似的東西,如果使用傳統(tǒng)的面向?qū)ο笳Z言去描述界面,效率遠(yuǎn)遠(yuǎn)沒有這么高,這就是在界面開發(fā)領(lǐng)域,聲明式編程比命令式編程適合的最重要原因。
但是,HTML的標(biāo)簽是有限的,如果我們需要的功能不在其中,怎么辦?在開發(fā)過程中,我們可能需要一個(gè)選項(xiàng)卡的功能,但是,HTML里面不提供選項(xiàng)卡標(biāo)簽,所以,一般來說,會(huì)使用一些li元素和div的組合,加上一些css,來實(shí)現(xiàn)選項(xiàng)卡的效果,也有的框架使用JavaScript來完成這些功能??偟膩碚f,這些代碼都不夠簡潔直觀。
如果能夠有一種技術(shù),能夠提供類似這樣的方式,該多么好呢?
<tabs>
<tab name="Tab 1">content 1</tab>
<tab name="Tab 2">content 2</tab>
</tabs>
回憶一下,我們?cè)谡鹿?jié)1.4 組件化的萌芽 里面,提到過一種叫做HTC的技術(shù),這種技術(shù)提供了類似的功能,而且使用起來也比較簡便,問題是,它屬于一種正在消亡的技術(shù),于是我們的目光投向了更為現(xiàn)代的前端世界,AngularJS拯救了我們。
在AngularJS的首頁,可以看到這么一個(gè)區(qū)塊“Create Components”,在它的演示代碼里,能夠看到類似的一段:
<tabs>
<pane title="Localization">
...
</pane>
<pane title="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兩個(gè)標(biāo)簽,并且限定了pane標(biāo)簽不能脫離tabs而單獨(dú)存在,tabs的controller定義了它的行為,兩者的template定義了實(shí)際生成的html,通過這種方式,開發(fā)者可以擴(kuò)展出自己需要的新元素,對(duì)于使用者而言,這不會(huì)增加任何額外的負(fù)擔(dān)。
四. 一些想說的話
關(guān)于ExtJS
注意到在本文中,并未提及這樣一個(gè)比較流行的前端框架,主要是因?yàn)樗猿梢幌?,思路跟其他框架不同,所做的事情,層次介于文中的二和三之間,所以沒有單獨(dú)列出。
寫作目的
在我10多年的Web開發(fā)生涯中,經(jīng)歷了Web相關(guān)技術(shù)的各種變革,從2003年開始,接觸并使用到了HTC,VML,XMLHTTP等當(dāng)時(shí)比較先進(jìn)的技術(shù),目睹了網(wǎng)景瀏覽器的衰落,IE的后來居上,F(xiàn)irefox和Chrome的逆襲,各類RIA技術(shù)的風(fēng)起云涌,對(duì)JavaScript的模塊化有過持續(xù)的思考。未來究竟是什么樣子?我說不清楚,只能憑自己的一些認(rèn)識(shí),把這些年一些比較主流的發(fā)展過程總結(jié)一下,供有需要了解的朋友們作個(gè)參考,錯(cuò)漏在所難免,歡迎大家指教。