--- title: "[Note] 禹神新版Vue3(2024)" --- [影片連結](https://www.youtube.com/playlist?list=PLmOn9nNkQxJEnGM4Jf0liBcyedAtuQq-O) # Vite 創建專案 ```console npm create vue@latest ### Run npm install npm run dev ``` # Watch 只能監視以下四種屬性 * ref 定義的資料 * reactive 定義的資料 * 函數回傳的一個數值 * 內容包含上述的陣列 ## 程式一:監視 ref 的基本型態 watch 函數有返回值,用來執行停止監視。 ```javascript <template> <div>{{ sum }}</div> <button @click="changeSum">Click</button> </template> <script lang="ts" setup> import { ref, watch } from "vue"; const sum = ref(0); const changeSum = () => { sum.value += 1; }; const stopWatch = watch(sum, (newVal, oldVal)=>{ console.log("Sum Change", newVal, oldVal); stopChange(); }); </script> ``` ## 程式二:監視 ref 的物件型態 * 監視的是物件的位址變化 * 監視物件的屬性需手動開啟深度監視,否則只匹配第一點情況 * **注意**:若只修改物件中的屬性,監視的新舊值會相同,原因:監視位址的變化 ```javascript <template> <div>{{ person.name }}--{{ person.age }}</div> <button @click="changeName">Name</button> <button @click="changeAge">Age</button> <button @click="change">All</button> </template> <script lang="ts" setup> import { ref, watch } from "vue"; const person = ref({ name: "JJ", age: 28, }); const changeName = () => { person.value.name += "~"; }; const changeAge = () => { person.value.age += 1; }; const change = () => { person.value = { name: "LL", age: 15 }; }; watch( person, (newVal, oldVal) => { console.log("change", newVal, oldVal); }, { deep: true, // 深度監視 immediate: true, // 立即監視 } ); </script> ``` ## 程式三:監視 reactive 的物件型態 * 預設就開啟深度監視 ```javascript <template> <div>{{ person.name }}--{{ person.age }}</div> <button @click="changeName">Name</button> <button @click="changeAge">Age</button> <button @click="change">All</button> </template> <script lang="ts" setup> import { reactive, watch } from "vue"; const person = reactive({ name: "JJ", age: 28, }); const changeName = () => { person.name += "~"; }; const changeAge = () => { person.age += 1; }; const change = () => { Object.assign(person, { name: "LL", age: 15 }); }; watch( person, (newVal, oldVal) => { console.log("change", newVal, oldVal); } ); </script> ``` ## 程式四:監視 ref/reactive 的物件的屬性 * reactive 監視單一基本型態的屬性,需寫成函數回傳該屬性 * reactive 監視物件屬性,可直接寫,也可寫成函數回傳形式 ```javascript <template> <div>{{ person.name }} <button @click="changeName">Update</button></div> <div>{{ person.age }} <button @click="changeAge">Update</button></div> <div> {{ person.car }} <button @click="changeBrand">Brand</button> <button @click="changePrice">Price</button> <button @click="changeCar">All</button> </div> </template> <script lang="ts" setup> import { reactive, watch } from "vue"; const person = reactive({ name: "JJ", age: 28, car: { brand: "Benz", price: 1000, }, }); const changeName = () => (person.name += "~"); const changeAge = () => (person.age += 1); const changeBrand = () => (person.car.brand += "!"); const changePrice = () => (person.car.price += 50); const changeCar = () => (person.car = { brand: "BWM", price: 5000 }); watch( () => person.name, () => console.log("Name Change") ); watch( () => person.age, () => console.log("Age Change") ); /** * 當物件中屬性變動時,則觸發。changeBrand、changePrice * 當物件整個變動時,則不會觸發。changeCar */ // watch(person.car, () => console.log("Car Change")); /** * 寫成函數形式,變為監聽位址,所以: * 當物件中整個變動時,則觸發。changeCar * 當物件屬性變動時,則不會觸發。changeBrand、changePrice * 函數形式要兩者情況皆觸發時需加上 { deep:true } */ watch( () => person.car, () => console.log("Car Change"), { deep: true } ); </script> ``` # WatchEffect ```javascript <template> <div>{{ temp }}-{{ height }}</div> <div> <button @click="changeTemp">Temp</button> <button @click="changeHeight">Height</button> </div> </template> <script lang="ts" setup> import { ref, watch, watchEffect } from "vue"; const temp = ref(10); const height = ref(0); const changeTemp = () => (temp.value += 10); const changeHeight = () => (height.value += 50); // watch([temp, height], (value) => { // const [newTemp, newHeight] = value; // console.log(newTemp, newHeight); // if (newTemp >= 60 || newHeight >= 80) { // console.log("Fetch"); // } // }); watchEffect(() => { if (temp.value >= 60 || height.value >= 80) { console.log("Fetch"); } }); </script> ``` # defineProps * src/types/index.ts ```typescript export interface IPerson { id: number; name: string } ``` * App.vue ```typescript <template> <Person first-name="Wang" last-name="JJ" :user="personList[0]" :user-list="personList" ></Person> </template> <script lang="ts" setup> import Person from "./components/Person.vue"; import type { IPerson } from "./types"; // 補充:在 vite 中引入 interface 或 type 型態,需加上 type 關鍵字 const personList = [ { id: 1, name: "JJ", }, { id: 2, name: "JJay", }, { id: 3, name: "Jay", }, ] as IPerson[]; </script> ``` * Person.vue ```javascript <template> <div>{{ firstName }}--{{ lastName }}</div> <hr> <div>{{ user?.id }}--{{ user?.name }}</div> <hr> <div v-for="item in userList" :key="item.id"> {{ item.name }} </div> </template> <script lang="ts" setup> import type { IPerson } from "@/types"; import type { PropType } from "vue"; defineProps({ firstName: { type: String, }, lastName: { type: String, required: true, }, user: { type: Object as PropType<IPerson>, required: false, }, userList: { type: Array<IPerson>, required: true, }, }); </script> ``` # 路由 Props * Demo.vue 定義使用的 Props ```typescript <template> <div>{{ id }}</div> </template> <script setup lang="ts"> import { defineProps } from "vue"; defineProps(["id", "name", "age"]); </script> ``` ## Params 參數轉成 Props ```typescript // url: /demo/001/JJ/25 { name: "demo", path: "/demo/:id/:name/:age", component: () => import("@/Demo.vue"), props: true } ``` ## Query 參數轉成 Props ```typescript // url: /demo?id=001&name=JJ&age=25 { name: "demo", path: "/demo", component: () => import("@/Demo.vue"), props: (route) => route.query } ``` ## 物件轉成 Pross (不常用) ```typescript { name: "demo", path: "/demo", component: () => import("@/Demo.vue"), props: { id: "001", name: "JJ", age: 28 } } ``` # Pinia ## 安裝 & 開始使用 * 安裝指令 ``` npm i pinia ``` * main.ts ```typescript import { createApp } from 'vue' import App from './App.vue' import { createPinia } from "pinia"; const pinia = createPinia(); createApp(App).use(pinia).mount('#app') ``` ## 建立 store Store 的建立方式可分為 Option Store、 Setup Store。 ### Option Store * src/store/count.ts ```typescript import { defineStore } from "pinia"; export const useCountStore = defineStore("count", { state() { return { count: 1, }; }, getters: { bigSum: (state) => state.count * 10, }, actions: { increament(value: number) { this.count += value; console.log(this, value); }, }, }); ``` :::warning * Store 的命名根據官網建議是以 `use` 開頭及 `Store` 結尾,例如:useCountStore ::: ### Setup Store * src/store/count.ts ```typescript import { ref } from "vue"; export const useCountStore = defineStore("count", () => { const count = ref(0); const increament = () => (count.value += 1); return { count, increament }; }); ``` ## 修改 Store 中的數值 * 方法一:直接修改 ```typescript import { useCountStore } from "@/store/count"; const countStore = useCountStore(); countStore.count +=1; ``` * 方法二:使用 $patch 批次修改 ```typescript import { useCountStore } from "@/store/count"; const countStore = useCountStore(); /*$patch 方法特性為可一次修改多的數值*/ countStore.$patch({ count: 50 }); ``` * 方法三:定義 Action ```typescript import { useCountStore } from "@/store/count"; countStore.increament() ``` # 組件溝通 ## 父子溝通 <!-- ### Props --> ### 自定義事件 * Father.vue ```typescript <template> <h4>Father</h4> {{ data }} <Child @change-msg="updateMsg" @change-count="updateCount"></Child> </template> <script setup lang="ts"> import Child from "./Child.vue"; import { ref } from "vue"; const data = ref({ msg: "Hi", count: 50 }); const updateMsg = () => (data.value.msg += "~"); const updateCount = (value: number) => (data.value.count += value); </script> ``` * Child.vue ```typescript <template> <h4>Child</h4> <div> <button @click="editMsg">Change Msg</button> &nbsp; <button @click="editCount">Change Count</button> </div> </template> <script setup lang="ts"> const emit = defineEmits(["change-msg", "change-count"]); const editMsg = () => emit("change-msg"); const editCount = () => emit("change-count", 5); </script> ``` ### $ref & $parent * Father.vue ```typescript <template> <div> {{ car }} <button @click="updateSon($refs)">Update Son</button> </div> <hr /> <Father ref="father"></Father> </template> <script setup lang="ts"> import { ref } from "vue"; import Father from "./Father.vue"; const car = ref("Benz"); // 組件提供存取的資料 defineExpose({ car }); const father = ref(); const updateSon = (refs: any) => { console.log(refs.father.toy); father.value.toy = "cat"; father.value.price += 10; }; </script> ``` * Child.vue ```typescript <template> {{ toy }}--{{ price }} {{ show($parent) }} <hr /> </template> <script setup lang="ts"> import { ref } from "vue"; const toy = ref("dog"); const price = ref(50); // 組件提供存取的資料 defineExpose({ toy, price }); // 使用 $parent 取得父組件開放存取的資料 const show = (parent: any) => console.log(parent); </script> ``` <!-- ## mitt --> ## 父子孫溝通 ### $attr 可用來接收 props 有傳入,但子組件沒有接收的資料 * GrandPa.vue ```typescript <template> <h2>GrandPa</h2> <hr> <Father :a="1" :b="2" :c="3"></Father> </template> <script setup lang="ts"> import Father from './Father.vue'; </script> ``` * Father.vue ```typescript <template> <h2>Father--{{ a }}--{{ $attrs }}</h2> <hr /> <Son v-bind="$attrs"></Son> </template> <script setup lang="ts"> import { useAttrs } from "vue"; import Son from "./Son.vue"; defineProps(["a"]); // props 只接收 a console.log(useAttrs()); // 在 js 中使用 </script> ``` * Son.vue ```typescript <template> <h2>Son--{{ b }}--{{ c }}</h2> <hr /> </template> <script setup lang="ts"> // 接收父層來的 $attr 仍需要定義 props 來取得 defineProps(["b", "c"]); </script> ``` ## 後代溝通 ### proivder & inject * Father.vue ```typescript <template> <h4>Father</h4> <h5>{{ count }}--{{ car }}</h5> <Child></Child> </template> <script setup lang="ts"> import Child from "./Child.vue"; import { ref, provide } from "vue"; const count = ref(50); const setCount = (value: number) => (count.value -= value); const car = ref({ brand: "Benz", price: 70 }); provide("countContext", { count, setCount }); provide("car", car); </script> ``` * Child.vue ```typescript <template> <h4>Child</h4> <button @click="setCount(5)">Set Count</button> <h5>{{ count }}--{{ car?.brand }}--{{ car?.price }}</h5> </template> <script setup lang="ts"> import { inject } from "vue"; const { count, setCount } = < { count: number; setCount: (value: number) => void } >inject("countContext"); const car = inject<CarType>("car"); type CarType = { brand: string; price: number }; </script> ``` ## v-model 的細節 ### 原理實現 ```typescript // 以下寫法等價 <input type="text" v-model="name" /> <input type="text" :value="name" @input="name = (<HTMLInputElement>$event.target).value" /> // 第二個寫法需加上 <HTMLInputElement> 作為 ts 的斷言 // 否則 ts 會顯示 $event 可能為 null 的警告 ``` ### 組件包裝的情況 * InputComponent.vue ```typescript <template> <input type="text" :value="modelValue" @input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)" /> <input type="text" :value="word" @input="emit('update:word', (<HTMLInputElement>$event.target).value)" /> </template> <script setup lang="ts"> defineProps(["modelValue", "word"]); // 此處注意 "update:modelValue" 是一個完整個事件名稱,並非拆開來看 const emit = defineEmits(["update:modelValue", "update:word"]); </script> ``` * App.vue ```typescript <template> <InputComponent :modelValue="name" @update:modelValue="name = $event" /> <!--簡寫形式--> <InputComponent v-model="name" v-model:word="word"/> </template> <script setup lang="ts"> import InputComponent from "./InputComponent.vue" import { ref } from "vue"; const name = ref("JJ"); const word = ref("Hello"); </script> ``` # Suspense * App.vue ```typescript <template> <div>APP</div> <Suspense> <template v-slot:default> <Child></Child> </template> <!--也可直接使用組件標籤即可--> <!--<Child></Child>--> // v-slot:fallback 為非同步執行中的區塊 <template v-slot:fallback> <div>Loading</div> </template> </Suspense> </template> <script lang="ts" setup> import { Suspense } from "vue"; import Child from "./Child.vue"; </script> ``` * Child.vue ```typescript <template> <div>Child</div> </template> <script setup lang="ts"> import axios from "axios"; const data = await axios.get("https://jsonplaceholder.typicode.com/todos/1"); console.log(data); </script> ```