前端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钩子开始, 视图向控制器发出消息, 控制器就会作一些数据请求操做, 这些操做会去调用模型接口得到数据并将数据写入控制器的数组属性中, 因为这些数组又是被视图中观察着的, 因此当数组增值时, 视图即刻自动响应并根据这些数据作一些重绘操做

相关文章
相关标签/搜索