01 組件化實戰(zhàn)
組件化
vue組件系統(tǒng)提供了?種抽象维咸,讓我們可以使用獨立可復用的組件來構建大型應用,任意類型的應用界面都可以抽象為?個組件樹。組件化能提高開發(fā)效率端辱,方便重復使用,簡化調試步驟虽画,提升項目可維護性舞蔽,便于多人協同開發(fā)。
組件通信常用方式
props
event
vuex
自定義事件
-
邊界情況
$parent
$children
$root
$refs
provide/inject
-
非prop特性
$attrs
$listeners
組件通信
props
父子傳值
// child
props: { msg: String }
// parent
<HelloWord msg="測試父子傳值" />
自定義事件
子父傳值
// child
this.$emit('sonToFather', 'son-->Father')
// parent
<Cart @sonToFather="testSonToFather($event)"></Cart>
事件總線
任意兩個組件之間傳值常用事件總線或 vuex 的方式码撰。
// Bus: 事件派發(fā)渗柿、監(jiān)聽和回調管理
class Bus {
constructor() {
this.event = {}
}
// 訂閱事件
$on (eventName, callback) {
if (!this.event[eventName]) {
this.event[eventName] = []
}
this.event[eventName].push(callback)
}
// 觸發(fā)事件(發(fā)布事件)
$emit (eventName, params) {
let eventArr = this.event[eventName]
if (eventArr) {
eventArr.map(item => {
item(params)
})
}
}
// 刪除訂閱事件
$off (eventName, callback) {
let arr = this.event[eventName]
if (arr) {
if (callback) {
let index = arr.indexOf(callback)
arr.splice(index, 1)
} else {
arr.length = 0
}
}
}
}
// main.js
Vue.prototype.$bus = new Bus()
// child1
this.$bus.$on('testBus',handle)
// child2
this.$bus.$emit('testBus')
實踐中通常用 Vue 代替 Bus,因為 Vue 已經實現了相應接口
vuex
創(chuàng)建唯?的全局數據管理者 store脖岛,通過它管理數據并通知組件狀態(tài)變更朵栖。
root
兄弟組件之間通信可通過共同祖輩搭橋,root鸡岗。
// brother1
this.$parent.$on('testParent',handle)
// brother2
this.$parent.$emit('testParent')
$children
父組件可以通過 $children 訪問子組件實現父子通信混槐。
// parent
this.$children[0].xx = 'xxx'
注意:$children 不能保證子元素順序
listeners
包含了父作用域中不作為 prop 被識別 (且獲取) 的特性綁定 ( class 和 style 除外)轩性。當?個組件沒有
聲明任何 prop 時声登,這里會包含所有父作用域的綁定 ( class 和 style 除外),并且可以通過 v-bind="$attrs" 傳入內部組件——在創(chuàng)建高級別的組件時非常有用揣苏。
// child:并未在props中聲明foo
<p>{{$attrs.foo}}</p>
// parent
<HelloWorld foo="foo"/>
refs
獲取子節(jié)點引用
// parent
<HelloWorld ref="testRef">
mounted() {
this.$refs.testRef.xx='xxx'
}
provide/inject
能夠實現祖先和后代之間傳值
// ancestor
provide() {
return {foo: 'foo'}
}
// descendant
inject: ['foo']
插槽
插槽語法是 Vue 實現的內容分發(fā) API悯嗓,用于復合組件開發(fā)。該技術在通用組件庫開發(fā)中有大量應用卸察。
匿名插槽
// comp1
<div>
<slot></slot>
</div>
// parent
<Comp>testSlot</Comp>
具名插槽
將內容分發(fā)到子組件指定位置
// comp2
<div>
<slot></slot>
<slot name="content"></slot>
</div>
// parent
<Comp2>
<!-- 默認插槽用default做參數 -->
<template v-slot:default>具名插槽</template>
<!-- 具名插槽用插槽名做參數 -->
<template v-slot:content>內容...</template>
</Comp2>
作用域插槽
分發(fā)內容要用到子組件中的數據
// comp3
<div>
<slot :foo="foo"></slot>
</div>
// parent
<Comp3>
<!-- 把v-slot的值指定為作用域上下文對象 -->
<template v-slot:default="slotProps">來自子組件數據:{{slotProps.foo}}</template>
</Comp3>
組件化實戰(zhàn)
通用表單組件
收集數據脯厨、校驗數據并提交。
需求分析
-
實現 KForm
- 指定數據坑质、校驗規(guī)則
-
KformItem
label 標簽添加
執(zhí)行校驗
顯示錯誤信息
-
KInput
- 維護數據
最終效果:Element 表單
KInput
創(chuàng)建 components/form/KInput.vue
<template>
<div>
<input :value="value" @input="onInput" v-bind="$attrs">
</div>
</template>
<script>
export default {
inheritAttrs:false,
props:{
value:{
type:String,
default:''
}
},
methods:{
onInput(e){
this.$emit('input',e.target.value)
}
}
}
</script>
使用 KInput
創(chuàng)建 components/form/index.vue合武,添加如下代碼:
<template>
<div>
<h3>Form表單</h3>
<hr>
<k-input v-model="model.username"></k-input>
<k-input type="password" v-model="model.password"></k-input>>
</div>
</template>
<script>
import KInput from './KInput'
export default {
components:{
KInput
},
data(){
return {
model:{
username:'tom',
password:''
}
}
}
}
</script>
實現 KFormItem
創(chuàng)建components/form/KFormItem.vue
<template>
<div>
<label v-if="label">{{label}}</label>
<slot></slot>
<p v-if="error">{{error}}</p>
</div>
</template>
<script>
export default {
props: {
label:{ // 輸入項標簽
type: String,
default:''
},
prop:{ // 字段名
type: String,
default: ''
}
},
data() {
return {
error: '' // 校驗錯誤
}
}
}
</script>
使用 KFormItem
components/form/index.vue,添加基礎代碼:
<template>
<div>
<h3>Form表單</h3>
<hr>
<k-form-item label="用戶名" prop="username">
<k-input v-model="model.username"></k-input>
</k-form-item>
<k-form-item label="確認密碼" prop="password">
<k-input type="password" v-model="model.password"></k-input>
</k-form-item>
</div>
</template>
實現 KForm
<template>
<form>
<slot></slot>
</form>
</template>
<script>
export default {
provide() {
return {
form: this // 將組件實例作為提供者涡扼,子代組件可方便獲取
}
},
props:{
model:{
type: Object,
required: true
},
rules:{
type: Object
}
}
}
</script>
使用 KForm
components/form/index.vue稼跳,添加基礎代碼:
<template>
<div>
<h3>Form表單</h3>
<hr/>
<k-form :model="model" :rules="rules" ref="loginForm">
...
</k-form>
</div>
</template>
<script>
import KForm from './KForm'
export default {
components: {
KForm
},
data() {
return {
rules: {
username: [{
required: true,
message: '請輸入用戶名'
}],
password: [{
required: true,
message: '請輸入密碼'
}]
}
}
},
methods: {
submitForm() {
this.$refs['loginForm'].validate(valid => {
if (valid) {
alert('請求登錄')
} else {
alert('校驗失敗')
}
})
}
}
}
</script>
數據校驗
Input 通知校驗
onInput(e) {
// ...
// $parent指FormItem
this.$parent.$emit('validate')
}
FormItem 監(jiān)聽校驗通知,獲取規(guī)則并執(zhí)行校驗
inject: ['form'], // 注入
mounted() { // 監(jiān)聽校驗事件
this.$on('validate',() => {this.validate()})
},
methods:{
validate() {
// 獲取對應 FormItem 校驗規(guī)則
console.log(this.form.rules[this.prop])
// 獲取校驗值
console.log(this.form.model[this.prop])
}
}
安裝 async-validator:
npm i async-validator -S
import Schema from 'async-validator'
validate() {
// 獲取對應 FormItem 校驗規(guī)則
const rules = this.form.rules[this.prop]
// 獲取校驗值
const value = this.form.model[this.prop]
// 校驗描述對象
const descriptor = {[this.prop]:rules}
// 創(chuàng)建校驗器
const schema = new Schema(descriptor)
// 返回 Promise吃沪,沒有觸發(fā) catch 就說明驗證通過
return schema.validate({[this.prop]:value},errors=>{
if (errors) {
// 將錯誤信息顯示
this.error = errors[0].message
} else {
// 校驗通過
this.error = ''
}
})
}
表單全局驗證汤善,為 Form 提供 validate 方法
validate(cb){
// 調用所有含有 prop 屬性的子組件的 validate 方法并得到 Promise 的值
const tasks = this.$children
.filter(item => item.prop)
.map(item => item.validate())
// 所有任務必須全部成功才算校驗通過魄眉,任一失敗則校驗失敗
Promise.all(tasks)
.then(() => cb(true))
.catch(() => cb(false))
}
實現彈窗組件
彈窗這類組件的特點是它們在當前 vue 實例之外獨立存在逞姿,通常掛載于 body揭朝;它們是通過 JS 動態(tài)創(chuàng)建
的猪半,不需要在任何組件中聲明。常見使用姿勢:
this.$create(Notice, {
title: '林慕-彈窗組件'
message: '提示信息',
duration: 1000
}).show()
create 函數
import Vue from 'vue'
// 創(chuàng)建函數接收要創(chuàng)建組件定義
function create(Component, props) {
// 創(chuàng)建一個 Vue 實例
const vm = new Vue({
render(h) {
// render 函數將傳入組件配置對象轉換為虛擬 dom
console.log(h(Component,{props}))
return h(Component, {props})
}
}).$mount() // 執(zhí)行掛載函數在旱,但未指定掛載目標摇零,表示只執(zhí)行初始化工作
// 將生成 dom 元素追加至 body
document.body.appendChild(vm.$el)
// 給組件實例添加銷毀方法
const comp = vm.$children[0]
comp.remove = () => {
document.body.removeChild(vm.$el)
vm.$destroy()
}
return comp
}
// 暴露調用接口
export default create
另一種創(chuàng)建組件實例的方式: Vue.extend(Component)
通知組件
新建通知組件,Notice.vue
<template>
<div class="box" v-if="isShow">
<h3>{{title}}</h3>
<p class="box-content">{{message}}</p>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: ''
},
message: {
type: String,
default: ''
},
duration: {
type: Number,
default: 1000
}
},
data() {
return {
isShow: false
}
},
methods: {
show() {
this.isShow = truw
setTimeout(this.hide, this.duration)
},
hide() {
this.isShow = false
this.remove()
}
}
}
</script>
<style>
.box {
position: fixed;
width: 100%;
top: 16px;
left: 0;
text-align: center;
pointer-events: none;
background-color: #fff;
border: grey 3px solid;
box-sizing: border-box;
}
.box-content {
width: 200px;
margin: 10px auto;
font-size: 14px;
padding: 8px 16px;
background: #fff;
border-radius: 3px;
margin-bottom: 8px;
}
</style>
使用 create api
測試颈渊,components/form/index.vue
<script>
import create from "@/utils/create"
import Notice from "@/components/Notice"
export default {
methods: {
submitForm(form) {
this.$refs[form].validate(valid => {
const notice = create(Notice, {
title: '林慕-create',
message: valid ? '請求登錄' : '校驗失敗',
duration: 1000
})
notice.show()
})
}
}
}
</script>
遞歸組件
// TODO
拓展
- 使用 Vue.extend 方式實現 create 方法
- 方法一:和第一個 create 方法類似
export function create2 (Component, props) {
let VueMessage = Vue.extend({
render(h) {
return h(Component, {props})
}
})
let newMessage = new VueMessage()
let vm = newMessage.$mount()
let el = vm.$el
document.body.appendChild(el)
const comp = vm.$children[0]
comp.remove = () => {
document.body.removeChild(vm.$el)
vm.$destroy()
}
return comp
}
- 方法二:利用 propsData 屬性
export function create3 (Component, props) {
// 組件構造函數如何獲人焓颉终佛?
// 1. Vue.extend()
const Ctor = Vue.extend(Component)
// 創(chuàng)建組件實例
const comp = new Ctor({ propsData: props })
comp.$mount()
document.body.appendChild(comp.$el)
comp.remove = function () {
document.body.removeChild(comp.$el)
comp.$destroy()
}
return comp
}
方法三:使用插件進一步封裝便于使用俊嗽,create.js
import Notice from '@/components/Notice.vue'
// ...
export default {
install(Vue) {
Vue.prototype.$notice = function (options) {
return create(Notice, options)
}
}
}
// 使用
this.$notice({title: 'xxx'})
- 修正 input 中 $parent 寫法的問題
mixin emitter
聲明 componentName
dispatch()
- 學習 Element 源碼