作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Balint Erdi
验证专家 在工程

在TDD流行之前,Balint就一直在实践它. 他是一个典型的PHP程序员,后来转向Java、Python和Ruby.

以前在

爱立信
分享

灰烬.js 是构建复杂客户端应用程序的全面框架吗. 它的原则之一是“约定优于配置”,,并坚信大多数web应用程序的开发有很大一部分是共同的, 因此,这是解决大多数日常挑战的最佳方法. 然而, finding the right abstraction, and covering all the cases, takes time and input from the whole 社区. 随着推理的进行, 最好花点时间找出解决核心问题的正确方法, 然后 bake it into the framework, 而不是举手投降,让每个人在需要找到解决方案的时候自生自灭.

灰烬.js is constantly evolving to make 开发更容易. 但是,与任何高级框架一样,灰烬开发者仍然可能陷入陷阱. 通过下面的帖子,我希望提供一个地图来逃避这些. 让我们开始吧!

常见错误No. 1:期望模型钩子在所有Context对象被传入时触发

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

路由器.地图(函数(){
  这.路线 ('乐队', {path: '乐队/:id'}, function() {
    这.路线(“歌曲”);
  });
});

乐队 route has a dynamic segment, id. When the app is loaded with a URL like /带/ 24, 24 传递给 模型 钩的 the corresponding route, 乐队. 模型钩子的作用是反序列化段,以创建一个对象(或一个对象数组),然后可以在模板中使用:

/ / app /线路/乐队.js

导出默认灰烬.路线.扩展({
  模型: function(params) {
    返回这.商店.找到(“乐队”,参数个数.id); // params.Id是24
  }
});

到目前为止一切顺利. 然而, 除了从浏览器导航栏加载应用程序之外,还有其他输入路由的方法. One of them is using the 链接到 来自模板的助手. 下面的代码片段遍历一个频带列表,并创建指向它们各自的链接 乐队 路线:

{{#each 乐队 as |乐队|}}
  {{链接带.名称“乐队”乐队}}
{{/每个}}

的 last argument for 链接到, 乐队是一个对象,它填充了路由的动态段,因此它的 id becomes the id segment for the route. 许多人陷入的陷阱是,在这种情况下没有调用模型钩子, 因为模型是已知的,并且已经传入. 这是有意义的,它可能会节省对服务器的请求,但不可否认,这并不直观. 一种巧妙的方法是传入的不是物体本身,而是它的id:

{{#each 乐队 as |乐队|}}
  {{链接带.名字“乐队”乐队.id}}
{{/每个}}

灰烬.js

灰烬’s Mitigation Plan

路由组件将很快出现在灰烬中, 可能是版本2.1 or 2.2. 当他们着陆时, 模型 hook will always be called, 无论如何转换到具有动态段的路由. Read the corresponding RFC 在这里.

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

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

这是很容易被忽视的 I stumbled into 这, too. 就我而言,我有一个包含乐队和歌曲的音乐目录应用程序. 的 songCreationStarted 旗帜上的旗帜 歌曲 控制器指示用户已开始为特定乐队创作歌曲. 问题是,如果用户切换到另一个波段,值 songCreationStarted 这首半成品的歌似乎是给另一支乐队的,这让人很困惑.

解决方案是手动重置我们不想逗留的控制器属性. One possible place to do 这 is the 集upController 类之后的所有转换都会调用该钩子 afterModel 钩子(顾名思义,在钩子之后) 模型 钩):

/ / app /线路/乐队.js

导出默认灰烬.路线.扩展({
  集upController: function(控制器, 模型) {
    这._super(控制器, 模型);
    控制器.集('songCreationStarted', false);
  }
});

灰烬’s Mitigation Plan

再一次,黎明 routeable组件 能解决这个问题,彻底终结控制器吗. 可路由组件的优点之一是,它们具有更一致的生命周期,并且在从它们的路由转换时总是被拆除. When they arrive, above problem will vanish.

常见错误No. 3: Not Calling the Default Implementation in 集upController

灰烬中的路由有一些生命周期钩子来定义特定于应用程序的行为. 我们已经看到 模型 哪个用于获取相应模板和的数据 集upController,用于设置控制器、模板的上下文.

后者, 集upController,有一个合理的默认值,即从 模型 像钩子一样 模型 财产 of the 控制器:

// ember-routing/lib/system/route.js

集upController(控制器, 上下文, transition) {
  如果控制器 && (上下文 !==未定义)){
    集(控制器, '模型', 上下文);
  }
}

(上下文 使用的名称是 ember-routing 我称之为 模型 上图)

集upController hook can be overridden for several purposes, 如重置控制器的状态(如常见错误No . 1). 2以上). 但是,如果忘记调用我上面在灰烬中复制的父实现.路由,一个人可以在一个长头痛的会话,因为控制器将没有它的 模型 属性集. 所以一定要打电话 这._super(控制器, 模型):

导出默认灰烬.路线.扩展({
  集upController: function(控制器, 模型) {
    这._super(控制器, 模型);
    // put the custom 集up 在这里
  }
});

灰烬’s Mitigation Plan

如前所述,控制器以及与它们一起的 集upController 钩子很快就会消失,所以这个陷阱将不再是一个威胁. 然而,re is a greater lesson to be learned 在这里, 哪一个是要注意祖先的实现. 的 初始化 函数,定义于 灰烬.Object烬中所有物体的母亲,是另一个你必须注意的例子.

常见错误No. 4:使用 这.梅尔 非父路由

灰烬路由器在处理URL时解析每个路由段的模型. 让我们假设我们的应用程序中有以下路由:

路由器.地图({
  这.route('乐队', function() {
    这.route('乐队', { path: ':id' }, function() {
      这.路线(“歌曲”);
    });
  });
});

给定URL为 /带/ 24 /歌曲, 模型 钩的 乐队, 乐队.乐队 然后 乐队.乐队.歌曲 are called, in 这 order. 的 route API has a particularly handy method, 梅尔, 可以在子路由中使用,从父路由中获取模型, 因为那个模型到那时肯定已经解决了.

方法中获取乐队对象的有效方法 乐队.乐队 路线:

/ / app /线路/乐队s/乐队.js

导出默认灰烬.路线.扩展({
  模型: function(params) {
    Var波段=这个.梅尔(“乐队”);
    返回乐队.filterBy(“id”,参数个数.id);
  }
});

然而,一个常见的错误是在梅尔中使用路由名 路由的父节点. 如果上面例子中的路由稍微改变一下:

路由器.地图({
  这.路线(“乐队”);
  这.路线 ('乐队', {path: '乐队/:id'}, function() {
    这.路线(“歌曲”);
  });
});

我们获取URL中指定的频带的方法将中断,因为 乐队 路线不再是父节点,因此它的模型没有被解析.

/ / app /线路/乐队s/乐队.js

导出默认灰烬.路线.扩展({
  模型: function(params) {
    Var波段=这个.梅尔(“乐队”); // `乐队` is undefined
    返回乐队.filterBy(“id”,参数个数.id); // => error!
  }
});

解决办法是使用 梅尔 只用于父路由,并使用其他方式检索必要的数据时 梅尔 can不 be used, such as fetching 从商店.

/ / app /线路/乐队s/乐队.js

导出默认灰烬.路线.扩展({
  模型: function(params) {
    返回这.商店.找到(“乐队”,参数个数.id);
  }
});

常见错误No. 5 .错误理解组件动作触发的上下文

嵌套组件一直是灰烬中最难理解的部分之一. With the introduction of block parameters in 灰烬 1.10, a lot of 这 complexity has been relieved, 但在很多情况下, 看一眼哪个组件是一个动作仍然很棘手, fired from a child component, 将被触发.

假设有a 乐队表 组件 乐队表-项s 在里面,我们可以把每个乐队标记为最喜欢的乐队.

// app/templates/components/乐队表.哈佛商学院

{{#each 乐队 as |乐队|}}
  {{乐队表-项 乐队=乐队 faveAction="集AsFavorite"}}
{{/每个}}

当用户单击按钮时应该调用的操作名称被传递到 乐队表-项 component, and becomes the value of its faveAction 财产.

的模板和组件定义 乐队表-项:

// app/templates/components/乐队表-项.哈佛商学院

{{乐队.name}}
// app/components/乐队表-项.js

导出默认灰烬.组件.扩展({
  乐队:空,
  faveAction:”,

  行动:{
    faveBand: {
      这.sendAction('faveAction', 这.('带'));
    }
  }
});

When the 用户 clicks the “Fave 这” button, faveBand 动作被触发,从而触发组件的 faveAction 这是在(集AsFavorite(在上述情况下), 在其父组件上, 乐队表.

这让很多人感到困惑,因为他们希望动作被触发的方式和路由驱动模板中的动作一样, 在控制器上(然后在活动路由上冒泡). What makes 这 worse is that no error message is logged; the parent component just swallows the error.

一般规则是在当前上下文上触发操作. In the case of non-component templates, that 上下文 is the current 控制器, while in the case of component templates, it is the parent component (if t在这里 is one), 如果组件没有嵌套,则再次使用当前控制器.

So in the above case, 乐队表 组件必须重新启动从 乐队表-项 以便将其起泡到控制器或路由.

// app/components/乐队表.js

导出默认灰烬.组件.扩展({
  乐队:[],
  favoriteAction: '集FavoriteBand',

  行动:{
    集AsFavorite: function(乐队) {
      这.sendAction('favoriteAction', 乐队);
    }
  }
});

如果 乐队表乐队 模板,然后 集FavoriteBand action would have to be handled in the 乐队 控制器或 乐队 route (or one of its parent routes).

灰烬’s Mitigation Plan

您可以想象,如果有更多的嵌套级别(例如, 通过 fav-button 内部组件 乐队表-项). 你必须从里面钻一个洞,穿过几层才能把你的信息发出来, defining meaningful names at each level (集AsFavorite, favoriteAction, faveAction等.)

This is made simpler by the “改进的行动RFC”,它已经在主分支上可用,并且可能会包含在1中.13.

的 above example would then be simplified to:

// app/templates/components/乐队表.哈佛商学院

{{#each 乐队 as |乐队|}}
  {{乐队表-项 乐队=乐队 集FavBand=(action "集FavoriteBand")}}
{{/每个}}
// app/templates/components/乐队表-项.哈佛商学院

{{乐队.name}}

常见错误No. 6: Using Array Properties As Dependent Keys

烬的计算属性依赖于其他属性, 这个依赖需要由开发人员显式地定义. 假设我们有一个 isAdmin 属性,当且仅当其中一个角色为 管理. This is how one might write it:

isAdmin:函数(){
  返回这.get(角色).包含('管理');
}.属性(角色)

With the above def初始化ion, value of isAdmin only gets invalidated if the 角色 数组对象本身发生变化,但如果向现有数组添加或删除项,则不会发生变化. 有一个特殊的语法来定义添加和删除也应该触发重新计算:

isAdmin:函数(){
  返回这.get(角色).包含('管理');
}.属性(的角色.[]')

常见错误No. 7: Not Using Observer-friendly Methods

让我们从常见错误1中扩展(现在已经修复)的例子. 6, and create a 用户 class in our application.

var 用户 = 灰烬.Object.扩展({
  初始化Roles: function() {
    Var 角色 = 这.(角色);
    if (!角色){
      这.设置(“角色”,[]);
    }
  }.(“初始化”),

  isAdmin:函数(){
    返回这.get(角色).包含('管理');
  }.属性(的角色.[]')
});

当我们加入 管理 这样的角色 用户, we’re in for a surprise:

var 用户 = 用户.create ();
用户.get('isAdmin'); // => false
用户.get(角色).推动(管理);
用户.get('isAdmin'); // => false ?

问题是,如果使用stock Javascript方法,观察者不会触发(因此计算的属性不会得到更新). This might change if the global adoption of Object.观察 在浏览器中得到了改进,但在此之前,我们必须使用灰烬提供的一组方法. 在目前的情况下, 推Object is the 观察r-friendly equivalent of :

用户.get(角色).推Object('管理');
用户.get('isAdmin'); // => true, finally!

常见错误No. 8: Mutating Passed in Properties in 组件s

假设我们有一个 星级 组件,该组件显示项目的评级并允许设置项目的评级. 评分可以是一首歌、一本书或一个足球运动员的运球技术.

You would use it like 这 in your template:

{{#each 歌曲 as |song|}}
  {{星级 项=song 评级=song.评级}}
{{/每个}}

让我们进一步假设组件显示恒星, one full star for each point, and empty stars after that, up until a maximum 评级. When a star is clicked, a action is fired on the 控制器, 它应该被解释为用户想要更新评级. 我们可以编写以下代码来实现这一点:

// app/components/星级.js

导出默认灰烬.组件.扩展({
  项目:空,
  等级:0,
  (...)
  行动:{
    集: function(newRating) {
      Var 项 = 这.(“项目”);
      项.集('评级', newRating);
      返回项目.save ();
    }
  }
});

这样就可以完成工作了,但是有几个问题. First, it assumes that the passed in 项 has a 评级 财产, 所以我们不能使用这个组件来管理梅西的运球技术(这个属性可能会被调用) 分数).

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

缓解这种情况复杂性的口号是“数据下降,行动上升”(DDAU)。. 数据应该向下传递(从路由到控制器再到组件), 而组件应该使用操作将这些数据的更改通知其上下文. So how should DDAU be applied 在这里?

让我们添加一个用于更新评级的动作名称:

{{#each 歌曲 as |song|}}
  {{星级 项=song 评级=song.评级 集Action="updateRating"}}
{{/每个}}

然后用那个名字发送动作:

// app/components/星级.js

导出默认灰烬.组件.扩展({
  项目:空,
  等级:0,
  (...)
  行动:{
    集: function(newRating) {
      Var 项 = 这.(“项目”);
      这.sendAction('集Action', {
        项目:.(“项目”),
        评级:newRating
      });
    }
  }
});

最后, action is handled upstream, by the 控制器或 route, 这是物品评级更新的地方:

/ / app /线路/球员.js

导出默认灰烬.路线.扩展({
  行动:{
    updateRating: function(params) {
      Var技能=参数.项,
          评级= params.评级;

      技能.设置(“分数”,评级);
      返回的技能.save ();
    }
  }
});

发生这种情况时,此更改将通过传递到的绑定向下传播 星级 组件,因此显示的全星数量会发生变化.

这种方式, mutation does 不 happen in the components, 由于唯一与应用相关的部分是对路由中动作的处理, component’s reusability does 不 suffer.

我们也可以在足球技能中使用相同的组件:

{{#每个玩家.技能为|技能|}}
  {{星级 项=技能 评级=技能.分数 集Action="updateSkill"}}
{{/每个}}

最后的话

It is important to 不e that some (most?我看到别人犯的错误(或者我自己犯的错误), including the ones I have written about 在这里, 是会消失还是会在21世纪早期得到极大的缓解.余烬x系列.js.

剩下的是我上面的建议,所以一旦你在灰烬 2中开发.x, you’ll have no excuse to make any more errors! If you’d like 这 article as a pdf, 去我的博客看看 and click the link at the bottom of the post.

关于我的

I came to the front-end world with 灰烬.js two years ago, and I am 在这里 to stay. 我对灰烬充满了热情,我开始在我自己的博客上写博客, as well as presenting at conferences. 我甚至写了一本书, Rock and Roll with 灰烬.js, for anyone who wants to learn 灰烬. You can download a sample chapter 在这里.

Further Reading on the Toptal 博客:

Hire a Toptal expert on 这 topic.
现在雇佣
Balint Erdi

Balint Erdi

验证专家 在工程

匈牙利布达佩斯

Member since February 26, 2014

作者简介

在TDD流行之前,Balint就一直在实践它. 他是一个典型的PHP程序员,后来转向Java、Python和Ruby.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

以前在

爱立信

World-class articles, delivered weekly.

Subscription implies consent to our 隐私政策

World-class articles, delivered weekly.

Subscription implies consent to our 隐私政策

Toptal开发者

加入总冠军® 社区.