# vue3 basic
{%hackmd BJrTq20hE %}
###### tags: `vue筆記`
- 載入法一:
```jsx=
<script src="https://unpkg.com/vue@3/dist/vue.global.js" integrity="sha384-8CdW77YPqMZ3v22pThUIR22Qp1FB5oisZG2WE3OpE0l1fTHAIsdIwjQZFf/rmQ/B" crossorigin="anonymous"></script>
```
起手式:
```jsx=
<div id="app">...</div>
<script>
const app = {
data () {
return {
text: 0,
}
}
}
Vue.createApp(app).mount('#app')
</script>
```
or
```jsx=
<script>
const { createApp, ref } = Vue;
const App = {
setup() {
const message = ref("<data:blog.title/>");
return {
message,
};
},
};
const app = createApp(App);
app.mount("#app")
app.component("home", {
template: `<teleport to='title'>小明</teleport> //傳送到class對應位置
`,
});
</script>
```
- 載入法二:直接寫在 js 裡面 [link](https://vuejs.org/guide/quick-start.html#using-the-es-module-build)
```javascript=
import { createApp, ref } from 'https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.9/vue.esm-browser.js';
```
- CDN 版本解釋:[link](https://juejin.cn/post/7043991342166310942)、[各版本下載](https://unpkg.com/vue@3/dist/)
- prd:是压缩版,体积小
- runtime版:可以理解为 **阉割版,体积小,不带动态编译模板的能力**
- **需要编译模板的能力的话,比如vue.extend,那么要用不带runtime的完整版,体积会稍微大一些**
- HOOK:

```javascript=
<script type="module" src="js/products.js"></script>
//每一個type="module"的作用域都是獨立的,無法讀取其他<script>區域,使用import元件時必加,因为Module 的加载实现的是es6语法,所以在浏览器加载html文件时,需要在script 标签中加入type="module"属性。
new Date().getTime(); //取得不重複timestamp
```
## css深度选择器:[link](https://juejin.cn/post/6978781674070884366)
- deep四種寫法:
```scss=
// 写法1 使用 ::v-deep /* sass 能用 */
<style lang="scss" scoped>
::v-deep .ant-card-head-title{
background: yellowgreen;
}
</style>
// 写法2 使用 >>> 操作符 /* sass 不能用 */
<style scoped>
>>>.ant-card-head-title{
background: yellowgreen;
}
</style>
// 写法3 使用 /deep/
<style scoped>
/deep/.ant-card-head-title{
background: yellowgreen;
}
</style>
// 写法4 使用 :deep(<inner-selector>)
<style lang="scss" scoped>
:deep(.ant-card-head-title){
background: yellowgreen;
}
</style>
```
PS:使用寫法四兼容性較好
其他:
```scss=
/* 可以用在 slot 裡面的內容 */
:slotted(.slot-class) {
background-color: #000;
}
/* 設置為全域 css */
:global(.my-class) {
background-color: #000;
}
```
- [看更多應用](https://zh-hk.vuejs.org/api/sfc-css-features)
## v-model 表單元件綁定的方法:
data():存放資料
- `v-model`:資料雙向綁定,綁定input
```javascript=
//Vue 3 前 v-model 是以下的簡寫
<MyInput :value="name" @input="newValue => this.name = name" />
//Vue 3 後 v-model 是以下的簡寫
<MyInput :modelValue="name" @update:modelValue="newValue => this.name = name" />
```
- checkbox單選框(法一,綁定data為bool):預設值為true、false,可以用三元運算子來放文字
```htmlembedded=
{{ checkAnswer ? '吃飽了' : '還沒'}}
<input
type="checkbox"
class="form-check-input"
id="check1"
v-model="checkAnswer"
/>
checkAnswer: false
```
- checkbox單選框(法二):
```javascript=
{{ selectAnswer2 }}
<input
type="checkbox"
class="form-check-input"
id="check2"
v-model="checkAnswer2"
true-value="吃飽了" //"吃飽了"
false-value="還沒"
/>
checkAnswer2: "" //單選格式會用字串
```
例二:
有时候我们希望切换checkbox时,让它绑定的值不是默认的false和true
而是我们自己期望的一些值,比如0和1
只要在checkbox上用v-bind绑定相应的变量就行了
```htmlembedded=
<input
class="form-check-input"
type="checkbox"
v-model="tempProduct.is_enabled"
:true-value="1" //1,假設沒有v-bind綁定,就會變成字串"1"
:false-value="0"
id="is_enabled"
/>
<label class="form-check-label" for="is_enabled">
是否啟用
</label>
```
- checkbox複選框:須代上value
```htmlembedded=
{{ checkAnswer3.join(' ') }} //join是把存到data的陣列轉換成字串
<input
type="checkbox"
class="form-check-input"
id="check3"
v-model="checkAnswer3"
value="蛋餅"
/>…*3
checkAnswer3: [] //複選格式會用陣列來儲存多筆資料
```
- radio 單選框:
```htmlembedded=
{{ radioAnswer }}
<input
type="radio"
class="form-check-input"
id="radio1"
value="蛋餅"
v-model="radioAnswer"
/>
radioAnswer: "蛋餅" //此蛋餅為radio的預設值,不需要可以留空
```
- select 單選:
```htmlembedded=
{{ selectAnswer }}
<select class="form-select" v-model="selectAnswer">
<option value="">說吧,你要吃什麼?</option> //如果要預設值需加入空value
<option
v-for="item in products"
:key="item.name"
:value="item.name"
>{{item.name}} / {{item.price}} 元
</option>
</select>
selectAnswer: ""
```
- select 多選:
```htmlembedded=
{{ selectAnswer2 }}
<select class="form-select" multiple v-model="selectAnswer2">
<option selected disabled value="">說吧,你要吃什麼?</option>
<option
:value="item.name"
v-for="item in products"
:key="item.name"
>
{{item.name}} / {{item.price}} 元</option
>
</select>
selectAnswer2: []
```
- `v-model-modifiers`(修飾符):除了資料綁定外的小方法,在model後面加.
- 延遲 Lazy:
```htmlembedded=
{{ lazyMsg }}
<input type="text" class="form-control" v-model.lazy="lazyMsg">
```
- 純數值 Number:前面type要確定有加上number
將輸入的字串自動轉型成數字
```htmlembedded=
{{ numberMsg }}{{ typeof numberMsg }}
<input type="number" class="form-control" v-model.number="numberMsg" />
```
- 修剪 Trim:
```htmlembedded=
這是一段{{ trimMsg }}緊黏的文字
<input type="text" class="form-control" v-model.trim="trimMsg" />
```
- v-model [自定義修飾符](https://cn.vuejs.org/guide/components/v-model.html#handling-v-model-modifiers)
```jsx=
<MyCounter v-model.number.alex="count" />
<script setup>
const props = defineProps({
modalValue: Number,
modelModifiers: {
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
// 使用computed來簡化在templete的設置 (參考官方範例)
const value = computed({
get(){
return props.modelValue
},
set(value){
// 如果有設定.alex修飾符的話,value*=10
if(props.modelModifiers.alex) value *= 10
emit('update:modelValue', value)
}
})
</script>
<template>
{{ props.modelModifiers }} // 有設定屬性就會給true:{ alex: true }
<input v-model="value" />
<button @click="value++">ADD</button>
</template>
```
### 多個 v-model [link](https://cloud.tencent.com/developer/article/1741250)
- 在 vue3 中可以给v-model的属性起名字,并且你可以拥有任意数量的v-model。
```jsx=
<InviteeForm
v-model:name="inviteeName"
v-model:email="inviteeEmail"
/>
```
- `v-focus`:自動聚焦
- 全域註冊:
```javascript=
const app = Vue.createApp({})
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
// 当被绑定的元素挂载到 DOM 中时……
mounted(el) {
// 聚焦元素
el.focus()
}
})
```
- 局部註冊:
```javascript=
const vFocus = {
mounted(el) {
el.focus()
}
}
```
- 使用:
```htmlembedded=
<input v-focus />
```
- `v-once`:單次綁定,只會初次渲染,之後編輯資料時不會立即變動
- `v-pre`:暫停轉譯花括弧,會原始呈現`{{ name }}`在`{{ position }}`吃早餐
```jsx=
<v-pre>{{ message }}</v-pre>
```
跳过这个元素和它的子元素的编译过程可以加快编譯

- `pre`:能夠優質顯示json在網頁上,而不會擠成一坨
```jsx=
<pre>{{ jsonData }}</pre>
const jsonData = {
"key": "名字",
"value": "张三丰"
}
```
- 未使用 
- 使用 
## methods():function方法
- `v-on:click="funName"`:觸發事件到method(不用())
- 帶入參數:
```htmlembedded=
<button class="btn btn-outline-primary" v-on:click="change('123')">選轉物件</button>
```
- form submit 事件:綁在form可以觸發submit
```htmlembedded=
<form @submit.prevent="submitForm">
<input type="text" v-model="name" /> //輸入完按enter即可觸發submit
<button>送出表單</button>
</form>
```
- 動態事件 []:(少用)
```htmlembedded=
<input type="text" @[event]="dynamicEvent" />
<input type="text" v-model="event" />
data() {
return {
event: "click",
};
},
method:{
dynamicEvent() {
console.log("這是一個動態事件", this.event);
}}
```
- 動態物件方法 {}:在一個物件上面加入多個事件,但注意:此方法無法傳入參數
```htmlembedded=
<button class="box" @="{mousedown: down, mouseup: up}"></button>
method:{
down() { console.log("按下");},
up() { console.log("放開");}
}
```
- 新特性:多事件監聽者
```javascript=
<button @click="one($event), two($event)">
Submit
</button>
```
- v-on修飾符 [link](https://zh-hk.vuejs.org/guide/essentials/event-handling.html#key-modifiers)
- 按鍵修飾符:keyAlias - 只當事件是從特定鍵觸發時才觸發。
- 別名修飾:
```htmlembedded=
<input type="text" class="form-control" v-model="text" @keyup.enter="trigger('enter')">
```
- 相應(組合)按鍵時才觸發的監聽器:
```htmlembedded=
<input type="text" class="form-control" v-model="text" @keyup.shift.enter="trigger('shift + Enter')">
```
- 特定鍵:
```htmlembedded=
<input type="text" class="form-control" v-model="text" @keyup.h="trigger('h')">
```
建立 keyboard 事件觸發器:
```jsx=
function trigger(keyEvent) {
// 創建一個新的 KeyboardEvent
const event = new KeyboardEvent('keyup', {
key: keyEvent,
bubbles: true, // 事件是否冒泡
cancelable: true // 事件是否可以取消
});
window.dispatchEvent(event);
}
```
- 常用:
- `.enter`
- `.tab`
- `.delete` (captures both "Delete" and "Backspace" keys)
- `.esc`
- `.space`
- `.up`
- `.down`
- `.left`
- `.right`
- 滑鼠修飾符:
`.left` 只當點擊鼠標左鍵時觸發。
`.right` 只當點擊鼠標右鍵時觸發。
`.middle` 只當點擊鼠標中鍵時觸發。
```htmlembedded=
<span class="box" @click.right="trigger('right button')"> </span>
```
- 事件修飾符:
`.stop` - 調用 event.stopPropagation()。 //阻止事件傳導( 冒泡 )
`.prevent` - 調用 event.preventDefault()。 //常用於超連結,阻止預設行為
上面兩者的差異:先用prevent阻止預設行為,如果外層還有行為則可再加上stop阻止傳遞 [https://dotblogs.com.tw/harry/2016/09/10/131956](https://dotblogs.com.tw/harry/2016/09/10/131956)
`.capture` - 添加事件偵聽器時使用 capture 模式。改變事件為捕獲方向,變成由外而內
- ```jsx=
// 點擊clicktwo會先觸發one後才觸發twoz
<div id="app">
<div id="div1" @click.capture="handleOnClickOne">
<div id="div2" @click="handleOnClickTwo">
</div>
</div>
</div>
```
`.self` - 只當事件是從偵聽器綁定的元素本身觸發時才觸發回調。
`.once` - 只觸發一次回調。
## 渲染方法:
- `{{變數}}`:Mustache,效果等同於在屬性加上 `v-text="變數"`
進階技巧:表達式(在花括弧裡使用語法)
- 樣板字面值:{{`${name}在${position}吃早餐`}} //使用樣板語法就可以只用一個花括弧
- 反轉字串:`{{text.split('').reverse().join('') }}`
- 綁定methods:`{{ say('杰倫') }}` //可以帶入函式
- JS運算:`{{ 1 + 1 }} or {{ a + b }}`
- v-for說明:有相同父元素的子元素必須有獨特的 key。重複的 key 會造成渲染錯誤。
`v-for="item in 陣列or物件"` //v-for一定要帶入key值,否則子元素如果沒變就不會重新渲染
兩個v-for:
```htmlembedded=
<h3>年紀大於 25 歲的同事</h3>
<ul>
<template v-for="all in collegueList" :key="all.name"> //取出多個物件
<li v-if="all.age >= 25">
<p v-for="(value, key) in all">屬性: {{key}},值: {{value}}</p> //取出物件裡的多個值
</li>
</template>
</ul>
```
`:key="item.id"`:唯一值
```htmlembedded=
<ul>
<li v-for="(item, key) in products" v-bind:key="item.name">
{{ key }} - {{ item.name}} / {{ item.price }} 元
<input type="text">
</li>
</ul>
```
進階技巧:
在 template 標籤使用 v-for (用template包住要重複的區塊(如有複數的node要重複,假設有兩個li要迴圈就是代表兩個節點))
- v-show:把DOM隱藏或不隱藏起來(display: none)
```htmlembedded=
<p v-show="true">小明 飽了</p>
```
- v-if:把DOM用判斷式隱藏或不隱藏(visibility: hidden)-較常用
```htmlembedded=
<div v-if="link === '小明'">小明吃早餐</div>
<div v-else-if="link === '小美'">小美去百貨公司</div>
<div v-else="link === '杰倫'">杰倫去幫助人</div>
```
如果要保留v-if的生命週期,可以用`<keep-alive></keep-alive>`包住
注意事項:v-for 與 v-if [不要](https://vue3js.cn/docs/zh/guide/conditional.html#v-if-%E4%B8%8E-v-for-%E4%B8%80%E8%B5%B7%E4%BD%BF%E7%94%A8)寫在同一個標籤,如需要混用,請搭配`<template>`標籤
- v-bind:綁定元素屬性
切換class的四種寫法:
1. 物件寫法:
如果要為屬性加上判斷,須把值轉成物件{key : value},其中因為key不支援符號,所以須當成 '字串' 處裡
```htmlembedded=
v-bind:class="table-success"
v-bind:class="{'className':判斷式}"
v-bind:class="{'table-success':true}"
v-bind:class="{'table-success':true, rotate: true}" //多個屬性可用陣列方式插入
v-bind:class="{'table-success rotate': true}" //同層級可以合併控制的寫法
```
不一定只能用在class,切換屬性也可使用。例如:
```htmlembedded=
<input type="text" :disabled="disabled" :placeholder="placeholder" v-model="value">
//or
<audio id="audio-don" src="/music/don.mp3" :muted="!ringbellStatus"></audio>
```
2. 物件寫法2:如上,但是直接寫好在data再整個帶入
```htmlmixed=
<div class="box" :class='objectClass'></div>
data() {
return {
objectClass:{
rotate: true,
'bg-danger': false
}
```
3. 陣列寫法:(不需要判斷的寫法)
```htmlmixed=
<button class="btn" :class="['btn-primary','disabled']">請操作本元件</button>
```
4. 綁定行內樣式:一樣為{key:value},key為style的屬性,value為style相對應的值,這邊的key可為字串or小駝峰的形式
```htmlmixed=
<div class="box" :style="{backgroundColor:'red'}"></div>
<div class="box" :style="styleObject"></div>
<div class="box" :style="[styleObject, styleObject2]"></div> //此為[{},{}]形式
data() {
return {
styleObject: {
backgroundColor: 'red',
borderWidth: '5px'
},
styleObject2: {
boxShadow: '3px 3px 5px rgba(0, 0, 0, 0.16)'
}
```
- 動態屬性綁定:使用[]包入變數
```htmlembedded=
<button type="button" v-on:click="dynamic = dynamic === 'disabled' ? 'readonly':'disabled'">切換為 {{ dynamic }}</button>
<input type="text" :[dynamic] :value="name">
v-bind:title="breakfastShop.name" //放在圖片標籤裡有替代文字效果
```
- 動態本地圖片:src https://codertw.com/%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC/232846/
1. 如果直接給img的src繫結一個字串
```htmlembedded=
<img :src=nowIcon />
```
```javascript=
data () {
return {
nowIcon: ''
}
},
this.nowIcon = '../assets/64/' + 圖片名 + '.png'
```
vue會將這個路徑當成字串,不會給這個圖片路徑編譯,圖片顯示不出來
此時的路徑是未經過編譯的
2. 解決辦法(將圖片作為模組載入)
```jsx=
// 寫法一:
<img :src="avatar" />
import avatar from '@/assets/logo.png'
// 寫法二:
<img :src=require('@/assets/logo.png') />
```
```javascript=
this.nowIcon = '../assets/64/' + 圖片名 + '.png'
改為
this.nowIcon = require('../assets/64/' + 圖片名 + '.png')
```
此時的程式碼是正常編譯後的路徑,圖片正常顯示
```htmlembedded=
<img src="/img/101.ce5f2cfc.png">
```
- 動態元件管理( 切換模組 ):https://book.vue.tw/CH2/2-3-async-dynamic-components.html
`v-bind:is`
- `v-html`="含html語法資料" //盡量避免用,以免XSS攻擊
```jsx=
<v-card v-html="htmlContent" class="pa-3"></v-card>
```
- vue語法糖簡寫
```jsx=
// vue綁定簡寫:
@:v-on
":":v-bind
#:v-slot
```
- Computed:主要是處理完資料回傳到畫面上 https://hackmd.io/v5US5x1zQtK4BC_BeiBzDw?view by Alysa Chan
使用時機:如果你的某個變數是依賴其他變數⽽來,這時候就適合使用computed。如果該目標變數不變,則不會執行。
1. 法一:將data裡的資料,運算處裡過後再渲染出來(無法主動呼叫,所以不可把參數帶入computed),例如總計or搜尋
```javascript=
{{total}} {{filterProducts}}
computed: {
total(){
let total = 0;
this.carts.forEach( item => { //當carts有變化會自動觸發
total += item.price;
});
return total;
},
filterProducts(){
reutrn this.products.filter( item => {
console.log(item);
return item.name.match(this.search); //回傳搜尋結果
}
}
}
```
2. 法二:變成物件型式,使用getter、setter方法(defineProperty)(如果需要用v-model綁定computed資料的話)
```javascript=
<button type="button" @click="total= num">更新</button>
computed: {
total:{
get() {
let total = 0;
this.carts.forEach( item => {
total += item.price;
});
return this.sum || total;
},
set(val){
this.sum = val;
}
},
}
```
3. 法三:監聽
- 監聽多個變數觸發事件
- 會產生一個值
```javascript=
computed: {
result2() {
retrun `嬤嬤買了 ${this.productName},共花費 ${this.productPrice}元,
另外這 ${this.productVegan ? "是" : "不是"} 素食的`
//此範例同時監聽了三個變數${}
}
}
```
## watch:監聽,可以透過方法修改資料
- 監聽單一"變數"觸發事件
- 該函式可同時操作多個變數
```javascript=
<input type="text" id="name" v-model="tempName">
watch: {
tempName(n, o) { //將要監聽的值當成函式的名稱,n為new(新的值),o為old(舊的值)}
}
```
深層監聽(監聽多個變數):如要監聽多個變數,該多筆資料須包在一個物件裡面
```javascript=
data() {
product: {
name: '蛋餅',
price: 30,
vegan: false,
}
},
watch: {
product: {
handler(n, o) { //handler控制器
this.result4 = `嬤嬤買了 ${this.product.name},共花費 ${this.product.price}元,另外這 ${this.product.vegan ? "是" : "不是"} 素食的
},
deep: true,
},
}
```
- computed與methods的分別:
| **computed**| **methods**|
|-----------------------|-----------------------|
| 如果computed的響應式==依賴(監視的值)沒有改動,就不會觸發。==(無法呼叫,被動執行)| 每次觸發methods,methods裏的函式一定會執行(可以呼叫&主動執行) |
| 有緩存資料的功能| 沒有緩存資料的功能|
| 不可帶入參數| 可帶入參數|
| 可在HTML裏直接使用該computed函式所回傳的值,因為computed函式本身是有get函式,最後一定會回傳一個值 | methods本身沒有規定一定要回傳一個值|
關於最後一點,打開Vue dev tool看就知道,computed本身就會回傳一個值,但methods就不會。
示範:[https://codepen.io/alysachan/pen/WNpgxeW?editors=1111](https://codepen.io/alysachan/pen/WNpgxeW?editors=1111)
以上例子可見,如果要把computed得出的值寫在畫面上,我們可直接把該computed函式寫在畫面裏,即是折扣價` {{ computedPrice }}` 元。但methods就要加上括號折扣價 `{{ methodsPrice() }}` 元
- computed與watch的分別:
| **computed** | **watch** |
|----------------------------------------|-------------------------|
| 監聽資料變動產生資料,資料衍生(算出新值) | 監聽資料變動產生行為,副作用處理(觸發行為) |
| 能監聽多個值的變動(只要該資料是在該computed函式裏和data屬性裏) | 只能監聽一個值 |
| 不能帶入參數 | 能帶入參數,第一個參數是新值,第二個參數是舊值 |
- 有緩存的功能,只要該computed所綁定的值沒有改變,computed函式就不會執行
- ~~不可對computed寫入值,因為computed只有getter屬性,沒有setter~~
緩存資料的意思:
computed本身有緩存資料的功能,如果緩存資料沒更動,該computed的函式就不會被執行。相反,methods沒有緩存功能,因此只要methods的函式被觸發,它就會執行。
++**computed緩存例子:**++
當按按鈕後num變成1,即使你之後再按多少次按鈕,add都不會被執行
[https://codepen.io/alysachan/pen/LYWmOLq](https://codepen.io/alysachan/pen/LYWmOLq)
++**methods沒有緩存的例子:**++
即使num變成1,之後你再按按鈕,add都會執行
[https://codepen.io/alysachan/pen/BaWxmYz](https://codepen.io/alysachan/pen/BaWxmYz)
好處:**緩存資料**的好處就是省略執行不必要的計算,尤其是在處理大量資料時,緩存資料的功能++有助提高效能++
==methods、computed、watch使用時機:==
1. methods: 這是需要主動觸發,且可以多次重複觸發
2. Computed: 當資料需要複雜運算時
3. Watch: 監控特定資料變化的 function 就放這裡
## Vue基本結構:
- option API風格:對於程式架構有分門別類
```javascript=
<div id="app">
{{ counter }} {{ text }} <br />
<input type="text" v-model="text" />
<button type="button" @click="clickMe(1)">按我</button>
</div>
<script>
Vue.createApp({
data() {
return {
counter: 0,
text: "這裡有一段文字",
};
},
}).mount("#app");
//上面可改寫為:
const App = {
// 資料(函式)
data() {
return { //一律使用return
counter: 0,
text: '這裡有一段文字'
}
},
// 方法(物件)
methods: {
clickMe(num) {
this.counter = this.counter + num;
}
},
// 生命週期(函式)
created() {
this.counter = 10;
},
computed: {},
mounted(){},
}
Vue.createApp(App).mount('#app'); //.mount(""):綁定DOM
</script>
```
- composition API:程式架構沒有順序之分,全部寫在裡面
- 元件:可以把一個頁面分割成多個組件,給多個工程師一起撰寫
元件使用的基本要點
- 元件需要在 createApp 後,mount 前進行定義
- 元件需指定一個名稱(元件名稱不可重複)
- 元件結構與最外層的根元件結構無異(除了增加 Template 的片段)
- 元件另有 prop, emits 等資料傳遞及事件傳遞
props:父傳子
emit:子傳父
## 單向數據流:
另外一點要注意 Props 是單向數據流,不能改變外部傳進來的值,所以當我們於內層元件要編輯傳進來的資料時,會提醒錯誤並不能更改,`[Vue warn]: Attempting to mutate prop "money". Props are readonly.` 。這時候可以在內層元件的 data 中新增一個新變數賦予外層傳進來的值,就可以利用新變數進行編輯摟~
```javascript=
app.component('editMoney', {
props: ['money'],
data(){
return{
newMoney:this.money,
}
},
template: `
<input type="text" v-model="newMoney"></input>
<div>value: {{newMoney}}</div>
`
});
```
### :star:技巧:前內、後外
## Props:使用屬性的方式傳入參數(PS.此參數只能讀不能寫,無法使用v-model方法)
```javascript=
const app = Vue.createApp({
data() {
return {
imgUrl:
"https://images.unsplash.com/photo-1605784401368-5af1d9d6c4dc?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80",
msgObj: ['也是', '可以', '傳遞陣列資料'],
};
},
});
app.component("photo", {
props: ["url","arraymsg"], //屬性名稱命名
//令src屬性等於url
template: `<img :src="url" class="img-thumbnail" alt>
<p>請正確呈現訊息:<span v-for="item in arraymsg">{{ item }}</span></p>`, //可以傳送陣列
});
<photo :url="imgUrl" :arraymsg="msgObj"></photo> //標籤綁定屬性名稱為props,( 把data參數傳入元件 )
```
- :star: 命名限制:因為網頁屬性只能呈現小寫,所以屬性命名如果用小駝峰可以用另一種方式呈現
```htmlembedded=
<photo3 :superUrl="imgUrl"></photo3>
<!--改為-->
<photo3 :super-url="imgUrl"></photo3>
```
- Props 型別驗證
```javascript=
<props-type money="300"></props-type> //屬性沒綁定,值一律為字串
<props-type :money="money"></props-type>
app.component('props-type', {
props: ['money'],
template: `<div>value: {{money}}, typeof:{{ typeof money }}</div>`
});
```
驗證檢查:不會出錯,只會提示
```javascript=
<props-validation
:prop-a="fun"
prop-c="required"
:prop-f="10000"
>
</props-validation>
app.component('props-validation', {
props: {
// 單一型別檢查,可接受的型別 String, Number, Object, Boolean, Function(在 Vue 中可使用 Function 驗證型別)
// null, undefined 會直接通過驗證
propA: Function,
// fun sample
callback: {
type: Function,
default: (item) => {
return item;
},
},
// 多個型別檢查
propB: [String, Number],
// 必要值
propC: {
type: String,
required: true,
},
// 預設值
propD: {
type: Number,
default: 300
},
// 自訂函式
propE: {
type: Object,
default() {
return {
money: 300
}
}
},
// 自訂驗證
propF: {
validator(value) {
return value > 1000
}
},
},
template: `{{propA}},{{propC}},{{propD}}<br>{{propE}},{{propF}}`
})
```
### 將非同步資料傳入 Props:
- [如何將非同步資料傳入 Prop ?](https://old-oomusou.goodjack.tw/vue/async-prop/)
解法重點:使用watch監視props,當props資料傳進來後再執行需要的操作
```jsx=
watch(
() => props.itemProperty,
() => {
getRule();
}
);
```
## Emit 觸發外部事件:從子元件傳送值or觸發事件,一律傳送至父元件函式
1. 先定義外層接收的方法(使用函式參數傳值)
2. 定義內層的 $emit 觸發方法
3. 使用 v-on 的方式觸發外層方法(口訣:前內、後外),在元件標籤裡撰寫
1:父元件
```javascript=
const app = Vue.createApp({
data() {
return {
num: 0,
text: "",
};
},
methods: {
getData(text) {
console.log("getData");
this.text = text;
},
},
});
```
2:子元件
```javascript=
app.component("button-text", {
data() {
return {
text: "內部資料",
};
},
methods: {
emit() {
// this.$emit('屬性名稱', 要傳遞的參數);
console.log("emit");
this.$emit("emit-text", this.text);
},
},
template: `<button type="button" @click="emit">emit data</button>`,
});
```
3:外層與內層間的橋梁
```htmlembedded=
內部傳來的文字:{{ text }}<br />
<button-text @emit-text="getData"></button-text>
```
emit消除警告:如果子元件修改到原始資料,就會出現警告
```javascript=
<script type="module">
const app = Vue.createApp({
data() {
return {
num: 0,
};
},
methods: {
addNum(num) {
this.num = this.num + num;
},
},
});
app.component("button-counter", {
data() {
return {
num: 1,
};
},
emits: ["add"], //有警告訊息建議加上emit選項,複數[","]
// 出現黃色警告訊息
// 主要會出現在該值是由 data 定義,但難以追蹤他的變化時會出現
template: `
<button type="button" @click="num++">調整 num 的值</button>
<button type="button" @click="$emit('add', num)">add</button>`, //子元件送出事件add呼叫父層addNum方法,並帶入num值
});
app.mount("#app");
<script>
{{ num }}
<button-counter @add="addNum"></button-counter>
```
emit驗證資料:使用物件,可放多個驗證
```javascript=
<h3>驗證資料內容</h3>
<button-counter2 @add="addNum"></button-counter2>
app.component("button-counter2", {
emits: {
add: (num) => {
if (typeof num !== "number") {
console.warn("add事件參數型別須為number");
}
return typeof num !== "number";
},
},
template: `<button type="button" @click="$emit('add', '1')">Emit 驗證是否為數值</button>`,
});
```
## 元件基本結構: // 注意,這段起手式與先前不同
- 全域註冊:直接掛載到app底下
此 createApp 下,任何子元件都可運用,在中小型專案、一般頁面開發很方便
- 區域註冊:掛載在app的根元件裡面( `Vue.createApp()`裡面 )
限制在特定元件下才可使用,在 Vue Cli 中很常使用此方法(便於管理)
- 模組化:可以把元件從外部匯進來
全域法一:
```javascript=
<alert></alert>
<script type="module">
//區域元件建立:
const alert3 = {
data() {
return {
text: "區域元件3",
};
},
template: `<div class="alert alert-primary" role="alert">{{ text }}</div>`,
};
const app = Vue.createApp({
data() {
return {
text: "外部元件文字",
};
},
components: { //區域註冊法一:(在根元件註冊),結尾需加s
alert3,
},
}).component("alert", { //(全域)註冊一個名叫alert的元件,對應到畫面的標籤名稱
data() {
return {
text: "內部文字",
};
},
template: `<div class="alert alert-primary" role="alert">{{ text }}</div>`,
});
app.mount("#app");
</script>
```
全域法二:註冊在app之後,amount之前
```javascript=
app.component("alert2", { //alert2為標籤名稱
data() {
return {
text: "內部文字",
};
},
template: `<div class="alert alert-primary" role="alert">{{ text }}</div>`,
//template也可為: '#alert2',
});
```
- 模組化(區域註冊法二):
// in alert-component.js:
- 外部匯出:
```javascript=
export default {
data() {
return {
text: '這是模組化元件'
};
},
template: `<div class="alert alert-primary" role="alert">
{{ text }}
</div>`
}
```
- 匯入:
`import alert4 from "./alert-component.js";`
- 註冊:
```javascript=
components: {
alert3,
},
```
元件樣板及綁定方式:
- 樣板建立方式:
- template:如上面範例,使用反引號
- x-template:把樣板寫在元件外面(需再app外面)
```javascript=
<script type="text/x-template" id="alert-template"> //id一律一樣(template、component、bootstrapModal)
<div class="alert alert-primary" role="alert id="alert-template"">
x-template 所建立的元件
</div>
</script>
<div id="app">
<alert2></alert2>
</div>
app.component("alert2", {
template: "#alert-template",
});
```
- 單文件元件(單一檔案包含 HTML, JS, CSS)
綁定方式:
- 直接使用 標籤 名稱
- 搭配 v-for 也是沒問題的
- 使用 v-is 綁定(可使用原HTML標籤),ps:裡面須加單引號,確保為字串格式
`<div v-is="'元件名稱'"></div>`
- 動態屬性
```htmlembedded=
<input type="text" v-model="componentName">
<div v-is="componentName"></div> //綁定資料的名稱
const app = Vue.createApp({
data() {
return {
array: [1, 2, 3],
componentName: "alert",
};
},
});
```
## 元件動態屬性:
可以同時綁定多個 v-model
```jsx=
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />
<!-- would be shorthand for: -->
<ChildComponent
:title="pageTitle"
@update:title="pageTitle = $event"
:content="pageContent"
@update:content="pageContent = $event"
/>
```
## ref 取得標籤
在標籤上加上ref屬性:該屬性即為此標籤的DOM元素名稱,可以用來呼叫(只能在自己元件使用,無法跨元件,跨元件需用mitt)
```javascript=
<product-modal ref="productModal" @update="getData"></product-modal>
//in父元件:
this.$refs.DOM元素名稱.要呼叫子元件的函式;
this.$refs.productModal.clearFile();
//in子元件的方法:
clearFile() {
this.$refs.file.value = null; //呼叫自己標籤的DOM元素,file為DOM名稱
}
```
使用Vue的ref屬性操作Modal:
```javascript=
<input type="text" ref="inputDom">
const app = Vue.createApp({
data() {
return {
bsModal: "",
};
},
methods: {
openModal() {
this.Modal.show();
},
},
mounted() {
this.$refs.inputDom.focus(); //在網頁載入時會自動focus進去input,ref只能在mounted看的到
//變數 = new bootstrap.Modal(DOM實體);
this.bsModal = new bootstrap.Modal(this.$refs.modal); //註冊Modal元件實體
},
});
```
動態ref取值: [https://blog.csdn.net/qq_26834399/article/details/119992536](https://blog.csdn.net/qq_26834399/article/details/119992536)
- 分頁組件範例:
```javascript=
const app = Vue.createApp({
data() {
return {
pagination: {},
};
},
});
app.component("pagination", {
template: "#pagination",
props: ["pages"],
methods: {
emitPage(page) {
this.$emit("emit-page", page); //傳送要點擊的頁數
},
},
});
<script type="text/x-template" id="pagination">
<nav aria-label="Page navigation example">
<ul class="pagination">
<li class="page-item" :class="{'disabled': pages.current_page == 1}">
<a class="page-link" href="#" aria-label="Previous" @click="emitPage(pages.current_page - 1)">
<span aria-hidden="true">«</span>
</a>
</li>
<li class="page-item" v-for="(item, index) in pages.total_pages" :key="index" :class="{'active':item == pages.current_page}">
<a class="page-link" href="#" @click.prevent="emitPage(item)">{{item}}</a>
</li>
<li class="page-item" :class="{'disabled': pages.current_page == pages.total_pages}">
<a class="page-link" href="#" aria-label="Next" @click.prevent="emitPage(pages.current_page + 1)">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
</script>
```
## 插槽:在內層開一個插槽,可以由外層操作內層插槽的HTML結構
Slot 插巢可以在外層元件,直接操作內層元件的某一區塊(例如插入 HTML 結構)。在內元件放入 `<slot>` 標籤,然後即可在 HTML 中的元件區塊放入要放的 HTML 結構。
https://hackmd.io/DEjj54NKQhOOjPuTmFZTWg?view
https://www.youtube.com/watch?v=_Dp-MlSSB_U&ab_channel=Alex%E5%AE%85%E5%B9%B9%E5%98%9B
- sample:
```javascript=
<card>
<p>這是由外層定義的文字</p> //直接在子元件插入HTML,如果沒插入就顯示子層的預設HTML
</card>
app.component("card", {
template: `
<div class="card" style="width: 18rem;">
<div class="card-header">
元件 Header
</div>
<div class="card-body">
<slot>
<p>這段是預設的文字</p>
</slot>
</div>
<div class="card-footer">
元件 Footer
</div>
</div>`,
});
```
- 具名插槽 (插入多個插槽):需使用==template==標籤來接
```htmlembedded=
<h3>具名插巢縮寫</h3> <!--v-slot縮寫為:#-->
<card2>
<template v-slot:header>我喜歡這張卡片</template> // 將此DOM插入到header插槽
<!--預設請加入 default-->
<template v-slot:default>這是卡片 2 號</template> //綁定預設是綁定沒給名稱的slot
<template v-slot:footer>這是卡片腳</template>
</card2>
app.component("card2", {
template: `
<div class="card" style="width: 18rem;">
<div class="card-header">
<slot name="header">元件 Header</slot> //接收插槽位置,使用name屬性取名
</div>
<div class="card-body">
<slot>這段是預設的文字</slot>
</div>
<div class="card-footer">
<slot name="footer">元件 Footer</slot>
</div>
</div>`
});
```
- 動態切換具名插槽:`v-slot:[]`
用法和用is切換component差不多,在這邊的寫法改為`v-slot:[]`,可以自己選擇位置放入更新內容。
```jsx=
// 主要頁面
<template>
<div class="">
<label v-for="(opt, i) in options" :key="i">
<!-- 選擇插入哪裡 -->
<input type="radio" :value="opt" v-model="dynamic_slot_name" />{{ opt }}
</label>
<div class="ma-5" />
<light-box>
<!-- 動態指定要插入的位置名稱(插在header、footer or center) -->
<template v-slot:[dynamic_slot_name]>
<h1>更新後內容</h1>
</template>
</light-box>
</div>
</template>
<script setup>
import { ref } from "vue";
import LightBox from "./LightBox.vue";
const options = ref(["header", "footer", "default"]); // 插入選項
const dynamic_slot_name = ref("header");
</script>
```
```jsx=
// LightBox模板頁面
<template>
<div class="lightbox">
<div class="modal-body">
<header>
<slot name="header">Default header</slot>
</header>
<hr />
<main>
<slot>Default body</slot>
</main>
<hr />
<footer>
<slot name="footer">Default footer</slot>
</footer>
</div>
</div>
</template>
```
### ==插槽props:== (作用域插槽)
slot資料都是由父層提供,若希望內層的data可以給外層使用,可以透過插巢的 props 則可把內元件的資料透過 props 丟給外層使用。
==作用域插槽==意指,當內層資料丟給外層使用時,該資料會限定子元件綁定的父層讀取
- 作用域範例:
```jsx=
// 從插槽取值後再show出來,因為有兩個插槽所以會出現兩次:
// Sample:【 OutSide {'abc'} OutSide {} 】
<MyButton v-slot="slotProps">
OutSide {{ slotProps }}
</MyButton>
```
```jsx=
// .\MyButton.vue
// 開兩個default插槽,一個有給父層值,一個沒有。
// 而資料只會顯示在插槽1本身的作用域,而插槽2不會因為也是default而取得插槽1的資料。
<script setup>
const myText = 'abc'
</script>
<template>
<button>
// 插槽1
<slot :text="myText"></slot> // 綁定的資料只會顯示在此插槽
// 插槽2
<slot></slot>
</button>
</template>
```
網頁寫法
- 一個(直接帶變數):
1. 使用 v-bind 將內層資料傳出去
```jsx=
<div id="app">
<card>
<template v-slot:default="slotProps">
<!-- 3) slotProps (自定義) 代表以物件包覆的,內元件傳入的資料-->
{{ slotProps }}
{{ slotProps.productNew.name }}
<!-- 4) 傳入的物件在屬性名 productNew 下可找到 name 屬性及資料-->
</template>
</card>
</div>
app.component('card',{
data(){
return{
product:{
name:'蛋餅',
price: 30,
vegan: false,
}
};
},
template:`
<div class='card'>
<div class='card-body'>
<slot :productNew='product'></slot>
<!-- 1) 把整個內元件的 product 放進「物件」傳出去-->
<!-- 2) 該「物件」的屬性名稱為「productNew」(自定義)-->
</div>
</div>`
})
```
- 多個(解構賦值):
```htmlembedded=
<card2 :product="product">
<template #default="{product, veganName}">
{{product}} {{veganName}}
<!--{ "name": "蛋餅", "price": 30, "vegan": false } 非素食-->
</template>
</card2>
app.component("card2", {
props: ["product"],
data() {
return {
veganName: "",
};
},
created() {
console.log();
this.veganName = this.product.vegan ? "素食" : "非素食";
},
template: `
<div class="card" style="width: 18rem;">
<div class="card-body" >
<slot :product="product" :veganName="veganName"></slot> //把整個內元件的 product 放進「物件」傳出去
</div>
</div>`,
});
```
## Mitt跨元件溝通:子元件to子元件
Or 使用 [vueUse 的 EventBus](https://vueuse.org/core/useEventBus/)。
```javascript=
<card-on></card-on>
<card-emit></card-emit> //把emit資料傳送到on
```
載入方式:
1. ```javascript=
import "https://unpkg.com/mitt/dist/mitt.umd.js"; //載入mitt套件
```
2. 安裝 `npm install --save mitt` https://juejin.cn/post/6973106775755063333
3.
```javascript=
//vue3 optional
import mitt from 'mitt'
const emitter = mitt();
//main.js (vite)
import mitt from "mitt"
app.config.globalProperties.$emitter = mitt()
4. 使用:
```javascript=
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
proxy.$emitter.emit('chartUpdate')
```
- 傳送端:
```javascript=
app.component("card-emit", {
data() {
return {
product: {
name: "蛋餅",
price: 30,
vegan: false,
},
};
},
methods: {
sendData() {
console.log("sendData");
//this.$emit
emitter.emit("sendProduct", this.product);
//自訂義名稱跨元件方法('傳值名稱', 要傳送的值)
},
},
template: `
<div class="card" style="width: 18rem;">
<div class="card-body" >
<button @click="sendData">送出</button>
</div>
</div>`,
});
```
- 接收端:接收端要作監聽
```javascript=
app.component("card-on", {
data() {
return {
item: {},
};
},
//接收資料建議放在created
created() {
emitter.on("sendProduct", (data) => { //使用.on對傳送過來的sendProduct作監聽,sendProduct名稱自訂
console.log(data);
this.item = data;
});
},
template: `
<div class="card" style="width: 18rem;">
<div class="card-body">
{{ item }}
</div>
</div>`,
});
```
## Teleport:
可以用id or class or HTML標籤的方式重用組件內的某段HTML,或是把HTML傳送到指定位置
使用限制:要傳送的位置只限自己script標籤包住的範圍,無法跨script傳送。
可傳送多個
```htmlembedded=
app.component("home", {
template: `<teleport to='.minHome'>小明</teleport> //傳送到class對應位置
<teleport to='#huaHome'>小華</teleport> //傳送到id對應位置
<teleport to='span'>小王</teleport> //傳送到標籤位置
`
});
<div id="app">
<div class="minHome">小明家:</div>
<div id="huaHome">小華家:</div>
<span>小王家:</span>
<home></home>
</div>
//結果:
小明家:小明
小華家:小華
小王家:小王
```
### SSR
請避免在 SSR 的同時把 Teleport 的目標設為 body——通常 `<body>`
會包含其他服務端渲染出來的內容,這會使得 Teleport 無法確定激活的正確起始位置。
推薦用一個獨立的只包含 teleport 的內容的容器,例如:`<div id="teleported"></div>`
## Provide:跨元件(巢狀元件)資訊傳遞
- 在外層加入 provide,可分為物件與函式方式(響應式)
物件:如果最底層有對資料作處理,只會影響本身的資料
函式:如果最底層有對資料作處理,會連帶影響中間層(常用)
- 內層元件補上 inject(物件格式)
```javascript=
//第三層
const userComponent = {
template: `<div>
{{ user.name }}
{{ user.uuid }}
</div>`,
inject: ["user"], //inject: ['要取得的變數']
created() {
console.log(this.user);
// 如果根元件沒有使用 function return 則不能使用響應式(資料連動)
this.user.name = "杰倫";
},
};
const app = Vue.createApp({
data() {
return {
user: {
name: "小明",
uuid: 78163,
},
};
},
// provide: { //物件結構
// user: {
// name: "小明",
// uuid: 78163,
// },
// },
provide() { //提供(provide)給這個元件都可以用的方法
return {
user: this.user, //可以使用this
};
},
});
//第二層
app.component("card", {
data() {
return {
title: "文件標題",
content: "文件內文",
toggle: false,
};
},
components: {
userComponent,
},
inject: ["user"], //中間層要取得也要加上inject,如果不需要就不用加 or 用props取得
template: `
<div class="card" style="width: 18rem;">
<div class="card-body">
<userComponent></userComponent>
{{[user.name](http://user.name)}}
</div>
</div>
`,
});
```
[provide與inject響應式綁定](https://juejin.cn/post/7018102866292244488)
```javascript=
//祖先组件----------------------------------
provide() {
return {
foo: this
}
},
//子孙组件----------------------------------
inject: ["foo"],
mounted: {
console.log(this.foo.someval)//===>> 获取data中的数据
console.log(this.foo.getVal())//===>>调用method中的方法
console.log(this.foo.calculation())//===>>调用computed中的方法
},
```
## 子元件資料雙向綁定父元件:
```javascript=
{{ text }} {{ text2 }}
<custom-input2
v-model:t1="text"
v-model:t2="text2">
</custom-input2> //屬性本為props,然後再加上v-model綁定父層
const app = Vue.createApp({
data() {
return {
text: "這是文字片段 1",
text2: "這是文字片段 2",
};
},
});
// $emit('update:text', $event.target.value) 搭配 props,可以將更新後的值寫回 v-model 內
app.component("custom-input2", {
props: ["t1", "t2"],
template: `
<input type="text" :value="t1" @input="$emit('update:t1', $event.target.value)"
class="form-control">
<input type="text" :value="t2" @input="$emit('update:t2', $event.target.value)"
class="form-control">
`,
});
```
## Mixin:混和元件,讓元件共用的方法(類似function可以不斷拿來用)
- 可以重複混合
- 生命週期可以各自觸發
- 同名的變數、方法則會被後者覆蓋
```javascript=
//元件一
const mixComponent1 = {
data() {
return {
name: "混合的元件",
};
},
created() {
console.log("混合的元件生命週期");
},
};
//元件二
const mixComponent2 = {
data() {
return {
name: "混合的元件 2",
};
},
created() {
console.log("混合的元件生命週期 2");
},
};
const app = Vue.createApp({});
app.component("card", {
template: `<div class="card">
<div class="card-body">{{ name }}</div>
</div>`,
mixins: [mixComponent1, mixComponent2], //混和元件的值會透過mixin帶進來,name會被後者覆蓋,只顯示"混合的元件 2"
created() {
console.log("card 的元件生命週期");
},
});
```
- 共用方法:
```javascript=
const filterMix = { //建立一個共用方法
created(){
this.cash = this.dollarSign(this.cash)
},
methods:{
dollarSign(dollar) {
return `${dollar}`;
}
},
};
```
在需要插入共用的方法新增mixins
```javascript=
import XXX form '路徑' //如果在不同檔案需要import
app.component('nike', {
data() {
return {
cash:3000,
title:'nike 球鞋',
img:'https://i.imgur.com/fiUT2Sx.jpg'
}
},
mixins: [filterMix],
template:
`<div class="card" style="width: 18rem;">
<img :src="img" class="card-img-top">
<div class="card-body">
<h5 class="card-title">{{title}}</h5>
<div>售價: <span>{{cash}}</span></div>
</div>
</div>`,
});
```
## Extend:擴展,沿用元件資料
- 擴展為==單一擴展==
- 生命週期可以與 mixins 重複觸發
- 權重:元件屬性 \> mixins > extend
- 執行順序:與權重相反
- 同名的變數、方法則會依據權重決定
```javascript=
app.component('card2', {
template: `<div class="card">
<div class="card-body">{{ name }}</div>
</div>`,
mixins: [mixinComponent],
extends: extendComponent1, //只能一個
created() {
console.log('card 的元件生命週期')
}
});
```
## Directive (Vue模板)
- https://ithelp.ithome.com.tw/articles/10266580
==只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令,否則皆應使用資料來驅動畫面或程式。==
```javascript=
import { createApp } from "vue";
import dayjs from "dayjs";
import App from "./App.vue";
const app = createApp(App);
// 先註冊一個 timeformat 的語法 (全域註冊)
app.directive("timeformat", {
mounted(el, binding) { // 當元件綁定時會觸發
const time = dayjs(binding.value).format("YYYY年MM月DD日");
el.innerText = time;
}
});
app.mount("#app");
```
使用:
```htmlembedded=
<p v-timeformat="card.post_date"></p>
```
## Renderless Component (無渲染組件)
- https://medium.com/%E4%BA%BA%E7%94%9F%E7%9A%84%E5%90%84%E7%A8%AE%E5%8F%AF%E8%83%BD/vue%E8%A8%AD%E8%A8%88%E6%A8%A1%E5%BC%8F-%E7%84%A1%E6%B8%B2%E6%9F%93%E7%B5%84%E4%BB%B6-renderless-component-182f1f1bb4a3
使用情境:
1. 如果你有很多個元件他的功能都很類似但卻希望有不一樣的外貌
1. 如果你希望讓User自定義UI的樣式的時候