前端MVC框架 EmberJS總結

觀察,計算屬性及綁定的區別

    觀察側重於在關注點屬性發生變化時自動觸發執行一系列響應操做css

    計算屬性側重於依據一些已有屬性來生成一個新屬性,並在依賴屬性發生變化時進行自動更新html

    綁定側重於提供一種在不一樣對象實例之間共享一個屬性的渠道node


    特別的,對於觀察屬性,不一樣類型的觀察對象須要不一樣形式的觀察路徑chrome

## 觀察對象的普通屬性attr
observes('node1.node2.attr')

## 觀察對象的數組屬性arrayXX, 當數組長度變化時觸發
observes('node1.node2.arrayXX.[]')

## 觀察對象的數組屬性arrayXX,當數組長度或數組項的屬性attr發生變化時都觸發
observes('node1.node2.arrayXX.@each.attr')


Handlebars模板引擎

Each循環中的三個Bug

1) 某些情形下#each的內層循環中模板變量獲取不到bootstrap

{{#each parent}}
	{{#each item in child}}
		{{templateVar1}} {{item.templateVar2}}
	{{/each}}
{{/each}}

## 如上代碼,parant數組每一個元素中有child屬性數組及templateVar1屬性,child數組每一個元素有templateVar2屬性。
## 亦即咱們會在模板以前對變量以下作初始化:
parent = [{
		templateVar1: '',
		child: [{templateVar2: ''}, ...],
	}, ... ];
	
## 此時Ember有bug致使內層each循環中不能正確獲取到templateVar1模板變量。

## 通過多番調試,大體找到兩種解決方案。	

## 第一種:外層#each循環必須使用in語法。
## (亦即咱們要避免在第一層未使用in語法,而在第二層中使用in語法)
{{#each one in parent}}
	{{#each item in one.child}}
		{{one.templateVar1}} {{item.templateVar2}}
	{{/each}}
{{/each}}

## 第二種:外層循環中咱們指定一個itemController。
{{#each parent itemController='XXX'}}
	{{#each item in child}}
		{{templateVar1}} {{item.templateVar2}}
	{{/each}}
{{/each}}

2)迭代數組時,數組的變化不會更新each中的元素對象的索引號contentIndex數組

    當咱們在模板中經過#each語法迭代一個數組時,咱們每每經過{{_view.contentIndex}}語法來得到當前循環的index瀏覽器

    不過這個index並不會像咱們預期的那樣,隨着被迭代數組的變化而自動更新。架構

    因此咱們僅僅能夠在第一次模板輸出中使用它,而當數組發生變化時,咱們便不能再在邏輯代碼中依賴這個變量了。app

    若有須要咱們只能經過相似以下代碼的jQuery方法來得到當前對象的index了。dom

$nodes.index($node);

3)each循環中每一個單元控制器的對象屬性會被共用 

    當咱們使用each語法指定itemController後,若是itemController包含對象類型的屬性時,全部itemcontroller實例會共用同一個對象屬性。

    緣由我估計在於每一個被建立並分配給each元素的itemController實例在底層其實是一個itemController類的拷貝對象,但這個拷貝是個淺拷貝的。

    也就是說類內的非對象屬性如整數型、字符串型能正常的拷貝傳值。

    而類內的對象屬性因爲非遞歸性的淺拷貝以及js特有的默認引用傳值特性,形成多個類實例指向了類的同一個對象屬性。

    爲了使每一個控制器都有私有的對象屬性,咱們能夠在控制器的init hook中爲控制器的對象屬性手工初始化一遍值。

    這樣就確保了每一個控制器的對象屬性都是私有的。


模板變量的輸出

    Ember在輸出模板變量時,會默認在變量外圍套上一層script標籤,從而確保變量的自動更新。

    然而當咱們須要在某個html標籤內部輸出一個模板變量時,script標籤便會破壞原有html標籤的結構。

    這是咱們能夠依據應用場景採用兩種方案

## 方案一:變量無需自動更新時,咱們能夠在模板變量上採用unbound語法
{{unbound templateVar}}

## 方案二:變量須要自動更新時,咱們能夠在模板變量上採用bind-attr語法
{{bind-attr attrXX=templateVar}}

## 此外,Ember默認對輸出的變量進行實體字符集的轉義從而避免XSS攻擊
## 那麼當咱們確實有須要在輸出變量中輸出html標籤時,咱們可使用三層花括號語法來關閉模板變量的轉義
{{{templateVar}}}


模板變量的單向綁定和Ember內置組件的雙向綁定 

    Ember模板變量默認會自動更新,可是這種更新是js上下文到Html DOM的單向更新。

    亦即,咱們對控制器或視圖屬性的變更會自動更新到模板變量的輸出。

    反之,若是咱們改變頁面值如html表單組件的值時,這個值並不會自動更新到js上下文中。

    在特定場景如表單輸入組件方面,當咱們須要實現js邏輯與html dom之間的雙向更新時,咱們能夠採用Ember內置的表單輸入組件助手

    如{{input}} 、 {{textarea}} 、 Ember.Select等,它們都能很好的響應頁面輸入並自動更新對應控制器或視圖的屬性


視圖

視圖與組件的對比 

    依據現有經驗,結合國外社區的討論,總結二者的對比

    從最終奧義來說,視圖能實現當前應用內的代碼複用,而組件則能實現應用無關的放之任何場景均可用的代碼複用


    因爲二者意圖的不一樣,視圖代碼會更貼合當前業務需求,但不利於獨立成一個往後可用的工具箱部件

    而組件則會極大的與當前業務需求解耦出來,它僅提供幾個有限的對外接口


    視圖的上下文是和當前所在路由控制器一致的,也就是共用了一套變量環境,並每每會與外部環境的控制器,消息傳遞發生交叉

    組件的上下文則是與外部上下文獨立的(固然,若有必要,組件依賴某些外部變量,仍是能經過屬性傳入的方法來導入)。

    另外組件也不能訪問外部環境的控制器。若是須要消息交互,則應該經過一個主操做接口來向外部環境傳出消息。


    整體來講,不少方面,組件都力圖作到內外部的解耦,充分的獨立。

    雖然這麼作能極方便的使其成爲咱們往後可用工具箱的一個部件,可是因爲較大的脫離了業務邏輯以及上下環境的隔離,其實現每每須要更多的代碼

    許多時候,還須要咱們處理一些細節來繞過組件特性帶來的約束。

   

建立一個不在視圖樹中的任意視圖,如對話框 

    Ember應用中,那些經過路由、容器視圖、視圖助手等等管理的視圖,都是構建在一個完整視圖樹層級中,並能被chrome瀏覽器中的Ember Ispector檢測到的
    然而有時候咱們可能會須要建立一個不在視圖樹層級中的視圖,某些場景下這每每能帶來邏輯實現上的方便
    例如一個對話框,官網教程cookbook上對於對話框的實現涉及了outlet插口,路由actions,並且這仍是個未集成上效果美觀的jQuery對話框插件的初步效果。咋看起來實現上仍是略略麻煩,且將UI的操做放進路由裏處理不太符合MVC的概念,這種界面上的響應操做我推薦寫進視圖的actions中
    下面講講我推薦的作法,

## 首先咱們能夠看到,一個對話框在應用中每每沒有明確的節點位置、視圖層級關係,那麼咱們能夠構建一個不在視圖樹層次中的視圖來實現它
## 同時咱們能夠手動指定一個控制器給它以提供合適的上下文環境,須要注意的是咱們必須傳入一個變量容器container給它(不然控制檯會報出一個反對信息)
## 最後,對於一個無層級的視圖,咱們須要經過調用視圖的append方法將其追加到body節點中
App.DialogView.create({
	controller: XXXX,
	container: this.container,
}).append();

## 當視圖插入到body中後,也就是說dialog的主體dom內容已經ready了,接下去咱們須要經過一個jQuery對話框插件將其彈出
## 最後,因爲對話框關閉時,僅僅是經過js的方式將其css式隱藏而沒有銷燬對話框的視圖對象
## 爲了不下次彈出對話框時重複,咱們這裏須要手動地在對話框關閉時候,將這個對話框視圖銷燬掉
App.DialogView.extend = Em.View.extend({
	didInsertElement: function(){
		this._super();

		var self = this;

		this.$().dialog({
			height: 200,
			width: 500,
			draggable: true,
			resizable: false,
			modal: true,
			title: "請選擇競爭車系......",
			close: function(event, ui){
				self.destroy();
			},
	}
});

## 最後還要提醒的是,因爲這個對話框是不在視圖層級的,因此Ember Ispector中調試時候,咱們是觀察不到它的
## 咱們須要手動的在console中輸出調試信息或者加debug斷點來測試它


時序問題:didInsertElement和Em.run的區別與各自應用場景

Ember提供了兩套邏輯來對應用生命週期的各個時間點進行管理

    經過生命週期鉤子對一個視圖view的生命週期進行管理,包括了willInsertElement、didInsertElement、willDestroyElement、willClearRender、becameVisible、becameHidden六個視圖層次的生命週期鉤子

    經過運行時循環對應用的一個事件響應週期進行管理,包括了sync, actions, routerTransitions, render, afterRender, destroy六個運行時隊列


一般的,咱們用的較多的分別是didInsertElement鉤子與afterRender運行時隊列

在didInsertElement中的操做確保了當前視圖及其父視圖已經ready,可是不能確保其子視圖的ready。
而afterRender運行時隊列確保了應用當前全部的運行視圖已經ready。

舉例一個應用場景,如商品列表頁面上有一組篩選項,它的結構是一個大容器視圖包含了許多個篩選項視圖,咱們但願在篩選項都渲染出來後,進行一個初始化操做,將部分篩選項臨時收拉起來。

首先咱們就發現不能在大容器視圖進入didInsertElement鉤子即容器視圖ready後進行初始化操做,由於此時篩選項做爲其子視圖尚未ready。那麼最後,咱們其實能夠在大容器視圖的didInsertElement鉤子中調度一個afterRender運行時隊列,這樣就確保了大容器及篩選項視圖的ready,並進一步的進行初始化操做:

App.FiltersContainerView = Em.View.extend({
		didInsertElement: function(){
			this._super();

			Em.run.afterRender('afterRender', this, function(){
				#初始化操做,經過調度afterRender隊列來等待子篩選項視圖的ready
			});
		}
	});


Bootstrap插件的啓動(需經過Jquery來啓動)

按bootstrap的官方文檔以下推薦:

「你能夠僅僅經過 data 屬性 API 就能使用全部的 Bootstrap 插件,無需寫一行 JavaScript代碼。
這是 Bootstrap 中的一等 API,也應該是你的首選方式。」

然而因爲EmberMVC特有的渲染機制(將不一樣的模塊模板封裝在script塊中,在運行時編譯成模板函數,渲染時輸出相應內容),咱們只能摒棄Bootstrap官方的推薦。 

由於在bootstrap初始化DataAPI的時候,每每的,Ember的渲染引擎還未向頁面輸出相應的DOM元素。 固然的,bootstrap的DataAPI也就不能成功啓動了。

所以,考慮到Ember應用中的時序問題,咱們只能手動的經過Bootstrap的原生JqueryAPI來啓動bootstrap插件,

OK,在時序問題的影響下,插件的啓動方法基本上就是在Bootstrap插件的包裝視圖中處理。 

大致就是在視圖的didInsertElement鉤子中註冊一個"afterRender"的運行循環操做。更多的細節請參看後續的EmberMVC分享。 


路由驅動控制器及視圖的生命週期

一個Ember應用中的控制器或者視圖區分爲路由驅動的(即路由自動建立分配的)及手工建立管理的(即咱們直接賦值的控制器)

不一樣的情形,它們的生命週期長度是不一樣的

路由驅動的控制器或視圖

    它們的生命週期是從進入路由被建立起至應用的結束。也就說它們的 inti() 鉤子在整個Ember應用的生命週期只會執行一次。只要Ember應用仍然運行着,那麼這些路由驅動的控制器或視圖的實例一旦建立後,便始終駐留在內存中,而不論你的路由切出與切回與否,因此咱們會發現路由幾經跳轉又返回某路由後,那些咱們先前給那個路由的控制器或視圖自定義的屬性如今還在實例中。

    特殊的,對於路由驅動的視圖,當應用路由切出後再切換回來,雖然其實例在第一次進入路由時已經建立並一直駐留在內存至今,但其view的state狀態還會被更新(destroyed -> preRender -> inBuffer -> inDOM),並繼續觸發其自身的6個生命週期鉤子(指didInsertElement等等鉤子)

    更爲特殊的,若是應用向當前路由路徑觸發一次再過渡,則view的六個生命週期鉤子如didInsertElement等等是不會被調用的,由於視圖view已經存在於內存中,且因爲沒有發生路由變化,其state狀態也沒有被更新,那麼view的六個依賴於state變化觸發的生命鉤子固然就不會被調用了 

建立管理的控制器或視圖

    一旦路由切換後,伴隨着它們所表明的dom內容的消失,相應的控制器或視圖的實例也會被從內存中銷燬,也就是說,每次路由的切回,它們的 init() 鉤子始終會被調用


合理部署actions到控制器、視圖

Ember應用響應用戶消息時會產生多種多樣的請求操做,合理的將這些請求處理分門別類劃分進合適的位置是頗有必要的,也是MVC模式所要求的
個人建議是將純界面性的操做處理劃入視圖view的actions中實現
將涉及數據請求的操做處理劃入控制器controller的actions中實現,若是會有界面上的更新,則還需考慮到數據與表現的分離
這樣就作到控制器和視圖各自合理的受理特定請求操做


控制器視圖的互訪

    view能夠很方便的獲取到相應的controller:

this.get('controller')

    controller中得到對應的view分爲兩種狀況

## 1) 路由驅動的控制器得到相應視圖對象 ##
this.get('view)

## 2) 手工指定的控制器及視圖 ##
# 這種狀況稍費周折,推薦的方法是在view的didInsertElement鉤子中,將視圖自身註冊到controller的view屬性中
didInsertElement:function(){
		this._super();
		this.get('controller').set('view', this);
	}
# 以後經過以下代碼在控制器中得到視圖對象
this.get('view')

## 控制器訪問視圖對象其餘偏門的方法還有兩種,但僅推薦在測試時候使用
1, container.lookup('router:main')._activeViews['路由名'][0]
2, Em.View.views[視圖DOM的id]


數據與表現分離的兩種構型

數據與表現分離是軟件設計中很重要的一點。在Ember應用中能夠經過兩種途徑實現

#each助手迭代特定數組屬性 

    這種方法是官方文檔上提出的一種解決方案 若是應用場景是一系列平行的、列表式的數據須要展現出來,這時便適合經過#each助手來實現。
    具體的應用場景如論壇帖子列表等等,每條帖子的模型數據,相互間沒有相關性,依賴性。

視圖中註冊observe觀察控制器中的特定屬性 

    若是應用場景是後續加入的數據會不斷將先前的數據刷爲髒數據,最新的展現不只僅依賴於當前獲取到的數據,同時依賴於先前加載了的數據。

    那麼,官方文檔的#each方法便不適用了,這時候我認爲可使用observes觀察器來處理。

    具體的應用場景例若有一張圖表,每次圖表刷新都依賴以前的數據, 這時候就不適合用each了, 須要咱們存儲每次的圖表數據到控制器的數組屬性中, 並在視圖中使用觀察器觀察該數組數據, 在數組變化時,自動響應刷新圖表


Promise針對不一樣http響應狀態的處理

一個承諾的執行狀況分爲resolve和reject,僅當http resbonse的status code爲200時,承諾纔是resolve的。也就是說通常意義上的202等等成功碼也會被認定是reject的。

一個承諾的執行狀況決定了在進入下一個then()鏈後是調用resolve仍是reject回調。

不管一個承諾是resolve仍是reject的承諾,只要其對外返回了值,就會被認定是fullfill履行的。這一點就會影響到路由中的model鉤子了。當model鉤子返回一個非fullfill的承諾,就會中止Ember應用的過渡。當model鉤子返回一個fullfill的鉤子時,Ember應用纔會進行完整的路由過渡並渲染出頁面。

須要留意的是,一個reject的承諾能夠fullfill返回值使路由繼續進行下去,但這個承諾依然是個reject性質的,若是有下一個then鏈,承諾將會運行進入下一個then鏈中的reject方法。



分享某項目二級模塊架構圖


大致說下系統運行流程:

一切從視圖的didInsertElement鉤子開始, 視圖向控制器發出消息, 控制器就會作一些數據請求操做, 這些操做會去調用模型接口得到數據並將數據寫入控制器的數組屬性中, 因爲這些數組又是被視圖中觀察着的, 因此當數組增值時, 視圖即刻自動響應並根據這些數據作一些重繪操做

相關文章
相關標籤/搜索