前端MVC框架 EmberJS总结

观察,计算属性及绑定的区别

    观察侧重于在关注点属性发生变化时自动触发执行一系列响应操作

    计算属性侧重于依据一些已有属性来生成一个新属性,并在依赖属性发生变化时进行自动更新

    绑定侧重于提供一种在不同对象实例之间共享一个属性的渠道


    特别的,对于观察属性,不同类型的观察对象需要不同形式的观察路径

## 观察对象的普通属性attr
observes('node1.node2.attr')

## 观察对象的数组属性arrayXX, 当数组长度变化时触发
observes('node1.node2.arrayXX.[]')

## 观察对象的数组属性arrayXX,当数组长度或数组项的属性attr发生变化时都触发
observes('[email protected]')


Handlebars模板引擎

Each循环中的三个Bug

1) 某些情形下#each的内层循环中模板变量获取不到

{{#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并不会像我们预期的那样,随着被迭代数组的变化而自动更新。

    所以我们仅仅可以在第一次模板输出中使用它,而当数组发生变化时,我们便不能再在逻辑代码中依赖这个变量了。

    如有需要我们只能通过类似如下代码的jQuery方法来获得当前对象的index了。

$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方法。



分享某项目二级模块架构图

前端MVC框架 EmberJS总结_第1张图片


大体说下系统运行流程:

一切从视图的didInsertElement钩子开始, 视图向控制器发出消息, 控制器就会做一些数据请求操作, 这些操作会去调用模型接口获得数据并将数据写入控制器的数组属性中, 由于这些数组又是被视图中观察着的, 所以当数组增值时, 视图即刻自动响应并根据这些数据做一些重绘操作


你可能感兴趣的:(mvc,Ember)