[译] Vue.js 的注意事项与技巧

原文连接:Vue.js — Considerations and Tricksjavascript

Vue.js 是一个很棒的框架。然而,当你开始构建一个大型 JavaScript 项目的时候,你将对 Vue.js 感到一些困惑。这些困惑并非来自框架自己,相反 Vue.js 团队会常常调整一些重要设计策略。css

相对于 React 和 Angular,Vue.js 面向一些不一样水平的开发者。它更加的友好,不论是对初学者仍是经验丰富的老手。它并不隐藏一些 DOM 操做,相反它与 DOM 配合的很好。html

这篇文章更像是一个目录,列举了我在 Vue.js 的初学路上遇到一些问题和技巧。理解这些关键性的设计技巧,有助于咱们构建大型的 Web 应用。vue

写这篇文章的时候是 2018 年 5 月 18 日,下面这些技巧依然是有效的。可是框架升级,或者浏览器底层或者 JS API 发生改变时,他们可能会变得不是那么有用。java

译者注:尽管 Vue.js 3 即将到来,可是下面的技巧大部分是有用的,由于 3 的版本并不会改变一些上层 API ,最大的特性多是底层数据 Observer 改有 proxy 实现,以及源码使用 typescript 构建。git


一、为何 Vue.js 不使用 ES Classes 的方式编写组件

若是你使用过相似于 Angular 的框架或者某些后端 OOP 语言后,那么你的第一个问题多是:为何不使用 Class 形式的组件?es6

Vue.js 的做者在 GitHub issues 中很好的回答了这个问题: Use standard JS classes instead of custom syntax?github

为何不使用 Class 这里有三个很重要的缘由:web

  1. ES Classes 不可以知足当前 Vue.js 的需求,ES Classes 标准尚未彻底规范化,而且老是朝着错误的方向发展。若是 Classes 的私有属性和装饰器(当前已进入 Stage 3)稳定后,可能会有必定帮助。
  2. ES Classes 只适合于那些熟悉面向对象语言的人,它对哪些不使用复杂构建工具和编译器的人不够友好。
  3. 优秀的 UI 组件层次结构通常都是组件的横向组合,它并非基于继承的层次结构。而 Classes 形式显然更擅长的是后者。

译者注:But,Vue.js 3.0 将支持基于 Class 的组件写法,真香。typescript

二、如何构建本身的抽象组件?

若是你想构建本身的抽象组件(好比 transition、keep-alive),这是一个比构建大型 web 应用更加疯狂地想法,这里有一些关于这个问题的讨论,可是并无什么进展。

Any plan for docs of abstract components?

译者注:在 Vue.js 内部组件(transition、keep-alive)中,使用了一个 abstract 属性,用于声明抽象组件,这个属性做者并不打算开放给你们使用,因此文档也没有说起。可是若是你要使用也是能够的,那么你必须深刻源码探索该属性有何做用。

可是不要惧怕,若是你能够很好地理解 slots ,你就能够构建本身的抽象组件了。这里有一篇很好的博客介绍了要如何作到这一点。

Writing Abstract Components with Vue.js

译者注:下面是《在 Vue.js 中构建抽象组件》的简单翻译

抽象组件与普通组件同样,只是它不会在界面上显示任何 DOM 元素。它们只是为现有组件添加额外的行为。
就像不少你已经熟悉的 Vue.js 的内置组件,好比:`<transition>`、`<keep-alive>`、`<slot>`。

如今展现一个案例,如何跟踪一个 DOM 已经进入了可视区域 ,让咱们使用 IntersectionObserver API 来实现一个解决这个问题的抽象组件。
(完整代码在这里:[vue-intersect](https://github.com/heavyy/vue-intersect))
复制代码
// IntersectionObserver.vue
export default {
  // 在 Vue 中启用抽象组件
  // 此属性不在官方文档中, 可能随时发生更改,可是咱们的组件必须使用它
  abstract: true,
  // 从新实现一个 render 函数
  render() {
    // 咱们不须要任何包裹的元素,只须要返回子组件便可
    try {
      return this.$slots.default[0];
    } catch (e) {
      throw new Error('IntersectionObserver.vue can only render one, and exactly one child component.');
    }

    return null;
  },
  mounted () {
    // 建立一个 IntersectionObserver 实例
    this.observer = new IntersectionObserver((entries) => {
      this.$emit(entries[0].isIntersecting ? 'intersect-enter' : 'intersect-leave', [entries[0]]);
    });

    // 须要等待下一个事件队列,保证子元素已经渲染
    this.$nextTick(() => {
      this.observer.observe(this.$slots.default[0].elm);
    });
  },
  destroyed() {
    // 确保组件移除时,IntersectionObserver 实例也会中止监听
    this.observer.disconnect();
  }
}
复制代码
让咱们看看如何使用它?
复制代码
<intersection-observer @intersect-enter="handleEnter" @intersect-leave="handleLeave">
  <my-honest-to-goodness-component></my-honest-to-goodness-component>
</intersection-observer>
复制代码

可是在这样作以前,请你三思。咱们通常依赖 mixins 和一些纯函数来解决一些特殊场景的问题,你能够将 mixins 直接看作一个抽象组件。

How do I extend another VueJS component in a single-file component? (ES6 vue-loader)

三、我不太喜欢 Vue.js 的单文件组件,我更但愿 HTML、CSS 和 JavaScript 分离。

没有人阻止你这样作,若是你是个注重分离的哲学家,喜欢把不一样的东西放在不一样文件,或者讨厌编辑器对 .vue 文件的不稳定行为,那么你这么作也是能够的。你要作的很简单:

<!--https://vuejs.org/v2/guide/single-file-components.html -->
<!-- my-component.vue -->
<template src="./my-component.html"></template>
<script src="./my-component.js"></script>
<style src="./my-component.css"></style>
复制代码

这么作,就会出现下一个问题:个人组件老是须要 4 个文件(vue + html + css + js)吗?我能不能摆脱 .vue 文件? 答案是确定的,你可使用 vue-template-loader

个人同事还为此写了一篇很棒的教程:

Using vue-template-loader with Vue.js to Compile HTML Templates

四、 函数式组件

感谢 React.js 让函数式组件很流行,这是由于他们无状态、易于测试。然而它们也存在一些问题。

译者注:不了解 Vue.js 函数式组件的能够先在官方文档查看:官方文档

4.1 为何我不能对功能组件使用基于 Class 的 @Component 装饰器?

再次回到 Classes,它只是一种用于保存本地状态的数据结构。若是函数式组件是无状态的,那么使用 @Component 装饰器就是无心义的。

这里有关于这个的讨论:

How to create functional component in @Component?

4.2 外部类和样式不该用于函数式组件

函数式组件不能像普通组件那样,绑定具体的类和样式,必须在 render 函数中手动应用这些绑定。

DOM class attribute not rendered properly with functional components

class attribute ignored on functional components

4.3 函数式组件老是会重复渲染?

TLDR:在函数式组件中使用有状态组件时务必要当心

Functional components are re-rendered when props are unchanged.

函数式组件至关于直接调用组件的 Render 函数,这意味着你应该:

避免在 render 函数中直接使用有状态组件,由于这会在每次调用 render 函数时建立不一样的组件实例。

若是函数式组件是叶子组件,会更好地利用它们。 须要注意的是,一样的行为也适用于 React.js。

4.4 如何在Vue.js 函数式组件中触发一个事件?

在从函数式组件中触发一个事件并不简单。不幸的是,文档中也没有提到这一点。函数式组件中不可用 $emit 方法。stack overflow 上有人讨论过这个问题:

How to emit an event from Vue.js Functional component?

五、Vue.js 的透明包裹组件

组件包裹一些DOM元素,而且公开了这些DOM元素的事件,而不是根DOM的节点实例。

例如:

<!-- Wrapper component for input -->
<template>
    <div class="wrapper-comp">
        <label>My Label</label>
        <input @focus="$emit('focus')" type="text"/>
    </div>
</template>
复制代码

这里咱们真正感兴趣的是 input 节点,而不是 div 根节点,由于它主要是为了样式和修饰而添加的。用户可能对这个组件的几个输入事件感兴趣,好比 blurfocusclickhover等等。这意味着咱们必须从新绑定每一个事件。咱们的组件以下所示。

<!-- Wrapper component for input -->
<template>
    <div class="wrapper-comp">
        <label>My Label</label>
        <input type="text" @focus="$emit('focus')" @click="$emit('click')" @blur="$emit('blur')" @hover="$emit('hover')" />
    </div>
</template>
复制代码

实际上这是彻底不必的。简单的解决方案是使用 Vue 实例上的属性 vm.$listeners 将事件从新绑定到所需DOM 元素上:

<!-- Notice the use of $listeners -->
<template>
    <div class="wrapper-comp">
        <label>My Label</label>
        <input v-on="$listeners" type="text"/>
    </div>
</template>
<!-- Uses: @focus event will bind to internal input element -->
<custom-input @focus="onFocus"></custom-input>
复制代码

六、为何你不能在 slot 上绑定和触发事件

我常常看到有些开发人员,在 slot 上进行事件的监听和分发,这是不可能的。

组件的 slot 由调用它的父组件提供,这意味着全部事件都应该与父组件相关联。尝试去倾听这些变化意味着你的父子组件是紧密耦合的,不过有另外一种方法能够作到这一点,Evan You解释得很好:

Is it possible to emit event from component inside slot #4332

Suggestion: v-on on slots

七、slot 中的 slot(访问孙辈slot)

在某些时候,可能会遇到这种状况。假设有一个组件,好比 A ,它接受一些 slot 。遵循组合的原则,使用组件 A 构建另外一个组件 B 。而后你把 B 用在 C 中。

那么如今问题来了: 如何将 slot 从 C 组件传递到 A 组件?

要回答这个问题,首先取决你使用何种方式构建组件? 若是你是用 render 函数,那就很简单。你只须要在组件 B 的 render 函数中进行以下操做:

// Render function for component B
function render(h) {
    return h('component-a', {
        // Passing slots as they are to component A
        scopedSlot: this.$scopedSlots
    }
}
复制代码

可是,若是你使用的是基于模板的方式,那么就有些糟糕了。幸运的是,在这个问题上有了进展:

feat(core): support passing down scopedSlots with v-bind


但愿这篇文章让你对 Vue.js 的设计思路有了更深刻的了解,并为你提供了一些在高级场景中的技巧。