偏前端-vue.js学习之路初级(二)组件化构建
vue.js 组件化构建
组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、自包含和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树:
一、使用组件
1.组件的全局注册--可以使用 Vue.component(tagName, options)
Vue.component('my-component', { // 选项 })
组件在注册之后,便可以在父实例的模块中以自定义元素** <my-component></my-component> **的形式使用。要确保在初始化根实例 之前 注册了组件:
<div id="example"> <my-component></my-component> </div>
// 注册 Vue.component('my-component', { template: '<div>A custom component!</div>' }) // 创建根实例 new Vue({ el: '#example' })
渲染为:
<div id="example"> <div>A custom component!</div> </div>
2.局部注册--不必在全局注册每个组件。通过使用组件实例选项注册,可以使组件仅在另一个实例/组件的作用域中可用:
var Child = { template: '<div>A custom component!</div>' } new Vue({ // ... components: { // <my-component> 将只在父模板可用,不添加到components选项里面则可适用于任何作用域 'my-component': Child } })
二、组件构造器选项
在 Vue 里,一个组件本质上是一个拥有预定义选项的一个 Vue 实例。因此,通过Vue构造器传入的各种选项大多数都可以在组件里用。 data 是一个例外,它必须是函数。
Vue组件构造器常用的选项:
// 注册 Vue.component('my-component', { template: '<div>A custom component!</div>', data: function() { return { var1: xxx, var2: xxx } }, computed: { sumOfVar2: function() { return (this.var2.innerVar1 + this.var2.innerVar2 + this.var2.innerVar3); } }, watch: { 'var1': function() { }, 'var2.innerVar1': function() { }, 'var2.innerVar2': function() { }, 'var2.innerVar3': function() { } }, methods: { func1: function() { }, func2: function() { }, func3: function() { } }, beforeCreate: function () { }, created: function () { }, beforeMount: function () { }, mounted: function() { }, beforeUpdate: function () { }, updated: function () { }, beforeDestroy: function() { }, destroyed: function() { } });
三、异步组件
在大型应用中,我们可能需要将应用拆分为多个小模块,按需从服务器下载。为了让事情更简单, Vue.js 允许将组件定义为一个工厂函数,动态地解析组件的定义。Vue.js 只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。
如: Vue.component('async-example', function (resolve, reject) { setTimeout(function () { // Pass the component definition to the resolve callback resolve({ template: '<div>I am async!</div>' }) }, 1000) })
工厂函数接收一个** resolve 回调**,在收到从服务器下载的组件定义时调用。也可以调用 reject(reason) 指示加载失败。这里 setTimeout 只是为了演示。怎么获取组件完全由你决定。
Vue.component('async-webpack-example', function (resolve) { // 这个特殊的 require 语法告诉 webpack // 自动将编译后的代码分割成不同的块, // 这些块将通过 Ajax 请求自动下载。 require(['./my-async-component'], resolve) })
我们可以将各个组件的template代码单独写到一个html文件,通过Ajax的get方法获取组件html文件的内容,一样可以实现代码切割的功能,方便模块化管理代码: Vue.component('my-component', function (resolve, reject) { $.get('url', function (text) { resolve({ template: text, // 选项 }); }); });
四、局部组件
局部组件一般是在创建子组件的情况出现。生命周期依赖于父组件。
创建局部组件很简单:
const subComponent = { template: 'I am subComponent', data: function() { return { var: xxx } } methods: { back: function () { this.$router.back(); }, }, // 其它选项 } 其它构造器选项和Vue实例、全局组件的构造器选项一样。
五、动态组件
使用保留的 <component> 元素,动态地绑定到它的 is 特性,我们让多个组件可以使用同一个挂载点,并动态切换:
var vm = new Vue({ el: '#example', data: { currentView: 'home' }, components: { home: { /* ... */ }, posts: { /* ... */ }, archive: { /* ... */ } } })
<component v-bind:is="currentView"> <!-- 组件在 vm.currentview 变化时改变! --> </component>
** # keep-alive **
如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数:
<keep-alive> <component :is="currentView"> <!-- 非活动组件将被缓存! --> </component> </keep-alive>
六、使用Slot分发内容
在使用组件时,我们常常要像这样组合它们:
<app> <app-header></app-header> <app-footer></app-footer> </app>
注意两点:
- <app> 组件不知道它的挂载点会有什么内容。挂载点的内容是由<app>的父组件决定的。
- <app> 组件很可能有它自己的模版。
** # 编译作用域 **
在深入内容分发 API 之前,我们先明确内容在哪个作用域里编译。假定模板为:
<child-component>
{{ message }}
</child-component>
message 应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是:
父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。
一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法:
<!-- 无效 --> <child-component v-show="someChildProperty"></child-component>
假定 someChildProperty 是子组件的属性,上例不会如预期那样工作。父组件模板不应该知道子组件的状态。
如果要绑定作用域内的指令到一个组件的根节点,你应当在组件自己的模板上做:
Vue.component('child-component', { // 有效,因为是在正确的作用域内 template: '<div v-show="someChildProperty">Child</div>', data: function () { return { someChildProperty: true } } })
** # 单个 Slot **
最初在 <slot> 标签中的任何内容都被视为备用内容。备用内容在子组件的作用域内编译,并且只有在宿主元素为空,且没有要插入的内容时才显示备用内容。
<div>
<h2>我是子组件的标题</h2>
<slot>
只有在没有要分发的内容时才会显示。
</slot>
</div>
父组件模版:
<div> <h1>我是父组件的标题</h1> <my-component> <p>这是一些初始内容</p> <p>这是更多的初始内容</p> </my-component> </div>
渲染结果:
<div> <h1>我是父组件的标题</h1> <div> <h2>我是子组件的标题</h2> <p>这是一些初始内容</p> <p>这是更多的初始内容</p> </div> </div>
** 具名Slot **
仍然可以有一个匿名 slot ,它是默认 slot ,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot ,这些找不到匹配的内容片段将被抛弃。
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
<app-layout> <h1 slot="header">这里可能是一个页面标题</h1> <p>主要内容的一个段落。</p> <p>另一个主要段落。</p> <p slot="footer">这里有一些联系信息</p> </app-layout>
渲染结果为:
<div class="container"> <header> <h1>这里可能是一个页面标题</h1> </header> <main> <p>主要内容的一个段落。</p> <p>另一个主要段落。</p> </main> <footer> <p>这里有一些联系信息</p> </footer> </div>
在组合组件时,内容分发 API 是非常有用的机制。
七、组建间的通信
大的Vue项目由组件构成,每个组件维护各自的状态数据。但再完美的架构,也不可能实现组件之间完全解耦。组件之间随时都可能进行数据交互。因此,组件之间通信是不可避免的。
Vue组件通信的三种方案:
Vue的组件间通信分2种情况:父子组件之间通信和非父子组件之间通信。
在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过events 给父组件发送消息。看看它们是怎么工作的。
Vue1.x版本的父子组件之间通信的方式有:Prop传递数据和自定义事件,非父子组件之间通信的方式有中央事件总线。
Vue2.x版本引入状态管理模式的概念,使用专用的状态管理层——Vuex。
@@父子组件间通信
1. 使用Prop传递数据
子组件要显示地用props选项声明它期待获得的数据:
Vue.component('child', { // 声明 props props: ['message'], // 就像 data 一样,prop 可以用在模板内 // 同样也可以在 vm 实例中像 “this.message” 这样使用 template: '<span>{{ message }}</span>' })
然后我们可以这样向它传入一个普通字符串:
<child message="hello!"></child>
结果:
hello!
# camelCase vs. kebab-case
HTML特性是不区分大小写的。所以,当使用的不是字符串模版,camelCased(驼峰式)命名的prop需要转换为相对应的kebab-case(短横线隔开式)命名:
Vue.component('child', { // camelCase in JavaScript props: ['myMessage'], template: '<span>{{ myMessage }}</span>' })
<!-- kebab-case in HTML --> <child my-message="hello!"></child>
*如果你使用字符串模版,则没有这些限制。
# 动态Prop
在模板中,要动态地绑定父组件的数据到子模板的props,与绑定到任何普通的HTML特性相类似,就是用 v-bind。每当父组件的数据变化时,该变化也会传导给子组件:
<div> <input v-model="parentMsg"> <br> <child v-bind:my-message="parentMsg"></child> </div>
# 字面量语法 vs 动态语法
初学者常犯的一个错误是使用字面量语法传递数值:
<!-- 传递了一个字符串 "1" --> <comp some-prop="1"></comp>
因为它是一个字面 prop ,它的值是字符串 "1" 而不是number。如果想传递一个实际的number,需要使用 v-bind ,从而让它的值被当作 JavaScript 表达式计算:
<!-- 传递实际的 number --> <comp v-bind:some-prop="1"></comp>
# 单向数据流
prop是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态--这会让应用的数据流难以理解。
另外,每次父组件更新时,子组件的所有prop都会更新为最新值。这意味这你不应该在子组件内部改变prop。如果你这么做了,Vue会在控制台给出警告。
为什么我们会有修改prop中数据的冲动呢?通常是这两种原因:
- prop 作为初始值传入后,子组件想把它当作局部数据来用;
- prop 作为初始值传入,由子组件处理成其它数据输出。
对这两种原因,正确的应对方式是:
1.定义一个局部变量,并用 prop 的值初始化它:
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
2.定义一个计算属性,处理 prop 的值并返回。
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
<u>*注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态</u>
# Prop验证
我们可以为组件的 props 指定验证规格。如果传入的数据不符合规格,Vue 会发出警告。当组件给其他人使用时,这很有用。
要指定验证规格,需要用对象的形式,而不能用字符串数组:
Vue.component('example', { props: { // 基础类型检测 (`null` 意思是任何类型都可以) propA: Number, // 多种类型 propB: [String, Number], // 必传且是字符串 propC: { type: String, required: true }, // 数字,有默认值 propD: { type: Number, default: 100 }, // 数组/对象的默认值应当由一个工厂函数返回 propE: { type: Object, default: function () { return { message: 'hello' } } }, // 自定义验证函数 propF: { validator: function (value) { return value > 10 } } } })
type 可以是下面原生构造器:
- String
- Number
- Boolean
- Function
- Object
- Array
type 也可以是一个自定义构造器函数,使用 instanceof 检测。
当 prop 验证失败,Vue会在抛出警告 (如果使用的是开发版本)。
2. 自定义事件
我们知道,父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,应该怎样做?那就是自定义事件!
# 使用 v-on绑定自定义事件
每个 Vue 实例都实现了事件接口(Events interface),即:
- 使用 **$on(eventName) **监听事件
- 使用 **$emit(eventName) **触发事件
Vue的事件系统分离自浏览器的EventTarget API。尽管它们的运行类似,但是$on和 $emit不是addEventListener和 dispatchEvent的别名。
另外,父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。
<u>*不能用$on侦听子组件抛出的事件,而必须在模板里直接用v-on绑定,就像以下的例子:</u>
<div id="counter-event-example"> <p>{{ total }}</p> <button-counter v-on:increment="incrementTotal"></button-counter> <button-counter v-on:increment="incrementTotal"></button-counter> </div>
Vue.component('button-counter', { template: '<button v-on:click="increment">{{ counter }}</button>', data: function () { return { counter: 0 } }, methods: { increment: function () { this.counter += 1 this.$emit('increment') } }, }) new Vue({ el: '#counter-event-example', data: { total: 0 }, methods: { incrementTotal: function () { this.total += 1 } } })
# 给组件绑定原生事件
有时候,你可能想在某个组件的根元素上监听一个原生事件。可以使用 .native 修饰 v-on 。例如:
<my-component v-on:click.native="doTheThing"></my-component>
@@非父子组件间通信
1. 中央事件总线
在简单的场景下,可以使用一个空的 Vue 实例作为中央事件总线(global event bus)。
比如,假设我们有个 todo 的应用结构如下:
Todos |-- NewTodoInput |-- Todo |-- DeleteTodoButton
可以通过单独的事件中心管理组件间的通信:
// 将在各处使用该事件中心 // 组件通过它来通信 var eventHub = new Vue()
然后在组件中,可以使用 $emit, $on, $off 分别来分发、监听、取消监听事件:
// NewTodoInput // ... methods: { addTodo: function () { eventHub.$emit('add-todo', { text: this.newTodoText }) this.newTodoText = '' } }
// DeleteTodoButton // ... methods: { deleteTodo: function (id) { eventHub.$emit('delete-todo', id) } }
// Todos // ... created: function () { eventHub.$on('add-todo', this.addTodo) eventHub.$on('delete-todo', this.deleteTodo) }, // 最好在组件销毁前 // 清除事件监听 beforeDestroy: function () { eventHub.$off('add-todo', this.addTodo) eventHub.$off('delete-todo', this.deleteTodo) }, methods: { addTodo: function (newTodo) { this.todos.push(newTodo) }, deleteTodo: function (todoId) { this.todos = this.todos.filter(function (todo) { return todo.id !== todoId }) } }