目錄
- <<回到導覽
- 組件
- 1.項目
- 1.1.Vue Cli
- 1.2.項目目錄
- 1.3.運行流程
- 1.4.組件的組成
- 1.5.注意事項
- 2.組件
- 2.1.組件注冊
- 2.2.scoped樣式沖突
- 2.3.data是一個函數
- 2.4.props詳解
- 2.5.data和prop的區別
- 3.組件通信
- 3.1.父子通信
- 3.1.1.父傳子(props)
- 3.1.2.子傳父($emit)
- 3.2.非父子通信
- 3.2.1.事件總線
- 3.2.2.provide-inject
- 3.3.v-model詳解
- 3.4.sync修飾符(已廢棄)
- 3.5.(重點)ref和$refs
- 3.6.ref和$refs選擇器
- 3.7.$nextTick
- 4.自定義指令
- 4.1.指令的值
- 4.2.封裝v-loading指令
- 5.插槽
- 5.1默認插槽
- 5.2.后備內容
- 5.3.具名插槽
- 5.4.作用域插槽
- 6.my-tag組件封裝
<<回到導覽
組件
1.項目
1.1.Vue Cli
官方提供的一個全局命令工具
,可以快速生成vue項目的標準化基礎架子(集成webpack),開箱即用
使用步驟:
-
全局安裝(安裝一次即可):
yarn global add @vue/cli// 或者 npm i @vue/cli -g
-
查看vue/cli版本:
vue --version
-
創建項目架子:
vue create project-name
-
啟動項目:
yarn serve// 或者npm run serve
啟動項目的命令并不固定,其取決于package.json文件
1.2.項目目錄
-
第三包文件夾(依賴)被刪除,只要package.json還存在,就可以安裝回來
npm i --force
或者npm i --legacy-peer-deps
1.3.運行流程
1.4.組件的組成
- 語法高亮插件
-
三部分構成
- template:結構 (有且只能一個根元素),在template中,最外層標簽只能為div盒子
- script: js邏輯
- style: 樣式 (可支持less,需要裝包)
-
讓組件支持less
- style標簽,lang=“less” 開啟less功能
- 裝包:
yarn add less less-loader -D // 或者 npm i less less-loader -D
1.5.注意事項
-
在組件的template中,最外面必須有一個div盒子
-
組件名稱最好采用大駝峰命名法,且至少兩個駝峰
-
有時候有些錯誤改正后依舊報錯(列如上面第二個報錯),重新啟動項目即可
-
啟動項目時在項目的根目錄下啟動(我們項目根目錄叫luyou)
2.組件
2.1.組件注冊
-
局部注冊
-
在components文件夾創建組件 xxx.vue
-
在根組件App.vue導入
// 導入組件 // import xxx from 'xxx.vue的文件路徑' import HmHeader from './components/HmHeader'export default{// 局部注冊components:{// '組件名':組件對象,// HmHeader:HmHeader 同名可簡寫HmHeader} }
-
-
全局注冊
-
在components文件夾創建組件 xxx.vue
-
在main.js進行全局注冊
// 導入組件 // import xxx from 'xxx.vue的文件路徑' import HmHeader from './components/HmHeader'// 調用Vue.component進行全局注冊 // Vue.component('組件名',組件對象) Vue.component('HmHeader',HmHeader)
一般都用局部注冊,如果發現是通用組件,再注冊為全局
-
2.2.scoped樣式沖突
-
默認組件中的樣式會全局生效
-
可以給組件加上scoped屬性,可以讓樣式只作用于當前組件
<style scoped></style>
scoped的原理:
- 組件被都添加 data-v-hash (添加自定義屬性)
- css選擇器添加 [data-v-hash] (添加自定義屬性的屬性選擇器)
2.3.data是一個函數
在最開始的vue基本語法學習中,data被寫為data: {},
鍵值(值為對象,對象又嵌套鍵值數據)
但在組件開發中,data選項必須是一個函數
data(){return {// 數據}
}
-
這是因為組件具有復用性,在同一個組件的復用過程中,data里的數據應該不一樣,且數據不相互影響
-
將data設置為一個函數,將數據寫進return返回值中,每次創建組件都會調用data函數,返回一個獨立數據對象,這些獨立的對象分別用于保存同一組件的不同次使用的數據。
2.4.props詳解
-
定義:在組件使用時,注冊的一些
自定義屬性
-
作用:向子組件傳遞數據
-
示例:
export default {props: ['w'], }
在為子組件傳遞數據時,我們應該為組件的 prop 指定驗證要求
props: {校驗的屬性名: {type: 類型, // Number String Boolean ...required: true, // 是否必填default: 默認值, // 默認值validator (value) {// 自定義校驗邏輯return 是否通過校驗}} },
- default和required一般不同時寫
- default后面如果是復雜類型,則需要以函數的形式return一個默認值
示例:
props: {w: {type: Number,//required: true,default: 0,validator(val) {if (val >= 100 || val <= 0) {console.error('傳入的范圍必須是0-100之間')return false} else {return true}},},},
2.5.data和prop的區別
- data 的數據是自己的 → 隨便改
- prop 的數據是外部的 → 不能直接改,要遵循 單向數據流
單向數據流
:父級props 的數據更新,會向下流動,影響子組件。這個數據流動是單向的
3.組件通信
上面講到,data是一個函數,同一組件的不同的使用之間數據獨立,同樣,不同組件之間數據也不共享
3.1.父子通信
3.1.1.父傳子(props)
-
父組件通過 props 將數據傳遞給子組件
-
在父組件中的子組件標簽添加動態屬性(App.vue)
<MySon :title="say"></MySon>
-
子組件通過props進行接收
export default {data() {return {};},props: ["title"], };
-
3.1.2.子傳父($emit)
-
子組件利用 $emit 通知父組件修改更新
-
給父組件元素添加事件和處理方法
<button @click="changeFn">say</button>
changeFn() {// change為后面為子組件添加的事件名// "Hello"為要修改的值,也就是后面的形參newSay// 并不一定要傳字符串,也可以將"Hello"改為變量傳遞this.$emit("change", "Hello"); },
-
觸發事件后,利用$emit將父組件發送改變數據的通知
-
在父組件中的子組件標簽添加事件,調用修改屬性的方法
<MySon :title="say" @change="changeFn"></MySon>
// 形參newSay為傳遞過來的值"Hello" changeFn(newSay) {this.say = newSay; },
-
3.2.非父子通信
3.2.1.事件總線
概念:創建創建一個都能訪問的事件總線,發送方向事件總線發送,
接收方向事件總線發送監聽
-
在src文件夾下創建一個utils文件夾,在該文件夾下創建一個js文件
-
創建一個空Vue實例(事件總線)
import Vue from 'vue' const Bus = new Vue() export default Bus
-
接受方和發送方都將數據總線引入
import Bus from '../utils/EventBus'
-
A組件(發送方),觸發Bus的$emit事件
// 'sendMsg為發送的數據的標識 Bus.$emit('sendMsg', '這是一個消息')
-
B組件(接受方),監聽Bus的 $on事件
created () {// 形參msg為傳遞的信息('這是一個消息')Bus.$on('sendMsg', (msg) => {this.msg = msg}) }
3.2.2.provide-inject
作用:跨層級共享數據
-
父組件 provide提供數據
export default {provide () {return {// 普通類型【非響應式】color: this.color, // 復雜類型【響應式】userInfo: this.userInfo, }} }
-
.子/孫組件 inject獲取數據
export default {inject: ['color','userInfo'],created () {console.log(this.color, this.userInfo)} }
- provide提供的簡單類型的數據不是響應式的,復雜類型數據是響應式。(
推薦提供復雜類型數據
) - 子/孫組件通過inject獲取的數據,不能在自身組件內修改
3.3.v-model詳解
-
v-model本質上是一個語法糖。例如應用在輸入框上,就是value屬性 和 input事件 的合寫
<template><div id="app" ><input v-model="msg" type="text"><!-- 等價于 --><!-- 數據變,視圖跟著變 :value --><!-- 視圖變,數據跟著變 @input --><input :value="msg" @input="msg = $event.target.value" type="text"></div> </template>
-
$event 用于在模板中,獲取事件的形參
-
$event.target.value即獲取輸入框的值
-
將輸入框的值賦值給data里的數據msg,再將msg通過
:value
賦值給輸入框,從而實現數據的雙向綁定。
注意:
-
不同的表單元素, v-model在底層的處理機制是不一樣的。
比如給checkbox使用v-model,底層處理的是
:checked
屬性和@change
事件。 -
v-model不能直接綁定父組件傳過來的數據,因為父組件通過prop屬性傳過來的數據不能直接修改。
3.4.sync修飾符(已廢棄)
-
在上面的父子通信中,子組件不能直接修改父組件數據,而是通過$emit間接修改
-
通過sync修飾符,子組件可以修改父組件傳過來的props值
-
以上面的父子通信為例
父組件:
<!-- 完整寫法 --> <MySon :title="say" @update:title="isShow = $event" ></MySon><!-- 簡寫 --> <MySon :title.sync="say"></MySon>
子組件:
props: {title: Boolean }, this.$emit('update:title', 'Hello')
3.5.(重點)ref和$refs
-
利用ref 和 $refs 可以用于 獲取
dom 元素
或組件實例
(要在dom渲染完后才能獲取) -
獲取元素還可以通過
document.querySeletctor()
獲取,但是此方法是從整個頁面開始獲取的- 類名問題:比如我們在子組件中獲取類名為app的元素,如果在此組件之前還有類名為app的元素,則會獲取之前的類名為app的元素
- 解決方法:如果我們利用ref 和 $refs 可以用于 獲取
dom 元素
,則會從當前組件開始獲取,避免類名問題。
-
示例:
-
給要獲取的盒子添加ref屬性
<div ref="demo">我是渲染圖表的容器</div>
-
通過 this.$refs.demo 獲取
mounted(){console.log(this.$refs.demo) }
-
3.6.ref和$refs選擇器
-
利用ref 和 $refs 還可以用于
組件實例
-
-
在組件示例上,添加ref屬性
<MySon ref="demo" ></MySon>
-
通過 this.$refs.demo 獲取,控制臺會打印組件對象
mounted(){console.log(this.$refs.demo)// 控制臺// VueComponent {_uid: 2, _isVue: true, __v_skip: true, _scope: EffectScope, $options: {…}, …} }
-
我們可以通過
this.$refs.demo.子組件方法名
調用子組件方法,或者訪問子組件屬性,實現組件通信。注意:這種組件通信只是調用了子組件方法或者訪問子組件屬性,沒有實現
雙向綁定
。
-
3.7.$nextTick
-
$nextTick用于實現vue的異步更新
-
應用案例:編輯標題, 編輯框自動聚焦
-
-
效果:點擊編輯,顯示編輯框,并且讓編輯框,立刻獲取焦點
-
預想代碼
this.isShowEdit = true // 顯示輸入框 this.$ref.input.focus() // 獲取焦點
-
預想代碼并不能實現該效果,因為:
- 當執行顯示輸入框代碼后,沒等dom渲染出顯示框,就執行到獲取焦點的代碼了(因為dom是
異步更新
),我們可以用$nextTick用于實現vue的異步更新。 - vue異步更新的原因是提高性能,當點擊編輯按鈕的@click綁定的事件執行完畢后,才會更新視圖,這樣避免每執行完一行代碼就更新一次視圖。
- 當執行顯示輸入框代碼后,沒等dom渲染出顯示框,就執行到獲取焦點的代碼了(因為dom是
-
利用$nextTick實現以上代碼的異步更新
this.isShowEdit = true // 顯示輸入框 this.$nextTick(() => {this.$refs.inp.focus() // 獲取焦點 })
- 當執行到$nextTick時,會更新渲染一次dom,輸入框渲染出來,再向下執行代碼
- 說到dom異步更新,很多人想到利用延時器實現,但是延時器延遲時間是固定的,而執行到dom渲染的時間并不固定(和網絡、設備性能有關)。
注意:
? $nextTick 內的函數體 一定是箭頭函數,這樣才能讓函數內部的this指向Vue實例
-
4.自定義指令
- 與$nextTick類似,inserted會在指令所在元素被插入頁面時觸發
-
全局注冊(下面以注冊實現上面點擊聚焦功能為例)
//在main.js中 Vue.directive('focus', {// el為指令綁定的元素 "inserted" (el) {el.focus()} })
-
局部注冊
//在Vue組件的配置項中 directives: {"focus": {inserted (el) {el.focus()}} }
-
指令使用
<input v-focus ref="inp" type="text">
4.1.指令的值
與vue內置指令相同,我們也可以為自定義指令設置值
-
示例:我們定義一個v-color指令,v-color的值變化時,帶有v-color指令的標簽也會變化
-
指令注冊(局部注冊)
directives: {color: {inserted (el, binding) {el.style.color = binding.value},update (el, binding) {el.style.color = binding.value}} }
- binding.value為指令值
- 指令值修改會觸發update函數(update也是一個生命周期鉤子)
- 我們指令的值可以設置為變量,這時我們就需要設置update函數
4.2.封裝v-loading指令
-
在加載時,頁面通常會有一個loading動畫效果
-
這個效果通常為一個蒙層(蒙層一般為偽元素)
-
loading的開啟和關閉只需要添加移除類即可(因為偽元素是css生成的,自然也可以通過操作css移除)
示例:
<template><div class="main"><div class="box" v-loading = "isLoading"><ul><li v-for="item in list" :key="item.id" class="news"><div class="left"><div class="title">{{ item.title }}</div><div class="info"><span>{{ item.source }}</span><span>{{ item.time }}</span></div></div><div class="right"><img :src="item.img" alt=""></div></li></ul></div></div> </template><script>import axios from 'axios'export default {data () {return {list: [],isLoading: true}},async created () {const res = await axios.get('http://hmajax.itheima.net/api/news')setTimeout(() => {this.list = res.data.data// 移除蒙層this.isloading = false}, 2000)},// 定義指令directives: {loading: {inserted (el, binding) {binding.value?el.classList.add('loading'):el.classList.remove('loading')},update (el, binding) {binding.value?el.classList.add('loading'):el.classList.remove('loading')}}}} </script><style>/* 偽元素 */.loading:before {content: '';position: absolute;left: 0;top: 0;width: 100%;height: 100%;background: #fff url('./loading.gif') no-repeat center;}.box2 {width: 400px;height: 400px;border: 2px solid #000;position: relative;}.box {width: 800px;min-height: 500px;border: 3px solid orange;border-radius: 5px;position: relative;}.news {display: flex;height: 120px;width: 600px;margin: 0 auto;padding: 20px 0;cursor: pointer;}.news .left {flex: 1;display: flex;flex-direction: column;justify-content: space-between;padding-right: 10px;}.news .left .title {font-size: 20px;}.news .left .info {color: #999999;}.news .left .info span {margin-right: 20px;}.news .right {width: 160px;height: 120px;}.news .right img {width: 100%;height: 100%;object-fit: cover;} </style>
知識點:
-
安裝axios :
yarn add axios
或npm i axios
-
定義指令部分:
directives: {loading: {inserted (el, binding) {// 如果加載,顯示蒙層(移除loading類),否則,顯示蒙層binding.value?el.classList.add('loading'):el.classList.remove('loading')},update (el, binding) {binding.value?el.classList.add('loading'):el.classList.remove('loading')}} }
-
-
v-loading工作流程:有些同學看到這里可能有些疑問,組件定義里面的函數是什么時候執行的?
- 上面有提到,inserted會在指令所在元素被
插入頁面時觸發
- 在上面示例中,先執行created生命周期鉤子,向服務器發送獲取數據請求,然后再將返回數據更新到 list 中,并改變isLoading
- 上面有提到,inserted會在指令所在元素被
5.插槽
5.1默認插槽
-
讓組件內部的一些 結構 支持 自定義
-
語法:
- 組件內需要定制的結構部分,改用
<slot></slot>
占位 - 給插槽傳入內容時,可以傳入純文本、html標簽、組件
- 在標簽內部, 傳入結構替換slot
- 組件內需要定制的結構部分,改用
-
示例:彈框插槽
5.2.后備內容
上面示例中,如果不傳內容,則會不會顯示
我們可以為插槽設置默認顯示內容,如果不傳內容,則會顯示默認顯示內容
- 我們只需要在封裝組件時,為預留的
<slot>
插槽提供后備內容即可(默認內容)
5.3.具名插槽
一個組件中,很多時候不單單只有一個插槽,這時我們需要使用name屬性區分不同插槽。
-
使用name屬性區分不同插槽。
-
template
配合v-slot:名字
來分發對應標簽 -
為方便書寫,上面可以將
v-slot:
替換為#
示例:
-
根組件(App.vue)
<div id="app"><HelloWorld><template #age>年齡</template><template #name>姓名</template><template #gender>性別</template></HelloWorld> </div>
-
子組件(HelloWorld.vue)
<div><slot name="name"></slot><hr /><slot name="age"></slot><hr /><slot name="gender"></slot> </div>
顯示順序取決于子組件的插槽位置
5.4.作用域插槽
-
作用域插槽不屬于插槽的一種分類
-
定義slot插槽的同時,可以傳值,給插槽綁定數據,這些數據與插槽綁定的組件也可以使用
-
給 slot 標簽, 以 添加屬性的方式傳值(組件中)
<slot :id="item.id" msg="測試文本"></slot>
-
在template中, 通過
#插槽名= "obj"
接收,默認插槽名為 default(根組件中)<MyTable :list="list"><template #default="obj"><button @click="del(obj.id)">刪除</button></template> </MyTable>
-
-
所有添加的屬性, 都會被收集到一個對象中,我們也可以將這個對象結構來使用
<MyTable :list="list"><template #default="{id, msg}"><button @click="del(id)">刪除</button></template> </MyTable>
6.my-tag組件封裝
實現功能:
- 雙擊顯示,自動聚焦
- 失去焦點,隱藏輸入框
- 回顯標簽內容
- 內容修改,回車,修改標簽信息
代碼:
-
MyTag
<template><div class="my-tag"><inputv-if="isEdit" v-focusclass="input"type="text"placeholder="輸入標簽":value="value"@blur="isEdit = false"@keyup.enter="handleEnter"/><div v-else@dblclick="handleClick"class="text">{{ value }}</div></div> </template><script> export default {props: {value: String},data () {return {isEdit: false}},methods: {handleClick () {// 雙擊后,切換到顯示狀態 (Vue是異步dom更新)this.isEdit = true},handleEnter (e) {// 非空處理if (e.target.value.trim() === '') return alert('標簽內容不能為空')// 由于父組件是v-model,觸發事件,需要觸發 input 事件this.$emit('input', e.target.value)// 提交完成,關閉輸入狀態this.isEdit = false}} } </script><style lang="less" scoped> .my-tag {cursor: pointer;.input {appearance: none;outline: none;border: 1px solid #ccc;width: 100px;height: 40px;box-sizing: border-box;padding: 10px;color: #666;&::placeholder {color: #666;}} } </style>
-
App.vue
<template><div><MyTag v-model="item.tag"></MyTag> </div> </template><script> import MyTag from './components/MyTag.vue'export default {name: 'TableCase',components: {MyTag,},data () {return {// 測試組件功能的臨時數據tempText: '水杯', }} } </script><style lang="less" scoped> .table-case {width: 1000px;margin: 50px auto;img {width: 100px;height: 100px;object-fit: contain;vertical-align: middle;} }</style>
-
main.js
import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false// 封裝全局指令 focus Vue.directive('focus', {// 指令所在的dom元素,被插入到頁面中時觸發inserted (el) {el.focus()} })new Vue({render: h => h(App), }).$mount('#app')
知識點:
-
雙擊觸發事件:@dblclick
-
兩種自動聚焦方法:
在實現點擊盒子切換為inpu標簽并自動聚焦時,我們可以通過ref和refs操作dom,再配合$nextTick異步實現,不過為了提高復用性,我們通過自定義指令,封裝到mian.js實現該功能
-
指令修飾符實現回車事件監聽,@keyup.enter,由于該案例數據是由父組件傳遞過來的,所以還要將該值發送給父組件