介紹javascript MVC框架:ember框架的基本概念
write by yinmingjun,引用請註明。
在看過knockout和angular之後,有些意猶未盡,感覺在web領域內對SPA的探索不應該止步於此,於是開始翻看ember.js框架,希望ember.js能給我帶來驚喜。ember.js在web上的資源較少,官方文件覆蓋度和深度不夠,很多細節需要到程式碼中尋找答案。不過迴歸到ember.js框架本身,這個框架的確帶給我很多驚喜,讓我會有寫文字介紹它的慾望。本文只從框架和概念的角度來解讀ember.js,對於ember.js各個部分的深入研究會寫在後續的文章之中。
在最初看ember的核心概念解釋的時候,看到router這個詞這讓我有些詫異,難道。。。。。。
在進一步瞭解ember之後,證實了我的懷疑:ember確實是通過router作為核心概念來組織其MVC體系架構的。這種方式與目前在伺服器端流行的一些MVC框架(ASP.NET MVC、Django或express等)的處理方式相似,當router的概念出現在面向客戶端的ember之中的時候,我們基本上可以猜測出來,ember已經將頁面間的狀態遷移納入自己的問題領域。
此外,ember詳細的規劃了model、view和controller的職責劃分,並支援雙向的資料繫結。
ember的另外一個讓我意外的地方是ember.data。客戶端的js框架很少會涉足實體關係的領域,筆者曾經在所在的公司開做過類似的JS實體關係對映體系,深知其複雜度和價值。ember將實體的定義和實體關係納入其問題領域,是眾多JS開發者的福音,ember.data有助於我們從資料管理的細節中解放出來
在讀到這裡的時候,大家會不會有這樣的感覺:ember會不會太重了?ember的js尺寸(v1.0.0版本)壓縮過的有200K,未壓縮過的有800K,的確驚人,還不包括ember.data(v0.13版本,250K,70K尺寸)和handlebar的js尺寸。這些訊息可以給我們一些啟示,ember是面向未來的、重型的web應用支撐框架,使用的時候需要良好的設計和規劃,如果不清楚這個定位也許會導致ember使用上的失敗經歷,這點請大家明白。
接下來進入正題,我們會逐步介紹ember裡面的核心概念,瞭解ember的應用程式的架構。
一、從DEMO開始
下面是來自ember官網的一個DEMO,我們看一下程式碼。
HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Ember Starter Kit</title>
<link rel="stylesheet" href="css/normalize.css">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<script type="text/x-handlebars">
<h2>Welcome to Ember.js</h2>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
<ul> {{#each item in model}}
<li>{{item}}</li>
{{/each}}
</ul>
</script>
<script src="js/libs/jquery-1.9.1.js"></script>
<script src="js/libs/handlebars-1.0.0-rc.4.js"></script>
<script src="js/libs/ember-1.0.0-rc.5.js"></script>
<script src="js/app.js"></script>
</body>
</html>
app.js:
App = Ember.Application.create();
App.Router.map( function() {
// put your routes here
});
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
}
});
例子很簡單,只是建立一個ember的appliation,然後將model中的資料依次的繫結到li之中。不過應用架構的角度來看,這個例子覆蓋了ember中的主要的功能。需要注意,script標記的型別是text/x-handlebars。Ember在載入頁面時,會抓取這些內容。
在這個例子中,我們基本上可以看到ember應用架構模式的主要脈絡。
1、預設的'/'到App.IndexRoute的對映
省略的等效程式碼:
App.Router.map( function() { this.resource( 'index', { path: '/' } ); });
2、預設的建立IndexRoute的controller
省略的程式碼:
App.IndexRoute = Ember.Route.extend({
setupController: function(controller) {
controller.set('content', ['red', 'yellow', 'blue']);
}
});
3、預設的application template
省略了applicaion的template名稱:
<script type="text/x-handlebars" data-template-name="application">
<h1>Application Template</h1>
{{outlet}}
</script>
4、App.IndexRoute對名字為'index'的template的引用
5、程式啟動後對{{outlet}}和'index'的template的繫結
這種name mapping的方式可以大幅度減少配置的工作量,在現代的MVC體系中廣泛採用,ember也引入了這種處理方式。後面我們會詳細的闡述ember的name mapping的方式。
實際上,如果最簡單的建立一個ember應用,僅需要一行程式碼:
App = Ember.Application.create({});
而ember會在後面預設的加上下面的程式碼:
// Create the application namespace
App = Ember.Application.create({});
// Create the global router to manage page state via URLs
App.Router.map( function() {});
// Create the default application route to set application-level state properties
App.ApplicationRoute = Ember.Route.extend({});
// Create the default application template
<script type="text/x-handlebars" data-template-name="application">
{{outlet}}
</script>
從這個demo中,我們能看到Application,Router,Template,Controller,Model等概念,這些概念我們會在下一節簡單的闡述。
二、ember的基本概念
為了容易理解,這幾個概念會盡可能簡單的描述,儘量不引入複雜的因素。
1、application的概念
在ember中,建立applicaion非常的簡單,只需要一行程式碼:
window.App = Ember.Application.create();
App的變數名字是什麼都可以,建立後的app中可以作為應用級別狀態和資料的載體。例如,建立了一個App中的view,可以這麼寫:
App.MyView = Ember.View.extend();
預設的情況下,呼叫Ember.Application.create()方法會自動的觸發Ember.Application.initialize()方法的呼叫,也可以通過配置來調整application的預設的行為。
在呼叫Ember.Application.create()方法(注1)的時候,可以傳遞一個object進去,以配置&調整應用建立的預設的行為。一般會在傳遞給create方法的中的obect中指定rootElement、ready事件的handler等。
注1:說明一下,create方法是ember的物件體系中的基礎服務
2、router的概念
router是Ember.Router類的例項,通過application例項的Router成員訪問。
在router中,有兩個概念,一個是resource,代表資源定位;另外一個是route,表示特定的頁面路由分發;
從簡單的開始,先看看如何新增route:
App.Router.map(function() {
this.route("about", { path: "/about" });
this.route("favorites", { path: "/favs" });
});
這段程式碼實際上映射了以下的對應關係:
URL Route Name Controller Route Template
/ index IndexConroller IndexRoute index
/about about AboutController AboutRoute about
/favs favorites FavoritesController FavoritesRoute favorites
其中,index的對映是預設的。
再看看resource是怎麼維護的:
App.Router.map(function() {
this.resource('posts', { path: '/posts' }, function() {
this.route('new');
});
});
其實如果resource的名字和path的名字是相同的,可以省略path部分的引數描述,下面是更簡單的寫法:
App.Router.map(function() {
this.resource('posts', function() {
this.route('new');
});
});
上面的程式碼實際上會產生下面的對映關係:
URL Route Name Controller Route Template
/ index IndexConroller IndexRoute index
N/A posts PostsController PostsRoute posts
/posts posts.index PostsController PostsRoute posts
->PostsIndexController ->PostsIndexRoute ->posts/index
/posts/new posts.new PostsController PostsRoute posts
->postsNewController ->PostsNewController ->posts/new
route和resource兩個概念,route用於處理具體的URL引數,resource用於做資源的重新定位。使用resource有助於將應用分解成多個小的區域,每個區域獨立的處理頁面的狀態遷移,這種結構是對大專案的任務分解和團隊協作有利的。
3、controller的概念
在ember中,controller是template和model之間的橋樑,將model中的資料轉換成面向template的顯示的資料。總的來說,model是面向server的,而controller是面向template的,擁有template需要顯示的資料,也包含template需要執行的操作。
ember的controller和ViewModel的概念有些相似。
如果一個controller是一個ArrayController,就可以在template中使用{{#each controller}}的語法。
在application層面上也存在controller,不過ember會給application提供預設的例項。如果需要處理application層面上的資料繫結和響應使用者操作,可以這麼做:
HTML:
<!-- application.handlebars -->
<header>
{{view Ember.TextField valueBinding="search" action="query"}}
</header>
{{outlet}}
JS:
App.ApplicationController = Ember.Controller.extend({
// the initial value of the `search` property
search: '',
query: function() {
// the current value of the text field
var query = this.get('search');
this.transitionToRoute('search', { query: query });
}
});
上門的程式碼為application提供了一個ApplicationController,響應application層面上的search資料請求和query請求操作。
注2:extend方法是ember物件體系提供的基礎服務
4、model的概念
在ember中,model的概念模糊而抽象,不容易理解。ember的model是純資料的載體,從簡單的層面上來看,model就是一個JSON的資料結構;從ember框架定義的角度上看,model對應後端的server上的資料,是實體和實體關係在客戶端的表達。為了更好的處理資料方面的資料管理的需求,ember.js專門開發了ember.data來處理更高層次的資料服務。
在ember的應用體系中,model的初值來自提供給route的model方法的返回值,並通過route的deserialize方法將獲取到的model資料填充到route的currentModel成員之中(currentModel可以理解為model的快照),而在route對資料的維護上來看currentModel是context最初的資料來源和最終的資料載體,某種意義上的等價物(特點的時間點上等價),而route的currentModel(或其context)成員最終會作為controller的content成員投放到controller之中。conroller中,model成員是其content的別名。
其同步的順序如下:
route.model
--->route.currentModel && route.context
--->controller.content && controller.model
另外,route的currentModel中資料是陣列還是物件,將會決定ember產生的controller的型別,對於array產生Ember.ArrayController的例項;對於object產生Ember.ObjectController的例項;其他會以Ember.Controller作為模版。
5、templat的概念
ember的template是建立在handlebars的基礎上的,也就是說,在ember的template中可以使用handlebars支援的語法來書寫template。真正屬於ember領域的問題是template和資料提供者controller之間的對應關係,其通過template的script標記的data-template-name屬性來指定:
<script type="text/x-handlebars" data-template-name="index">
<ul>
{{#each item in model}}
<li>{{item}}</li>
{{/each}}
</ul>
</script>
上面定義的template和名字是index的controller之間建立了對應關係。
如果需要操縱ArrayController中的內容,可以按下面的方式來書寫程式碼。
HTML:
<script type="text/x-handlebars" data-template-name="index">
<ul>
{{#each item in model}}
<li>{{item}} {{controller.postFix}}</li>
{{/each}}
</ul>
<button {{action doIncrease}}> doIncrease </button>
</script>
JS:
App.IndexController = Ember.Controller.extend(
{
postFix: 'post',
'content': ['aaa', 'bbb'],
doIncrease: function() {
var content = this.get('content');
content.pushObject('demo');
}
});
ember對屬性是通過get、set包裝器和觀察者模式來管理的,對controller的資料變化,需要通過對應的方法來發布變更通知,這裡是ember包裝的陣列的pushObject方法來觸發。ember包裝的陣列還有其他類似的方法,詳細內容參考ember的API文件。
註解:
需要主要的是ArrayController的特殊性,如果route的model中的資料是陣列,那麼ember的each helper會針對each塊內的內容建立子view,並將當前列舉項的內容作為子view的content屬性;所在的索引作為view的contentIndex屬性。ember的each helper也會針對陣列中的每個元素建立一個數據繫結的上下文,並將列舉的元素(這裡是item)、controller和view作為子view(就是each塊內的模版內容)的資料訪問的上下文。如果ArrayController中的陣列元素不是Controller的例項,那麼上下文中的controller就是父view上下文中的controller(這裡是App.IndexController);否則子view的controller是ArrayController的content中的陣列元素中的controller。
handlebars的語法和資料定位的方式,我轉了一篇文章,被小編遮蔽了,不過web上很多,大家可以自己找找。
6、view的概念
ember的view是基於handlebars的擴充套件,其背後是有兩種推動力:
1、是對特定事件的監聽和過濾需要(支援雙向的資料繫結的需求);
2、是避免大量重複的書寫template中的內容;
接下來,我們還是通過例子來了解ember中view的概念吧。
例子1:
通過view的append方法來將view新增到DOM。
HTML:
<script type="text/x-handlebars" data-template-name="say-hello">
Hello, <b>{{view.name}}</b>
</script>
JS:
App.MyView = Ember.View.create({
templateName: 'say-hello',
name: "YMJ"
});
App.MyView.append();
上面這段程式碼中,view通過templateName指定了使用的DOM模版,然後在模版中訪問view中的資料,然後通過其append方法將view釋出到document.body的元素的最後。view還有類似的appendTo方法(通過jquery的appendTo實現),將view釋出到指定的位置。
上面是使用view的一種方式。
例子2:
另外一直使用view的方式是在template中使用view。
HTML:
<script type="text/x-handlebars" data-template-name="say-hello">
Hello, <b>{{view.name}}</b>
</script>
<script type="text/x-handlebars" data-template-name="index">
{{view App.MyView}}
<ul>
{{#each item in model}}
<li>{{item}} {{controller.postFix}}</li>
{{/each}}
</ul>
<button {{action doIncrease}}> doIncrease </button>
</script>
JS:
App.MyView = Ember.View.create({
templateName: 'say-hello',
name: "YMJ"
});
上面的JS部分只給出了必要的程式碼。view和其DOM的template之間的關係還是通過其templateName的成員來指出,關鍵是在index的template中,我們通過view helper來引入了App.MyView的例項,這樣在template中輸出了App.MyView的內容。
ember內建了很多view,實際上ember的雙向的資料繫結就是基於view的概念實現的。
ember內建的view如下:
> Ember.Checkbox
> Ember.TextField
> Ember.Button
> Ember.TextArea
> Ember.SelectOption
> Ember.Select
詳細的API的說明可以參考ember的官方文件,我們這裡只看一個小例子,看看如何做資料繫結:
<label>
{{view Ember.CheckboxcheckedBinding="model.isDone"}}
{{model.title}}
</label>
view相關的深入研究,我們在其他文件中來探討,作為view的使用者,大概瞭解到上面這些內容,基本上就可以動手寫東西了。
三、小結
上面,我們在概念的層面和ember.js做了一個親密的接觸,將ember.js框架中所有的核心概念闡述了一遍,希望讀者能對ember.js有一個整體上的認識,知道這是一個什麼框架,可以做那些事情。
在接觸到的javascript MVC框架中,ember可能是最野心勃勃的一個,從目前我看到的事實是它試圖接管客戶端的一切,對MVC體系的有全方位的支援,相比angular來說更全面、更復雜,對應小組工作提供更多的概念支援,是架構師比較樂於採用的前端框架。ember.js的文件支援方面,很難讓人滿意。線上文件的深度和廣度都不夠,很多東西需要到程式碼中找答案,ember在文件層面落後於angular等其他框架。
在框架的選擇上來看,很難說那個框架更優秀,結合應用領域或問題領域來選擇,可能會有更明確的結果。對應大型&複雜的應用,我本人會傾向於選擇ember.js,因為我對ember足夠的瞭解,其他人或許應考慮ember的文件方面的不足。
後面會寫一些東西,對ember的各個分支領域做相對深入的分析。