本文概述
- 常见错误1:传递所有上下文对象时, 期望模型挂钩触发
- 常见错误2:忘记路由驱动的控制器是单例
- 常见错误3:未在setupController中调用默认实现
- 常见错误4:在非父级路由中使用this.modelFor
- 常见错误5:误解上下文时会触发组件操作
- 常见错误6:使用数组属性作为从属键
- 常见错误7:不使用对观察员友好的方法
- 最后的话
- 关于我
Ember.js是用于构建复杂的客户端应用程序的综合框架。它的宗旨之一是”约定之上的配置”, 并且坚信大多数Web应用程序都有很大一部分开发是通用的, 因此这是解决大多数日常挑战的最佳方法。但是, 找到正确的抽象并涵盖所有案例需要时间和整个社区的投入。顺理成章, 最好花点时间为核心问题找到解决方案, 然后将其烘焙到框架中, 而不是费力地让每个人在需要解决方案时自力更生。
Ember.js不断发展以使开发变得更加容易。但是, 与任何高级框架一样, Ember开发人员仍然存在陷阱。在以下文章中, 我希望提供一个地图以规避这些问题。让我们跳进去吧!
常见错误1:传递所有上下文对象时, 期望模型挂钩触发
假设我们的应用程序具有以下路线:
Router.map(function() {
this.route('band', { path: 'bands/:id' }, function() {
this.route('songs');
});
});
波段路线具有动态段ID。当应用程序加载有诸如/ bands / 24之类的URL时, 会将24传递给相应路线band的模型挂钩。模型挂钩具有反序列化段的作用, 以创建可以在模板中使用的对象(或对象数组):
// app/routes/band.js
export default Ember.Route.extend({
model: function(params) {
return this.store.find('band', params.id); // params.id is '24'
}
});
到目前为止, 一切都很好。但是, 除了从浏览器的导航栏中加载应用程序外, 还有其他输入路线的方法。其中之一是使用模板的链接至帮助器。以下代码段浏览了频段列表, 并创建了指向其各自频段路由的链接:
{{#each bands as |band|}}
{{link-to band.name "band" band}}
{{/each}}
链接到带的最后一个参数band是一个对象, 该对象填充了路由的动态段, 因此其id成为路由的id段。很多人陷入的陷阱是在这种情况下不调用模型挂钩, 因为模型已经知道并已传递。这很有意义, 可以将请求保存到服务器, 但是, 诚然, 它不是直观。解决这个问题的一种巧妙方法是传递, 而不是传递对象本身, 而是传递其ID:
{{#each bands as |band|}}
{{link-to band.name "band" band.id}}
{{/each}}
灰烬的缓解计划
可路由组件将很快在Ember中推出, 可能在2.1或2.2版本中。当它们着陆时, 无论如何过渡到具有动态线段的路线, 始终都会调用模型挂钩。在此处阅读相应的RFC。
常见错误2:忘记路由驱动的控制器是单例
Ember.js中的路由在用作相应模板上下文的控制器上设置属性。这些控制器是单例控制器, 因此即使控制器不再处于活动状态, 在其上定义的任何状态仍将保留。
这是很容易忽略的事情, 我也偶然发现了这一点。就我而言, 我有一个带有乐队和歌曲的音乐目录应用程序。歌曲控制器上的songCreationStarted标志指示用户已开始为特定乐队创建歌曲。问题在于, 如果用户随后切换到另一个乐队, 则songCreationStarted的值将保持不变, 并且似乎半完成的歌曲是针对另一个乐队的, 这令人困惑。
解决方案是手动重置我们不想保留的控制器属性。一个可能的地方是相应路由的setupController钩子, 该钩子在afterModel钩子之后(在其命名之后, 在模型钩子之后)在所有过渡上被调用:
// app/routes/band.js
export default Ember.Route.extend({
setupController: function(controller, model) {
this._super(controller, model);
controller.set('songCreationStarted', false);
}
});
灰烬的缓解计划
同样, 可路由组件的出现将解决此问题, 从而彻底终结控制器。可路由组件的优点之一是它们具有更一致的生命周期, 并且在远离其路由过渡时总是被拆掉。当他们到达时, 上述问题将消失。
常见错误3:未在setupController中调用默认实现
Ember中的路由具有少数生命周期挂钩, 以定义特定于应用程序的行为。我们已经看到了用于提取相应模板和setupController数据的模型, 用于设置模板上下文的控制器。
后者, setupController, 有一个合理的默认值, 即从模型挂钩中分配模型作为控制器的model属性:
// ember-routing/lib/system/route.js
setupController(controller, context, transition) {
if (controller && (context !== undefined)) {
set(controller, 'model', context);
}
}
(上下文是ember-routing程序包在我上面所说的模型中使用的名称)
可以出于多种目的而覆盖setupController挂钩, 例如, 重置控制器的状态(如上述常见错误2)。但是, 如果忘记了调用上面在Ember.Route中复制的父实现, 则可能需要进行长时间的头抓会话, 因为控制器将不会设置其模型属性。因此, 请始终调用this._super(controller, model):
export default Ember.Route.extend({
setupController: function(controller, model) {
this._super(controller, model);
// put the custom setup here
}
});
灰烬的缓解计划
如前所述, 控制器以及带有setupController钩子的控制器即将消失, 因此这种陷阱将不再是威胁。但是, 这里有一个更大的教训要学习, 那就是要记住祖先的实现。在Ember.Object中定义的init函数是Ember中所有对象的母亲, 它是另一个你需要注意的示例。
常见错误4:在非父级路由中使用this.modelFor
Ember路由器在处理URL时会为每个路由段解析模型。假设我们的应用程序具有以下路线:
Router.map({
this.route('bands', function() {
this.route('band', { path: ':id' }, function() {
this.route('songs');
});
});
});
给定URL为/ bands / 24 / songs, 依次调用bands, bands.band和bands.band.songs的模型挂钩。路由API有一个特别方便的方法modelFor, 该方法可在子路由中使用, 以从其中一个父路由中获取模型, 因为那一点肯定已经解决了该模型。
例如, 以下代码是在bands.band路由中获取band对象的有效方法:
// app/routes/bands/band.js
export default Ember.Route.extend({
model: function(params) {
var bands = this.modelFor('bands');
return bands.filterBy('id', params.id);
}
});
但是, 常见的错误是在modelFor中使用不是路由父级的路由名称。如果以上示例中的路线已稍作更改:
Router.map({
this.route('bands');
this.route('band', { path: 'bands/:id' }, function() {
this.route('songs');
});
});
由于波段路由不再是父级, 因此我们的方法无法获取URL中指定的波段, 因此其模型尚未解析。
// app/routes/bands/band.js
export default Ember.Route.extend({
model: function(params) {
var bands = this.modelFor('bands'); // `bands` is undefined
return bands.filterBy('id', params.id); // => error!
}
});
解决方案是仅将modelFor用于父路由, 并在无法使用modelFor时使用其他方式来检索必要的数据, 例如从商店中获取。
// app/routes/bands/band.js
export default Ember.Route.extend({
model: function(params) {
return this.store.find('band', params.id);
}
});
常见错误5:误解上下文时会触发组件操作
嵌套组件一直是Ember最难推理的部分之一。在Ember 1.10中引入了块参数后, 许多这种复杂性已得到缓解, 但是在许多情况下, 一目了然地看到从子组件触发的动作仍是棘手的。
假设我们有一个带列表组件, 其中有带列表项, 并且我们可以将每个带标记为列表中的最爱。
// app/templates/components/band-list.hbs
{{#each bands as |band|}}
{{band-list-item band=band faveAction="setAsFavorite"}}
{{/each}}
用户单击按钮时应调用的操作名称将传递到band-list-item组件中, 并成为其faveAction属性的值。
现在让我们看一下band-list-item的模板和组件定义:
// app/templates/components/band-list-item.hbs
<div class="band-name">{{band.name}}</div>
<button class="fav-button" {{action "faveBand"}}>Fave this</button>
// app/components/band-list-item.js
export default Ember.Component.extend({
band: null, faveAction: '', actions: {
faveBand: {
this.sendAction('faveAction', this.get('band'));
}
}
});
当用户单击” Fave this”按钮时, 将触发faveBand操作, 该操作在其父组件band-list上触发传入的组件的faveAction(在上述情况下为setAsFavorite)。
这使很多人感到不安, 因为他们希望在控制器上以与路由驱动模板中的操作相同的方式触发操作(然后在活动路由上冒泡)。更糟糕的是没有错误消息被记录。父组件只是吞下了错误。
一般规则是在当前上下文上触发操作。在非组件模板的情况下, 该上下文是当前控制器, 而在组件模板的情况下, 它是父组件(如果有的话), 或者是当前控制器(如果未嵌套该组件)。
因此, 在上述情况下, 乐队列表组件将必须重新激发从乐队列表项收到的动作, 以将其冒泡到控制器或路由。
// app/components/band-list.js
export default Ember.Component.extend({
bands: [], favoriteAction: 'setFavoriteBand', actions: {
setAsFavorite: function(band) {
this.sendAction('favoriteAction', band);
}
}
});
如果在bands模板中定义了band-list, 则必须在bands控制器或bands路由(或其父路由之一)中处理setFavoriteBand操作。
灰烬的缓解计划
你可以想象, 如果嵌套的级别更高(例如, 通过在band-list-item中包含fav-button组件), 这将变得更加复杂。你必须从内部钻几个洞才能发出消息, 并在每个级别定义有意义的名称(setAsFavorite, favouriteAction, faveAction等)。
通过”改进的操作RFC”可以简化此操作, 该操作已在master分支上提供, 并且可能包含在1.13中。
上面的示例将简化为:
// app/templates/components/band-list.hbs
{{#each bands as |band|}}
{{band-list-item band=band setFavBand=(action "setFavoriteBand")}}
{{/each}}
// app/templates/components/band-list-item.hbs
<div class="band-name">{{band.name}}</div>
<button class="fav-button" {{action "setFavBand" band}}>Fave this</button>
常见错误6:使用数组属性作为从属键
Ember的计算属性取决于其他属性, 开发人员必须明确定义此依赖关系。说我们有一个isAdmin属性, 当且仅当角色之一是admin时, 它才应为true。这是可能的写法:
isAdmin: function() {
return this.get('roles').contains('admin');
}.property('roles')
使用上面的定义, 只有在角色数组对象本身发生更改时, isAdmin的值才会无效, 而如果将项目添加或删除到现有数组中, 则isAdmin的值将无效。有一种特殊的语法来定义添加和删除也应触发重新计算:
isAdmin: function() {
return this.get('roles').contains('admin');
}.property('roles.[]')
常见错误7:不使用对观察员友好的方法
让我们扩展常见错误6的示例(现已修复), 并在我们的应用程序中创建User类。
var User = Ember.Object.extend({
initRoles: function() {
var roles = this.get('roles');
if (!roles) {
this.set('roles', []);
}
}.on('init'), isAdmin: function() {
return this.get('roles').contains('admin');
}.property('roles.[]')
});
当我们向此类用户添加管理员角色时, 我们感到很惊讶:
var user = User.create();
user.get('isAdmin'); // => false
user.get('roles').push('admin');
user.get('isAdmin'); // => false ?
问题在于, 如果使用常规的Javascript方法, 观察者将不会触发(因此, 计算的属性将不会更新)。如果改进了Object.observe在浏览器中的全球采用率, 这种情况可能会改变, 但是在那之前, 我们必须使用Ember提供的方法集。在当前情况下, pushObject与push的观察者友好等效:
user.get('roles').pushObject('admin');
user.get('isAdmin'); // => true, finally!
常见错误8:组件中传递的属性突变
想象一下, 我们有一个星级评分组件, 它可以显示商品的评分并允许设置该商品的评分。评分可以是歌曲, 书籍或足球运动员的运球技巧。
你可以在模板中像这样使用它:
{{#each songs as |song|}}
{{star-rating item=song rating=song.rating}}
{{/each}}
让我们进一步假设该组件显示星星, 每个点一个完整的星星, 然后显示空星星, 直到达到最高评分。单击星号时, 将在控制器上触发设置动作, 并且应将其解释为要更新评级的用户。我们可以编写以下代码来实现此目的:
// app/components/star-rating.js
export default Ember.Component.extend({
item: null, rating: 0, (...)
actions: {
set: function(newRating) {
var item = this.get('item');
item.set('rating', newRating);
return item.save();
}
}
});
这样就可以完成工作, 但是有一些问题。首先, 假设传入的项目具有等级属性, 因此我们不能使用此组件来管理Leo Messi的运球技巧(此属性可能称为得分)。
其次, 它会更改组件中商品的等级。这导致很难了解为什么某些属性发生更改的情况。想象一下, 我们在同一模板中还有另一个组件, 该组件还使用该等级, 例如, 用于计算足球运动员的平均得分。
减轻这种情况的复杂性的口号是”数据减少, 行动加快”(DDAU)。数据应向下传递(从路由到控制器再到组件), 而组件应使用操作将这些数据的更改通知其上下文。那么DDAU应该在这里应用吗?
让我们添加一个应发送的操作名称, 以更新评分:
{{#each songs as |song|}}
{{star-rating item=song rating=song.rating setAction="updateRating"}}
{{/each}}
然后使用该名称发送操作:
// app/components/star-rating.js
export default Ember.Component.extend({
item: null, rating: 0, (...)
actions: {
set: function(newRating) {
var item = this.get('item');
this.sendAction('setAction', {
item: this.get('item'), rating: newRating
});
}
}
});
最后, 操作是由控制器或路线在上游进行处理的, 这是商品评分的更新位置:
// app/routes/player.js
export default Ember.Route.extend({
actions: {
updateRating: function(params) {
var skill = params.item, rating = params.rating;
skill.set('score', rating);
return skill.save();
}
}
});
发生这种情况时, 这种变化会通过传递给星级组件的绑定向下传播, 结果显示的满星数量也会发生变化。
这样, 组件中不会发生突变, 并且由于唯一的应用程序特定部分是路由中操作的处理, 因此组件的可重用性不会受到影响。
我们也可以将相同的组件用于足球技能:
{{#each player.skills as |skill|}}
{{star-rating item=skill rating=skill.score setAction="updateSkill"}}
{{/each}}
最后的话
重要的是要注意, 我看到人们犯下的某些错误(大部分?), 包括我在这里写的那些错误, 将在2.x系列的早期消失或大大减轻。 Ember.js。
我上面的建议解决了仍然存在的问题, 因此, 一旦你在Ember 2.x中进行开发, 就不会再犯任何错误!如果你希望本文以pdf格式显示, 请转到我的博客, 然后单击帖子底部的链接。
关于我
两年前, 我使用Ember.js来到了前端世界, 而我待在这里。我对Ember变得非常热情, 以至于我开始在来宾帖子和我自己的博客上都写博客, 并在会议上发表演讲。我什至为想要学习Ember的任何人写了一本书, 《用Ember.js摇滚》。你可以在此处下载示例章节。
评论前必须登录!
注册