0x00 概述

  • 阅读以下内容需要具备一定的 Vue2 基础
  • 代码采用规范为:TypeScript + 组合式 API + setup 语法糖

(1)Vue3 简介

  • Vue3 第一个正式版发布于 2020 年 9 月 18 日
  • Vue3 中文官网
  • Vue3 相比 Vue2 的优势:
    1. 性能提升:打包体积减小,初次渲染和更新渲染都更快,内存使用减少
    2. 源码升级:使用 Proxy 实现响应式,重写虚拟 DOM 的实现和 Tree-Shaking
    3. 支持 TypeScript
    4. 新增特性:组合式 API、新内置组件、新生命周期钩子等

(2)TypeScript 概述

0x01 第一个项目

(1)创建项目

创建项目共有两种方法

  1. 使用 vue-cli 创建

    1. 命令提示符中使用命令 npm install -g @vue/cli 下载并全局安装 vue-cli

      如果已经安装过,则可以使用命令 vue -V 查看当前 vue-cli 的版本,版本要求在 4.5.0 以上

      如果需要多版本 vue-cli 共存,则可以参考文章:安装多版本Vue-CLI的实现方法 | 脚本之家-webgiser

    2. 使用命令 vue create project_name 开始创建项目

    3. 使用方向键选择 Default ([Vue3 babel, eslint])

    4. 等待创建完成后,使用命令 cd project_name 进入项目目录

    5. 使用命令 npm serve 启动项目

  2. 使用 vite 创建

    相比使用 vue-cli 创建,使用 vite 的优势在于

    • 轻量快速的热重载,实现极速的项目启动
    • 对 TypeScript、JSX、CSS 等支持开箱即用
    • 按需编译,减少等待编译的时间
    1. 使用命令 npm create vue@latest 创建 Vue3 项目
    2. 输入项目名称
    3. 添加 TypeScript 支持
    4. 不添加 JSX 支持、Vue Router、Pinia、Vitest、E2E 测试、ESLint 语法检查、Prettier 代码格式化
    5. 使用命令 cd [项目名称] 进入项目目录
    6. 使用命令 npm install 添加依赖
    7. 使用命令 npm run dev 启动项目

(2)项目结构

  • node_modules:项目依赖

  • public:项目公共资源

  • src:项目资源

    • assets:资源目录

    • components:组件目录

    • main.ts:项目资源入口文件

      import './assets/main.css'		// 引入样式文件
      
      import { createApp } from 'vue'	// 引入创建 Vue 应用方法
      import App from './App.vue' // 引入 App 组件 createApp(App).mount('#app') // 创建以 App 为根组件的应用实例,并挂载到 id 为 app 的容器中(该容器为 index.html)
    • App.vue:应用根组件

      <template>
      <!-- 模型 -->
      </template> <script lang="ts">
      /* 控制 */
      </script> <style>
      /* 样式 */
      </style>
  • env.d.ts:TypeScript 环境配置文件,包括文件类型声明等

  • index.html:项目入口文件

  • vite.config.ts:项目配置文件

0x02 核心语法

(1)组合式 API

  • 首先使用 Vue2 语法完成一个组件

    • 项目结构

      graph TB
      src-->components-->Person.vue
      src-->App.vue & main.ts
    • 详细代码

      • main.ts

        import { createApp } from 'vue'
        import App from './App.vue' createApp(App).mount('#app')
      • App.vue

        <template>
        <div class="app">
        <h1>App</h1>
        <Person />
        </div>
        </template> <script lang="ts">
        import Person from './components/Person.vue';
        export default {
        name: 'App', // 当前组件名
        components: { // 注册组件
        Person
        }
        }
        </script> <style scoped>
        .app {
        padding: 20px;
        }
        </style>
      • Person.vue

        <template>
        <h2>Name: {{ name }}</h2>
        <h2>Age: {{ age }}</h2>
        <button @click="showDetail">Detail</button>
        <hr />
        <p><button @click="changeName">Change Name</button></p>
        <p><button @click="changeAge">Change Age</button></p>
        </template> <script lang="ts">
        export default {
        name: "Person",
        data() { // 数据配置
        return { // 包含组件的所有数据
        name: "John",
        age: 18,
        telephone: "1234567890",
        email: "john@gmail.com"
        }
        },
        methods: { // 方法配置
        showDetail() { // 包含组件的全部方法
        alert(`Detail: \ntelephone: ${this.telephone}\n email: ${this.email}`)
        },
        changeName() {
        this.name = "Jane";
        },
        changeAge() {
        this.age += 1;
        }
        }
        }
        </script> <style>
        </style>
    • 目前,使用 Vue2 语法完成的组件 Person 中使用了选项式 API(Options API),其中 namedatamethods 均称为选项(或称配置项)

    • 选项式 API 的问题在于:数据、方法、计算属性等分散在 datamethodscomputed 等选项中,当需要新增或修改需求时,就需要分别修改各个选项,不便于维护和复用

  • Vue3 采用组合式 API

(2)setup

a. 概述

  • setup 是 Vue3 中一个新的配置项,值是一个函数,其中包括组合式 API,组件中所用到的数据、方法、计算属性等均配置在 setup
  • 特点
    1. setup 函数返回的可以是一个对象,其中的内容可以直接在模板中使用;也可以返回一个渲染函数
    2. setup 中访问 thisundefined,表明 Vue3 已经弱化 this 的作用
    3. setup 函数会在方法 beforeCreate 之前调用,在所有钩子函数中最优先执行

b. 使用 setup

修改本章第(1)小节采用 Vue2 语法的项目

  • App.vue

    <template>
    <Person />
    </template> <script lang="ts">
    import Person from './components/Person.vue';
    export default {
    name: 'App',
    components: {
    Person
    }
    }
    </script> <style scoped>
    </style>
  • Person.vue

    1. 引入 setup

      <script lang="ts">
      export default {
      name: "Person",
      setup() {
      }
      }
      </script>
    2. setup 中声明数据

      <script lang="ts">
      export default {
      name: "Person",
      setup() {
      let name = "John"
      let age = 18
      let telephone = "1234567890"
      let email = "john@gmail.com"
      }
      }
      </script>
    3. 将需要使用的数据返回到模板中

      可以为返回的变量设置别名:如 n: name

      <template>
      <h2>Name: {{ n }}</h2>
      <h2>Age: {{ age }}</h2>
      </template> <script lang="ts">
      export default {
      name: "Person",
      setup() {
      let name = "John"
      let age = 18
      let telephone = "1234567890"
      let email = "john@gmail.com" return {
      n: name,
      age
      }
      }
      }
      </script> <style>
      </style>
    4. setup 中声明方法并返回

      <template>
      <h2>Name: {{ n }}</h2>
      <h2>Age: {{ age }}</h2>
      <button @click="showDetail">Detail</button>
      <hr />
      <p><button @click="changeName">Change Name</button></p>
      <p><button @click="changeAge">Change Age</button></p>
      </template> <script lang="ts">
      export default {
      name: "Person",
      setup() {
      // let ... function showDetail() {
      alert(`Detail: \ntelephone: ${telephone}\n email: ${email}`)
      }
      function changeName() {
      name = "Jane";
      }
      function changeAge() {
      age += 1;
      } return {
      n: name,
      age,
      showDetail,
      changeName,
      changeAge
      }
      }
      }
      </script> <style>
      </style>

      此时可以发现,点击“姓名修改”或“年龄修改”按钮后,页面并未发生改变,这是因为之前使用 let 声明的数据不是响应式的,具体方法参考本章第 x 小节

  • setup 可以与 datamethods 同时存在

    • datamethods 中使用 this 均可以访问到在 setup 中声明的数据与函数

      • 因为setup 的执行早于 datamethods
      <template>
      <h2>Age: {{ a }}</h2>
      </template> <script lang="ts">
      export default {
      name: "Person",
      data() {
      return { a: this.age }
      },
      setup() {
      let name = "John"
      let age = 18
      return { n: name, age }
      }
      }
      </script>
    • setup 中无法使用 datamethods 中声明的数据或函数

c. 语法糖

  • 在上述项目中,每次在 setup 中声明新数据时,都需要在 return 中进行“注册”,之后才能在模板中使用该数据,为解决此问题,需要使用 setup 语法糖

  • <script setup lang="ts"></script> 相当于如下代码:

    <script lang="ts">
    export default {
    setup() {
    return {}
    }
    }
    </script>
  • 修改 Person.vue

    <script setup lang="ts">
    let name = "John";
    let age = 18;
    let telephone = "1234567890";
    let email = "john@gmail.com"; function showDetail() {
    alert(`Detail: \ntelephone: ${telephone}\n email: ${email}`);
    }
    function changeName() {
    name = "Jane";
    }
    function changeAge() {
    age += 1;
    }
    </script>
  • <script setup lang="ts"></script> 中无法直接设置组件的 name 属性,因此存在以下两种方式进行设置:

    1. 使用两个 <script> 标签

      <script lang="ts">
      export default {
      name: "Person"
      }
      </script>
      <script setup lang="ts">
      // ...
      </script>
    2. (推荐)基于插件实现

      1. 使用命令 npm install -D vite-plugin-vue-setup-extend 安装需要的插件

      2. 在 vite.config.ts 中引入并调用这个插件

        // import ...
        import vpvse from 'vite-plugin-vue-setup-extend' export default defineConfig({
        plugins: [
        vue(),
        vpvse(),
        ],
        // ...
        })
      3. 重新启动项目

      <script setup lang="ts" name="Person">
      // ...
      </script>
  • 将 App.vue 中的 Vue2 语法修改为 Vue3

    <template>
    <Person />
    </template> <script setup lang="ts" name="App">
    import Person from './components/Person.vue';
    </script> <style scoped>
    </style>

(3)创建基本类型的响应式数据

使用 ref 创建

  1. 引入 ref

    <script setup lang="ts" name="Person123">
    import {ref} from 'vue'
    </script>
  2. 对需要成为响应式的数据使用 ref

    <script setup lang="ts" name="Person123">
    import {ref} from 'vue' let name = ref("John");
    let age = ref(18);
    let telephone = "1234567890";
    let email = "john@gmail.com";
    </script>
    • 使用 console.log() 可以发现使用 refname 变成了

      // RefImpl
      {
      "__v_isShallow": false,
      "dep": {},
      "__v_isRef": true,
      "_rawValue": "John",
      "_value": "John"
      }

      未使用 reftelephone 依然是字符串 '1234567890'

  3. 修改方法

    <script setup lang="ts" name="Person123">
    import {ref} from 'vue' let name = ref("John");
    let age = ref(18);
    let telephone = "1234567890";
    let email = "john@gmail.com"; function showDetail() {
    alert(`Detail: \ntelephone: ${telephone}\n email: ${email}`);
    }
    function changeName() {
    name.value = "Jane";
    }
    function changeAge() {
    age.value += 1;
    }
    </script>
    • 在函数方法中使用 name 之类被响应式的数据,需要在属性 value 中获取或修改值,而在模板中不需要

      <template>
      <h2>Name: {{ name }}</h2>
      <h2>Age: {{ age }}</h2>
      <button @click="showDetail">Detail</button>
      <hr />
      <p><button @click="changeName">Change Name</button></p>
      <p><button @click="changeAge">Change Age</button></p>
      </template>

(4)创建对象类型的响应式数据

a. 使用 reactive 创建

  1. 创建对象以及函数方法,并在模板中使用

    <template>
    <h2>Name: {{ Teacher.name }}</h2>
    <h2>Age: {{ Teacher.age }}</h2>
    <p><button @click="changeTeacherAge">Change Teacher Age</button></p>
    </template> <script setup lang="ts" name="Person">
    let Teacher = { name: "John", age: 18 } function changeTeacherAge() {
    Teacher.age += 1;
    }
    </script>
  2. 引入 reactive

    <script setup lang="ts" name="Person">
    import {reactive} from 'vue' // ...
    </script>
  3. 对需要成为响应式的对象使用 reactive

    <script setup lang="ts" name="Person">
    import {reactive} from 'vue' let Teacher = reactive({ name: "John", age: 18 }) // ...
    </script>
    • 使用 console.log() 可以发现 John 变成了 Proxy(Object) 类型的对象
  4. 按钮方法不做调整:Teacher.age += 1;

  5. 添加对象数组以及函数方法,并在模板中遍历

    <template>
    <!-- ... -->
    <hr />
    <ul>
    <li v-for="student in Student" :key="student.id">{{ student.name }}</li>
    </ul>
    <button @click="changeFirstStudentName">Change First Student Name</button>
    </template> <script setup lang="ts" name="Person">
    // ...
    let Student = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { id: 3, name: "Charlie" }
    ] // ...
    function changeFirstStudentName() {
    Student[0].name = "Alex";
    }
    </script>
  6. 将对象数组变为响应式

    <script setup lang="ts" name="Person">
    // ...
    let Student = reactive([
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { id: 3, name: "Charlie" }
    ]) // ...
    </script>

    此时点击按钮即可修改第一个学生的名字为 Alex

b. 使用 ref 创建

  1. 修改上述使用 reactive 的组件内容

    <template>
    <h2>Name: {{ Teacher.name }}</h2>
    <h2>Age: {{ Teacher.age }}</h2>
    <p><button @click="changeTeacherAge">Change Teacher Age</button></p>
    <hr />
    <ul>
    <li v-for="student in Student" :key="student.id">{{ student.name }}</li>
    </ul>
    <button @click="changeFirstStudentName">Change First Student Name</button>
    </template> <script setup lang="ts" name="Person">
    let Teacher = { name: "John", age: 18 }
    let Student = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { id: 3, name: "Charlie" }
    ] function changeTeacherAge() {}
    function changeFirstStudentName() {}
    </script>
  2. 引入 ref 并为需要成为响应式的对象使用

    <script setup lang="ts" name="Person">
    import {ref} from 'vue' let Teacher = ref({ name: "John", age: 18 })
    let Student = ref([
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { id: 3, name: "Charlie" }
    ]) // ...
    </script>
  3. 修改函数方法

    <script setup lang="ts" name="Person">
    // ... function changeTeacherAge() {
    Teacher.value.age += 1;
    }
    function changeFirstStudentName() {
    Student.value[0].name = "Alex";
    }
    </script>

c. 对比 refreactive

  • 功能上:

    • ref 可以用来定义基本类型数据对象类型数据

      • 在使用 ref 创建响应式对象过程中,ref 底层实际借用了 reactive 的方法
    • reactive 可以用来定义对象类型数据
  • 区别在于:

    • 使用 ref 创建的数据必须使用 .value

      • 解决方法:使用插件 volar 自动添加

        插件设置方法:

        1. 点击左下角齿轮图标(管理),并选择“设置”
        2. 在左侧选项列表中依次选择“扩展”-“Volar”
        3. 将功能“Auto Insert: Dot Value”打勾即可
    • 使用 reactive 重新分配一个新对象会失去响应式(reactive局限性

      • 解决方法:使用 Object.assign 整体替换

        举例:修改 Teacher 对象

        • reactive

          <template>
          <h2>Name: {{ Teacher.name }}</h2>
          <h2>Age: {{ Teacher.age }}</h2>
          <p><button @click="changeTeacher">Change Teacher</button></p>
          </template> <script setup lang="ts" name="Person">
          import {reactive} from 'vue' let Teacher = reactive({ name: "John", age: 18 }) function changeTeacher() {
          Object.assign(Teacher, { name: "Mary", age: 19 })
          }
          </script>
        • ref

          <template>
          <h2>Name: {{ Teacher.name }}</h2>
          <h2>Age: {{ Teacher.age }}</h2>
          <p><button @click="changeTeacher">Change Teacher</button></p>
          </template> <script setup lang="ts" name="Person">
          import {ref} from 'vue' let Teacher = ref({ name: "John", age: 18 }) function changeTeacher() {
          Teacher.value = { name: "Mary", age: 19 }
          }
          </script>
  • 使用原则:

    1. 若需要一个基本类型的响应式数据,则只能选择 ref
    2. 若需要一个对象类型、层级较浅的响应式数据,则选择任何一个都可以
    3. 若需要一个对象类型、层级较的响应式数据,则推荐选择 reactive

(5)toRefstoRef

  • 作用:将一个响应式对象中的每一个属性转换为 ref 对象
  • toRefstoRef 功能相同,toRefs 可以批量转换
  1. 修改 Person.vue

    <template>
    <h2>Name: {{ John.name }}</h2>
    <h2>Age: {{ John.age }}</h2>
    <p><button @click="changeName">Change Name</button></p>
    <p><button @click="changeAge">Change Age</button></p>
    </template> <script setup lang="ts" name="Person">
    import {reactive} from 'vue' let John = reactive({ name: "John", age: 18 }) function changeName() {
    John.name += "~"
    }
    function changeAge() {
    John.age += 1
    }
    </script> <style>
    </style>
  2. 声明两个新变量,并使用 Person 对其进行赋值,之后修改模板

    <template>
    <h2>Name: {{ name }}</h2>
    <h2>Age: {{ age }}</h2>
    <p><button @click="changeName">Change Name</button></p>
    <p><button @click="changeAge">Change Age</button></p>
    </template> <script setup lang="ts" name="Person">
    // ... let John = reactive({ name: "John", age: 18 })
    let { name, age } = John // ...
    </script>

    此时,新变量 nameage 并非响应式,点击按钮无法在页面上修改(实际上已发生修改)

  3. 引入 toRefs,将变量 nameage 变为响应式

    <script setup lang="ts" name="Person">
    import {reactive, toRefs} from 'vue' let John = reactive({ name: "John", age: 18 })
    let { name, age } = toRefs(John) // ...
    </script>
  4. 修改函数方法

    <script setup lang="ts" name="Person">
    // ... function changeName() {
    name.value += "~"
    }
    function changeAge() {
    age.value += 1
    }
    </script>
  5. 引入 toRef 替代 toRefs

    <script setup lang="ts" name="Person">
    import {reactive, toRef} from 'vue' let John = reactive({ name: "John", age: 18 })
    let name = toRef(John, 'name')
    let age = toRef(John, 'age') function changeName() {
    name.value += "~"
    }
    function changeAge() {
    age.value += 1
    }
    </script>

(6)computed

  • computed 是计算属性,具有缓存,当计算方法相同时,computed 会调用缓存,从而优化性能
  1. 修改 Person.vue

    <template>
    <h2>First Name: <input type="text" v-model="firstName" /></h2>
    <h2>Last Name: <input type="text" v-model="lastName" /></h2>
    <h2>Full Name: <span>{{ firstName }} {{ lastName }}</span></h2>
    </template> <script setup lang="ts" name="Person">
    import {ref} from 'vue' let firstName = ref("john")
    let lastName = ref("Smith")
    </script> <style>
    </style>
  2. 引入计算属性 computed

    <script setup lang="ts" name="Person">
    import {ref, computed} from 'vue' // ...
    </script>
  3. 将姓与名的首字母大写,修改模板与控制器

    <template>
    <!-- ... -->
    <h2>Full Name: <span>{{ fullName }}</span></h2>
    </template> <script setup lang="ts" name="Person">
    // ... let fullName = computed(() => {
    return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + " " + lastName.value.slice(0, 1).toUpperCase() + lastName.value.slice(1)
    })
    </script>

    此时的 fullName只读的,无法修改,是一个使用 ref 创建的响应式对象

  4. 修改 fullName,实现可读可写

    <script setup lang="ts" name="Person">
    // ... let fullName = computed({
    get() {
    return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + " " + lastName.value.slice(0, 1).toUpperCase() + lastName.value.slice(1)
    },
    set() {}
    })
    </script>
  5. 在模板中添加按钮,用于修改 fullName,并在控制器中添加相应的函数方法

    <template>
    <!-- ... -->
    <button @click="changeFullName">Change Full Name</button>
    </template> <script setup lang="ts" name="Person">
    // ... function changeFullName() {
    fullName.value = "bob jackson"
    }
    </script>
  6. 修改计算属性中的 set() 方法

    <script setup lang="ts" name="Person">
    // ... let fullName = computed({
    get() {
    return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + " " + lastName.value.slice(0, 1).toUpperCase() + lastName.value.slice(1)
    },
    set(value) {
    const [newFirstName, newLastName] = value.split(" ")
    firstName.value = newFirstName
    lastName.value = newLastName
    }
    }) // ...
    </script>

(7)watch

  • watch 是监视属性
  • 作用:监视数据变化(与 Vue2 中的 watch 作用一致)
  • 特点:Vue3 中的 watch 只监视以下数据:
    1. ref 定义的数据
    2. reactive 定义的数据
    3. 函数返回一个值
    4. 一个包含上述内容的数组

a. 情况一:监视 ref 定义的基本类型数据

直接写数据名即可,监视目标是 value 值的改变

  1. 修改 Person.vue

    <template>
    <h2>Sum: {{ sum }}</h2>
    <button @click="changeSum"> +1 </button>
    </template> <script setup lang="ts" name="Person">
    import {ref} from 'vue' let sum = ref(0) function changeSum() {
    sum.value += 1
    }
    </script> <style>
    </style>
  2. 引入 watch

    <script setup lang="ts" name="Person">
    import {ref, watch} from 'vue' // ...
    </script>
  3. 使用 watch,一般传入两个参数,依次是监视目标相应的回调函数

    <script setup lang="ts" name="Person">
    import {ref, watch} from 'vue' // ... watch(sum, (newValue, oldValue) => {
    console.log("Sum changed from " + oldValue + " to " + newValue)
    })
    </script>
  4. 修改 watch,设置“停止监视”

    <script setup lang="ts" name="Person">
    // ... const stopWatch = watch(sum, (newValue, oldValue) => {
    console.log("Sum changed from " + oldValue + " to " + newValue)
    if(newValue >= 10) {
    stopWatch()
    }
    })
    </script>

b. 情况二:监视 ref 定义的对象类型数据

  • 直接写数据名即可,监视目标是对象的地址值的改变
  • 如需监视对象内部的数据,需要手动开启深度监视
  • 若修改的是 ref 定义的对象中的属性,则 newValuenewValue 都是新值,因为它们是同一个对象
  • 若修改整个 ref 定义的对象,则 newValue 是新值,oldValue 是旧值,因为它们不是同一个对象
  1. 修改 Person.vue

    <template>
    <h2>Name: {{ person.name }}</h2>
    <h2>Age: {{ person.age }}</h2>
    <p><button @click="changeName">Change Name</button></p>
    <p><button @click="changeAge">Change Age</button></p>
    <hr />
    <p><button @click="changeAll">Change All</button></p>
    </template> <script setup lang="ts" name="Person">
    import { ref, watch } from "vue" let person = ref({
    name: "John",
    age: 18
    }) function changeName() {
    person.value.name += '~'
    }
    function changeAge() {
    person.value.age += 1
    }
    function changeAll() {
    person.value = {
    name: "Mary",
    age: 19
    }
    }
    </script> <style>
    </style>
  2. 使用 watch 监视整个对象的地址值变化

    <script setup lang="ts" name="Person">
    // ... watch(person, (newValue, oldValue) => {
    console.log(newValue, oldValue)
    })
    </script>

    此时,只有点击“Change All” 按钮才会触发监视,newValueoldValue 不同

  3. 手动开启深度监视,监视对象内部的数据

    <script setup lang="ts" name="Person">
    // ... watch(
    person,
    (newValue, oldValue) => {
    console.log(newValue, oldValue)
    },
    {
    deep: true,
    }
    )
    </script>

    此时,点击“Change Name”或“Change Age”也能触发监视,newValueoldValue 相同,但是点击“Change All”时,newValueoldValue 依旧不同

c. 情况三:监视 reactive 定义的对象类型数据

该情况下,默认开启了深度监视且无法关闭

  1. 修改 Person.vue

    <template>
    <h2>Name: {{ person.name }}</h2>
    <h2>Age: {{ person.age }}</h2>
    <p><button @click="changeName">Change Name</button></p>
    <p><button @click="changeAge">Change Age</button></p>
    <hr />
    <p><button @click="changeAll">Change All</button></p>
    </template> <script setup lang="ts" name="Person">
    import { reactive, watch } from "vue" let person = reactive({
    name: "John",
    age: 18,
    }) function changeName() {
    person.name += "~"
    }
    function changeAge() {
    person.age += 1
    }
    function changeAll() {
    Object.assign(person, {
    name: "Mary",
    age: 19,
    })
    }
    </script> <style>
    </style>
  2. 使用 watch 监视对象

    <script setup lang="ts" name="Person">
    // ... watch(person, (newValue, oldValue) => {
    console.log(newValue, oldValue)
    })
    </script>

    对于使用 reactive 创建的对象,在使用 Object.assign() 时,仅修改了对象里的内容(覆盖原来的内容),并非创建了新对象,故无法监视对象地址值的变化(因为没有变化)

d. 情况四:监视 refreactive 定义的对象类型数据中的某个属性

若该属性值不是对象类型,则需写成函数形式

若该属性值是对象类型,则建议写成函数形式

  1. 修改 Person.vue

    <template>
    <h2>Name: {{ person.name }}</h2>
    <h2>Age: {{ person.age }}</h2>
    <h2>Nickname: {{ person.nickname.n1 }}/{{ person.nickname.n2 }}</h2>
    <p><button @click="changeName">Change Name</button></p>
    <p><button @click="changeAge">Change Age</button></p>
    <hr />
    <p><button @click="changeNickname1">Change Nickname 1</button></p>
    <p><button @click="changeNickname2">Change Nickname 2</button></p>
    <p><button @click="changeNickname">Change Nickname</button></p>
    </template> <script setup lang="ts" name="Person">
    import { reactive, watch } from "vue" let person = reactive({
    name: "John",
    age: 18,
    nickname: {
    n1: "J",
    n2: "Jack"
    }
    }) function changeName() {
    person.name += "~"
    }
    function changeAge() {
    person.age += 1
    }
    function changeNickname1() {
    person.nickname.n1 = "Big J"
    }
    function changeNickname2() {
    person.nickname.n2 = "Joker"
    }
    function changeNickname() {
    person.nickname = {
    n1: "Joker",
    n2: "Big J"
    }
    }
    </script> <style>
    </style>
  2. 使用 watch 监视全部的变化

    <script setup lang="ts" name="Person">
    // ... watch(person, (newValue, oldValue) => {
    console.log(newValue, oldValue)
    })
    </script>
  3. 修改 watch,设置监视基本类型数据 person.name

    <script setup lang="ts" name="Person">
    // ... watch(
    () => {
    return person.name;
    },
    (newValue, oldValue) => {
    console.log(newValue, oldValue);
    }
    })
    </script>
  4. 修改 watch,设置监视对象类型数据 person.nickname

    <script setup lang="ts" name="Person">
    // ... watch(
    () => person.nickname,
    (newValue, oldValue) => {
    console.log(newValue, oldValue);
    },
    { deep: true }
    })
    </script>
    • 以下写法仅能监视对象类型内部属性数据变化

      <script setup lang="ts" name="Person">
      // ... watch(
      person.nickname,
      (newValue, oldValue) => {
      console.log(newValue, oldValue);
      }
      })
      </script>
    • 以下写法仅能监视对象类型整体地址值变化

      <script setup lang="ts" name="Person">
      // ... watch(
      () => person.nickname,
      (newValue, oldValue) => {
      console.log(newValue, oldValue);
      }
      })
      </script>

e. 情况五:监视多个数据

  1. 修改 Person.vue

    <template>
    <h2>Name: {{ person.name }}</h2>
    <h2>Age: {{ person.age }}</h2>
    <h2>Nickname: {{ person.nickname.n1 }}/{{ person.nickname.n2 }}</h2>
    <p><button @click="changeName">Change Name</button></p>
    <p><button @click="changeAge">Change Age</button></p>
    <hr />
    <p><button @click="changeNickname1">Change Nickname 1</button></p>
    <p><button @click="changeNickname2">Change Nickname 2</button></p>
    <p><button @click="changeNickname">Change Nickname</button></p>
    </template> <script setup lang="ts" name="Person">
    import { reactive, watch } from "vue" let person = reactive({
    name: "John",
    age: 18,
    nickname: {
    n1: "J",
    n2: "Jack"
    }
    }) function changeName() {
    person.name += "~"
    }
    function changeAge() {
    person.age += 1
    }
    function changeNickname1() {
    person.nickname.n1 = "Big J"
    }
    function changeNickname2() {
    person.nickname.n2 = "Joker"
    }
    function changeNickname() {
    person.nickname = {
    n1: "Joker",
    n2: "Big J"
    }
    }
    </script> <style>
    </style>
  2. 使用 watch 监视 person.nameperson.nickname.n1

    <script setup lang="ts" name="Person">
    // ... watch(
    [() => person.name, () => person.nickname.n1],
    (newValue, oldValue) => {
    console.log(newValue, oldValue);
    },
    { deep: true }
    )
    </script>

(8)watchEffect

  • 作用:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数
  • watchEffectwatch 对比,两者都能监视响应式数据,但是监视数据变化的方式不同
    • watch 需要指明监视的数据
    • watchEffect 则不用指明,函数中用到哪些属性,就监视哪些属性
  1. 修改 Person.vue

    <template>
    <h2>Sum1: {{ sum1 }}</h2>
    <h2>Sum2: {{ sum2 }}</h2>
    <p><button @click="changeSum1">Sum1 +1</button></p>
    <p><button @click="changeSum2">Sum2 +3</button></p>
    </template> <script setup lang="ts" name="Person">
    import { ref, watch } from 'vue' let sum1 = ref(0)
    let sum2 = ref(0) function changeSum1() {
    sum1.value += 1
    }
    function changeSum2() {
    sum2.value += 3
    }
    </script> <style>
    </style>
  2. 使用 watch 监视 sum1sum2,获取最新的值

    <script setup lang="ts" name="Person">
    // ... watch([sum1, sum2], (value) => {
    console.log(value)
    })
    </script>
  3. sum1sum2 进行条件判断

    <script setup lang="ts" name="Person">
    // ... watch([sum1, sum2], (value) => {
    let [newSum1, newSum2] = value
    if(newSum1 >= 10 || newSum2 >= 30) {
    console.log('WARNING: Sum1 or Sum2 is too high')
    }
    })
    </script>

    此时,仅对 sum1sum2 进行监视,当需要监视的目标更多时,建议使用 watchEffect

  4. 引入 watchEffect

    <script setup lang="ts" name="Person">
    import { ref, watch, watchEffect } from 'vue' // ...
    </script>
  5. 使用 watchEffect

    <script setup lang="ts" name="Person">
    // ... watchEffect(() => {
    if(sum1.value >= 10 || sum2.value >= 30) {
    console.log('WARNING: Sum1 or Sum2 is too high')
    }
    })
    </script>

(9)标签的 ref 属性

  • 作用:用于注册模板引用

    • 用在普通 DOM 标签上,获取到 DOM 节点
    • 用在组件标签上,获取到组件实例对象
  1. 修改 Person.vue

    <template>
    <h2>Vue 3</h2>
    <button @click="showH2">Show Element H2</button>
    </template> <script setup lang="ts" name="Person">
    function showH2() {
    console.log()
    }
    </script> <style>
    </style>
  2. 引入标签的 ref 属性

    <template>
    <h2 ref="title">Vue 3</h2>
    <button @click="showH2">Show Element H2</button>
    </template> <script setup lang="ts" name="Person">
    import { ref } from 'vue' // ...
    </script>
  3. 创建一个容器,用于存储 ref 标记的内容

    容器名称与标签中 ref 属性值相同

    <script setup lang="ts" name="Person">
    import { ref } from 'vue' let title = ref() // ...
    </script>
  4. 修改函数方法 showH2(),输出标签

    <script setup lang="ts" name="Person">
    import { ref } from 'vue' let title = ref() function showH2() {
    console.log(title.value)
    }
    </script>

    此时,ref 属性用在普通 DOM 标签上,获取到 DOM 节点

  5. 在 App.vue 中,对组件 Person 设置 ref 属性

    <template>
    <Person ref="person" />
    <hr />
    <button @click="showPerson">Show Element Person</button>
    </template> <script setup lang="ts" name="App">
    import Person from './components/Person.vue';
    import { ref } from 'vue' let person = ref() function showPerson() {
    console.log(person.value)
    }
    </script> <style scoped>
    </style>

    此时,ref 属性用在组件标签上,获取到组件实例对象

  6. 在 Person.vue 中引入 defineExpose 实现父子组件通信

    <script setup lang="ts" name="Person">
    import { ref, defineExpose } from 'vue' let title = ref()
    let number = ref(12345) function showH2() {
    console.log(title.value)
    } defineExpose({ title, number })
    </script>

    此时,再次点击按钮“Show Element Person”便可在控制台中看到来自 Person.vue 组件中的 titlenumber

  7. 重置 App.vue

    <template>
    <Person />
    </template> <script setup lang="ts" name="App">
    import Person from './components/Person.vue'
    </script> <style scoped>
    </style>

(9.5)TypeScript 回顾

TypeScript 基本概述:学习 TypeScript | 稀土掘金-SRIGT

在 Vue3 项目中,TS 接口等位于 ~/src/types/index.ts

  1. 定义接口,用于限制对象的具体属性

    interface IPerson {
    id: string,
    name: string,
    age: number
    }
  2. 暴露接口

    暴露接口有三种方法:默认暴露、分别暴露、统一暴露,以下采用分别暴露方法

    export interface IPerson {
    id: string,
    name: string,
    age: number
    }
  3. 修改 Person.vue,声明新变量,引入接口对新变量进行限制

    <template>
    </template> <script setup lang="ts" name="Person">
    import { type IPerson } from '@/types' let person:IPerson = {
    id: "dpb7e82nlh",
    name: "John",
    age: 18
    }
    </script> <style>
    </style>

    此时,仅声明了一个变量,对于同类型的数组型变量,需要使用泛型

  4. 修改 Perosn.vue,声明一个数组,使用泛型

    <script setup lang="ts" name="Person">
    import { type IPerson } from '@/types' let personList:Array<IPerson> = [
    { id: "dpb7e82nlh", name: "John", age: 18 },
    { id: "u55dyu86gh", name: "Mary", age: 19 },
    { id: "ad3d882dse", name: "Niko", age: 17 }
    ]
    </script>
  5. 修改 index.ts,定义一个自定义类型,简化对数组限制的使用

    export interface IPerson {
    id: string,
    name: string,
    age: number,
    } // 自定义类型
    export type Persons = Array<IPerson>

    或:export type Persons = IPerson[]

  6. 修改 Person.vue

    <script setup lang="ts" name="Person">
    import { type Persons } from '@/types' let personList:Persons = [
    { id: "dpb7e82nlh", name: "John", age: 18 },
    { id: "u55dyu86gh", name: "Mary", age: 19 },
    { id: "ad3d882dse", name: "Niko", age: 17 }
    ]
    </script>

(10)props

  1. 修改 App.vue

    <template>
    <Person />
    </template> <script setup lang="ts" name="App">
    import Person from './components/Person.vue' import { reactive } from 'vue' let personList = reactive([
    { id: "dpb7e82nlh", name: "John", age: 18 },
    { id: "u55dyu86gh", name: "Mary", age: 19 },
    { id: "ad3d882dse", name: "Niko", age: 17 }
    ])
    </script> <style scoped>
    </style>
  2. 引入接口并使用

    <script setup lang="ts" name="App">
    // ...
    import { type Persons } from '@/types'; let personList = reactive<Persons>([
    { id: "dpb7e82nlh", name: "John", age: 18 },
    { id: "u55dyu86gh", name: "Mary", age: 19 },
    { id: "ad3d882dse", name: "Niko", age: 17 }
    ])
    </script>
  3. 修改父组件 App.vue 的模板内容,向子组件 Person.vue 发送数据

    <template>
    <Person note="This is a note" :list="personList" />
    </template>
  4. 修改子组件 Person.vue,从父组件 App.vue 中接收数据

    <template>
    <h2>{{ note }}</h2>
    <ul>
    <li v-for="person in list" :key="person.id">{{ person.name }}-{{ person.age }}</li>
    </ul>
    </template> <script setup lang="ts" name="Person">
    import { defineProps } from 'vue' // 只接收
    // defineProps(['note', 'list']) // 接收并保存
    let personList = defineProps(['note', 'list'])
    console.log(personList)
    console.log(personList.note)
    </script> <style>
    </style>
    • 接收限制类型

      <script setup lang="ts" name="Person">
      import { defineProps } from 'vue'
      import type { Persons } from '@/types' defineProps<{ list:Persons }>()
      </script>
    • 接收限制类型并指定默认值

      <script setup lang="ts" name="Person">
      import { defineProps } from 'vue'
      import type { Persons } from '@/types' withDefaults(defineProps<{ list?: Persons }>(), {
      list: () => [{ id: "dpb7e82nlh", name: "John", age: 18 }],
      })
      </script>
  5. 重置 App.vue

    <template>
    <Person />
    </template> <script setup lang="ts" name="App">
    import Person from './components/Person.vue'
    </script> <style scoped>
    </style>

(11)生命周期

  • 组件的生命周期包括:创建、挂载、更新、销毁/卸载
  • 组件在特定的生命周期需要调用特定的生命周期钩子(生命周期函数)

a. Vue2 的生命周期

  • Vue2 生命周期包括八个生命周期钩子

    生命周期 生命周期钩子
    创建 创建前 beforeCreate
    创建完成后 created
    挂载 挂载前 beforeMount
    挂载完成后 mounted
    更新 更新前 beforeUpdate
    更新完成后 updated
    销毁 销毁前 beforeDestroy
    销毁完成后 destroyed
  1. 使用命令 vue create vue2_testvue init webpack vue2_test 创建一个 Vue2 项目

  2. 重置 ~/src/App.vue

    <template>
    </template> <script>
    export default {
    name: 'App',
    }
    </script> <style>
    </style>
  3. 在 ~/src/components 中新建 Person.vue

    <template>
    <div>
    <h2>Sum: {{ sum }}</h2>
    <button @click="changeSum">+1</button>
    </div>
    </template> <script>
    export default {
    // eslint-disable-next-line
    name: 'Person',
    data() {
    return {
    sum: 0
    }
    },
    methods: {
    changeSum() {
    this.sum += 1
    }
    }
    }
    </script> <style scoped>
    </style>
  4. 在 App.vue 引入 Person.vue

    <template>
    <PersonVue />
    </template> <script>
    import PersonVue from './components/Person.vue' export default {
    name: 'App',
    components: {
    PersonVue
    }
    }
    </script> <style>
    </style>
  5. 使用生命周期钩子

    <script>
    export default {
    // eslint-disable-next-line
    name: 'Person',
    data() {
    // ...
    },
    methods: {
    // ...
    }, // 创建前
    beforeCreate() {
    console.log("beforeCreate")
    },
    // 创建完成后
    created() {
    console.log("created")
    }, // 挂载前
    beforeMount() {
    console.log("beforeMount")
    },
    // 挂载完成后
    mounted() {
    console.log("mounted")
    }, // 更新前
    beforeUpdate() {
    console.log("beforeUpdate")
    },
    // 更新完成后
    updated() {
    console.log("updated")
    }, // 销毁前
    beforeDestroy() {
    console.log("beforeDestroy")
    },
    // 销毁完成后
    destroyed() {
    console.log("destroyed")
    }
    }
    </script>
  6. 使用命令 npm run serve 启动项目,在开发者工具中查看生命周期过程

b. Vue3 的生命周期

  • Vue3 生命周期包括七个生命周期钩子

    生命周期 生命周期钩子
    创建 setup
    挂载 挂载前 onBeforeMount
    挂载完成后 onMounted
    更新 更新前 onBeforeUpdate
    更新完成后 onUpdated
    卸载 卸载前 onBeforeUnmount
    卸载完成后 onUnmounted
  1. 在之前的 Vue3 项目中,修改 Person.vue

    <template>
    <h2>Sum: {{ sum }}</h2>
    <button @click="changeSum">+1</button>
    </template> <script setup lang="ts" name="Person">
    import { ref } from 'vue' let sum = ref(0) function changeSum() {
    sum.value += 1
    }
    </script> <style>
    </style>
  2. 引入并使用生命周期钩子

    <script setup lang="ts" name="Person">
    import {
    onBeforeMount,
    onBeforeUnmount,
    onBeforeUpdate,
    onMounted,
    onUnmounted,
    onUpdated,
    ref
    } from 'vue' // ... // 创建
    console.log("setup") // 挂载前
    onBeforeMount(() => {
    console.log("onBeforeMount")
    })
    // 挂载完成后
    onMounted(() => {
    console.log("onMounted")
    }) // 更新前
    onBeforeUpdate(() => {
    console.log("onBeforeUpdate")
    })
    // 更新完成后
    onUpdated(() => {
    console.log("onUpdated")
    }) // 卸载前
    onBeforeUnmount(() => {
    console.log("onBeforeUnmount")
    })
    // 卸载完成后
    onUnmounted(() => {
    console.log("onUnmounted")
    })
    </script>

(12)自定义Hooks

使用命令 npm install -S axios 安装 Axios,用于网络请求

  1. 修改 Person.vue

    <template>
    <h2>Sum: {{ sum }}</h2>
    <button @click="changeSum">+1</button>
    <hr />
    <img
    v-for="(img, index) in imgList"
    :src="img"
    :key="index"
    style="height: 100px"
    />
    <p><button @click="changeImg">Next</button></p>
    </template> <script setup lang="ts" name="Person">
    import { reactive, ref } from "vue"; let sum = ref(0);
    let imgList = reactive([""]); function changeSum() {
    sum.value += 1;
    }
    function changeImg() {}
    </script> <style>
    </style>
  2. 引入 Axios

    <script setup lang="ts" name="Person">
    import axios from 'axios'
    // ...
    </script>
  3. 使用 Axios 获取图片地址

    <script setup lang="ts" name="Person">
    import axios from 'axios'
    // ...
    async function changeImg() {
    try {
    let result = await axios.get(
    "https://dog.ceo/api/breed/pembroke/images/random"
    );
    imgList.push(result.data.message);
    } catch (error) {
    alert("Error! Please try again.");
    console.log(error);
    }
    }
    </script>

    此时,多个功能(求和、请求图片)同时在组件中互相交织,可以使用 Hooks 重新组织代码

  4. 在 src 目录下新建目录 hooks,其中分别新建 useSum.ts、useImg.ts

    • useSum.ts

      import { ref } from 'vue'
      
      export default function () {
      let sum = ref(0) function changeSum() {
      sum.value += 1
      } return {
      sum,
      changeSum
      }
      }
    • useImg.ts

      import axios from 'axios'
      import { reactive } from 'vue' export default function () {
      let imgList = reactive([""]) async function changeImg() {
      try {
      let result = await axios.get(
      "https://dog.ceo/api/breed/pembroke/images/random"
      )
      imgList.push(result.data.message)
      } catch (error) {
      alert("Error! Please try again.")
      console.log(error)
      }
      } return {
      imgList,
      changeImg
      }
      }
  5. 在 Person.vue 中引入并使用两个 Hooks

    <script setup lang="ts" name="Person">
    import useSum from '@/hooks/useSum'
    import useImg from '@/hooks/useImg' const { sum, changeSum } = useSum()
    const { imgList, changeImg } = useImg()
    </script>

0x03 路由

(1)概述

  • 此处的路由是指前后端交互的路由
  • 路由(route)就是一组键值的对应关系
  • 多个路由需要经过路由器(router)的管理
  • 路由组件通常存放在 pages 或 views 文件夹,一般组件通常存放在 components 文件夹

(2)路由切换

  1. 重置 ~/src 目录结构

    graph TB
    src-->components & pages & App.vue & main.ts
    components-->Header.vue
    pages-->Home.vue & Blog.vue & About.vue
  2. 修改 App.vue(样式可忽略)

    <template>
    <div class="app">
    <Header />
    <div class="navigation">
    <a href="/home" class="active">Home</a>
    <a href="/blog">Blog</a>
    <a href="/about">About</a>
    </div>
    <div class="main-content">
    Content
    </div>
    </div>
    </template> <script setup lang="ts" name="App">
    import Header from './components/Header.vue'
    </script> <style scoped>
    .app {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    }
    .navigation {
    width: 15%;
    height: 506px;
    float: left;
    background: #03deff;
    }
    .navigation a {
    display: block;
    text-decoration-line: none;
    color: black;
    text-align: center;
    font-size: 28px;
    padding-top: 10px;
    padding-bottom: 10px;
    border-bottom: 3px solid #fff;
    }
    .navigation a.active {
    background: rgb(1, 120, 144);
    color: white;
    }
    .main-content {
    display: inline;
    width: 80%;
    height: 500px;
    float: left;
    margin-left: 10px;
    font-size: 26px;
    border: 3px solid #000;
    }
    </style>
  3. 修改 Header.vue、Home.vue、Blog.vue、About.vue

    • Header.vue

      <template>
      <h2 class="title">Route Test</h2>
      </template> <script setup lang="ts" name="Header">
      </script> <style scope>
      .title {
      width: 100%;
      text-align: center;
      }
      </style>
    • Home.vue、Blog.vue、About.vue

      <template>
      <h2>Home</h2>
      <!-- <h2>Blog</h2> -->
      <!-- <h2>About</h2> -->
      </template> <script setup lang="ts">
      </script> <style scope>
      </style>
  4. 创建路由器

    1. 使用命令 npm install vue-router -S 安装路由器

    2. ~/src 目录下新建目录 router,其中新建 index.ts,用于创建一个路由器并暴露

    3. 引入 createRoutercreateWebHistory

      import {
      createRouter,
      createWebHistory
      } from 'vue-router'

      其中 createWebHistory 是路由器的工作模式,在本章第(3)小节有详细介绍

    4. 引入子组件

      // ...
      
      import Home from '@/pages/Home.vue'
      import Blog from '@/pages/Blog.vue'
      import About from '@/pages/About.vue'
    5. 创建路由器

      // ...
      
      const router = createRouter({
      history: createWebHistory(),
      routes: []
      })
  5. 制定路由规则

    // ...
    routes: [
    {
    path: '/home',
    component: Home
    },
    {
    path: '/blog',
    component: Blog
    },
    {
    path: '/about',
    component: About
    }
    ]
    // ...
  6. 暴露路由

    // ...
    
    export default router
  7. 修改 main.ts,引入并使用路由器

    // ...
    import router from './router' createApp(App).use(router).mount('#app')
  8. 修改 App.vue,引入路由器视图,修改模板中的超链接

    <template>
    <div class="app">
    <Header />
    <div class="navigation">
    <RouterLink to="/home" active-class="active">Home</RouterLink>
    <RouterLink to="/blog" active-class="active">Blog</RouterLink>
    <RouterLink to="/about" active-class="active">About</RouterLink>
    </div>
    <div class="main-content">
    <RouterView></RouterView>
    </div>
    </div>
    </template> <script setup lang="ts" name="App">
    import Header from './components/Header.vue'
    import { RouterLink, RouterView } from 'vue-router'
    </script> <style scoped>
    /* ... */
    </style>
    • 此时,点击导航后,“消失”的路由组件默认是被卸载的,需要的时候再重新挂载

      • 可以在组件中使用生命周期钩子验证
    • <RouterLink> 中的 to 属性有两种写法
      1. to="/home"
      2. :to="{path:'/home'}"
        • 此写法的优点在本章第()小节介绍

(3)路由器的工作模式

a. history 模式

  • 优点:URL 更美观,更接近于传统网站的 URL

  • 缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有 404 报错

  • 使用方法:

    • Vue2:mode: 'history'

    • Vue3:

      // router/index.ts
      import {
      createRouter,
      createWebHistory
      } from 'vue-router' const router = createRouter({
      history: createWebHistory(),
      // ...
      })
    • React:BrowserRouter

b. hash 模式

  • 优点:兼容性更好,不需要服务端处理路径

  • 缺点:URL 中带有 # 号,在 SEO 优化方面相对较差

  • 使用方法:

    • Vue3:

      // router/index.ts
      import {
      createRouter,
      createWebHashHistory
      } from 'vue-router' const router = createRouter({
      history: createWebHashHistory(),
      // ...
      })

(4)命名路由

  • 作用:简化路由跳转与传参
  1. 在 router/index.ts 中为路由命名

    // ...
    routes: [
    {
    name: 'zy',
    path: '/home',
    component: Home
    },
    {
    name: 'bk',
    path: '/blog',
    component: Blog
    },
    {
    name: 'gy',
    path: '/about',
    component: About
    }
    ]
    // ...
  2. 修改 App.vue,使用命名路由的方法进行跳转

    <template>
    <!-- ... -->
    <div class="navigation">
    <RouterLink :to="{ name: 'zy' }" active-class="active">Home</RouterLink>
    <RouterLink to="/blog" active-class="active">Blog</RouterLink>
    <RouterLink to="/about" active-class="active">About</RouterLink>
    </div>
    <!-- ... -->
    </template>

    此时,共有两类方式三种方法实现路由跳转:

    字符串写法 to="/home"
    对象写法 命名跳转 :to="{ name: 'zy' }"
    路径跳转 :to="{ path: '/home' }"

(5)嵌套路由

在博客页面实现路由的嵌套,实现博客内容根据选择进行动态展示

  1. 在 pages 目录下新建 Detail.vue

    <template>
    <ul>
    <li>id: id</li>
    <li>title: title</li>
    <li>content: content</li>
    </ul>
    </template> <script setup lang="ts" name="detail">
    </script> <style scope>
    ul {
    list-style: none;
    padding-left: 20px;
    }
    ul>li {
    line-height: 30px;
    }
    </style>
  2. 修改 router/index.ts,引入 Detail.vue

    // ...
    import Detail from '@/pages/Detail.vue' const router = createRouter({
    // ...
    routes: [
    // ...
    {
    path: '/blog',
    component: Blog,
    children: [
    {
    path: 'detail',
    component: Detail
    }
    ]
    },
    // ...
    ]
    }) export default router
  3. 修改 Blog.vue,添加博客导航列表、博客内容展示区、相关数据以及样式

    <template>
    <h2>Blog</h2>
    <ul>
    <li v-for="blog in blogList" :key="blog.id">
    <RouterLink to="/blog/detail">{{ blog.title }}</RouterLink>
    </li>
    </ul>
    <div class="blog-content">
    <RouterView></RouterView>
    </div>
    </template> <script setup lang="ts" name="Blog">
    import { reactive } from 'vue' const blogList = reactive([
    { id: 'fhi27df4sda', title: 'Blog01', content: 'Content01' },
    { id: 'opdcd2871cb', title: 'Blog02', content: 'Content02' },
    { id: 'adi267f4hp5', title: 'Blog03', content: 'Content03' }
    ])
    </script> <style scope>
    ul {
    float: left;
    }
    ul li {
    display: block;
    }
    ul li a {
    text-decoration-line: none;
    }
    .blog-content {
    float: left;
    margin-left: 200px;
    width: 70%;
    height: 300px;
    border: 3px solid black;
    }
    </style>

(6)路由传参

  • 在 Vue 中,路由用两种参数:query 和 params

a. query

  1. 修改 Blog.vue,发送参数

    修改 to 属性为 :to,使用模板字符串,在路由后添加 ?,之后使用格式 key1=val1&key2=val2 的方式传递参数

    <template>
    <!-- ... -->
    <li v-for="blog in blogList" :key="blog.id">
    <RouterLink :to="`/blog/detail?id=${blog.id}&title=${blog.title}&content=${blog.content}`">
    {{ blog.title }}
    </RouterLink>
    </li>
    <!-- ... -->
    </template>
  2. 简化传参

    <template>
    <!-- ... -->
    <RouterLink
    :to="{
    path: '/blog/detail',
    query: {
    id: blog.id,
    title: blog.title,
    content: blog.content
    }
    }"
    >
    {{ blog.title }}
    </RouterLink>
    <!-- ... -->
    </template>
  3. 修改 Detail.vue,接收参数

    使用 Hooks useRoute 接收

    <template>
    <ul>
    <li>id: {{ route.query.id }}</li>
    <li>title: {{ route.query.title }}</li>
    <li>content: {{ route.query.content }}</li>
    </ul>
    </template> <script setup lang="ts" name="detail">
    import { useRoute } from 'vue-router'
    const route = useRoute()
    </script>
  4. 简化数据展示

    <template>
    <ul>
    <li>id: {{ query.id }}</li>
    <li>title: {{ query.title }}</li>
    <li>content: {{ query.content }}</li>
    </ul>
    </template> <script setup lang="ts" name="detail">
    import { toRefs } from 'vue'
    import { useRoute } from 'vue-router'
    const route = useRoute()
    let { query } = toRefs(route)
    </script>

    此时,模板中仍旧使用了很多 query.xxx 的语法,为省略 query.,本章第(7)小节有相关处理方法

b. params

  1. 修改 router/index.ts

    // ...
    children: [
    {
    path: 'detail/:id/:title/:content',
    component: Detail
    }
    ]
    // ...

    可以在相关参数后添加 ? 设置参数的必要性,如:path: 'detail/:id/:title/:content?'

  2. 修改 Blog.vue,发送参数

    <template>
    <!-- ... -->
    <RouterLink :to="`/blog/detail/${blog.id}/${blog.title}/${blog.content}`">
    {{ blog.title }}
    </RouterLink>
    <!-- ... -->
    </template>
  3. 简化传参

    1. 修改 router/index.ts,为 /detail 路由命名

      // ...
      children: [
      {
      name: 'detail',
      path: 'detail/:id/:title/:content',
      component: Detail
      }
      ]
      // ...
    2. 修改 Blog.vue

      <template>
      <!-- ... -->
      <RouterLink
      :to="{
      name: 'detail',
      params: {
      id: blog.id,
      title: blog.title,
      content: blog.content
      }
      }"
      >
      {{ blog.title }}
      </RouterLink>
      <!-- ... -->
      </template>
  4. 修改 Detail.vue,接收参数

    <template>
    <ul>
    <li>id: {{ route.params.id }}</li>
    <li>title: {{ route.params.title }}</li>
    <li>content: {{ route.params.content }}</li>
    </ul>
    </template> <script setup lang="ts" name="detail">
    import { useRoute } from 'vue-router'
    const route = useRoute()
    </script>
  5. 简化数据展示

    <template>
    <ul>
    <li>id: {{ params.id }}</li>
    <li>title: {{ params.title }}</li>
    <li>content: {{ params.content }}</li>
    </ul>
    </template> <script setup lang="ts" name="detail">
    import { toRefs } from 'vue'
    import { useRoute } from 'vue-router'
    const route = useRoute()
    let { params } = toRefs(route)
    </script>

(7)路由的 props 配置

  • 作用:让路由组件更方便地接收参数
  • 原理:将路由参数作为 props 传递给组件

a. 对象写法

  1. 修改 Blog.vue

    <template>
    <!-- ... -->
    <RouterLink
    :to="{
    name: 'detail',
    query: {
    id: blog.id,
    title: blog.title,
    content: blog.content
    }
    }"
    >
    {{ blog.title }}
    </RouterLink>
    <!-- ... -->
    </template>
  2. 修改 router/index.ts

    {
    name: 'detail',
    path: 'detail',
    component: Detail,
    props(route) {
    return route.query
    }
    }

    此时相当于将 <Detail /> 修改为 <Detail id=xxx title=xxx content=xxx />,可以按照第二章第(10)小节的方法将参数从 props 中取出使用

  3. 修改 Detail.vue

    <template>
    <ul>
    <li>id: {{ id }}</li>
    <li>title: {{ title }}</li>
    <li>content: {{ content }}</li>
    </ul>
    </template> <script setup lang="ts" name="detail">
    defineProps(['id', 'title', 'content'])
    </script>

b. 布尔值写法

  1. 修改 Blog.vue

    <template>
    <!-- ... -->
    <RouterLink
    :to="{
    name: 'detail',
    params: {
    id: blog.id,
    title: blog.title,
    content: blog.content
    }
    }"
    >
    {{ blog.title }}
    </RouterLink>
    <!-- ... -->
    </template>
  2. 修改 router/index.ts

    // ...
    {
    name: 'detail',
    path: 'detail/:id/:title/:content',
    component: Detail,
    props: true
    }
    // ...
  3. 修改 Detail.vue

c. 对象写法

不常用

  1. 修改 router/index.ts

    // ...
    {
    name: 'detail',
    path: 'detail/:id/:title/:content',
    component: Detail,
    props: {
    id: xxx,
    title: yyy,
    content: zzz
    }
    }
    // ...
  2. 修改 Detail.vue

(8)replace

  • 默认情况下,采用 push 模式,即记录浏览先后顺序,允许前进和回退

  • replace 模式不允许前进和回退

  • 修改 App.vue,在 RouterLink 标签中添加 replace 属性

    <template>
    <!-- ... -->
    <RouterLink replace to="/home" active-class="active">Home</RouterLink>
    <RouterLink replace to="/blog" active-class="active">Blog</RouterLink>
    <RouterLink replace to="/about" active-class="active">About</RouterLink>
    <!-- ... -->
    </template>

(9)编程式导航

  • RouterLink 标签的本质是 a 标签

  • 编程式导航的目的是脱离 RouterLink 标签,实现路由跳转

    • 修改 Home.vue,实现进入该页面 3 秒后,以 push 的模式跳转到 /blog

      <script setup lang="ts">
      import { onMounted } from 'vue'
      import { useRouter } from 'vue-router' const router = useRouter() onMounted(() => {
      setTimeout(() => {
      router.push('/blog')
      }, 3000)
      })
      </script>
  1. 重置 Home.vue

  2. 修改 Blog.vue

    <template>
    <h2>Blog</h2>
    <ul>
    <li v-for="blog in blogList" :key="blog.id">
    <button @click="showDetail(blog)">More</button>
    <!-- ... -->
    </li>
    <!-- ... -->
    </template> <script setup lang="ts" name="Blog">
    import { useRouter } from "vue-router"; // ... const router = useRouter(); interface IBlog {
    id: string;
    title: string;
    content: string;
    } function showDetail(blog: IBlog) {
    router.push({
    name: "detail",
    query: {
    id: blog.id,
    title: blog.title,
    content: blog.content,
    },
    });
    }
    </script>

(10)重定向

0x04 pinia

(1)概述

  • pinia 是 Vue3 中的集中式状态管理工具

    • 类似的工具有:redux(React)、vuex(Vue2)等
    • 集中式:将所有需要管理的数据集中存放在一个容器中,相对的称为分布式
  • pinia 官网

(2)准备

  1. 重置 ~/src 目录结构

    graph TB
    src-->components & App.vue & main.ts
    components-->Count.vue & Text.vue
  2. Count.vue

    <template>
    <div class="count">
    <h2>Sum: {{ sum }}</h2>
    <select v-model.number="number">
    <option value="1" selected>1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    </select>
    <button @click="add">Add</button>
    <button @click="sub">Sub</button>
    </div>
    </template> <script setup lang="ts" name="Count">
    import { ref } from 'vue' let sum = ref(0)
    let number = ref(1) function add() {
    sum.value += number.value
    }
    function sub() {
    sum.value -= number.value
    }
    </script> <style scope>
    .count {
    text-align: center;
    width: 50%;
    height: 120px;
    background: #03deff;
    border: 3px solid black;
    }
    select, button {
    margin: 10px;
    }
    </style>

    当在 select 标签中进行选择时,为向变量 number 中传入数字,可以使用以下方法之一:

    1. 修改 select 标签中的 v-modelv-model.number(上面使用的方法)
    2. 修改 option 标签中的 value:value
  3. Text.vue

    <template>
    <div class="text">
    <button @click="getText">Get Text</button>
    <ul>
    <li v-for="text in textList" :key="text.id">
    {{ text.content }}
    </li>
    </ul>
    </div>
    </template> <script setup lang="ts" name="Text">
    import { reactive } from 'vue'
    import axios from 'axios' let textList = reactive([
    { id: '01', content: "Text01" },
    { id: '02', content: "Text02" },
    { id: '03', content: "Text03" }
    ]) async function getText() {
    let { data:{result:{content}} } = await axios.get('https://api.oioweb.cn/api/common/OneDayEnglish')
    textList.unshift({ id: Date.now().toString(), content})
    console.log(content)
    }
    </script> <style scope>
    .text {
    width: 50%;
    height: 150px;
    background: #fbff03;
    border: 3px solid black;
    padding-left: 10px;
    }
    </style>
  4. App.vue

    <template>
    <Count />
    <br />
    <Text />
    </template> <script setup lang="ts" name="App">
    import Count from './components/Count.vue'
    import Text from './components/Text.vue'
    </script> <style scope>
    </style>
  5. main.ts

    import { createApp } from 'vue'
    import App from './App.vue' createApp(App).mount('#app')

(3)搭建环境

  • 使用命令 npm install -S pinia 安装 pinia

  • 在 main.ts 中引入并安装 pinia

    import { createApp } from 'vue'
    import App from './App.vue' import { createPinia } from 'pinia' createApp(App).use(createPinia()).mount('#app')

(4)存储与读取数据

  • store 是一个保存状态业务逻辑的实体,每个组件都可以读写它

  • store 中有三个概念,与组件中的一些属性类似

    store 属性
    state data
    getter computed
    actions methods
  • store 目录下的 ts 文件命名与组件的相同

以下涉及对文件、变量等命名的格式均符合 pinia 官方文档规范

  1. ~/src 目录下,新建 store 目录,其中新建 count.ts 和 text.ts

    • count.ts

      import { defineStore } from 'pinia'
      
      export const useCountStore = defineStore('count', {
      state() {
      return {
      sum: 10
      }
      }
      })
    • text.ts

      import { defineStore } from 'pinia'
      
      export const useTextStore = defineStore('text', {
      state() {
      return {
      textList: [
      { id: '01', content: "Text01" },
      { id: '02', content: "Text02" },
      { id: '03', content: "Text03" }
      ]
      }
      }
      })
  2. 修改 Count.vue 和 Text.vue

    • Count.vue

      <template>
      <div class="count">
      <h2>Sum: {{ countStore.sum }}</h2>
      <select v-model.number="number">
      <option value="1" selected>1</option>
      <option value="2">2</option>
      <option value="3">3</option>
      </select>
      <button @click="add">Add</button>
      <button @click="sub">Sub</button>
      </div>
      </template> <script setup lang="ts" name="Count">
      import { ref } from 'vue'
      import { useCountStore } from '@/store/count' const countStore = useCountStore() let number = ref(1) function add() {}
      function sub() {}
      </script>
    • Text.vue

      <template>
      <div class="text">
      <button @click="getText">Get Text</button>
      <ul>
      <li v-for="text in textStore.textList" :key="text.id">
      {{ text.content }}
      </li>
      </ul>
      </div>
      </template> <script setup lang="ts" name="Text">
      import { useTextStore } from '@/store/text' const textStore = useTextStore() async function getText() {}
      </script>

(5)修改数据

  1. 方式一:直接手动修改

    修改 Count.vue

    <template>
    <div class="count">
    <!-- ... -->
    <br />
    <button @click="change">Change</button>
    </div>
    </template> <script setup lang="ts" name="Count">
    // ...
    import { useCountStore } from '@/store/count' const countStore = useCountStore()
    // ...
    function change() {
    countStore.sum = 100
    }
    </script>
  2. 方式二:手动批量修改

    修改 Count.vue

    <script setup lang="ts" name="Count">
    // ...
    function change() {
    countStore.$patch({
    sum: 100,
    num: 1
    })
    }
    </script>
  3. 方式三:使用 actions 修改

    1. 修改 count.ts

      import { defineStore } from 'pinia'
      
      export const useCountStore = defineStore('count', {
      // ...
      actions: {
      increment(value:number) {
      if(this.sum < 20) {
      this.sum += value
      }
      }
      }
      })
    2. 修改 Count.vue

      <script setup lang="ts" name="Count">
      // ... function add() {
      countStore.increment(number.value)
      }
      // ...
      </script>

(6)storeToRefs

  1. 对于从 store 中获得的数据,可以按以下方法在模板中展示

    <template>
    <div class="count">
    <h2>Sum: {{ countStore.sum }}</h2>
    <!-- ... -->
    </template> <script setup lang="ts" name="Count">
    // ...
    import { useCountStore } from '@/store/count'
    const countStore = useCountStore()
    </script>
  2. 为使模板更加简洁,可以使用 toRefssum 从 store 中解构出来

    <template>
    <div class="count">
    <h2>Sum: {{ sum }}</h2>
    <!-- ... -->
    </template> <script setup lang="ts" name="Count">
    // ...
    import { toRefs } from 'vue'
    const { sum } = toRefs(countStore)
    </script>
  3. 直接使用 toRefs 会将 store 中携带的方法也变为响应式数据,因此可以使用 pinia 提供的 storeToRefs

    <script setup lang="ts" name="Count">
    // ...
    import { storeToRefs } from 'pinia'
    const { sum } = storeToRefs(countStore)
    </script>

(7)getters

  • state 中的数据需要经过处理后再使用时,可以使用 getters 配置
  1. 修改 store/count.ts

    export const useCountStore = defineStore('count', {
    state() {
    return {
    sum: 10
    }
    },
    getters: {
    bigSum(state) {
    return state.sum * 100
    }
    },
    // ...
    })
  2. 使用 this 可以替代参数 state 的传入

    getters: {
    bigSum(): number {
    return this.sum * 100
    }
    },
  3. 如果不使用 this 则可以使用箭头函数

    getters: {
    bigSum: state => state.sum * 100
    },
  4. 修改 Count.vue,使用 bigSum

    <template>
    <div class="count">
    <h2>Sum: {{ sum }}, BigSum: {{ bigSum }}</h2>
    <!-- ... -->
    </template> <script setup lang="ts" name="Count">
    // ...
    const { sum, bigSum } = storeToRefs(countStore)
    </script>

(8)$subscribe

  • 作用:订阅,监听 state 及其变化
  1. 修改 Text.vue

    <script setup lang="ts" name="Text">
    // ...
    textStore.$subscribe(() => {
    console.log('state.textList changed')
    })
    </script>

    此时,点击按钮后,在开发者工具中可以看到输出了“state.textList changed”

  2. $subscribe 中可以添加参数

    • mutate:本次修改的数据
    • state:当前的数据
    <script setup lang="ts" name="Text">
    // ...
    textStore.$subscribe((mutate, state) => {
    console.log(mutate, state)
    })
    </script>
  3. 使用 state,借助浏览器本地存储,实现数据变化后,不会在刷新后初始化

    1. 修改 Text.vue

      <script setup lang="ts" name="Text">
      import { useTextStore } from '@/store/text' const textStore = useTextStore()
      textStore.$subscribe((mutate, state) => {
      localStorage.setItem('textList', JSON.stringify(state.textList))
      }) function getText() {
      textStore.getText()
      }
      </script>
    2. 修改 store/text.ts

      import axios from 'axios'
      import { defineStore } from 'pinia' export const useTextStore = defineStore('text', {
      state() {
      return {
      textList: JSON.parse(localStorage.getItem('textList') as string) || []
      }
      },
      actions: {
      async getText() {
      let { data:{result:{content}} } = await axios.get('https://api.oioweb.cn/api/common/OneDayEnglish')
      this.textList.unshift({ id: Date.now().toString(), content})
      console.log(content)
      }
      }
      })

(9)store 组合式写法

  • 之前的内容使用了类似 Vue2 语法的选项式写法

  • 修改 Text.ts

    import axios from 'axios'
    import { defineStore } from 'pinia'
    import { reactive } from 'vue' export const useTextStore = defineStore('text', () => {
    const textList = reactive(JSON.parse(localStorage.getItem('textList') as string) || []) async function getText() {
    let { data: { result: { content } } } = await axios.get('https://api.oioweb.cn/api/common/OneDayEnglish')
    textList.unshift({ id: Date.now().toString(), content })
    console.log(content)
    } return {
    textList,
    getText
    }
    })

0x05 组件通信

(0)概述

  • Vue3 中移出了事件总线,可以使用 pubsub 代替

    • Vuex 换成了 pinia
    • .sync 优化到 v-model
    • $listeners 合并到 $attrs
  • 重置 ~/src 目录结构

    graph TB
    src-->components & App.vue & main.ts
    • Parent.vue

      <template>
      <div class="parent">
      <h2>Parent</h2>
      <Child />
      </div>
      </template> <script setup lang="ts" name="Parent">
      import Child from './Child.vue'
      </script> <style scope>
      .parent {
      width: 50%;
      padding: 20px;
      background: #03deff;
      border: 3px solid black;
      }
      </style>
    • Child.vue

      <template>
      <div class="child">
      <h2>Child</h2>
      </div>
      </template> <script setup lang="ts" name="Child">
      </script> <style scope>
      .child {
      width: 50%;
      padding: 20px;
      background: #fbff03;
      border: 3px solid black;
      }
      </style>
    • App.vue

      <template>
      <Parent />
      </template> <script setup lang="ts" name="App">
      import Parent from './components/Parent.vue'
      </script> <style scope>
      </style>
    • main.ts

      import { createApp } from 'vue'
      import App from './App.vue' createApp(App).mount('#app')
  • 共有 \(7+2\) 种通信方式

(1)props

a. 父组件向子组件传递数据

  1. 修改 Parent.vue,在控制器中创建数据,在模板标签中发送数据

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <h4>Parent's number: {{ numberParent }}</h4>
    <Child :number="numberParent" />
    </div>
    </template> <script setup lang="ts" name="Parent">
    import Child from './Child.vue'
    import { ref } from 'vue' let numberParent = ref(12345)
    </script>
  2. 修改 Child.vue,接收数据

    <template>
    <div class="child">
    <h2>Child</h2>
    <h4>Child's number: {{ numberChild }}</h4>
    <h4>Number from Parent: {{ number }}</h4>
    </div>
    </template> <script setup lang="ts" name="Child">
    import { ref } from 'vue' let numberChild = ref(56789) defineProps(['number'])
    </script>

b. 子组件向父组件传递数据

  1. 修改 Parent.vue,创建用于接收子组件发送数据的方法,将方法发送给子组件

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <h4>Parent's number: {{ numberParent }}</h4>
    <h4 v-show="number">Number from Child: {{ number }}</h4>
    <Child :number="numberParent" :sendNumber="getNumber" />
    </div>
    </template> <script setup lang="ts" name="Parent">
    import Child from './Child.vue'
    import { ref } from 'vue' let numberParent = ref(12345)
    let number = ref(null) function getNumber(value: Number) {
    number.value = value
    }
    </script>
  2. 修改 Child.vue,接收父组件发送的方法,并添加按钮使用该方法

    <template>
    <div class="child">
    <h2>Child</h2>
    <h4>Child's number: {{ numberChild }}</h4>
    <h4>Number from Parent: {{ number }}</h4>
    <button @click="sendNumber(numberChild)">Send Child's number to Parent</button>
    </div>
    </template> <script setup lang="ts" name="Child">
    import { ref } from 'vue' let numberChild = ref(56789) defineProps(['number', 'sendNumber'])
    </script>

(2)自定义事件

  • 获取事件,即使用 $event

    <template>
    <div>
    <button @click="getEvent($event)">Event</button>
    </div>
    </template> <script setup lang="ts">
    function getEvent(event: Event) {
    console.log(event)
    }
    </script>
  1. 修改 Child.vue,声明自定义事件

    <template>
    <div class="child">
    <h2>Child</h2>
    <h4>Child's number: {{ numberChild }}</h4>
    <button @click="emit('custom-event', numberChild)">Send Child's number to Parent</button>
    </div>
    </template> <script setup lang="ts" name="Child">
    import { ref } from 'vue' let numberChild = ref(12345) const emit = defineEmits(['custom-event'])
    </script>
  2. 修改 Parent.vue,绑定自定义事件

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <h4 v-show="number">Number from Child: {{ number }}</h4>
    <Child @custom-event="getNumber" />
    </div>
    </template> <script setup lang="ts" name="Parent">
    import Child from './Child.vue'
    import { ref } from 'vue' let number = ref(null) function getNumber(value: Number) {
    number.value = value
    }
    </script>

(3)mitt

a. 概述与准备

  • 可以实现任意组件通信
  • 与 pubsub、$bus 相类似,都是消息订阅与发布
    • 接收方:提前绑定事件,即订阅消息
    • 发送方:适时触发事件,即发布消息
  1. 使用命令 npm install -S mitt 安装 mitt

  2. ~/src 目录下新建 utils 目录,其中新建 emitter.ts,引入、调用、暴露 mitt

    import mitt from 'mitt'
    
    const emitter = mitt()
    
    export default emitter
    • emitter.all():获取所有绑定事件
    • emitter.emit():触发指定事件
    • emitter.off():解绑指定事件
    • emitter.on():绑定指定事件

b. 基本使用方法

  1. 在 main.ts 中引入 emitter.ts

    // ...
    import emitter from './utils/emitter'
  2. 修改 utils/emitter.ts,绑定事件

    import mitt from 'mitt'
    
    const emitter = mitt()
    
    emitter.on('event1', () => {
    console.log('event1')
    })
    emitter.on('event2', () => {
    console.log('event2')
    }) export default emitter
  3. 触发事件

    // ...
    setInterval(() => {
    emitter.emit('event1')
    emitter.emit('event2')
    }, 1000) export default emitter
  4. 解绑事件

    // ...
    setTimeout(() => {
    emitter.off('event1')
    console.log('event1 off')
    }, 3000) export default emitter
    • 解绑所有事件

      setTimeout(() => {
      emitter.all.clear()
      console.log('All clear')
      }, 3000)

c. 实际应用

目录结构:

graph TB
src-->components & utils & App.vue & main.ts
components-->Child1.vue & Child2.vue & Parent.vue
utils-->emitter.ts
  • Child1.vue(Child2.vue)

    <template>
    <div class="child">
    <h2>Child1</h2>
    <h4>Name: {{ name }}</h4>
    </div>
    </template> <script setup lang="ts" name="Child1">
    import { ref } from 'vue' // Child1.vue
    let name = ref('Alex')
    // Child2.vue
    // let name = ref('Bob')
    </script> <style scope>
    .child {
    width: 50%;
    padding: 20px;
    background: #fbff03;
    border: 3px solid black;
    }
    </style>
  • Parent.vue

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <Child1 />
    <br />
    <Child2 />
    </div>
    </template> <script setup lang="ts" name="Parent">
    import Child1 from './Child1.vue'
    import Child2 from './Child2.vue'
    </script> <style scope>
    .parent {
    width: 50%;
    padding: 20px;
    background: #03deff;
    border: 3px solid black;
    }
    </style>
  • emitter.ts

    import mitt from 'mitt'
    
    const emitter = mitt()
    
    export default emitter
  1. 修改 Child2.vue,绑定事件

    <template>
    <div class="child">
    <!-- ... -->
    <h4 v-show="brotherName">Brother's name: {{ brotherName }}</h4>
    </div>
    </template> <script setup lang="ts" name="Child2">
    // ...
    import emitter from '@/utils/emitter' let brotherName = ref('') emitter.on('send-name', (value: string) => {
    brotherName.value = value
    })
    </script>
  2. 修改 Child1.vue,发送数据

    <template>
    <div class="child">
    <!-- ... -->
    <button @click="emitter.emit('send-name', name)">Send Name</button>
    </div>
    </template> <script setup lang="ts" name="Child1">
    // ...
    import emitter from '@/utils/emitter'
    </script>
  3. 修改 Child2.vue,卸载组件时解绑事件

    <script setup lang="ts" name="Child2">
    // ...
    import { ref, onUnmounted } from 'vue' onUnmounted(() => {
    emitter.off('send-name')
    })
    </script>

(4)v-model

  • 实际开发中不常用,常见于 UI 组件库

a. HTML 标签

  • 修改 Parent.vue,将 v-model 用在 input 标签上

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <input type="text" v-model="name" placeholder="Enter your name" />
    </div>
    </template> <script setup lang="ts" name="Parent">
    import { ref } from 'vue' let name = ref("John")
    </script> <style scope>
    .parent {
    width: 50%;
    padding: 20px;
    background: #03deff;
    border: 3px solid black;
    }
    </style>
  • input 标签中使用 v-model 相当于:

    <input type="text" :value="name" @input="name=(<HTMLInputElement>$event.target).value" />
    • (<HTMLInputElement>$event.target):TypeScript 断言检查

b. 组件标签

  1. 在 components 中新建 CustomInput.vue

  2. 修改 Parent.vue

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <CustomInput :modelValue="name" @update:modelValue="name = $event" />
    </div>
    </template> <script setup lang="ts" name="Parent">
    import { ref } from 'vue'
    import CustomInput from './CustomInput.vue' let name = ref("John")
    </script>
  3. 修改 CustomInput.vue

    <template>
    <input
    type="text"
    :value="modelValue"
    @input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)"
    />
    </template> <script setup lang="ts" name="CustomInput">
    defineProps(["modelValue"])
    const emit = defineEmits(["update:modelValue"])
    </script> <style scope>
    </style>
    • 对于 $eventtarget 判定:

      • 当触发的是原生事件时,$event 是事件对象,需要 .target
      • 当触发的是自定义事件时,$event 是触发事件数据,不需要 .target
  4. 修改 Parent.vue,使用 v-model

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <!-- <CustomInput :modelValue="name" @update:modelValue="name = $event" /> -->
    <CustomInput v-model="name" />
    </div>
    </template>

    此时,可以通过设置 v-model,实现修改 modelValue 以及添加多个 v-model

  5. 修改 Parent.vue

    <template>
    <div class="parent">
    <!-- ... -->
    <CustomInput v-model:m1="name1" v-model:m2="name2" />
    </div>
    </template> <script setup lang="ts" name="Parent">
    // ...
    let name1 = ref("John")
    let name2 = ref("Mary")
    </script>
  6. 修改 CustomInput.vue

    <template>
    <input
    type="text"
    :value="m1"
    @input="emit('update:m1', (<HTMLInputElement>$event.target).value)"
    /> <input
    type="text"
    :value="m2"
    @input="emit('update:m2', (<HTMLInputElement>$event.target).value)"
    />
    </template> <script setup lang="ts" name="CustomInput">
    defineProps(["m1", "m2"])
    const emit = defineEmits(["update:m1", "update:m2"])
    </script>

(5)$attrs

  • $attrs 是一个对象,包含所有父组件传入的标签属性,用于实现祖父组件向孙子组件通信

目录结构:

graph TB
components-->Grand.vue & Parent.vue & Child.vue
  1. 修改 Grand.vue,创建数据,并通过 props 发送

    <template>
    <div class="grand">
    <h2>Grand</h2>
    <h4>Number: {{ number }}</h4>
    <Parent :number="number" />
    </div>
    </template> <script setup lang="ts" name="Grand">
    import Parent from './Parent.vue'
    import { ref } from 'vue' let number = ref(123)
    </script>
  2. 修改 Parent.vue,使用 $attrs 将来自 Grand.vue 的数据以 props 的方式转发给 Child.vue

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <Child v-bind="$attrs"/>
    </div>
    </template> <script setup lang="ts" name="Parent">
    import Child from './Child.vue'
    </script>
  3. 修改 Child.vue,接收数据并展示

    <template>
    <div class="child">
    <h2>Child</h2>
    <h4>Number from Grand: {{ number }}</h4>
    </div>
    </template> <script setup lang="ts" name="Child">
    defineProps(['number'])
    </script>

    此时,祖父组件 Grand.vue 也可以向孙子组件 Child.vue 传递方法,从而可以在孙子组件中修改祖父组件的数据

(6)$refs & $parent

  • 父子组件通信

    属性 作用 说明
    $refs 父组件向子组件通信


    包含所有被 ref 属性标识的 DOM 元素或组件实例
    $parent 子组件向父组件通信 值为对象,当前组件的父组件实例对象

目录结构:

graph TB
components-->Parent.vue & Child1.vue & Child2.vue

a. $refs

  1. 修改 Child1.vue 和 Child2.vue,创建数据并允许访问

    <template>
    <div class="child">
    <h2>Child1</h2>
    <!--
    Child2.vue
    <h2>Child2</h2>
    -->
    <h4>Number: {{ number }}</h4>
    </div>
    </template> <script setup lang="ts" name="Child1">
    import { ref } from 'vue' let number = ref(456)
    // Child2.vue
    // let number = ref(789) defineExpose({ number })
    </script>
  2. 修改 Parent.vue,设置按钮使其能够修改子组件中的 number

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <button @click="changeNumber">Change Child1's number</button>
    <hr />
    <Child1 ref="c1" />
    <br />
    <Child2 ref="c2" />
    </div>
    </template> <script setup lang="ts" name="Parent">
    import Child1 from './Child1.vue'
    import Child2 from './Child2.vue'
    import { ref } from 'vue' let c1 = ref()
    let c2 = ref() function changeNumber() {
    c1.value.number = 123
    c2.value.number = 123
    }
    </script>
  3. 使用 $refs,使其能够批量修改

    <template>
    <div class="parent">
    <!-- ... -->
    <button @click="getAll($refs)">Get All Child's number</button>
    <!-- ... -->
    </div>
    </template> <script setup lang="ts" name="Parent">
    // ...
    function getAll(refs: any) {
    for (let key in refs) {
    refs[key].number = 123
    }
    }
    </script>

b. $parent

  1. 修改 Parent.vue,创建数据并允许访问

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <h4>Number: {{ number }}</h4>
    <hr />
    <Child1 />
    <br />
    <Child2 />
    </div>
    </template> <script setup lang="ts" name="Parent">
    import Child1 from './Child1.vue'
    import Child2 from './Child2.vue'
    import { ref } from 'vue' let number = ref(123) defineExpose({ number })
    </script>
  2. 修改 Child1.vue

    <template>
    <div class="child">
    <h2>Child1</h2>
    <button @click="changeNumber($parent)">Change Number</button>
    </div>
    </template> <script setup lang="ts" name="Child1">
    function changeNumber(parent: any) {
    parent.number = 456
    }
    </script>

(7)provide & inject

  • 祖孙组件通信

  • 使用方法:

    graph LR
    A(祖父组件)--provide 发送数据-->B(孙子组件)--inject 接收数据-->A

目录结构:

graph TB
components-->Grand.vue & Parent.vue & Child.vue
  1. 修改 Grand.vue,创建数据

    <template>
    <div class="grand">
    <h2>Grand</h2>
    <h4>Number: {{ number }}</h4>
    <Parent />
    </div>
    </template> <script setup lang="ts" name="Grand">
    import Parent from './Parent.vue'
    import { ref } from 'vue' let number = ref(123)
    </script>
  2. 使用 provide 发送数据

    <script setup lang="ts" name="Grand">
    // ...
    import { ref, provide } from 'vue' provide('number', number)
    </script>
  3. 修改 Child.vue,接收并展示数据

    <template>
    <div class="child">
    <h2>Child</h2>
    <h4>Number from Grand: {{ number }}</h4>
    </div>
    </template> <script setup lang="ts" name="Child">
    import { inject } from 'vue'
    let number = inject('number')
    </script>
  4. inject 的数据不存在时,可以展示默认值

    <template>
    <div class="child">
    <h2>Child</h2>
    <h4>Number from Grand: {{ number }}</h4>
    <h4>Number from Parent: {{ numberP }}</h4>
    </div>
    </template> <script setup lang="ts" name="Child">
    import { inject } from 'vue'
    let number = inject('number')
    let numberP = inject('numberP', 456)
    </script>
  5. 修改 Grand.vue,提供方法用于修改祖父组件中的数据

    <script setup lang="ts" name="Grand">
    // ...
    function changeNumber() {
    number.value = 456
    } provide('number', {
    number, changeNumber
    })
    </script>
  6. 修改 Child.vue,接收方法并使用按钮触发

    <template>
    <div class="child">
    <h2>Child</h2>
    <h4>Number from Grand: {{ number }}</h4>
    <button @click="changeNumber">Change Number</button>
    </div>
    </template> <script setup lang="ts" name="Child">
    import { inject } from 'vue'
    let { number, changeNumber } = inject('number')
    </script>

(8)pinia

参考第四章内容

(9)slot

  • slot 翻译为“插槽”,分为默认插槽、具名插槽、作用域插槽

a. 默认插槽

  1. 修改 Parent.vue,创建数据

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <div class="category">
    <Child title="C1"></Child>
    <Child title="C2"></Child>
    <Child title="C3"></Child>
    </div>
    </div>
    </template> <script setup lang="ts" name="Parent">
    import Child from './Child.vue'
    import { reactive } from 'vue' let c1List = reactive([
    { id: "01", Text: "C1-Text1" },
    { id: "02", Text: "C1-Text2" },
    { id: "03", Text: "C1-Text3" }
    ])
    let c2List = reactive([
    { id: "01", Text: "C2-Text1" },
    { id: "02", Text: "C2-Text2" },
    { id: "03", Text: "C2-Text3" }
    ])
    let c3List = reactive([
    { id: "01", Text: "C3-Text1" },
    { id: "02", Text: "C3-Text2" },
    { id: "03", Text: "C3-Text3" }
    ])
    </script> <style scope>
    .parent {
    width: 80%;
    padding: 20px;
    background: #03deff;
    border: 3px solid black;
    }
    .category {
    display: flex;
    justify-content: space-around;
    }
    .category div {
    margin: 10px;
    }
    </style>
  2. 修改 Child.vue,接收数据并展示

    <template>
    <div class="child">
    <h2>{{ title }}</h2>
    </div>
    </template> <script setup lang="ts" name="Child">
    defineProps(['title'])
    </script> <style scope>
    .child {
    width: 50%;
    padding: 20px;
    background: #fbff03;
    border: 3px solid black;
    }
    </style>
  3. 修改 Parent.vue 中的模板内容

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <div class="category">
    <Child title="C1">
    <ul>
    <li v-for="item in c1List" :key="item.id">{{ item.text }}</li>
    </ul>
    </Child>
    <Child title="C2">
    <ul>
    <li v-for="item in c2List" :key="item.id">{{ item.text }}</li>
    </ul>
    </Child>
    <Child title="C3">
    <!-- <ul>
    <li v-for="item in c3List" :key="item.id">{{ item.text }}</li>
    </ul> -->
    </Child>
    </div>
    </div>
    </template>
  4. 修改 Child.vue,引入插槽并设置默认值

    <template>
    <div class="child">
    <h2>{{ title }}</h2>
    <slot>Default</slot>
    </div>
    </template>

    此时,C1 和 C2 的内容均正常展示;C3 的内容无法展示,而是显示默认值

b. 具名插槽

具有名字的插槽

  1. 修改 Child.vue,为插槽添加名字

    <template>
    <div class="child">
    <h2>{{ title }}</h2>
    <slot name="list">Default</slot>
    </div>
    </template>
  2. 修改 Paren.vue,为引用的组件标签添加 v-slot: 属性

    使用 # 简便写法可以替代 v-slot: 写法

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <div class="category">
    <Child title="C1" #list>
    <ul>
    <li v-for="item in c1List" :key="item.id">{{ item.text }}</li>
    </ul>
    </Child>
    <Child title="C2" v-slot:List>
    <ul>
    <li v-for="item in c2List" :key="item.id">{{ item.text }}</li>
    </ul>
    </Child>
    <Child title="C3">
    <ul>
    <li v-for="item in c3List" :key="item.id">{{ item.text }}</li>
    </ul>
    </Child>
    </div>
    </div>
    </template>

    此时,C1 的内容正常展示;C2 和 C3 的内容均无法展示,而是显示默认值

  3. 修改 Child.vue,设置更多的具名插槽,使 title 通过插槽进行传递

    <template>
    <div class="child">
    <slot name="title"><h2>No Title</h2></slot>
    <slot name="slot">Default</slot>
    </div>
    </template> <script setup lang="ts" name="Child">
    </script>
  4. 修改 Parent.vue,使用 template 标签

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <div class="category">
    <Child>
    <template v-slot:title>
    <h2>C1</h2>
    </template>
    <template v-slot:list>
    <ul>
    <li v-for="item in c1List" :key="item.id">{{ item.text }}</li>
    </ul>
    </template>
    </Child>
    <Child>
    <template v-slot:list>
    <ul>
    <li v-for="item in c2List" :key="item.id">{{ item.text }}</li>
    </ul>
    </template>
    <template v-slot:title>
    <h2>C2</h2>
    </template>
    </Child>
    <Child>
    <template v-slot:list>
    <ul>
    <li v-for="item in c3List" :key="item.id">{{ item.text }}</li>
    </ul>
    <h2>C3</h2>
    </template>
    </Child>
    </div>
    </div>
    </template>

    此时,C1 和 C2 的内容均正常展示;C3 的内容无法展示,而是显示默认值

c. 作用域插槽

  1. 修改 Child.vue,原始数据在子组件中,使用 slot 将数据传递到父组件中

    <template>
    <div class="child">
    <slot :cl="cList">Default</slot>
    </div>
    </template> <script setup lang="ts" name="Child">
    import { reactive } from 'vue' let cList = reactive([
    { id: "01", text: "C-Text1" },
    { id: "02", text: "C-Text2" },
    { id: "03", text: "C-Text3" }
    ])
    </script>
  2. 修改 Parent.vue,接收并使用子组件传递来的数据

    <template>
    <div class="parent">
    <h2>Parent</h2>
    <div class="category">
    <Child>
    <template v-slot="params"> <!-- 接收传递来的数据 -->
    <ul>
    <li v-for="item in params.cl" :key="item.id">{{ item.text }}</li>
    </ul>
    </template>
    </Child>
    <Child>
    <template v-slot="{ cl }"> <!-- 解构传递来的数据 -->
    <ol>
    <li v-for="item in cl" :key="item.id">{{ item.text }}</li>
    </ol>
    </template>
    </Child>
    <Child>
    <template v-slot:default="{ cl }"> <!-- 具名作用域插槽(default是插槽默认名) -->
    <h4 v-for="item in cl" :key="item.id">{{ item.text }}</h4>
    </template>
    </Child>
    </div>
    </div>
    </template> <script setup lang="ts" name="Parent">
    import Child from './Child.vue'
    </script>

(10)总结

组件关系 通信方式
父传子 props
v-model
$refs
默认插槽 / 具名插槽
子传父 props
自定义事件
v-model
$parent
作用域插槽
祖孙互传 $attrs
provide & inject
任意组件 mitt
pinia

0x06 其他 API

  • 以下 API 使用场景不多,但需要了解

(1)shallowRef & shallowReactive

  • 两种方法一般用于绕开深度响应,提高性能,加快访问速度

a. shallowRef

  • 作用:创建一个响应式数据,只对顶层属性进行响应式处理

  • 特点:只跟踪引用值的变化,不跟踪值内部的属性的变化

  • 用法

    1. 修改 App.vue,创建数据与方法,并在模板中展示

      <template>
      <div class="app">
      <h2>Name: {{ person.name }}</h2>
      <h2>Age: {{ person.age }}</h2>
      <p><button @click="changerName">Change Name</button></p>
      <p><button @click="changerAge">Change Age</button></p>
      <p><button @click="changerAll">Change All</button></p>
      </div>
      </template> <script setup lang="ts" name="App">
      let person = {
      name: 'John',
      age: 18
      } function changerName() {
      person.value.name = 'Mary'
      }
      function changerAge() {
      person.value.age = 19
      }
      function changerAll() {
      person.value = {
      name: 'Mary',
      age: 19
      }
      }
      </script>
    2. 引入 shallowRef 并使用

      <script setup lang="ts" name="App">
      import { shallowRef } from 'vue' let person = shallowRef({
      name: 'John',
      age: 18
      })
      // ...
      </script>

      此时,姓名和年龄均不可修改,而所有信息可以通过自定义的 changeAll() 方法修改

b. shallowReactive

  • 作用:创建一个浅层响应式对象,只将对象的最顶层属性变成响应式,其他属性不变

  • 特点:对象的顶层属性是响应式的,嵌套对象的属性不是

  • 用法

    1. 修改 App.vue,创建数据与方法,并在模板中展示

      <template>
      <div class="app">
      <h2>Name: {{ person.name }}</h2>
      <h2>Age: {{ person.detail.age }}</h2>
      <p><button @click="changerName">Change Name</button></p>
      <p><button @click="changerAge">Change Age</button></p>
      <p><button @click="changerAll">Change All</button></p>
      </div>
      </template> <script setup lang="ts" name="App">
      let person = {
      name: 'John',
      detail: {
      age: 18
      }
      } function changerName() {
      person.name = 'Mary'
      }
      function changerAge() {
      person.detail.age = 19
      }
      function changerAll() {
      Object.assign(person, {
      name: 'Mary',
      detail: {
      age: 19
      }
      })
      }
      </script>
    2. 引入 shallowReactive 并使用

      <script setup lang="ts" name="App">
      import { shallowReactive } from 'vue' let person = shallowReactive({
      name: 'John',
      detail: {
      age: 18
      }
      })
      // ...
      </script>

      此时,姓名可以修改,但年龄不可修改,而所有信息可以通过自定义的 changeAll() 方法修改

(2)readonly & shallowReadonly

a. readonly

  • 作用:创建一个对象的深只读副本

  • 特点:

    • 对象的所有嵌套属性变为只读
    • 阻止对象被修改
  • 应用场景:

    • 创建不可变的状态快照
    • 保护全局状态/配置不可修改
  • 用法

    1. 修改 App.vue,创建数据

      <template>
      <div class="app">
      <h2>Sum: {{ sum1 }}</h2>
      <h2>Sum readonly: {{ sum2 }}</h2>
      <p><button @click="changeSum1">Change Sum</button></p>
      <p><button @click="changeSum2">Change Sum readonly</button></p>
      </div>
      </template> <script setup lang="ts" name="App">
      import { ref } from 'vue' let sum1 = ref(0)
      let sum2 = ref(0) function changeSum1() {
      sum1.value += 1
      }
      function changeSum2() {
      sum2.value += 1
      }
      </script>
    2. 引入 readonly 并使用

      <script setup lang="ts" name="App">
      import { ref, readonly } from 'vue'
      // ...
      let sum2 = readonly(sum1)
      // ...
      </script>

      此时,sum2 会随着 sum1 改变而改变,但点击按钮“Change Sum readonly”不会对 sum 修改

b. shallowReadonly

  • 作用:创建一个对象的浅只读副本

  • 特点:只将对象的顶层属性设置为只读,对象内部的嵌套属性依旧可读可写

  • 应用场景:仅需对对象顶层属性保护时使用

  • 用法

    1. 修改 App.vue,创建数据

      <template>
      <div class="app">
      <h2>Name: {{ personRO.name }}</h2>
      <h2>Age: {{ personRO.detail.age }}</h2>
      <p><button @click="changerName">Change Name</button></p>
      <p><button @click="changerAge">Change Age</button></p>
      <p><button @click="changerAll">Change All</button></p>
      </div>
      </template> <script setup lang="ts" name="App">
      import { reactive } from 'vue' let person = reactive({
      name: 'John',
      detail: {
      age: 18
      }
      })
      let personRO = person function changerName() {
      personRO.name = 'Mary'
      }
      function changerAge() {
      personRO.detail.age = 19
      }
      function changerAll() {
      Object.assign(personRO, {
      name: 'Mary',
      detail: {
      age: 19
      }
      })
      }
      </script>
    2. 引入 shallowReadonly 并使用

      <script setup lang="ts" name="App">
      import { reactive, shallowReadonly } from 'vue'
      // ...
      let personRO = shallowReadonly(person)
      // ...
      </script>

      此时,姓名不可以修改,年龄可以修改,所有信息可以通过自定义的 changeAll() 方法修改

(3)toRaw & markRaw

a. toRaw

  • 作用:获取一个响应式对象的原始对象

  • 特点:返回的对象不是响应式的,不会触发视图更新

  • 应用场景:将响应式对象传递到非 Vue 的库或外部系统时使用

  • 用法

    1. 修改 App.vue,创建数据

      <script setup lang="ts" name="App">
      import { reactive } from 'vue' let person = reactive({
      name: 'John',
      age: 18
      }) console.log(person)
      </script>
    2. 引入 toRaw 并使用

      <script setup lang="ts" name="App">
      import { reactive, toRaw } from 'vue' let person = reactive({
      name: 'John',
      age: 18
      }) console.log(person)
      console.log(toRaw(person))
      </script>

b. markRaw

  • 作用:标记一个对象,使其永远不会变成响应式对象

  • 应用场景:例如使用 Mock.js 插件时,为防止误把 mockjs 变成响应式对象而使用

    Mock.js 官网

  • 用法

    1. 修改 App.vue,创建数据

      <script setup lang="ts" name="App">
      let person = {
      name: 'John',
      age: 18
      }
      </script>
    2. 引入 markRaw 并使用

      <script setup lang="ts" name="App">
      import { reactive, markRaw } from 'vue' let person = markRaw({
      name: 'John',
      age: 18
      }) person = reactive(person)
      console.log(person)
      </script>

      此时,person 并未变成响应式对象

(4)customRef

  • 作用:创建一个自定义的 ref,并对其依赖项跟踪更新触发进行逻辑控制

  • 用法

    1. 修改 App.vue,引入 customRef

      <template>
      <div class="app">
      <h2>Message: {{ msg }}</h2>
      <input type="text" v-model="msg" />
      </div>
      </template> <script setup lang="ts" name="App">
      import { customRef } from 'vue' let msg = customRef(() => {
      return {
      get() {},
      set() {}
      }
      })
      </script>

      其中,get() 在变量 msg 被读取时调用;set() 在变量 msg 被修改时调用

    2. 声明新变量 initMsg 作为默认值在 get() 中返回

      <script setup lang="ts" name="App">
      import { customRef } from 'vue' let initMsg = "Default"
      let msg = customRef(() => {
      return {
      get() {
      return initMsg
      },
      set() {}
      }
      })
      </script>
    3. msg 的修改结果通过 set() 接收

      <script setup lang="ts" name="App">
      // ...
      let msg = customRef(() => {
      return {
      // ...
      set(value) {
      console.log('set', value)
      }
      }
      })
      </script>
    4. set() 中修改 initMsg

      <script setup lang="ts" name="App">
      // ...
      let msg = customRef(() => {
      return {
      // ...
      set(value) {
      // console.log('set', value)
      initMsg = value
      }
      }
      })
      </script>
    5. 【核心内容】在 customRef 的回调函数中,接收两个参数:track(跟踪)、trigger(触发)

      <script setup lang="ts" name="App">
      // ...
      let msg = customRef((track, trigger) => {
      return {
      get() {
      track()
      return initMsg
      },
      set(value) {
      initMsg = value
      trigger()
      }
      }
      })
      </script>
      • track():依赖项跟踪,告知 Vue 需要对变量 msg 持续关注,一旦变化立即更新
      • trigger():更新触发,告知 Vue 变量 msg 发生了变化
    6. 可以在 set() 通过设置延时函数实现指定时间后触发更新

      <script setup lang="ts" name="App">
      // ...
      let msg = customRef((track, trigger) => {
      return {
      // ...
      set(value) {
      setTimeout(() => {
      initMsg = value
      trigger()
      }, 1000)
      }
      }
      })
      </script>
    7. 但是当输入过快时,会导致输入的数据被覆盖丢失,此时引入防抖方法

      <script setup lang="ts" name="App">
      // ...
      let timer: number
      let msg = customRef((track, trigger) => {
      return {
      // ...
      set(value) {
      clearTimeout(timer)
      timer = setTimeout(() => {
      initMsg = value
      trigger()
      }, 1000)
      }
      }
      })
      </script>

      防抖原理:清除上一次 set() 中生产的定时器,并以最新定时器为准

    8. 在 src 目录下新建 hooks 目录,其中新建 useMsgRef.ts,用于将上述自定义 ref 封装成 Hooks

      import { customRef } from 'vue'
      
      export default function(initMsg: string, delaySecond: number) {
      let timer: number
      let msg = customRef((track, trigger) => {
      return {
      get() {
      track()
      return initMsg
      },
      set(value) {
      clearTimeout(timer)
      timer = setTimeout(() => {
      initMsg = value
      trigger()
      }, delaySecond * 1000)
      }
      }
      })
      return { msg }
      }
    9. 修改 App.vue,使用上述 Hooks

      <script setup lang="ts" name="App">
      import useMsgRef from '@/hooks/useMsgRef' let { msg } = useMsgRef('Default', 1)
      </script>

0x07 新组件

(1)Teleport

  • 是一种能够将组件 HTML 结构移动到指定位置的技术

  • 举例:在页面上封装一个弹窗组件

    目录结构:

    graph TB
    src-->components & App.vue & main.ts
    components-->ModelDialog.vue
    1. 修改 App.vue

      <template>
      <div class="outer">
      <h2>App Component</h2>
      <ModelDialog />
      </div>
      </template> <script setup lang="ts" name="App">
      import ModelDialog from './components/ModelDialog.vue'
      </script> <style scope>
      .outer {
      width: 80%;
      height: 500px;
      padding: 20px;
      background: #03deff;
      }
      </style>
    2. 修改 ModelDialog.vue

      <template>
      <button>Show Dialog</button>
      <div class="md">
      <h2>Dialog Title</h2>
      <p>Content Here</p>
      <button>Close</button>
      </div>
      </template> <script setup lang="ts" name="ModelDialog">
      </script> <style scope>
      .md {
      text-align: center;
      width: 50%;
      height: 30%;
      background: #fbff03;
      border: 3px solid black;
      }
      </style>
    3. 设置弹窗的显示与隐藏

      <template>
      <button @click="isShowDialog = true">Show Dialog</button>
      <div class="md" v-show="isShowDialog">
      <h2>Dialog Title</h2>
      <p>Content Here</p>
      <button @click="isShowDialog = false">Close</button>
      </div>
      </template> <script setup lang="ts" name="ModelDialog">
      import { ref } from 'vue'
      let isShowDialog = ref(false)
      </script>
    4. 通过 CSS 调整弹窗位于视口正中央

      <style scope>
      .md {
      // ...
      position: fixed;
      top: 5%;
      left: 50%;
      margin-left: -100px;
      }
      </style>
    5. 修改 App.vue,通过 CSS 设置变灰

      <style scope>
      .outer {
      // ...
      filter: saturate(0%);
      }
      </style>

      此时,弹窗并未按照第 4 步的设置显示在视口正中央,可以通过 Teleport 解决此问题

    6. 修改 ModelDialog.vue

      <template>
      <button @click="isShowDialog = true">Show Dialog</button>
      <Teleport to="body">
      <div class="md" v-show="isShowDialog">
      <h2>Dialog Title</h2>
      <p>Content Here</p>
      <button @click="isShowDialog = false">Close</button>
      </div>
      </Teleport>
      </template>
      • to 属性用于指定移动的目的地,上述代码将弹窗内容移动到 body 标签中

      • 此时,网页渲染后的 DOM 结构如下:

        <!-- ... -->
        <body>
        <div id="app" data-v-app><!-- ... --></div>
        <!-- ... -->
        <div id="md"><!-- ... --></div>
        </body>

(2)Suspense

  • 等待异步组件时,渲染一些额外的内容,改善用户体验

目录结构:

graph TB
src-->components & App.vue & main.ts
components-->Child.vue
  1. 修改 App.vue

    <template>
    <div class="app">
    <h2>App</h2>
    <Child />
    </div>
    </template> <script setup lang="ts" name="App">
    import Child from './components/Child.vue'
    </script> <style scope>
    .app {
    width: 80%;
    height: 500px;
    padding: 20px;
    background: #03deff;
    }
    </style>
  2. 修改 Child.vue

    <template>
    <div class="child"></div>
    </template> <script setup lang="ts" name="Child">
    </script> <style scope>
    .child {
    width: 200px;
    height: 150px;
    background: #fbff03;
    }
    </style>
  3. 添加异步任务

    <script setup lang="ts" name="Child">
    import axios from 'axios' let { data: { result: { content } } } = await axios.get("https://api.oioweb.cn/api/common/OneDayEnglish") console.log(content)
    </script>

    此时,子组件会在页面上“消失”,可以在 App.vue 中使用 Suspense 解决此问题

  4. 修改 App.vue,引入 Suspense 并使用

    <template>
    <div class="app">
    <h2>App</h2>
    <Suspense>
    <template #default>
    <Child />
    </template>
    </Suspense>
    </div>
    </template> <script setup lang="ts" name="App">
    import Child from './components/Child.vue'
    import { Suspense } from 'vue'
    </script>
    • Suspense 中预设了两个插槽,default 插槽用于展示异步完成的内容,fallback 插槽用于展示异步进行中的内容
    • 当网络状态不是很好时,子组件不会立即渲染完成,因此可以借助 Suspense 添加加载提示
  5. 设置加载内容:Loading...

    <template>
    <div class="app">
    <h2>App</h2>
    <Suspense>
    <template #default>
    <Child />
    </template>
    <template #fallback>
    <h2>Loading...</h2>
    </template>
    </Suspense>
    </div>
    </template>

(3)全局 API 转移到应用对象

graph LR
A(Vue2 全局API<br/>Vue.xxx) --> B(Vue3 应用对象API<br/>app.xxx)
  1. app.component

    1. 修改 main.ts,注册全局组件

      import { createApp } from 'vue'
      import App from './App.vue'
      import Child from './components/Child.vue' const app = createApp(App)
      app.component('Child', Child)
      app.mount('#app')
    2. 修改 App.vue,使用全局组件

      <template>
      <div class="app">
      <h2>App</h2>
      <Child />
      </div>
      </template> <script setup lang="ts" name="App">
      </script>
  2. app.config

    1. 修改 main.ts,注册全局属性

      import { createApp } from 'vue'
      import App from './App.vue' const app = createApp(App)
      app.config.globalProperties.x = 12345 declare module 'vue' {
      interface ComponentCustomProperties {
      x: number;
      }
      } app.mount('#app')
    2. 修改 App.vue,使用全局属性

      <template>
      <div class="app">
      <h2>App</h2>
      <h4>{{ x }}</h4>
      </div>
      </template> <script setup lang="ts" name="App">
      </script>

      实际开发中,不建议该使用方法,容易污染全局

  3. app.directive

    1. 修改 main.ts,注册全局指令

      import { createApp } from 'vue'
      import App from './App.vue' const app = createApp(App)
      app.directive('custom', (element, {value}) => {
      element.innerText += value
      element.style.fontSize = '50px';
      })
      app.mount('#app')
    2. 修改 App.vue,使用全局指令

      <template>
      <div class="app">
      <h2>App</h2>
      <p v-custom="value">Hello,</p>
      </div>
      </template> <script setup lang="ts" name="App">
      let value = "world!"
      </script>
  4. app.mount

    • 在 main.ts 中挂载应用

      import { createApp } from 'vue'
      import App from './App.vue' const app = createApp(App)
      app.mount('#app')
  5. app.unmount

    • 在 main.ts 中卸载应用

      import { createApp } from 'vue'
      import App from './App.vue' const app = createApp(App)
      app.mount('#app') setTimeout(() => {
      app.unmount()
      }, 3000)
  6. app.use

    • 在 main.ts 中安装插件,如 pinia

      import { createApp } from 'vue'
      import App from './App.vue'
      import { PiniaVuePlugin } from 'pinia' const app = createApp(App)
      app.use(PiniaVuePlugin)
      app.mount('#app')

(4)非兼容性改变

详见官方文档

-End-

Vue3 + TypeScript 开发指南的更多相关文章

  1. Vue3 + TypeScript 开发实践总结

    前言 迟来的Vue3文章,其实早在今年3月份时就把Vue3过了一遍.在去年年末又把 TypeScript 重新学了一遍,为了上 Vue3 的车,更好的开车.在上家公司4月份时,上级领导分配了一个内部的 ...

  2. 《Vue3.x+TypeScript实践指南》已出版

    转眼回长沙快2年了,图书本在去年就已经完稿,因为疫情,一直耽搁了,直到这个月才出版!疫情之下,众生皆苦!感觉每天都是吃饭.睡觉.上班.做核酸! 图书介绍 为了紧跟技术潮流,该书聚焦于当下火的Vue3和 ...

  3. TypeScript学习指南--目录索引

    关于TypeScript: TypeScript是一种由微软开发的自由和开源的编程语言.它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程. TypeS ...

  4. TypeScript入门指南(JavaScript的超集)

    TypeScript入门指南(JavaScript的超集)   你是否听过 TypeScript? TypeScript 是 JavaScript 的超集,TypeScript结合了类型检查和静态分析 ...

  5. [转帖]2019 简易Web开发指南

    2019 简易Web开发指南     2019年即将到来,各位同学2018年辛苦了. 不管大家2018年过的怎么样,2019年还是要继续加油的! 在此我整理了个人认为在2019仍是或者将成为主流的技术 ...

  6. 现代前端库开发指南系列(二):使用 webpack 构建一个库

    前言 在前文中,我说过本系列文章的受众是在现代前端体系下能够熟练编写业务代码的同学,因此本文在介绍 webpack 配置时,仅提及构建一个库所特有的配置,其余配置请参考 webpack 官方文档. 输 ...

  7. 2019 Vue开发指南:你都需要学点啥?

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者.原文出处:https://dzone.com/articles/vue-development-in-2019 ...

  8. Vite ❤ Electron——基于Vite搭建Electron+Vue3的开发环境【一】

    背景 目前社区两大Vue+Electron的脚手架:electron-vue和vue-cli-plugin-electron-builder, 都有这样那样的问题,且都还不支持Vue3,然而Vue3已 ...

  9. 基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用

    刚完成一些前端项目的开发,腾出精力来总结一些前端开发的技术点,以及继续完善基于SqlSugar的开发框架循序渐进介绍的系列文章,本篇随笔主要介绍一下基于Vue3+TypeScript的全局对象的注入和 ...

  10. 基于SqlSugar的开发框架循序渐进介绍(22)-- Vue3+TypeScript的前端工作流模块中实现统一的表单编辑和表单详情查看处理

    在工作流页面中,除了特定的业务表单信息外,往往也需要同时展示通用申请单的相关信息,因此在页面设计的时候需要使用一些组件化的概念来实现动态的内容展示处理,本篇随笔介绍Vue3+TypeScript+El ...

随机推荐

  1. redis 安装的参考文章

    redis 安装  :  https://www.runoob.com/redis/redis-install.html

  2. [Linux] Linux 自动挂载mount --bind 实现类似目录硬链的效果 (包含ZFS方案)

    说明 这个命令用以将一个目录挂载到另一个目录,以实现类似于硬链的操作 但是这个命令只是在内存中建立了一个映射,重启系统之后挂载就消失了 而linux是不支持目录硬链的,具体原因见linux为什么不能硬 ...

  3. golang官方包管理vendor模式无法引用非go文件

    主页 微信公众号:密码应用技术实战 博客园首页:https://www.cnblogs.com/informatics/ 背景&问题 golang作为高级计算机语言之一,在云原生以及web网站 ...

  4. 远程服务调用(RPC与Rest本质区别)

    一.背景 远程服务将计算机程序的工作范围从单机扩展到网络,从本地延伸至远程,是构建分布式系统的首要基础.远程服务调用(Remote Procedure Call,RPC)在计算机科学中已经存在了超过四 ...

  5. Linux 系统进程管理

    Linux 系统进程管理 目录 Linux 系统进程管理 一.进程的概述 1.1 什么是进程? 1.2 进程和程序的区别 1.3 进程的生命周期 1.4 进程的运行过程 二. 静态显示进程状态-ps ...

  6. ansible 自动化运维(1)

      ansible 简介 ansible 是什么? ansible是新出现的自动化运维工具,基于Python开发,集合了众多运维工具(puppet.chef.func.fabric)的优点,实现了批量 ...

  7. 2.4G无线音频一对多传输解决方案难点解析

    前记     2.4G无线音频传输是一个非主流的应用,做这个的人 相对要比较少.但是,这个领域所涉及到的知识却不少,也就导致了这个领域是好入门,但是东西想做好特别难.这里涉及到声学,无线协议,电子,设 ...

  8. Typora自定义主题详解--打造自己的专属样式

    你真的会使用Typora吗? 欢迎关注博主公众号「Java大师」, 专注于分享Java领域干货文章, 关注回复「主题」, 获取大师使用的typora主题: http://www.javaman.cn/ ...

  9. iot梳理

    近段时间一直在搞公司的iot项目,没啥时间学习新的知识(也是自己懒),这边记录下整体对iot知识领域的认识. 首先说到iot会想到物联网,对于我们开发来说物联网很明显要用到几个不太常用到的技术,如mq ...

  10. String类为什么要用final修饰?

    final修饰符的意义? https://www.cnblogs.com/loren-Yang/p/13380318.html String类被实现的目标是什么? 效率和安全 如何实现期望? 参考文献 ...