Components – Understanding and Using
在第一章, 我们讨论了组件甚至创建了一些。 在这章, 我们将继续使用组件并实践一些有趣的指令。 换句话说, 我们要:
- 重返组件话题回顾组件到底是什么
- 为我们的方程式创建组件
- 学习什么是单文件组件
- 学习如何用特性去达到响应式的 CSS 变换
正如你在前面章节所记得, 组件是 Vue 方程式中拥有自己作用域, 数据, 方法的特殊部分。 方程式可以使用组件并重用它。 前一章, 你知道了组件以 Vue.extend({…}) 方法创建, 以 Vue.component() 语法注册。 所以为了创建使用组件我们需要这样做:
我们在 HTML 中这样使用组件:
<hello-component></hello-component>
</div>
初始化及注册可以写成单个 Vue 组件的规范写法:
Vue.component('hello-component', {template: '<h1> Hello </h1>'});
使用组件的好处
在深入探究组件重写方程式前我们了解一些东东。 在本章, 我们将覆盖以下内容: 在组件中控制 data 及 el 属性, 组件模板, 作用域和预处理器。
在 HTML 中声明模板
在我们这个例子中, 我们创建了一个用字符串重写的 Vue 组件。 它非常简单, 因为我们需要的都在其中。 现在, 想象一下我们的组件有一个更为复杂的 HTML 结构。 用字符串模板编写复杂组件容易出错, 冗余而且违反最佳实践。
我指的最佳实践是说简洁可维护的代码。 用字符串写的复杂的 HTML 完全不具可维护性。
Vue 可以通过 template 标签在 HTML 声明模板。
在 HTML 中重写我们的事例。
<template id="hello">
<h1>Hello</h1>
</template>
然后再里面加上我们的组件, 我们需要一个 ID 来指定模板。
Vue.component('hello-component', {
template: '#hello'
})
我们的整个代码看上去像这样:
<body>
<template id="hello">
<h1>Hello</h1>
</template>
<div id="app">
<hello-component></hello-component>
</div>
<script src="vue.js"></script>
<script>
Vue.component('hello-component', {
template: '#hello'
});
new Vue({
el: '#app'
});
</script>
</body>
在前面的事例里, 我们只在组件中使用了 template 特性。 现在让我们来看看 data 和 el 特性应该在组件里如何配置。
在组件里控制 data 和 el 属性
正如前面提及的, 组件的语法和 Vue 实例的语法很想, 但是它必须扩展自 Vue 而非直接被调用。 有了这个前提, 貌似这样创建组件才对:
var HelloComponent = Vue.extend({
el: '#hello',
data: {msg: 'hello'}
})
但是这样会导致作用域泄漏, 每一个 HelloComponent 实例都会共享相同的 data 和 el。 这不是我们想要的。 这就是 Vue 为什么会明确地要求我们以函数的形式来声明这些属性。
var HelloComponent = Vue.component('hello-component', {
el: function () {
return '#hello';
},
data: function () {
return {
msg: 'Hello'
}
}
});
甚至当你以对象的方式声明 data 和 el 时, Vue 会有善意的警告。
Vue 会在你用对象作为数据时发出警告
组件的作用域
正如已经提到的, 所有的组件拥有它们自己的作用域, 而且不会被其他组件访问到。 然而, 全局的方程式作用域可以被所有注册过的组件访问到哦。
你可以看到组件的作用域是本地的, 而方程式的作用域是全局的。 这是当然的。 但是, 在组件内不能使用父作用域。你不得不明确指出到底哪个组件的父级数据属性可以被访问,通过使用 prop 属性,然后再用 v-bind 语法把他们绑定到组件实例上。
我们可以先声明 HelloComponent 组件并包含数据及 msg 特性:
Vue.component('hello-component', {
data: function () {
return {
msg: 'Hello'
}
}
});
创建 Vue 实例并包含一些数据:
new Vue({
el: '#app',
data: {
user: 'hero'
}
});
在我们的 HTML 里, 创建模板并以 ID 的形式应用到组件上:
//模板声明
<template id="hello">
<h1>{{msg}} {{user}}</h1>
</template>
//在组件中使用模板
template: '#hello',
data: function () {
return {
msg: 'Hello'
}
}
});
为了在页面中看到组件, 我们应该在 HTML 中调用它:
<div id="app">
<hello-component></hello-component>
</div>
如果你在浏览器中打开页面, 你只能看到 Hello; user 数据属性未被绑定到组件中:
父级的数据属性未被绑定到 Vue 组件中。
为了绑定父级的数据, 我们不得不做以下两件事:
- 在 props 特性中指明这个属性
- 把它绑定到 hello-component 调用
刷新页面, 你将看到如下消息:
纠正向父级数据属性的绑定后, 一切按期执行了。
Tip
实际上, v-bind:user 语法可以有如下的简写:
<hello-component :user="user"></hello-component>
组件的完美之处在于它们可以在其它组件里面重用这就像乐高中的积木一样! 我们来创建另一个组件; 一个叫 greetings 由两个二级组件(form asking 和 hello component )组成的组件。
<!--form 模板-->
<template id="form">
<div>
<label for="name">What's your name?</label>
<input v-model="user" type="text" id="name">
</div>
</template>
//saying hello 模板
<template id="hello">
<h1>{{msg}} {{user}}</h1>
</template>
现在,我们在这些模板的基础上来注册两个 Vue 组件:
//注册 form 组件
Vue.component('form-component', {
props: ['user']
});
//注册 hello 组件
Vue.component('hello-component', {
template: '#hello',
data: function () {
return {
msg: 'Hello'
}
},
props: ['user']
});
最后, 我们将创建我们的 greetings 模板, 它使用了 form 和 hello 组件。 别忘了我们已经向组件绑定了 user 属性。
<template id="greetings">
<div>
<form-component :user="user"></form-component>
<hello-component :user="user"></hello-component>
</div>
</template>
此时, 我们可以创建我们的 greetings 组件并在内使用 greetings 模板。
我们在这个组件内初始化带 user 名字的 data 函数:
//基于 greetings 模板创建 greetings 组件
Vue.component('greetings-component', {
template: '#greetings',
data: function () {
return {
user: 'hero'
}
}
});
在我们的中枢方程式中, 调用 greetings 组件:
<div id="app">
<greetings-component></greetings-component>
</div>
别忘了初始化 Vue 实例
new Vue({
el: '#app'
})
在浏览器中打开页面, 你可以看到如下输出:
一个由不同组件构成的页面
尝试在 input 内改变 name 值。你会期望它改变因为我们已经绑定了它, 但是奇怪的是, 改变并未发生。 啊哦(⊙⊙;).__, 就是这样。 默认情况下, 所有的属性遵守单向数据绑定。 这意味着在父级作用域内的变化将通知到所有子极组件反之却会失效。 这可以防止子极组件意外破坏父级状态。 就是这样, 但是, 也可以通过调用 events 强迫子极组件与他们的父级通信。 具体查看 Vue 文档
<咚咚咚, ( •̀ ω •́ )✧ 大家快记重点。 组件间数据传递的方法>
在我们的例子中, 我们可以在每次输入变化时向 form input 组件绑定 user 模型, 然后分发 input 事件。 我们通过使用 v-on:input 修饰符来完成它, 就如在这里描述的一样 https://vuejs.org/guide/components.html#Form-Input-Components-using-Custom-Events 。
因此, 我们必须向 form-component 传入 v-model=”user” :
<form-component v-model="user"></form-component>
然后, form-component 应该接收 value 属性并分发 input 事件:
Vue.component('form-component', {
template: '#form',
props: ['value'],
methods: {
onInput: function (event) {
this.$emit('input', event.target.value)
}
}
});
在 form-component 模板内的输入框应该绑定 v-on:input 和用 v-on:input 修饰符的 onInput 方法:
<input v-bind:value="value" type="text" id="name" v-on:input="onInput">
Tip
事实上, 在先前的 Vue 2.0 中,这种在组件和父级间双向的同步是可以通过 sync 修饰符来交流属性的:
<form-component :user.sync="user"></form-component>
刷新页面。 你现在就可以改变输入框内的值了, 它会迅速地传递给父级作用域, 从而也可通知给其它子组件。
通过 .sync 修饰符可以在父级和子组件间形成双向绑定。
你可以在这里发现这个事例 https://jsfiddle.net/chudaol/1mzzo8yn/。
在 Vue 2.0 版本前, 这里还有其它数据绑定修饰符, .once 。用这个修饰符, 数据将只绑定一次, 任何其它的变化不再影响到组件的状态。 比较下面几种方式:
用一个简单组件来重写购物清单
既然我们已经深入了解了组件, 我们就来用组件重写购物清单吧。
Tip
对于需要重写的方程式, 我们需要基于这个版本
当我们开始讨论组件的时候, 我们就已经这样做过了。 不过在那时我们使用了模板字符串来设置组件。 现在我们用刚刚学习的组件配置来重写。 我们再来看看组件的界面和标识。
我们的购物清单有四个组件
因此, 我建议应该包含这个四个组件:
- AddItemComponent: 增加新的列表项
- ItemComponent: 渲染后的购物列表
- ItemsComponent: 渲染并操作列表
- ChangeTitleComponent: 改变标题
为组件们定义模板
为我们的组件提供模板, 假设组件已经被定义注册。
注意
CamelCase VS kebab-case 你可能已经注意到我们在声明组件名字时使用了驼峰式 ( var HelloComponent = Vue.extend({…})) ,我们以短横线隔开式来命名它们: Vue.component(‘hello-component’, {…}) 。 我们这么做是因为 HTML 不区别大小写的特性。 因此呢, 我们的组件将会是这个样子地:
add-item-component
item-component
items-component
change-title-component
可以在这里看一下我们以前的例子 ( https://jsfiddle.net/chudaol/vxfkxjzk/3/ )。
我们来用模板和组件名重写它。 在这部分, 我们将只关心呈现层, 对于数据绑定的控制留到后面。 我们只复制粘贴方程式的部分 HTML 然后在我们的组件中重用。 我们的模板看起来像是下面这样:
<!--add new item template-->
<template id="add-item-template">
<div class="input-group">
<input @keyup.enter="addItem" v-model="newItem"
placeholder="add shopping list item" type="text"
class="form-control">
<span class="input-group-btn">
<button @click="addItem" class="btn btn-default"
type="button">Add!</button>
</span>
</div>
</template>
<!--list item template-->
<template id="item-template">
<li :class="{ 'removed': item.checked }">
<div class="checkbox">
<label>
<input type="checkbox" v-model="item.checked"> {{ item.text }}
</label>
</li>
</template>
<!--items list template-->
<template id="items-template">
<ul>
<item-component v-for="item in items" :item="item">
</item-component>
</ul>
</template>
<template id="change-title-template">
<div>
<em>Change the title of your shopping list here</em>
<input v-bind:value="value" v-on:input="onInput"/>
</div>
</template>
<div id="app" class="container">
<h2>{{ title }}</h2>
<add-item-component></add-item-component>
<items-component :items="items"></items-component>
<div class="footer">
<hr/>
<change-title-component v-model="title"></change-title-component>
</div>
</div>
如你所见, 模板的主要部分都是复制粘贴了相应的 HTML 代码。
但是呢, 这里有很多重要的不同点。 在 list item 模板中, 做了轻微的改动。 你已经在前面学习了 v-for 指令。 在前面的例子中, 我们把它用在 li 这样的元素上。 现在你将看到我们同样把它应用在 Vue 自定义组件上。
你同样可能观察到了在标题模板上的小变化。 现在它有了一个绑定到它身上的值, 也使用了 v-on:input 修饰符进行分发 onInput 方法。 正如你在前面所学习的, 子组件不能在没有事件系统的情况下直接影响父级数据。
先看一眼我们先前做的购物清单方程式: 。 我们来加点创建组件的代码。 我们将使用模板的 ID 来定义组件的模板特性。 同时, 别忘了从父级传入的 props 特性。 因此, 我们的代码如下:
//add item component
Vue.component('add-item-component', {
template: '#add-item-template',
data: function () {
return {
newItem: ''
}
}
});
//item component
Vue.component('item-component', {
template: '#item-template',
props: ['item']
});
//items component
Vue.component('items-component', {
template: '#items-template',
props: ['items']
});
//change title component
Vue.component('change-title-component', {
template: '#change-title-template',
props: ['value'],
methods: {
onInput: function (event) {
this.$emit('input', event.target.value)
}
}
});
如你所见, 在每个组件的 props 特性中, 我们都传入了不同的数据特性。 我们同样在 add-item-template 组件里移入了 newItem 特性。 在 change-title-template 组件里增加了 onInput 方法用来分发输入事件, 所以用户的操作才会影响到父级组件。
在浏览器里打开 HTML 文件。 界面竟和以前的一模一样(⊙o⊙)?! 我们完成的代码可以在这里查看 https://jsfiddle.net/chudaol/xkhum2ck/1/ 。
练习
尽管我们的方程式看起来没什么变化, 它的功能却不在了。 不仅无法增加列表项, 而且会在控制台输出错误信息。
请使用事件系统为我们的组件添加功能。
在附录里的练习答案参考里有一个解决方案。
单文件组件
我们知道以前的最佳实践是分离 HTML 、 CSS、 JavaScript。 一些例如 React 的现代化的框架慢慢越过了这个规则。 当今在一个单文件里写结构, 样式, 逻辑代码已经很普遍了。 事实上, 对于一些小型组件, 我们完全可以转换成这种架构。 Vue 同样允许你在同一个文件里定义一切东东。 这种类型的组件被认为是单文件组件。
单文件组件以 .vue 结束。 想这种类型的方程式可以使用 webpack vue 来配置。 生成这种类型的方程式, 最简单的方法就是使用 vue-cli () 。
一个 Vue 组件可以由三部分组成:
- script
- template
- style
每个部分就如你所想的那样。 在 template 标签内放入 HTML 模板, 在 script 标签内放入 JavaScript 代码, 在 style 标签内放入 CSS 样式。
你还记得我们的 hello-component 组件吗? 从这里回顾一下 https://jsfiddle.net/chudaol/mf82ts9a/2/ 。
通过使用 vue-cli 的 webpack-simple 命令来生成脚手架。
npm install -g vue-cli vue init webpack-simple
以 Vue 组件来重写它, 创建我们的 HelloComponent.vue 文件, 增加如下代码:
<template>
<h1>{{ msg }}</h1>
</template>
<script>
export default {
data () {
return {
msg : 'Hello!'
}
}
}
</script>
注意我们不需要为我们的组件增加特殊的模板标记。
作为一个单文件组件, 它已经隐式地说明了模板已经只作用这个文件了。 你可能注意到这里有些 ES6 的语法。 当然, 也别忘了数据特性应该是个函数而非对象。
在我们的中枢脚本中, 我们需要创建 Vue app 来通知脚本使用 HelloComponent 组件:
import Vue from 'vue'
import HelloComponent from './HelloComponent.vue'
new Vue({
el: '#app',
components: { HelloComponent }
});
我们在 index.html 中的标记将不会改变。 它依然需要调用 hello-component :
<body>
<div id="app">
<hello-component></hello-component>
</div>
<script src="./dist/build.js"></script>
</body>
我们现在只需要安装 npm 依赖了(如果还没有的话), 构建方程式。
npm install
npm run dev
搞定, 你的浏览器将自动打开 localhost:8080 页面。
你可以在 查看代码。
你也可以在 webpackbin 中修改, 测试。 查看 hello 组件 http://www.webpackbin.com/N1LbBIsLb。
Tip
Webpackbin 是一项可以运行测试以 Webpack 构建的方程式的很棒的服务。 尽管还是测试版依然是一款好工具。 当然也有一些小问题, 例如, 当你下载整个项目时, 它将不会运行。
Vue 的创造者和贡献者也想着咱们开发者呢, 这里有一堆他们为我们开发的 IDE 插件。 你可以这里找到 https://github.com/vuejs/awesome-vue#syntaxhighlighting 。
如果喜欢使用 WebStorm IDE, 根据下面的说明来安装 Vue 插件。
- 找到 Preferences | Plugins
- 点击 Browse repositories
- 于搜索框内输入 vue
- 选择 Vue.js 点击安装
为 webstorm 安装 Vue 插件
样式和作用域
很明显, 模板和脚本都只属于其附属的组件。 但是对于样式就不一样了。 试试在我们的 hello 组件中为 h1 增加一些 CSS 规则:
<style>
h1 {
color: red;
}
</style>
现在, 刷新页面, Hello! 标题的颜色如期变成的红色。 然后在 index.html 文件中增加一个 h1 标签。 你可能会对这个标签同样变成了红色而感到吃惊:
<div id="app">
<h1>This is a single file component demo</h1>
<hello-component></hello-component>
所有的 h1 标签都有了我们在组件内定义的样式