# 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: ![](https://i.imgur.com/ZUQUTvU.png) ```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> ``` 跳过这个元素和它的子元素的编译过程可以加快编譯 ![](https://img-blog.csdnimg.cn/20200731153955110.png) - `pre`:能夠優質顯示json在網頁上,而不會擠成一坨 ```jsx= <pre>{{ jsonData }}</pre> const jsonData = { "key": "名字", "value": "张三丰" } ``` - 未使用 ![](https://img-blog.csdnimg.cn/20200731154435370.png) - 使用 ![](https://img-blog.csdnimg.cn/20200731154609655.png) ## 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">&laquo;</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">&raquo;</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的樣式的時候