该篇为翻译文章:原文为:Intro to Vue.js: Components, Props, and Slots

该系列文章目录:
Vue.js #1-渲染、指令、事件(原文)

Vue.js #2-组件、属性、Slots (当前)

Vue.js #3-Vue-cli及生命周期(原文)

Vue.js #4-Vuex(原文)

Vue.js #5-动画(原文)

组件当中的数据传输

如果你熟悉React或者Angular2的话,那么对于组件之间传递数据想关的操作你一定不会陌生。不熟悉也没关系,我们接下来就会说到。

一个网站应用不管是大还是小,它都是由一个个小模块组成的,这样非常易于维护,例如:

<header></header>
<aside>
  <sidebar-item v-for="item in items"></sidebar-item>
</aside>
<main>
  <blogpost v-for="post in posts"></blogpost>
</main>
<footer></footer>

这是一个非常简单的例子,你可以发现这种方式非常的易于管理。

Vue可以通过几种不同的方式来创建组件,让我们由浅入深。但是记住,再复杂的应用都是由一个一个很普通的组件组成的。

app.$mount('#app');

var app = new Vue({
  el: 'hello',
  template: '<h1>Hello World!</h1>'
});

上面的代码完全可以跑起来,但是它并不是一个好例子,因为它只并不能被重复使用。我们要的是一个可复用的组件,从而方便我们给不同的组件实例传递不同的参数。从父组件传递到子组件的一种方法是使用props

下面我来举一个简单的例子,下面的代码中,Vue.component是一个组件,new Vue叫做Vue实例。在一个应用当中,可以有多个Vue实例,但一般来说,我们在一个应用当中有一个实例和一些组件,这个实例就是我们整个APP的实例。

Vue.component('child', {
  props: ['text'],
  template: `<div>{{ text }}<div>`
});

new Vue({
  el: "#app",
  data() {
    return {
      message: 'hello mr. magoo'
    }
  }
});

<div id="app">
  <child :text="message"></child>
</div>
<div id="app">
  <child :text="message"></child>
</div>

结果:http://codepen.io/sdras/pen/788a6a21e95589098af070c321214b78

现在,我们可以在整个应用当中重复使用这个组件:

<div id="app">
  <child :text="message"></child>
  <child :text="message"></child>
</div>

结果:http://codepen.io/sdras/pen/9c04bdcf1a2d0c770d6a1fd4af3c66f3

我们也可以为属性添加验证,就像React中的PropTypes。这个功能非常好,因为这样可以实现「自我文档化」,如果参数不是我们想要的,那么将会返回错误信息,但要注意的是,这只在开发模式下才起作用:

Vue.component('child', {
  props: {
    text: {
      type: String,
      required: true
    }
  },
  template: `<div>{{ text }}<div>`
});

下面的例子当中,我加载的是Vue开发版,而我偏偏传入了一个错误类型的值。这个时候你可以在console面板当中看到一条错误信息。

Vue.component('child', {
  props: {
    text: {
      type: Boolean,
      required: true
    }
  },
  template: `<div>{{ text }}<div>`
});

结果:http://codepen.io/sdras/pen/d6fcaeee50a530d9a5e5832f0aec0773

对象和数组应该由一个工厂函数来进行返回。你甚至可以创建一个自定义的验证函数。你可以根据你的业务逻辑来检测你的值是否准确。更多关于验证函数的内容,官方的「Prop Validation」写的非常详细。

当然 ,除了传入一个变量数据之外 ,你还可以传入一个静态值:

Vue.component('child', {
  props: { 
    count: {
      type: Number,
      required: true
    }
  },
  template: `<div class="num">{{ count }}</div>`
})

new Vue({
  el: '#app',
  data() {
    return {
      count: 0    
    }
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    }
  }
})

<div id="app">
  <h3>
    <button @click="increment">+</button>
    Adjust the state
    <button @click="decrement">-</button>
  </h3>
  <h2>This is the app state: <span class="num">{{ count }}</span></h2>
  <hr />
  <h4><child count="1"></child></h4> 
  <p>This is a child counter that is using a static integer as props</p>
  <hr />
  <h4><child :count="count"></child></h4>
  <p>This is the same child counter and it is using the state as props</p>
</div>

结果:http://codepen.io/sdras/pen/5a34f6ed12cf954202c6d38f1ceba633

它们的区别在于你是否绑定一个属性:

不绑定属性:<child count="1"></child>

VS

绑定属性:<child :count="count"></child>

在例子当中,你注意到我们使用了ES6当中的「模板字符串」(强烈推荐大家使用Babel来对ES6进行编译转换):

Vue.component('individual-comment', {
  template: 
  `<li> {{ commentpost }} </li>`,
  props: ['commentpost']
});

new Vue({
  el: '#app',
  data: {
    newComment: '',
    comments: [
      'Looks great Julianne!',
      'I love the sea',
      'Where are you at?'
    ]
  },
  methods: {
    addComment: function () {
      this.comments.push(this.newComment)
      this.newComment = ''
    }
  }
});

<ul>
    <li is="individual-comment"
      v-for="comment in comments"
      v-bind:commentpost="comment"
    ></li>
  </ul>
  <input v-model="newComment"
    v-on:keyup.enter="addComment"
    placeholder="Add a comment"
  />

结果:http://codepen.io/sdras/pen/cd81de1463229a9612dca7559dd666e0

译者:上面的代码当中在调用组件的时候,使用了is关键字来代替之前的直接将组件名称写成一个标签,这样做是为了避免HTML报错,详情见:
DOM Template Parsing Caveats

「模板字符串」对我们来说非常方便有用。但是如果需要写很多HTML在里面的话,仍然不是一个好办法,因为看起来会很乱。所以,我们使用一个单独的「模板」来解决这个问题,即将模板写在一个特殊的script标签当中供我们调用:

<!-- This is the Individual Comment Component -->
<script type="text/x-template" id="comment-template">
<li> 
  <img class="post-img" :src="commentpost.authorImg" /> 
  <small>{{ commentpost.author }}</small>
  <p class="post-comment">"{{ commentpost.text }}"</p>
</li>
</script>

Vue.component('individual-comment', {
  template: '#comment-template',
  props: ['commentpost']
});

结果:http://codepen.io/sdras/pen/egEgXb

Slots (空位、占位符)

假如我们现在有两个组件,这两个组件之间有一些细小的不同(可能是内容的不同,也可能是样式的不同)。那么我们可以传入一些属性参数(props),针对每个组件传入不同的参数来达到目的,或者我们也可以多写几个类似的组件。但是这些解决方案只适用于用到类似的数据或者功能的组件。如果组件之间的变化不同之处特别大的话,那么这时候就需要Slots(占位符)介入了。

假如,我们现在在一个应用实例里用到了两次<app -child>组件。而这两个组件当中,有相同的内容,有不同的内容。对于相同的内容,我们把这放到一个p标签当中,而针对不同的内容,我们就需要使用<slot></slot>标签了。

<script type="text/x-template" id="childarea">
  <div class="child">
    <slot></slot>
    <p>It's a veritable slot machine!<br> 
    Ha ha aw</p>
  </div>
</script>

然后,在应用实例里面,我们可以在</app><app -child>组件标签中写任何我们想要的内容,这些内容到时候会替换<slot>标签:

<div id="app">
  <h2>We can use slots to populate content</h2>
  <app-child>
    <h3>This is slot number one</h3>
  </app-child>
  <app-child>
    <h3>This is slot number two</h3>
    <small>I can put more info in, too!</small>
  </app-child>
</div>

结果:http://codepen.io/sdras/pen/e06f9393e73054e7185ff48dfa36e161

当然,你也可以写一个默认的内容在</slot><slot>当中,例如</slot><slot>I am some default text</slot>。这时,当你在调用组件的时候,如果你不写其它的内容的话,就会显示这块默认的内容。

你也可以给<slot>取名字。如果你在一个组件当中有两个</slot><slot>,这时候就需要给它们取不同的名字来进行区分了。比如,</slot><slot name="headerinfo"></slot>,那么在调用组件的时候则需要指定slot:<h1 slot="headerinfo">I will populate the headerinfo slot!</h1>,下面来看一个例子:

<div id="post">
  <main>
    <slot name="header"></slot>
    <slot></slot>
  </main>
</div>

<app-post>
  <h1 slot="header">This is the main title</h1>
  <p>I will go in the unnamed slot!</p>
</app-post>

<main>
  <h1>This is the main title</h1>
  <p>I will go in the unnamed slot!</p>
</main>

就我个人而言,如果用到了多个<slot>的话,我会一一给它们命名,这样就不会乱。但不管怎么说,Vue提供了非常灵活的API。

Slots实例

我们可以为不同的组件设计不同的样式,但这些组件的内容是一样的。下面我们来做一个「酒瓶标签生成器」的例子,其中一个按钮是用来切换组件的颜色用的,当改变了瓶子的颜色后,瓶子的标签和文字的颜色将同时发生改变,而里面的内容是不变的。

const app = new Vue({
  ...
  components: {
    'appBlack': {
      template: '#black'
    }
  }
});

APP实例的HTML:

<main>
 <component :is="selected">
    ...
    <path class="label" d="M12,295.9s56.5,5,137.6,0V409S78.1,423.6,12,409Z" transform="translate(-12 -13.8)" :style="{ fill: labelColor }"/>
    ...
  </component>

<h4>Color</h4>
  <button @click="selected ='appBlack', labelColor = '#000000'">Black Label</button>
  <button @click="selected ='appWhite', labelColor = '#ffffff'">White Label</button>
  <input type="color" v-model="labelColor" defaultValue="#ff0000">

变色组件HTML:

<script type="text/x-template" id="white">
  <div class="white">
     <slot></slot>
  </div>
</script>

(这个例子相对于前面的来说比较的大,所以你最好一边看代码一边看结果)

wine-label1

这个例子当中,我们在里面插入了很多SVG,这些SVG都是放到组件的</slot><slot>当中去的。我们通过一些按钮来改变内容或者样式。
我们把所有的东西都放到了一个</slot><slot>当中,现在我们把它改成多个</slot><slot>

<!-- main vue app instance -->
<app-comment>
  <p slot="comment">{{ comment.text }}</p>
</app-comment>

<!-- individual component -->
<script type="text/x-template" id="comment-template">
  <div>
    <slot name="comment"></slot>
  </div>
</script>

我们可以在不同的组件之间进行切换,但使用的是同一个</slot><slot>。但当我们在来回切换的时候,如果想保持每个组件自身的值不受到影响该怎么办呢?现在的例子里面,当我们在「黑」「白」之间进行切换的时候,模板进行了切换,但它们展现的内容(数据)是一样的。那么如果我们需要「黑」「白」都有自己的label数据的话,那么我们就需要在组件外面套上一个<keep -alive></keep>来进行实现。

<keep-alive>
  <component :is="selected">
    ...
  </component>
</keep-alive>

wine-label2

结果:http://codepen.io/sdras/pen/db71c231f760ee3a53e9d4e65f8745b8

为了方便给大家展示,我们举的示例是把所有的东西都写在了一个或两个文件当中。其实当我们在真正做一个项目的时候,最好是把不同的组件放到不同的文件当中,然后在需要的时候进行导入即可。具体的细节,我们下回再说,下次让我们来聊聊Vue项目管理相关的东西,比如Vue-cli,Vuex等等。