Vue.js 2.x笔记:组件(5)
1. 组件简介
组件(Component)是 Vue.js 最强大的功能之一,组件可以扩展 HTML 元素,封装可重用的代码。
组件:为了拆分Vue实例的代码量,以不同的组件来划分不同的功能模块,需要什么样的功能,可以去调用对应的组件。
模块化和组件化的区别:
◊ 模块化:是从代码逻辑的角度进行划分的;方便代码分层开发,保证每个功能模块的职能单一。
◊ 组件化:是从UI界面的角度进行划分的;前端的组件化,方便UI组件的重用。
2. 注册组件
Vue.js提供两种组件注册方式:全局注册和局部注册。
2.1 全局组件
全局注册需要在根实例初始化之前注册,这样组件才能在任意实例中被使用。
注册全局组件语法格式:
Vue.component(tagName, options)
其中,tagName 为组件名,options 为配置选项。
这条语句需要写在var vm = new Vue({ options })之前。
注册组件后调用方式:
<tagName></tagName>
所有实例都能用全局组件。
组件名定义方式:PascalCase和kebab-case。在组件命名时可以采用PascalCase或kebab-case,但在DOM中只能使用kebab-case。
PascalCase示例:
<div id="app"> <my-component></my-component> </div> <script> Vue.component('MyComponent', { template: '<div>标题</div>' }); var vm = new Vue({ el: "#app" }); </script>
kebab-case示例:
<div id="app"> <my-component></my-component> </div> <script> Vue.component('my-component', { template: '<div>标题</div>' }); var vm = new Vue({ el: "#app" }); </script>
<div id="app"> <home></home> </div> <script> Vue.component("home", { template: "<div>{{text}}</div>", data: function () { return { text: "主页" }; } }); new Vue({ el: "#app" }); </script>
<div id="app"> <home></home> </div> <script> var homeTpl = Vue.extend({ template: "<div>{{text}}</div>", data: function () { return { text: "主页" }; } }); Vue.component('home', homeTpl); new Vue({ el: "#app" }); </script>
使用template标签:
<div id="app"> <home></home> </div> <template id="tpl"> <div>{{text}}</div> </template> <script> Vue.component("home", { template: "#tpl", data: function () { return { text: "主页" }; } }); new Vue({ el: "#app" }); </script>
2.2 局部组件
局部组件只能在被注册的组件中使用,不能在其他组件中使用。
<div id="app"> <home></home> </div> <script> new Vue({ el: "#app", components: { "home": { template: "<div>{{text}}</div>", data: function () { return { text: "主页" }; } } } }); </script>
2.3 Vue.extend
2.3.1 基本使用
<div id="app"> <home></home> </div> <script> var home = Vue.extend({ template: "<div>标题</div>" }); Vue.component("home", home); new Vue({ el: "#app" }); </script>
2.3.2 参数data
data:在 Vue.extend() 中必须是函数。
<body> <task></task> <script> var task = Vue.extend({ template:"<div>{{ taskName }}</div>", data:function(){ return { taskName:"任务名称" } } }); new task().$mount("task"); </script> </body>
2.3.3 使用$mount
在实例中没有el选项时,可通过mount挂载。
mount:挂载,将vue实例挂靠在某个dom元素上的一个过程。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>libing.vue</title> <script src="node_modules/vue/dist/vue.min.js"></script> </head> <body> <div id="app"></div> <script> var home = Vue.extend({ template: "<div>标题</div>" }); new home().$mount("#app"); </script> </body> </html>
3. 组件通信
3.1 props:父组件向子组件传递数据
prop 是组件用来传递数据的自定义特性,在组件上注册自定义属性。
prop特性注册成为组件实例的属性。
props
:父组件向子组件传递数据。
一个组件默认可以拥有任意数量的 prop,任何值都可以传递给任何 prop。
3.1.1 静态props
示例:
<div id="app"> <home text="主页"></home> </div> <script> var homeTpl = Vue.extend({ props:["text"], template: "<div>{{text}}</div>" }); Vue.component('home', homeTpl); new Vue({ el: "#app" }); </script>
3.1.2 动态props
使用 v-bind 动态绑定 props 的值到父组件的数据中。每当父组件的数据变化时,该变化也会传导给子组件。
<div id="app"> <home v-bind:text="text"></home> </div> <script> var homeTpl = Vue.extend({ props: ["text"], template: "<div>{{text}}</div>" }); Vue.component('home', homeTpl); new Vue({ el: "#app", data: { text: "主页" } }); </script>
由于HTML Attribute不区分大小写,当使用DOM模板时,camelCase的props名称要转为kebab-case。
<div id="app"> <home warning-text="提示信息"></home> </div> <script> Vue.component('home', { props: ['warningText'], template: '<div>{{ warningText }}</div>' }); var vm = new Vue({ el: "#app" }); </script>
传递的数据可以是来自父级的动态数据,使用指令v-bind来动态绑定props的值,当父组件的数据变化时,也会传递给子组件。
<div id="app"> <home v-bind:warning-text="warningText"></home> </div> <script> Vue.component('home', { props: ['warningText'], template: '<div>{{ warningText }}</div>' }); var vm = new Vue({ el: "#app", data: { warningText: '提示信息' } }); </script>
注:prop 是单向传递,当父组件的属性变化时,将传递给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态。
示例:
<template> <li>{{ id }}-{{ text }}</li> </template> <script> export default { name: "TodoItem", props: ["id", "text"] }; </script>
<template> <ul> <TodoItem v-for="item in list" :key="item.id" :id="item.id" :text="item.text" ></TodoItem> </ul> </template> <script> import TodoItem from "./TodoItem"; export default { name: "TodoList", components: { TodoItem }, data: function() { return { list: [ { id: 1, text: "To Do" }, { id: 2, text: "In progress" }, { id: 3, text: "Done" } ] }; } }; </script>
<template> <div id="app"> <TodoList /> </div> </template> <script> import TodoList from './views/TodoList' export default { name: 'App', components: { TodoList } } </script>
3.1.3 props验证
为组件的 prop 指定验证要求,如果有一个需求没有被满足,则 Vue 会在控制台中警告。
Vue.component('my-component', { 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 ['success', 'warning', 'danger'].indexOf(value) !== -1 } } } });
类型检查:type可以是下列原生构造函数中的一个:String、Number、Boolean、Array、Object、Date、Function、Symbol,也可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认。
示例:
<div id="app"> <parent-component></parent-component> </div> <template id="child-component1"> <h2>{{ message }}</h2> </template> <template id="child-component2"> <h2>{{ message }}</h2> </template> <template id="parent-component"> <div> <child-component1></child-component1> <child-component2></child-component2> </div> </template> <script> Vue.component('parent-component', { template: '#parent-component', components: { 'child-component1': { template: '#child-component1', data() { return { message: '子组件1' }; } }, 'child-component2': { template: '#child-component2', data() { return { message: '子组件2' }; } } } }); var vm = new Vue({ el: "#app" }); </script>
示例:
<div id="app"> <todo :todo-data="taskList"></todo> </div> <template id="tpl-todo-item"> <li>{{ id }} - {{ text }}</li> </template> <template id="tpl-todo-list"> <ul> <todo-item v-for="item in todoData" :id="item.id" :text="item.text"></todo-item> </ul> </template> <script> // 构建一个子组件 var todoItem = Vue.extend({ template: "#tpl-todo-item", props: { id: { type: Number, required: true }, text: { type: String, default: '' } } }) // 构建一个父组件 var todoList = Vue.extend({ template: "#tpl-todo-list", props: { todoData: { type: Array, default: [] } }, // 局部注册子组件 components: { todoItem: todoItem } }) // 注册到全局 Vue.component('todo', todoList) new Vue({ el: "#app", data: { taskList: [{ id: 1, text: 'New' }, { id: 2, text: 'InProcedure' }, { id: 3, text: 'Done' } ] } }); </script>
3.2 自定义事件:子组件向父组件传递数据
每一个Vue实例都实现事件接口:
$on(eventName)
:监听事件
$emit(eventName)
:触发事件,自定义事件。推荐始终使用 kebab-case 的事件名。
子组件需要向父组件传递数据时,子组件用$emit(eventName)
来触发事件,父组件用$on(eventName)
来监听子组件的事件。
示例1:
<template> <div> <button @click="onparent">子组件触发父组件</button> </div> </template> <script> export default { methods: { onparent() { this.$emit("onchild"); } } }; </script>
<template> <div> <Child @onchild="inparent"></Child> </div> </template> <script> import Child from "./Child"; export default { components: { Child }, methods: { inparent() { console.log("父组件响应了"); } } }; </script>
<template> <div id="app"> <Parent /> </div> </template> <script> import Parent from './views/Parent' export default { name: 'App', components: { Parent } } </script>
示例2:
<div id="app"> <searchbar></searchbar> </div> <template id="tpl-search-form"> <div class="input-group form-group" style="width: 500px;"> <input type="text" class="form-control" placeholder="请输入查询关键字" v-model="keyword" /> <span class="input-group-btn"> <input type="button" class="btn btn-primary" value="查询" @click="search"> </span> </div> </template> <template id="tpl-search-bar"> <searchform @onsearch="search"></searchform> </template> <script> // 构建一个子组件 var searchform = Vue.extend({ template: "#tpl-search-form", data: function () { return { keyword: 'libing' }; }, methods: { search: function () { this.$emit('onsearch', this.keyword); } } }); // 构建一个父组件 var searchbar = Vue.extend({ template: "#tpl-search-bar", components: { searchform: searchform }, methods: { search(keyword) { console.log(keyword); } } }) // 注册到全局 Vue.component('searchbar', searchbar); new Vue({ el: "#app" }); </script>
购物车示例:
<div id="app"> <shoppingcart :shopppingcarts="products" @calc="getTotal"></shoppingcart> <div>总计:{{ totalPrice }}</div> </div> <template id="shoppingcart"> <table> <tr> <th>商品ID</th> <th>商品名称</th> <th>单价</th> <th>数量</th> </tr> <tr v-for="item in shopppingcarts"> <td>{{ item.ID }}</td> <td>{{ item.ProductName }}</td> <td>{{ item.UnitPrice }}</td> <td><input type="text" v-model="item.Quantity" @change="calcTotal" /></td> </tr> </table> </template> <script> var shoppingcart = Vue.extend({ template: "#shoppingcart", props: ["shopppingcarts"], methods: { calcTotal: function () { this.$emit("calc"); } } }); new Vue({ el: "#app", components: { shoppingcart: shoppingcart }, data: { totalPrice: 100, products: [{ ID: 1, ProductName: "手机", UnitPrice: 1000, Quantity: 2 }, { ID: 2, ProductName: "电脑", UnitPrice: 5000, Quantity: 5 }] }, methods: { getTotal() { console.log(new Date()); this.totalPrice = 0; this.products.forEach(product => { this.totalPrice += product.UnitPrice * product.Quantity; }); } }, mounted() { //当vue执行完毕之后,去执行函数 this.getTotal(); } }); </script>
3.3 EventBus:非父子组件通信
非父子组件包括:兄弟组件、跨级组件。
通过实例化一个Vue对象 (如:const bus = new Vue() ) 作为总线,在组件中通过事件传递参数( bus.$emit(event, [...args]) ),再在其他组件中通过bus来监听此事件并接受参数( bus.$on(event, callback) ),从而实现通信。
示例:
bus.js
import Vue from 'vue' const bus = new Vue(); export default bus;
Send.vue
<template> <div class="send"> <h1>发送参数:{{msg}}</h1> <button @click="send">发送</button> </div> </template> <script> import bus from "../utils/bus.js"; export default { data() { return { msg: "Hello World" }; }, methods: { send() { bus.$emit("receive", this.msg); } } }; </script>
Receive.vue
<template> <div class="receive"> <h1>接收参数:{{msg}}</h1> </div> </template> <script> import bus from "../utils/bus.js"; export default { data() { return { msg: "Hello" }; }, created() { bus.$on("receive", param => { this.msg = param; }); }, beforeDestroy() { bus.$off("receive"); } }; </script>
App.vue
<template> <div id="app"> <Send></Send> <Receive></Receive> </div> </template> <script> import Send from './views/Send' import Receive from './views/Receive' export default { name: 'App', components: { Send, Receive } } </script>