Try   HackMD

影片連結

Vite 創建專案

npm create vue@latest

### Run
npm install
npm run dev

Watch

只能監視以下四種屬性

  • ref 定義的資料
  • reactive 定義的資料
  • 函數回傳的一個數值
  • 內容包含上述的陣列

程式一:監視 ref 的基本型態

watch 函數有返回值,用來執行停止監視。

<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 的物件型態

  • 監視的是物件的位址變化
  • 監視物件的屬性需手動開啟深度監視,否則只匹配第一點情況
  • 注意:若只修改物件中的屬性,監視的新舊值會相同,原因:監視位址的變化
<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 的物件型態

  • 預設就開啟深度監視
<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 監視物件屬性,可直接寫,也可寫成函數回傳形式
<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

<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
export interface IPerson {
  id: number;
  name: string
}
  • App.vue
<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
<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
<template>
  <div>{{ id }}</div>
</template>

<script setup lang="ts">
import { defineProps } from "vue";
defineProps(["id", "name", "age"]);
</script>

Params 參數轉成 Props

// url: /demo/001/JJ/25
{
    name: "demo",
    path: "/demo/:id/:name/:age",
    component: () => import("@/Demo.vue"),
    props: true
}

Query 參數轉成 Props

// url: /demo?id=001&name=JJ&age=25
{
    name: "demo",
    path: "/demo",
    component: () => import("@/Demo.vue"),
    props: (route) => route.query
}

物件轉成 Pross (不常用)

{
    name: "demo",
    path: "/demo",
    component: () => import("@/Demo.vue"),
    props: {
        id: "001",
        name: "JJ",
        age: 28
    }
}

Pinia

安裝 & 開始使用

  • 安裝指令
npm i pinia
  • main.ts
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
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);
    },
  },
});
  • Store 的命名根據官網建議是以 use 開頭及 Store 結尾,例如:useCountStore

Setup Store

  • src/store/count.ts
import { ref } from "vue";
export const useCountStore = defineStore("count", () => {
  const count = ref(0);
  const increament = () => (count.value += 1);
  return { count, increament };
});

修改 Store 中的數值

  • 方法一:直接修改
import { useCountStore } from "@/store/count";
const countStore = useCountStore();
countStore.count +=1;
  • 方法二:使用 $patch 批次修改
import { useCountStore } from "@/store/count";
const countStore = useCountStore();

/*$patch 方法特性為可一次修改多的數值*/
countStore.$patch({
    count: 50
});
  • 方法三:定義 Action
import { useCountStore } from "@/store/count";
countStore.increament()

組件溝通

父子溝通

自定義事件

  • Father.vue
<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
<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
<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
<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>

父子孫溝通

$attr

可用來接收 props 有傳入,但子組件沒有接收的資料

  • GrandPa.vue
<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
<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
<template>
  <h2>Son--{{ b }}--{{ c }}</h2>
  <hr />
</template>

<script setup lang="ts">
// 接收父層來的 $attr 仍需要定義 props 來取得
defineProps(["b", "c"]);
</script>

後代溝通

proivder & inject

  • Father.vue
<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
<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 的細節

原理實現

// 以下寫法等價
<input type="text" v-model="name" />
<input
    type="text"
    :value="name"
    @input="name = (<HTMLInputElement>$event.target).value"
/>

// 第二個寫法需加上 <HTMLInputElement> 作為 ts 的斷言
// 否則 ts 會顯示 $event 可能為 null 的警告

組件包裝的情況

  • InputComponent.vue
<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
<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
<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
<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>