资讯专栏INFORMATION COLUMN

[Tips on Ember 2] How components works when out of

jk_v1 / 1487人阅读

摘要:因为组件的存在范围被限制在以内,这就是这种机制目前存在的意义所在。组件都是可以传递参数或外部作用域的,利用此机制进行判断来执行可选行为,这是对用户友好的举措。

这一篇还是一个简单的例子所引发的思考。

你看,如今的框架和库,无论规模大小功能多少,它们在本质上都朝着“组件化”的思路快速演进着。Angular 有 directives,Angular 2应该也还是这个叫法;Ember 从 View 过渡到了 Component,并且接下来的迭代会朝向 WebComponent 的标准来设计生命周期及其 API;React 自身就是一个组件化的范式;还有 Polymer,那就是 Google 为 WebComponent 搞得一套 polyfills……大家都这么玩。

我们都无法完整精确的预测 WebComponent 能让 Web 进化到什么程度,但是现在已经有了这么多可用的工具了,大家自然是跃跃欲试的搞起。我也没有例外,在之前使用 Angular 的时候就在尽力做组件的抽象,把 directives 那一套算是玩儿转了——那个时候并没有意识到一件事情,直到最近返回 Ember 之后才开始有所体会。几天前我在 Ember Community 提了个问题来讨论此事,虽然也得到很多建议却还是模模糊糊的;后来又和 @darkbaby123 询问了一番,感谢他给了我很多启迪。然而始终没有想到一个确切的应用场景来验证一番。

说了半天估计你们都看糊涂了:到底什么事情啊?

Components 用来封装可重用的 HTML+CSS+JavaScript 片段,这很好,然而当下的框架们都要处理事件委托的问题,这就意味着框架会给你一个范围来开发你的应用,这个范围的边界就是框架用于事件委托的临界线,出了这条线就是浏览器自身的事件处理机制在作用了。那么,当你不得不“迈过”这条线的时候,框架(以及它提供的组件化机制)该如何帮助你呢?

举例来说,Angular 的边界在你声明 ng-app 的地方(所以我见过不少人把它放在 上来扩大这个界限);Ember 默认在 上,当然你可以改;React 也是一样,你总是要把你的第一个 component 渲染到 下面。那么,当你要做的事情超出 body 之外,它们应该如何处理呢?

对于像 jQuery 这样以 DOM 为中心(当然还有 BOM)的工具来说,这个问题很简单——以 DOM 为中心就意味着浏览器有什么你就用什么,无非就是它原生的 API 不好用或不够用,你拿过来用 jQuery 封装一下就好了,换汤不换药。所以当你要操作 body 以外的东西,原生的东西随便你用,比如 document(DOM),比如 window(BOM) 等等,你唯一要做的就是外面套一个 $() 壳子。

在我们的大脑模型里已经习惯了把 HTML 和 DOM 视为一体,但除此之外 DOM 和 BOM 还提供了丰富的接口来处理很多额外的事情,每一个框架或库都会多多少少提供这些额外接口的封装,比如 Angular 里的 $cookie$location,甚至干脆彻底的 $document$window,然而当这些东西和 component 关联在一起的时候,事情会变得微妙起来:什么可以做不可以做?何时/何处来做?这些问题的界线变得摇摆不定。

Angular 有 DI(依赖注入)的机制,在 directives 的层面上,它巧妙的设计了一个 Attribute Level 的 directive 定义,通过 DI 你可以把超出 body 以外的操作通过 HTML 的属性绑定给其他的 Tag Level 的 directives。因为组件的存在范围被限制在 body 以内,这就是这种机制(目前)存在的意义所在。我们还不知道当 WebComponents 尘嚣落定之时会给出我们怎样的答案,当然届时 JavaScript 已经有了 modules,所以全局污染的问题已经不复存在,现在唯一不明朗的就是如何与组件的生命周期关联起来。

让我们来看一个例子。现在有很多应用都有这样的设计:Header 与 Main Content 没有明显的界限,看起来像一个整体。但如果 Main Content 的内容超出了浏览器一屏的高度,那么当用户向下滚动的时候,Header 会“浮”起来(通过下放的阴影)并固定在窗口顶部,很不错的视觉效果。

问题就在于监听用户滚动事件的动作应该是发生在 BOM 范围内的,如果你的应用是基于以组件为中心的思想开发的,这个动作到底应该在哪里做?

这个问题其实会有很多变数,比如说你可以设想这个动作和任何具体的组件无关,而是在应用程序初始化的时候直接执行。很好,但是有两个问题:

固定 Header 并为它添加阴影是需要 Header 已经存在于 DOM 之中的,通常在应用程序初始化的时候这个条件尚未达成

这个动作并不是发生在全局范围之内的,比如说某个路由进入之后或某种组件渲染之后才发生

以上任意一点都可以否决初始化执行这个方案,如果你考虑长远和周全一些的话就必须另寻出路。

好,我不废话了,先把最近用 Ember 完成的这个例子代码写出来,最后我再说一点对此的想法吧。

第一步,把 Header 抽象为组件

这个很简单,直接 ember generate component app-header 就好了,代码略过。

第二步,在组件渲染之后执行监听用户向下滚动的事件并为组件添加 class,这个 class 完成了阴影等效果。
const SCROLL_THRESHOLD = 50  // header" height is 50px

export default Ember.Component.extend({
  classNameBindings: ["sticky"],
  didInsertElement() {
    window.addEventListener("scroll", () => {
      if (window.scrollY >= SCROLL_THRESHOLD) {
        this.set("sticky", true)
      } else {
        this.set("sticky", false)
      }
    }
  }
})
第三步,当组件销毁后,注销监听回调

这就可以发生在 body 以外的操作能和组件的生命周期紧密联系在一起。这一点很重要,不管你用 Ember 还是 Angular/React,一定要注意组件的生命周期,特别是组件销毁时这些框架都会提供对应的 hook,要注意清理“垃圾”,移除绑定,释放内存等等,避免内存泄漏

const SCROLL_THRESHOLD = 50  // header" height is 50px

function _stickHeaderHandler() {
  if (window.scrollY >= SCROLL_THRESHOLD) {
    this.set("sticky", true)
  } else {
    this.set("sticky", false)
  }
}

export default Ember.Component.extend({
  classNameBindings: ["sticky"],
  didInsertElement() {
    window.addEventListener("scroll", _stickHeaderHandler.bind(this))  // remember to bind!!!
  },
  willDestroyElement() {
    window.removeEventListener("scroll", _stickHeaderHandler)
  }
})
DONE

我们还可以怎样改进它呢?问题有二:

组件不应该固化特殊的行为,如果这个组件是跨应用共享的(比如你发布成 Addon),那么其他应用可能是不需要置顶的使用者期望的是如下的可选项:

{{app-header stickyOnScroll=50}}

监听滚动那一套行为如果不是组件特有的(这就派出了发步成 Addon 的条件)而是应用内共享的,则应该想办法抽象出去——监听滚动这个事情很典型

对于问题一,答案已经揭示在那里了。组件都是可以传递参数或外部作用域的,利用此机制进行判断来执行可选行为,这是对用户友好的举措。

对于问题二,在 Ember 里你至少有三个选项:

抽象成 Mixin。这个很直观,缺点是 Mixin 提供的属性不是 default value,它不能由你主动去覆盖,不够灵活;

定义成新的 Component。需要继承的其他组件可以 extend 它,解决 Mixin 不够灵活的问题,局限是只能给组件用——不过对于处理浏览器事件和操作 DOM/BOM 已够用了;

抽象成 Service。这个等价于 Angular 的 DI,可以由你自己定义丰富的接口来配置和调用,最灵活,适合封装需要的外部接口等等。

关于 Service,具体的代码先 hold,以后我会专门讲 Service 在 Ember 里的用法。今天这个例子不适合抽象 Service,原因就是上面的第二点。

当我在几天前对此还很困惑时,我一度认为像 Angular 的 Attribute Level Directives 才是处理此类问题的最佳方案,然而 WebComponent 并没有 Attribute Level Component 这种设计,这也是我困惑的最初原因。现在想一想,Attribute Level Directives 等于无视组件的生命周期(当然它有自己的生命周期,但是和要附着的目标组件无关,你得管理两份),它把可选行为附着于目标组件的过程等同于你创建一个新的特殊的 Service(特殊之处就在于它可以放在模版里),然后利用这个 Service 去写实现代码并且还可以再 DI 其他的 Services,以此来实现可选性和可复用性。这种设计乍看讨巧但也有很多缺点,比如说多个 directives 共存的时候要考虑优先级和行为覆盖的问题,比如说和未来的 WebComponents 不兼容改造起来很费事,等等。

现在我们看到,React 一开始做得就很不错(后起之秀借鉴了很多前辈们的经验教训),不过它只是一个渲染引擎,做大型应用还需要你在整体架构上下功夫;Ember 的架构很完整,以前的问题很多但现在都在一一完善,设计思路没有什么错误,拿来做 UI 交互复杂的 web 应用的确是很不错的选择。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/85991.html

相关文章

  • React Native Vs. Xamarin Vs. Ionic Vs. Flutter

    React Native Vs. Xamarin Vs. Ionic Vs. Flutter:Which Is Best For Cross-Platform Mobile App Development? While developing Native Mobile Apps, Android apps are written in Java, and iOS ones in Swift and...

    Clect 评论0 收藏0
  • [Tips on Ember 2] 如何尝试 angle-bracket component

    摘要:警告版本是很不稳定的,并不推荐使用于要上线的应用。如果你要尝试新的特性,要么是新建一个测试用的,要么是你的应用离正式上线还早并且你和你的团队折腾得起。在此功能正式发布之后应该是不需要这段补丁代码的,目前来说也不会影响使用。 Ruby China 的朋友大概都知道我很喜欢 Ember,然而我用 Ember 的经历其实远比不上 Angular 那么丰富(Ember 业余爱好,Angular...

    Yu_Huang 评论0 收藏0
  • [Tips on Ember 2] Ember CLI with Webstorm

    摘要:好,你用就用吧,各种问题自己也不会看文档问谷歌,成天怨声载道的不得不吐槽一下现在的年轻人。为什么使用有关和的纠结历史可以去谷歌一下,此处不再啰嗦最根本的原因就是对的支持更好,更新和维护也更勤快。 Tips on Ember 2 对我来说是没什么计划性的写作,我只是把它当做是每天工作的总结日志,一个很重要的目的是为团队做一些技术事务的整理,以帮助一些新人快速成长起来。如果有些内容不能满足...

    curlyCheng 评论0 收藏0
  • 使用REACT VR构建web虚拟现实

    Building virtual reality experiences on the web with React VR Over the past year, virtual reality has made major strides toward becoming the next computing platform. With Oculus Rift, consumer-grade h...

    anquan 评论0 收藏0

发表评论

0条评论

jk_v1

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<