前端開發技術,從狹義的定義來看,是指圍繞HTML、JavaScript、CSS這樣一套體系的開發技術,它的運行宿主是瀏覽器。從廣義的定義來看,包括了: 專門爲手持終端設計的相似WML這樣的類HTML語言,相似WMLScript這樣的類JavaScript語言。html
- VML和SVG等基於XML的描述圖形的語言。
- 從屬於XML體系的XML,XPath,DTD等技術。
- 用於支撐後端的ASP,JSP,ASP.net,PHP,nodejs等語言或者技術。
- 被第三方程序打包的一種相似瀏覽器的宿主環境,好比Adobe AIR和使用HyBird方式的一些開發技術,如PhoneGap(它使用Android中的WebView等技術,讓開發人員使用傳統Web開發技術來開發本地應用)
- Adobe Flash,Flex,Microsoft Silverlight,Java Applet,JavaFx等RIA開發技術。
本文從狹義的前端定義出發,探討一下這方面開發技術的發展過程。前端 從前端開發技術的發展來看,大體能夠分爲如下幾個階段:node 一. 刀耕火種數據庫 1. 靜態頁面編程 最先期的Web界面基本都是在互聯網上使用,人們瀏覽某些內容,填寫幾個表單,而且提交。當時的界面以瀏覽爲主,基本都是HTML代碼,有時候穿插一些JavaScript,做爲客戶端校驗這樣的基礎功能。代碼的組織比較簡單,並且CSS的運用也是比較少的。後端 最簡單的是這樣一個文件:數組
<html> <head> <title>測試一</title> </head> <body> <h1>主標題</h1> <p>段落內容</p> </body> </html> |
2. 帶有簡單邏輯的界面瀏覽器 這個界面帶有一段JavaScript代碼,用於拼接兩個輸入框中的字符串,而且彈出窗口顯示。前端框架
html> <head> <title>測試二</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. 結合了服務端技術的混合編程 因爲靜態界面不能實現保存數據等功能,出現了不少服務端技術,早期的有CGI(Common Gateway Interface,多數用C語言或者Perl實現的),ASP(使用VBScript或者JScript),JSP(使用Java),PHP等等,Python和Ruby等語言也常被用於這類用途。 有了這類技術,在HTML中就可使用表單的post功能提交數據了,好比:
<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>
|
在這個階段,因爲客戶端和服務端的職責未做明確的劃分,好比生成一個字符串,能夠由前端的JavaScript作,也能夠由服務端語言作,因此一般在一個界面裏,會有兩種語言混雜在一塊兒,用<%和%>標記的部分會在服務端執行,輸出結果,甚至常常有把數據庫鏈接的代碼跟頁面代碼混雜在一塊兒的狀況,給維護帶來較大的不便。
<html> <body> <p>Hello world!</p> <p> <% response.write("Hello world from server!") %> </p> </body> </html>
|
4. 組件化的萌芽 這個時代,也逐漸出現了組件化的萌芽。比較常見的有服務端的組件化,好比把某一類服務端功能單獨作成片斷,而後其餘須要的地方來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的出現。 1. AJAX AJAX實際上是一系列已有技術的組合,早在這個名詞出現以前,這些技術的使用就已經比較普遍了,GMail由於恰當地應用了這些技術,得到了很好的用戶體驗。 因爲Ajax的出現,規模更大,效果更好的Web程序逐漸出現,在這些程序中,JavaScript代碼的數量迅速增長。出於代碼組織的須要,「JavaScript框架」這個概念逐步造成,當時的主流是prototype和mootools,這二者各有千秋,提供了各自方式的面向對象組織思路。 2. JavaScript基礎庫 Prototype框架主要是爲JavaScript代碼提供了一種組織方式,對一些原生的JavaScript類型提供了一些擴展,好比數組、字符串,又額外提供了一些實用的數據結構,如:枚舉,Hash等,除此以外,還對dom操做,事件,表單和Ajax作了一些封裝。 Mootools框架的思路跟Prototype很接近,它對JavaScript類型擴展的方式別具一格,因此在這類框架中,常常被稱做「最優雅的」對象擴展體系。 從這兩個框架的所提供的功能來看,它們的定位是核心庫,在使用的時候通常須要配合一些外圍的庫來完成。 jQuery與這二者有所不一樣,它着眼於簡化DOM相關的代碼。例如: ?DOM的選擇 jQuery提供了一系列選擇器用於選取界面元素,在其餘一些框架中也有相似功能,可是通常沒有它的簡潔、強大。
$("*") //選取全部元素 $("#lastname") //選取id爲lastname的元素 $(".intro") //選取全部class="intro"的元素 $("p") //選取全部<p>元素 $(".intro.demo") //選取全部 class="intro"且class="demo"的元素 |
在jQuery中,可使用鏈式表達式來連續操做dom,好比下面這個例子: 若是不使用鏈式表達式,可能咱們須要這麼寫:
var neat = $("p.neat"); neat.addClass("ohmy"); neat.show("slow");
|
可是有了鏈式表達式,咱們只須要這麼一行代碼就能夠完成這些:
$("p.neat").addClass("ohmy").show("slow");
|
除此以外,jQuery還提供了一些動畫方面的特效代碼,也有大量的外圍庫,好比jQuery UI這樣的控件庫,jQuery mobile這樣的移動開發庫等等。 3. 模塊代碼加載方式 以上這些框架提供了代碼的組織能力,可是未能提供代碼的動態加載能力。動態加載JavaScript爲何重要呢?由於隨着Ajax的普及,jQuery等輔助庫的出現,Web上能夠作很複雜的功能,所以,單頁面應用程序(One Page One Application)也逐漸多了起來。 單個的界面想要作不少功能,須要寫的代碼是會比較多的,可是,並不是全部的功能都須要在界面加載的時候就所有引入,若是可以在須要的時候才加載那些代碼,就把加載的壓力分擔了,在這個背景下,出現了一些用於動態加載JavaScript的框架,也出現了一些定義這類可被動態加載代碼的規範 在這些框架裏,知名度比較高的是RequireJS,它遵循一種稱爲AMD(Asynchronous Module Definition)的規範。 好比下面這段,定義了一個動態的匿名模塊,它依賴math模塊
define(["math"], function(math) { return { addTen : function(x) { return math.add(x, 10); } }; });
|
假設上面的代碼存放於adder.js中,當須要使用這個模塊的時候,經過以下代碼來引入adder:
<script src="require.js"></script> <script> require(["adder"], function(adder) { //使用這個adder }); </script>
|
RequireJS除了提供異步加載方式,也可使用同步方式加載模塊代碼。AMD規範除了使用在前端瀏覽器環境中,也能夠運行於nodejs等服務端環境,nodejs的模塊就是基於這套規範定義的。(修訂,這裏弄錯了,nodejs是基於相似的CMD規範的) 三. 工業革命 這個時期,隨着Web端功能的日益複雜,人們開始考慮這樣一些問題:
- 如何更好地模塊化開發
- 業務數據如何組織
- 界面和業務數據之間經過何種方式進行交互
在這種背景下,出現了一些前端MVC、MVP、MVVM框架,咱們把這些框架統稱爲MV*框架。這些框架的出現,都是爲了解決上面這些問題,具體的實現思路各有不一樣,主流的有Backbone,AngularJS,Ember,Spine等等,本文主要選用Backbone和AngularJS來說述如下場景。 1. 數據模型 在這些框架裏,定義數據模型的方式與以往有些差別,主要在於數據的get和set更加有意義了,好比說,能夠把某個實體的get和set綁定到RESTful的服務上,這樣,對某個實體的讀寫能夠更新到數據庫中。另一個特色是,它們通常都提供一個事件,用於監控數據的變化,這個機制使得數據綁定成爲可能。 在一些框架中,數據模型須要在原生的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方法用於提供模型的默認值,initialize方法用於作一些初始化工做,這兩個都是約定的方法,toggle是自定義的,用於保存todo的選中狀態。 除了對象,Backbone也支持集合類型,集合類型在定義的時候要經過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'); } });
|
數據模型也能夠包含一些方法,好比自身的校驗,或者跟後端的通信、數據的存取等等,在上面兩個例子中,也都有體現。 AngularJS的模型定義方式與Backbone不一樣,能夠不須要通過一層封裝,直接使用原生的JavaScript簡單數據、對象、數組,相對來講比較簡便。 2. 控制器 在Backbone中,是沒有獨立的控制器的,它的一些控制的職責都放在了視圖裏,因此其實這是一種MVP(Model View Presentation)模式,而AngularJS有很清晰的控制器層。 仍是以這個todo爲例,在AngularJS中,會有一些約定的注入,好比$scope,它是控制器、模型和視圖之間的橋樑。在控制器定義的時候,將$scope做爲參數,而後,就能夠在控制器裏面爲它添加模型的支持。
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這個數組,addTodo,remaining和archive三個方法,而後,能夠在視圖中對他們進行綁定。 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(); } });
|
上面這個例子是一個典型的「部件」視圖,它對於界面上的已有元素沒有依賴。也有那麼一些視圖,須要依賴於界面上的已有元素,好比下面這個,它經過el屬性,指定了HTML中id爲todoapp的元素,而且還在initialize方法中引用了另一些元素,一般,須要直接放置到界面的頂層試圖會採用這種方式,而「部件」視圖通常由
// 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; }, //...... });
|
對於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> |
在這個例子中,使用ng-controller注入了一個TodoCtrl的實例,而後,在TodoCtrl的$scope中附加的那些變量和方法均可以直接訪問了。注意到其中的ng-repeat部分,它遍歷了todos數組,而後使用其中的單個todo對象建立了一些HTML元素,把相應的值填到裏面。這種作法和ng-model同樣,都創造了雙向綁定,即: ?改變模型能夠隨時反映到界面上 ?在界面上作的操做(輸入,選擇等等)能夠實時反映到模型裏。 並且,這種綁定都會自動忽略其中可能由於空數據而引發的異常狀況。 4. 模板 模板是這個時期一種很典型的解決方案。咱們經常有這樣的場景:在一個界面上重複展現相似的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("發佈日期:" + new Date().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>'; 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是一個很好的選擇,固然,也能夠選用其它的模板庫,好比Mustache等等。 若是使用AngularJS,能夠不須要額外的模板庫,它自身就提供了相似的功能,好比上面這個例子能夠改寫成這樣:
<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> 發佈日期:{{post.postedTime | date:'medium'}} </div> </div> |
主流的模板技術都提供了一些特定的語法,有些功能很強。值得注意的是,他們雖然與JSP之類的代碼寫法相似甚至相同,但原理差異很大,這些模板框架都是在瀏覽器端執行的,不依賴任何服務端技術,即便界面文件是.html也能夠,而傳統好比JSP模板是須要後端支持的,執行時間是在服務端。 5. 路由 一般路由是定義在後端的,可是在這類MV*框架的幫助下,路由能夠由前端來解析執行。好比下面這個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) { ... } }); |
在上述例子中,定義了一些路由的映射關係,那麼,在實際訪問的時候,若是在地址欄輸入"#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文件,這些在主界面中載入就能夠了。 6. 自定義標籤 用過XAML或者MXML的人必定會對其中的可擴充標籤印象深入,對於前端開發人員而言,基於標籤的組件定義方式必定是優於其餘任何方式的,看下面這段HTML:
<div> <input type="text" value="hello, world"/> <button>test</button> </div>
|
即便是剛剛接觸這種東西的新手,也可以理解它的意思,而且可以照着作出相似的東西,若是使用傳統的面嚮對象語言去描述界面,效率遠遠沒有這麼高,這就是在界面開發領域,聲明式編程比命令式編程適合的最重要緣由。 可是,HTML的標籤是有限的,若是咱們須要的功能不在其中,怎麼辦?在開發過程當中,咱們可能須要一個選項卡的功能,可是,HTML裏面不提供選項卡標籤,因此,通常來講,會使用一些li元素和div的組合,加上一些css,來實現選項卡的效果,也有的框架使用JavaScript來完成這些功能。總的來講,這些代碼都不夠簡潔直觀。 若是可以有一種技術,可以提供相似這樣的方式,該多麼好呢?
<tabs> <tab name="Tab 1">content 1</tab> <tab name="Tab 2">content 2</tab> </tabs> |
回憶一下,咱們在章節1.4 組件化的萌芽 裏面,提到過一種叫作HTC的技術,這種技術提供了相似的功能,並且使用起來也比較簡便,問題是,它屬於一種正在消亡的技術,因而咱們的目光投向了更爲現代的前端世界,AngularJS拯救了咱們。 在AngularJS的首頁,能夠看到這麼一個區塊「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兩個標籤,而且限定了pane標籤不能脫離tabs而單獨存在,tabs的controller定義了它的行爲,二者的template定義了實際生成的html,經過這種方式,開發者能夠擴展出本身須要的新元素,對於使用者而言,這不會增長任何額外的負擔。 四. 一些想說的話 關於ExtJS 注意到在本文中,並未說起這樣一個比較流行的前端框架,主要是由於他自成一系,思路跟其餘框架不一樣,所作的事情,層次介於文中的二和三之間,因此沒有單獨列出。 寫做目的 在我10多年的Web開發生涯中,經歷了Web相關技術的各類變革,從2003年開始,接觸並使用到了HTC,VML,XMLHTTP等當時比較先進的技術,目擊了網景瀏覽器的衰落,IE的後來居上,Firefox和Chrome的逆襲,各種RIA技術的風起雲涌,對JavaScript的模塊化有過持續的思考。將來到底是什麼樣子?我說不清楚,只能憑本身的一些認識,把這些年一些比較主流的發展過程總結一下,供有須要瞭解的朋友們做個參考,錯漏在所不免,歡迎你們指教。 |