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

Balint Erdi

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

以前在

爱立信
分享

灰烬.js 是构建复杂客户端应用程序的全面框架吗. 它的原则之一是“约定优于配置”,,并坚信大多数web应用程序的开发有很大一部分是共同的, 因此,这是解决大多数日常挑战的最佳方法. 然而, 找到正确的抽象, 并且涵盖了所有的情况, 需要整个社区的时间和投入. 随着推理的进行, 最好花点时间找出解决核心问题的正确方法, 然后将其放入框架中, 而不是举手投降,让每个人在需要找到解决方案的时候自生自灭.

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

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

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

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

乐队 路由有一个动态段, id. 当用URL加载应用程序时 /带/ 24, 24 传递给 模型 对应路线的钩子, 乐队. 模型钩子的作用是反序列化段,以创建一个对象(或一个对象数组),然后可以在模板中使用:

/ / app /线路/乐队.js

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

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

{{#每个波段作为|波段|}}
  {{链接带.名称“乐队”乐队}}
{{/每个}}

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

{{#每个波段作为|波段|}}
  {{链接带.名字“乐队”乐队.id}}
{{/每个}}

灰烬.js

安伯的缓解计划

路由组件将很快出现在灰烬中, 可能是版本2.1 or 2.2. 当他们着陆时, 模型钩子总是会被调用, 无论如何转换到具有动态段的路由. 请阅读相应的RFC 在这里.

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

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

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

解决方案是手动重置我们不想逗留的控制器属性. 一个可能的地方是 集upController 类之后的所有转换都会调用该钩子 afterModel 钩子(顾名思义,在钩子之后) 模型 钩):

/ / app /线路/乐队.js

导出默认灰烬.路线.扩展({
  集upController:函数(控制器,模型){
    这._super(控制器、模型);
    控制器.设置(“songCreationStarted”,假);
  }
});

安伯的缓解计划

再一次,黎明 routeable组件 能解决这个问题,彻底终结控制器吗. 可路由组件的优点之一是,它们具有更一致的生命周期,并且在从它们的路由转换时总是被拆除. 当他们到达时,上述问题就会消失.

常见错误No. 3:不调用默认实现 集upController

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

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

/ / ember-routing / lib /系统/路线.js

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

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

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

导出默认灰烬.路线.扩展({
  集upController:函数(控制器,模型){
    这._super(控制器、模型);
    //将自定义设置放在这里
  }
});

安伯的缓解计划

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

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

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

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

给定URL为 /带/ 24 /歌曲, 模型 钩的 乐队, 乐队.乐队 然后 乐队.乐队.歌曲 按这个顺序. 路由API有一个特别方便的方法, 梅尔, 可以在子路由中使用,从父路由中获取模型, 因为那个模型到那时肯定已经解决了.

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

/ /应用程序/线路/带/乐队.js

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

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

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

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

/ /应用程序/线路/带/乐队.js

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

解决办法是使用 梅尔 只用于父路由,并使用其他方式检索必要的数据时 梅尔 不能使用,比如取回 从商店.

/ /应用程序/线路/带/乐队.js

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

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

嵌套组件一直是灰烬中最难理解的部分之一. 随着 块参数在灰烬 1.10, 这种复杂性在很大程度上得到了缓解, 但在很多情况下, 看一眼哪个组件是一个动作仍然很棘手, 从子组件触发, 将被触发.

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

/ /应用程序/模板/组件/乐队表.哈佛商学院

{{#每个波段作为|波段|}}
  {{乐队表-项 乐队=乐队 faveAction="集AsFavorite"}}
{{/每个}}

当用户单击按钮时应该调用的操作名称被传递到 乐队表-项 组件,并成为其值 faveAction 财产.

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

/ /应用程序/模板/组件/ 乐队表-项.哈佛商学院

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

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

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

当用户点击“喜欢这个”按钮时 faveBand 动作被触发,从而触发组件的 faveAction 这是在(集AsFavorite(在上述情况下), 在其父组件上, 乐队表.

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

一般规则是在当前上下文上触发操作. 对于非组件模板, 这个上下文就是当前控制器, 而对于组件模板来说, 它是父组件(如果有的话), 如果组件没有嵌套,则再次使用当前控制器.

在上面的例子中 乐队表 组件必须重新启动从 乐队表-项 以便将其起泡到控制器或路由.

/ / app /组件/乐队表.js

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

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

如果 乐队表乐队 模板,然后 集FavoriteBand 操作必须在 乐队 控制器或 乐队 路由(或它的父路由之一).

安伯的缓解计划

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

这就简化了 “改进的行动RFC”,它已经在主分支上可用,并且可能会包含在1中.13.

然后将上述示例简化为:

/ /应用程序/模板/组件/乐队表.哈佛商学院

{{#每个波段作为|波段|}}
  {{乐队表-项 乐队=乐队 集FavBand=(action "集FavoriteBand")}}
{{/每个}}
/ /应用程序/模板/组件/ 乐队表-项.哈佛商学院

{{乐队.name}}

常见错误No. 6:使用数组属性作为依赖键

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

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

根据上述定义,的值 isAdmin 只有当 角色 数组对象本身发生变化,但如果向现有数组添加或删除项,则不会发生变化. 有一个特殊的语法来定义添加和删除也应该触发重新计算:

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

常见错误No. 7:不使用对观察者友好的方法

让我们从常见错误1中扩展(现在已经修复)的例子. 6、在我们的应用中创建一个用户类.

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

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

当我们加入 管理 这样的角色 用户,我们会有一个惊喜:

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

问题是,如果使用stock Javascript方法,观察者不会触发(因此计算的属性不会得到更新). 这可能会改变,如果全球采用 Object.观察 在浏览器中得到了改进,但在此之前,我们必须使用灰烬提供的一组方法. 在目前的情况下, 推Object 观察者友好的等效物是 :

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

常见错误No. 8:改变组件中传递的属性

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

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

{{#每首歌作为|song|}}
  {{星级 项=song 评级=song.评级}}
{{/每个}}

让我们进一步假设组件显示恒星, 一颗星代表一个点, 然后是空荡荡的星星, 直到最高评级. 当一个星号被点击时,一个 动作在控制器上被触发, 它应该被解释为用户想要更新评级. 我们可以编写以下代码来实现这一点:

/ / app /组件/星级.js

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

这样就可以完成工作了,但是有几个问题. 首先,它假设传入的项有 评级 财产, 所以我们不能使用这个组件来管理梅西的运球技术(这个属性可能会被调用) 分数).

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

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

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

{{#每首歌作为|song|}}
  {{星级 项=song 评级=song.评级集Action = " updateRating "}}
{{/每个}}

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

/ / app /组件/星级.js

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

最后, 该操作在上游处理, 通过控制器或路由, 这是物品评级更新的地方:

/ / app /线路/球员.js

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

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

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

这种方式, 突变不会发生在成分中, 由于唯一与应用相关的部分是对路由中动作的处理, 组件的可重用性不会受到影响.

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

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

最后的话

重要的是要注意一些(大多数?我看到别人犯的错误(或者我自己犯的错误), 包括我在这里写过的那些, 是会消失还是会在21世纪早期得到极大的缓解.余烬x系列.js.

剩下的是我上面的建议,所以一旦你在灰烬 2中开发.X,你就没有借口再犯错误了! 如果你想要这篇文章的pdf格式, 去我的博客看看 点击文章底部的链接.

关于我的

我来到了前端世界 灰烬.js 两年前,我将留在这里. 我对灰烬充满了热情,我开始在我自己的博客上写博客, 以及在会议上发言. 我甚至写了一本书, 与烬一起摇滚.js,适合任何想学习烬的人. 您可以下载一个示例章节 在这里.

关于总博客的进一步阅读:

聘请Toptal这方面的专家.
现在雇佣
巴林特·尔迪的头像
Balint Erdi

位于 匈牙利布达佩斯

成员自 2014年2月26日

作者简介

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

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

以前在

爱立信

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.