大多数 Ember 教程都告诉我们 Ember 有 partial
view
render
outlet
component
等等等等,让人眼花缭乱之余,除了赞叹:“哇!好灵活!!好强大!!”意外,剩下的就是:“尼玛……到底什么时候用什么呀?”
我用一个实际案例测试了以上诸多方法的使用过程和效果,略有感悟。为后来计,记录于此。
需求
需求很简单,在没有资源(路由)嵌套的前提下,在一个单一页面显示一个以上的数据来源,且这些来源没有关联关系,获取它们必然是通过两个数据源,也就是不同的两个 API resource。
因为只有一个路由可用,所以要灵活的利用多种模版/视图的渲染方式,考虑到对数据后续处理的扩展性,还有可能要指定多个控制器(根据不同的实现方法)。
举例来说,一个页面(好比首页),左边显示时间线,列举当天将要发生的事件;右边显示所有事件的分类列表。当天的事件和所有事件分类列表显然是不同的且没有直接关联的数据源。
实现
Note: 本文不是最佳实践教程,而是所有可能的实现记录。或许你会觉得某种方法太白痴,若是你做肯定不会这么玩——你也许是对的。我不做结论,只做记录。
结构准备
- HTML(只列举 Ember 部分)
- JS(written in CoffeeScript)
App = Ember.Application.create()
App.IndexRoute = Ember.Route.extend()
So far,就需要这么多。开整~
1. Partials
先从 partials 开始,因为它的灵活性最低,变数也最少。Partial 就是一个纯粹的 Handlebars 模版,partial 中的数据来源自它被渲染的那个模版的作用域。因为作用域是由 controller 提供的,所以在我们的例子里,若是把 partials 渲染在 index
模版内,那么它们的数据也只能从 index
的作用域里获得。
为了使用 partials 需要做两件事,一是分离模版,而是提供数据。
- 分离模版的时候要注意 partials 的命名一定要用
_
开头,但是可以置入子路径,_foo
或者bar/_foo
这样都行。
现在两个分离出来的 partials 一起渲染进了 index
模版,但是 currentTime
和 categoryName
这两个值是来自不同数据源的,怎么办?
- 当你自定义数据源的时候,一定要记得正确的处理异步请求(利用 Promieses),用 jQuery 的请求已经封装成了类 Promise 的对象,但是 jQuery 的实现略有瑕疵,详情请参考
Ember.RSVP
的 API 文档。在本例中,我直接用Ember.RSVP
构造一个伪异步请求。
App.IndexRoute = Ember.Route.extend
model: ->
Ember.RSVP.hash
currentTime: new Date().toISOString()
categoryName: 'Category No.1'
利用 IndexRoute
的 model
挂钩封装两个不同的数据源,它们将一起传递给 IndexController
,于是我们就可以直接用了。
Partials 就是这么简单!
http://jsfiddle.net/nightire/XBYkp/
提示:旧版本的 Ember 还有一种类似的 helper 叫
{{template}}
,但是在{{partial}}
出现之后就被废弃了。不要把它们弄混了。
2. Views
Partials 虽然简单,但是几乎没有可定制性,特别是在 UI 的部分。如果我们想定制 DOM 结构(不想写死成 HTML)或者想定制数据渲染时的表现形式该怎么办?比如说 currentTime
挺不友好的,我想把他处理成适合人阅读的样子该怎么办?这种时候就适合 Views 出马了。
提示:实际中你可能更倾向于自定义 Handlebars Helper 来处理数据的实际表现形式,但本文不探讨这个,见谅。
以下是 Partials 和 Views 的主要区别:
- 当使用 partials 的时候,比如
{{partial timeline}}
- Ember 不会为你创建
TimelineView
的实例; - 你只能使用名称为
_timeline
的模板。
- Ember 不会为你创建
- 当使用 views 的时候,比如
{{view App.TimelineView}}
- Ember 会为你创建
TimelineView
的实例; - 具体使用的模版是可以在
TimelineView
里定义的,由于 views 支持嵌套,所以你还可以进一步拆分,弄出子模版等花样来。
- Ember 会为你创建
OK,理论太抽象,咱上代码:
App.TimelineView = Ember.View.extend
templateName: 'timeline'
tagName: 'section'
classNames: ['column', 'timeline']
timeNow: Ember.computed ->
moment(@get 'currentTime').format 'L'
App.CategoriesView = Ember.View.extend
templateName: 'categories'
tagName: 'section'
classNames: ['column', 'timeline']
Views 的灵活性是很大的(主要体现在对 DOM 和交互的处理上),但很多时候也没有必要搞得这么繁琐的。相比之前的 Partials,这些改变主要体现了如下的变化:
- #1:这里我使用了 Blockless 的 Handlebars Helper,实际上这不是唯一的选择,在这里只是为了演示自定义
View
的代码。你可以选择把一些属性丢到 helpers 里来写,这个度自己权衡。 - #2:我改写了之前
timeline
里的currentTime
属性,用 Moment.js 把它处理的好看了些。从下面的 coffee 代码可以看到,views 也没有改变作用域,数据的来源依然是当前的 model 和 当前的 controller,只是我们可以把“装潢”类的逻辑分离开来。 - #3: 因为使用 views 可以扩展
Ember.View
这个类,所以一些静态的标签和属性就可以从模版里去掉了,你甚至可以继承出Ember.View
的子类,把相似的部分抽象出来。
除此之外,Ember.View
还有很多灵活之处可以利用,但是相关的内容太多了,这篇文章无法全部覆盖,还是细细读文档才是。还有,views 有一个“复数版本”叫做 collections,按理说我的时间线应该呈现的是一个数组(若干结构相同的时间点),所以我应该用 collections 才对。只是本例中我就不建立这么复杂的 model 了。
http://jsfiddle.net/nightire/5JMgd/
注意:因为 jsfiddle 有 Bug,导致我这部分的代码在其中不能正常工作,所以我不得不使用定义
IndexView
的子视图技巧来完成这个 Demo。下边放出了在 jsbin 写的另外一个版本,实现的功能一模一样,但是不需要写额外的子视图,和正文的代码是一样的。
http://jsbin.com/gerof/1/
3. Renders
刚才讲 views 的时候,有一个最重要的问题没交代,就是特意要引出接下来的 renders,这个问题就是,请看下图:
我用 Ember Inspector 查看两个 views 的渲染结构,如果你仔细观察就会发现 TimelineView
里面有 categoryName
这个属性,反过来也一样,在 CategoriesView
里也会有 currentTime
属性。其实这本来不是问题,因为大家的数据都是从 IndexRoute
里拿到的嘛!但是我们分离这两块的意义就在于它们本来就是没有关联的数据实体,万一以后出现重名属性怎么办?或者出现有歧义的属性名怎么办?(这其实也还有别的办法来解决,但这里主要是为了带出 renders,所以别太较真)
所以我们更加需要的是一个能把数据分离的展示层,之前的 partials 和 views 都无法做到这一点是因为它们共享了入口模版的作用域,它们自己并没有自己的控制器,而这就是 renders 和它们的最大区别,除此之外,它和 views 如出一辙。
如果我们对比下 view 和 render 的用法,我们会更明了:
{{view path[, options]}}
{{render path[, context, options]}}
唯一的区别是 context
,它是什么呢?实际上 context
就类似于 route 传递给 controller 的 model
,大部分情况下,你可以将 context
与 model
视作等价(实际上它们不是的,但这是一个相当复杂的话题)。那么在这里,context
就是我们要传给 controller 的数据对象了,记住:这里的 controller 就是 renders 自己的 controller。
为了配合对于 renders 的演示,我要先把模拟的异步数据请求稍微改动一下:
App.IndexRoute = Ember.Route.extend
model: ->
Ember.RSVP.hash
timeline:
currentTime: new Date().toISOString()
categories:
categoryName: 'Category No.1'
然后我们使用和刚才使用 views 时一样的模板,因为 renders 也会使用 Ember.View:
到了这一步,你已经可以看到和之前使用 views 一样的效果了。接下来,我们尝试把 {{view.timeNow}}
转移到 controller 里面去:
App.TimelineController = Ember.Controller.extend
timeNow: Ember.computed ->
moment(@get 'currentTime').format 'L'
最后,我们需要把模版里的 {{view.timeNow}}
改成 {{timeNow}}
,因为现在这个变量已经是通过 controller 来提供了。
目前看起来似乎和 views 相比区别甚微,但是我们可以看一下 Ember Inspector 解析出来的结构:
这就是我之前说的,在渲染结构上将两个不相关的数据源分离开来。
http://jsfiddle.net/nightire/S3rWZ/
http://jsbin.com/gerof/6/
Partials,Views,Renders,这些是我们处理分离模版/视图最常用的手段,以上我们已经通过实例清晰地认识到了它们各自的用法和优缺点,他们的实现复杂度是递增的,但是提供的扩展性和灵活性也是越来越强的。相信现在你就可以正确的选择它们来处理的各种应用场景了。
官方文档有一个很简洁清楚的表格对比,可以用于速查:http://emberjs.com/guides/templates/rendering-with-helpers/#toc_comparison-table
但是我们还遗留了 components 和 outlets 没有讲,这主要是因为它们的适用场景和本文设定的场景各有不同。
Components
Web 应用的基础是 HTML 语言,作为一种描述式的标记语言,HTML 简单、清晰、语义性极强。但是 HTML 的缺点是它的扩展性比较局限于标准规范——可用的描述性标签就那么多,有时候为了找到更符合业务领域的表述方式,我需要依赖更多的辅助手段。Ember 为我们提供了 components,一种把可重用的 HTML(包括 Handlebars 模版)片段封装起来作为新的描述性标签来使用的机制。
为了更好的示范 components,我们需要把之前设定的应用场景做一些调整。现在我们需要一些具有相同结构的数据集合,这样才能发挥“可重用”的标签的作用,所以我在原来代码的基础上添加了一些 fixtures:
App = Ember.Application.create()
App.ApplicationAdapter = DS.FixtureAdapter
App.IndexRoute = Ember.Route.extend
model: ->
Ember.RSVP.hash
timeline:
currentTime: new Date().toISOString()
categories: @store.findAll 'category' # Note: #1
# 省略了尚未变化的部分……
App.Category = DS.Model.extend # Note: #2
categoryName: DS.attr 'string'
description: DS.attr 'string'
App.Category.FIXTURES = [ # Note: #3
id: 1
categoryName: 'Category No.1'
description: 'This is Category No.1'
,
id: 2
categoryName: 'Category No.2'
description: 'This is Category No.2'
,
id: 3
categoryName: 'Category No.3'
description: 'This is Category No.3'
]
先解释一下几处变化:
- 现在,我们不再用伪数据而改用真正的模拟服务器返回的数据集合了
- 我们定义了 model
- 我们定义了 fixtures(模拟服务器反悔的数据集合)
接下来,我们把原来的 categories
模版里可重用的部分抽取成 component,然后渲染它们:
好了!咱们先看看效果:
http://jsfiddle.net/nightire/CEQhU/
然后我来解释几个重点:
- Components 的命名一定要遵守以下两点约定:
- 一定要以
components/
开头,如果你是分离模版开发的话,模版存放的路径应该是:YOUR_TEMPLATES_PATH/components/caterogy-itme.hbs
,这样编译后的模版才能被正确识别。 - 后面的部分一定要有
-
做间隔,比如说categoryName
这样是肯定不行的。为什么?因为要避免和 HTML 标签重名(甚至考虑到未来 HTML 的标签扩充),HTML 的标签名是不会存在-
符号的,所以这样才能保证万无一失。
- 一定要以
- 调用 components 时,可以直接用它的模版名字了(不需要前面的
components/
部分),这就是描述性的标签。 - Components 的作用域是完全隔离的!它无法读取外部作用域的变量,如果需要的话就显式传入进去,这样才好达到“可重用”的目的。
此时,一个很常见的疑问来了:如果里面不想用固定的标签或标签结构怎么办?比如说 {{description}}
部分,假设服务器返回的本来就是 HTML 结构(或者别的文档结构),而且不一定一致怎么办?
这也不难办到,我们可以用块级 components 做进一步优化。首先给返回的数据做点手脚,让它们有各自不同的标签格式:
App.Category.FIXTURES = [
id: 1
categoryName: 'Category No.1'
description: 'This is Category No.1'
,
id: 2
categoryName: 'Category No.2'
description: 'This is Category No.2
'
,
id: 3
categoryName: 'Category No.3'
description: 'This is Category No.3'
]
接着,改写我们的模版:
留意这两处:
- 由于我们改用了块级结构,因此被块级结构内部采集的部分可以直接获取外部作用域的变量,不用再像之前那样传值了。(注意
categoryName
仍然不属于被采集的部分,所以还是要显式传值的)另外,我们使用了 3 个{
和}
,这样就不会把 HTML 标签 Escape 掉了。 -
{{yield}}
就是采集的意思,它将块级结构传进来的内容原样展示出来,所以你以后可以多次重用,并且每次都可以传不一样的结构进来。在这个例子里,我特意重复了一遍,你可以查看生成的 HTML 就明白了。
http://jsfiddle.net/nightire/CEQhU/4/
如果你仔细看看最终生成的 HTML,你会发现每一个 component 生成的标签都是 就是这么简单!当然还有很多可以自定义的东西,这就需要你去看文档啦! 关于 components 我就先说到这里,还有一些控制交互和逻辑的部分,留待以后再单独探讨吧。 http://jsfiddle.net/nightire/CEQhU/3/ 怎么办?如果我想添加
class
等属性怎么办?别着急,和 views 类似的,components 也有控制自己视图的方式,只不过名字不叫 View
而叫 Component
。看个例子:
App.CategoryItemComponent = Ember.Component.extend
tagName: 'li'
classNames: ['category', 'item']