Ember.js 开发人员最常犯的 8 个错误

已发表: 2022-03-11

Ember.js 是一个用于构建复杂客户端应用程序的综合框架。 它的一个原则是“约定优于配置”,并坚信大多数 Web 应用程序都有很大一部分开发是通用的,因此是解决大多数这些日常挑战的单一最佳方法。 然而,找到正确的抽象并涵盖所有案例需要时间和整个社区的投入。 照理说,最好花时间找到正确的核心问题的解决方案,然后将其融入框架中,而不是在需要找到解决方案时举起手来,让每个人都自生自灭。

Ember.js 不断发展以使开发更加容易。 但是,与任何高级框架一样,Ember 开发人员仍可能陷入陷阱。 通过下面的帖子,我希望提供一个地图来规避这些。 让我们直接跳进去!

常见错误一:期望模型钩子在所有上下文对象都被传入时触发

假设我们的应用程序中有以下路由:

 Router.map(function() { this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });

band路由有一个动态段id 。 当应用程序使用/bands/24之类的 URL 加载时, 24被传递到相应路由的model钩子band 。 模型挂钩的作用是反序列化段以创建一个对象(或对象数组),然后可以在模板中使用该对象:

 // app/routes/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); // params.id is '24' } });

到目前为止,一切都很好。 但是,除了从浏览器的导航栏加载应用程序之外,还有其他方法可以输入路由。 其中之一是使用来自模板的link-to 。 下面的代码片段遍历了一个波段列表,并创建了一个指向它们各自band路径的链接:

 {{#each bands as |band|}} {{link-to band.name "band" band}} {{/each}}

link-to 的最后一个参数band是一个填充路由动态段的对象,因此它的id成为路由的 id 段。 许多人陷入的陷阱是在这种情况下不调用模型钩子,因为模型是已知的并且已经被传入。这是有道理的,它可能会将请求保存到服务器,但不可否认的是,它不是直觉的。 一个巧妙的方法是传入,不是对象本身,而是它的 id:

 {{#each bands as |band|}} {{link-to band.name "band" band.id}} {{/each}} 

Ember.js

Ember 的缓解计划

可路由组件将很快出现在 Ember 中,可能在 2.1 或 2.2 版本中。 当它们着陆时,模型钩子将始终被调用,无论如何过渡到具有动态路段的路线。 在此处阅读相应的 RFC。

常见错误 2:忘记路由驱动的控制器是单例的

Ember.js 中的路由在控制器上设置属性,作为相应模板的上下文。 这些控制器是单例的,因此即使控制器不再处于活动状态,它们上定义的任何状态也会持续存在。

这是很容易忽视的事情,我也偶然发现了这一点。 就我而言,我有一个包含乐队和歌曲的音乐目录应用程序。 songs控制器上的songCreationStarted标志表明用户已开始为特定乐队创作歌曲。 问题是,如果用户随后切换到另一个乐队, songCreationStarted的值仍然存在,并且似乎半完成的歌曲是给另一个乐队的,这令人困惑。

解决方案是手动重置我们不想逗留的控制器属性。 一个可能的地方是相应路由的setupController钩子,它在afterModel钩子之后的所有转换中被调用(顾名思义,它位于model钩子之后):

 // app/routes/band.js export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); controller.set('songCreationStarted', false); } });

Ember 的缓解计划

同样,可路由组件的出现将解决这个问题,彻底终结控制器。 可路由组件的优点之一是它们具有更一致的生命周期,并且在从其路由转换时总是被拆除。 当他们到达时,上述问题将消失。

常见错误 3:没有在setupController中调用默认实现

Ember 中的路由有一些生命周期钩子来定义特定于应用程序的行为。 我们已经看到了用于为相应模板和setupController获取数据的model ,用于设置控制器,模板的上下文。

后者setupController有一个合理的默认值,即从model钩子中分配模型作为控制器的model属性:

 // ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }

contextember-routing包使用的名称,我称之为上面的model

setupController挂钩可以出于多种目的被覆盖,例如重置控制器的状态(如上面的常见错误 2)。 但是,如果忘记调用我在 Ember.Route 中复制的父实现,则可能会陷入长时间的头疼会话,因为控制器不会设置其model属性。 所以总是调用this._super(controller, model)

 export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); // put the custom setup here } });

Ember 的缓解计划

如前所述,控制器以及与它们一起的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 ,按此顺序调用bandsbands.bandbands.band.songsmodel钩子。 路由 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 中指定的 band 的方法会中断,因为bands路由不再是父路由,因此它的模型尚未解析。

 // 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不能使用时,使用其他方式获取必要的数据,比如从store中获取。

 // 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 中块参数的引入,这种复杂性已经减轻了很多,但在许多情况下,仍然很难一眼看出从子组件触发的动作将在哪个组件上触发。

假设我们有一个band-list组件,其中包含band-list-items ,我们可以将每个乐队标记为列表中的收藏夹。

 // 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 )。

这让很多人感到困惑,因为他们希望在控制器上触发动作的方式与来自路由驱动模板的动作相同(然后在活动路由上冒泡)。 更糟糕的是没有记录错误消息。 父组件只是吞下错误。

一般规则是在当前上下文中触发操作。 在非组件模板的情况下,该上下文是当前控制器,而在组件模板的情况下,它是父组件(如果有的话),或者如果组件没有嵌套,则再次是当前控制器。

因此在上述情况下, band-list组件必须重新触发从band-list-item接收到的操作,以便将其冒泡到控制器或路由。

 // app/components/band-list.js export default Ember.Component.extend({ bands: [], favoriteAction: 'setFavoriteBand', actions: { setAsFavorite: function(band) { this.sendAction('favoriteAction', band); } } });

如果band-list是在bands模板中定义的,那么setFavoriteBand操作必须在bands控制器或bands路由(或其父路由之一)中处理。

Ember 的缓解计划

您可以想象,如果有更多级别的嵌套,这会变得更加复杂(例如,通过在band-list-item中有一个fav-button组件)。 你必须从内部钻一个洞穿过几个层才能得到你的信息,在每个级别定义有意义的名称( setAsFavoritefavoriteActionfaveAction等)

“Improved Actions 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的值只有在roles数组对象本身发生变化时才会失效,但如果项目被添加或删除到现有数组中则不会。 有一种特殊的语法来定义添加和删除也应该触发重新计算:

 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.[]') });

当我们将admin角色添加到这样的User时,我们会感到惊讶:

 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:在组件中更改传入的属性

想象一下,我们有一个star-rating组件,它显示项目的评级并允许设置项目的评级。 评分可以是歌曲、书籍或足球运动员的运球技巧。

您可以在模板中这样使用它:

 {{#each songs as |song|}} {{star-rating item=song rating=song.rating}} {{/each}}

让我们进一步假设该组件显示星星,每个点一个满星,然后是空星,直到最大评级。 单击星号时,会在控制器上触发set操作,并且应将其解释为用户想要更新评级。 我们可以编写以下代码来实现这一点:

 // 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(); } } });

这样可以完成工作,但存在一些问题。 首先,它假设传入的 item 有一个rating属性,所以我们不能使用这个组件来管理 Leo Messi 的运球技巧(这个属性可能被称为score )。

其次,它会改变组件中项目的评级。 这会导致很难看出为什么某个属性会发生变化的情况。 想象一下,我们在同一个模板中有另一个组件,其中也使用了该评分,例如,用于计算足球运动员的平均得分。

减轻这种情况复杂性的口号是“数据下降,行动上升”(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(); } } });

发生这种情况时,此更改会通过传递给star-rating组件的绑定向下传播,因此显示的满星数会发生更改。

这样,组件中不会发生突变,并且由于唯一特定于应用程序的部分是路由中操作的处理,因此组件的可重用性不会受到影响。

我们也可以将相同的组件用于足球技巧:

 {{#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 的人写了一本书, Rock and Roll with Ember.js 。 您可以在此处下载示例章节。