Vue3-项目实战

Vue3 的优势

  • 优势

    优势
    VUMnhV.png

项目初始化

  • 环境

    • node 16+
  • 项目初始化

    1
    2
    # create-vue 是 Vue 官方新的脚手架工具,底层切换到了 vite(下一代前端工具链),为开发提供极速响应
    npm init vue@latest
    选项
    VDClWJ.png
  • 项目启动

    • 安装依赖

      1
      2
      npm config set registry https://registry.npmmirror.com
      npm install
    • 项目启动

      1
      npm run dev

熟悉项目目录和关键文件

  • 关键文件
    • vite.config.js: 项目的配置文件基于vite 的配置
    • package.json: 项目包文件核心依赖项变成了 vue3xvite
    • main.js: 入口文件,createApp 函数构建应用实例
    • app.vue 根组件SFC 单文件组件script-template-style
      • 变化一: 脚本script 和模板template 顺序调整
      • 变化二: 模板template 不再要求唯一根元素
      • 变化三: 脚本script 添加setup 标识支持组合式API
    • index.html: 单页入口提供 id 为 app 的挂载点

组合式 API

setup
  • 写法和执行时机

    执行时机
    VDCO4o.png
  • 时机验证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script>
    export default {
    setup() {
    console.log("setUp....");
    },
    beforeCreate() {
    console.log("beforeCreate...");
    }
    }
    </script>
  • 基础使用测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <script>
    export default {
    setup() {
    console.log("setUp....");
    // 定义变量
    const message = "Hello Vue3.....";
    // 定义函数
    const sayHello = () => {
    console.log("hello methods.......");
    }

    // 导出
    return {
    message,
    sayHello
    }
    },
    beforeCreate() {
    console.log("beforeCreate...");
    }
    }
    </script>

    <template>
    <div class="container">
    {{ message }}
    </div>
    <button @click="sayHello">测试点击按钮</button>
    </template>
  • setup 的语法糖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- 不用再导出 -->
    <script setup>
    console.log("setUp....");
    // 定义变量
    const message = "Hello Vue3.....";
    // 定义函数
    const sayHello = () => {
    console.log("hello methods.......");
    }
    </script>

    <template>
    <div class="container">
    {{ message }}
    </div>
    <button @click="sayHello">测试点击按钮</button>
    </template>
  • 总结

    • setup 选项的执行时机是什么?

      beforeCreate 钩子之前,自动执行

    • setup 写代码的特点是什么?

      定义数据 + 函数,然后以对象方式return

    • <script setup> 解决了什么问题?

      经过语法糖的封装更简单的使用组合式API

    • setup 中的this 还指向组件实例吗?

      指向undefined

reactive 和 ref函数
  • reactive()

    • 作用: 接受对象类型数据的参数并返回一个响应式的对象

    • 使用的核心步骤

      1
      2
      3
      4
      5
      6
      7
      <script setup>
      // 1. 从 vue 包中导入 reactive 函数
      import { reactive } from 'vue';

      // 2. 在
      const state = reactive(对象类型数据)
      </script>
      • 使用

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        <script setup>
        // 1. 导入
        import { reactive } from 'vue';

        // 执行函数 传入参数 变量接受
        const state = reactive({
        count: 0
        })

        const setCount = () => {
        state.count++
        }
        </script>

        <template>
        <button @click="setCount">
        {{ state.count }}
        </button>
        </template>
  • ref()

    • 作用: 接受简单类型或者对象类型的数据传入并返回一个响应式的对象

    • 使用的核心步骤

      1
      2
      3
      4
      5
      6
      7
      <script setup>
      // 1. 从 vue 包中导入 ref 函数
      import { ref } from 'vue';

      // 2. 在
      const count = ref(简单类型或者复杂类型的数据)
      </script>
    • 使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      <script setup>
      // 1. 从 vue 包中导入 ref 函数
      import { ref } from 'vue';

      // 2. 在
      const count = ref(1)

      const setCount = () => {
      // 脚本区域修改 ref 产生的响应式对象数据 必须通过.value 属性
      count.value++
      }
      </script>


      <template>
      <div class="container">
      <button @click="setCount">
      {{ count }}
      </button>
      </div>
      </template>
    • 总结

      • reactive ref 函数的共同作用是什么

        用函数调用的方式生成响应式数据

      • reactive vs ref

        1. reactive 不能处理简单类型的数据
        2. ref 参数类型支持更好但是必须通过.value 访问修改
        3. ref 函数的内部实现依赖于reactive 函数
      • 在实际工作中推荐使用那个?

        推荐使用ref 函数,更加灵活。

computed
  • computed 计算属性函数

    • 计算属性基本思想和vue2 完全一致,组合式API 下的计算属性只是修改了写法

    • 使用的核心步骤

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <script setup>
      // 1. 从 vue 包中导入 computed 函数
      import { computed } from 'vue';

      // 2. 执行函数在回调参数中 return 基于响应式数据做计算的值,用变量接受
      const computedState = computed(() => {
      return 基于响应式数据做计算后的值
      })
      </script>
    • 使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      <script setup>
      // 1. 从 vue 包中导入 computed 函数
      import { ref, computed } from 'vue';
      const list = ref([1, 2, 3, 4, 5, 6]);

      // 2. 执行函数在回调参数中 return 基于响应式数据做计算的值,用变量接受
      const computedState = computed(() => {
      return list.value.filter(item => item > 2)
      })
      </script>


      <template>
      <div class="container">
      <p> 原始响应数组: {{ list }}</p>
      <p> 计算属性后的数组: {{ computedState }}</p>
      </div>
      </template>
      计算属性的使用
      VDwZcZ.png
    • 总结

      1. 计算属性中不应该有副作用

        比如: 异步请求/修改dom

      2. 避免直接修改计算属性的值

        计算属性应该是只读的

watch
  • watch 函数

    • 基本使用-侦听单个数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      <script setup>
      // 1. 从 vue 包中导入 computed 函数
      import { ref, watch } from 'vue';
      const count = ref(0)
      const setCount = () => {
      count.value++
      }

      // 2. 执行 watch 函数传入要侦听的响应式数据(ref对象)和回调函数
      watch(count, (newValue, oldValue) => {
      console.log(`count 发生了变化,老值为 ${oldValue},新值为: ${newValue}`);
      })
      </script>


      <template>
      <div class="container">
      <p> 原始响应数组: {{ count }}</p>
      <button @click="setCount">改变数据</button>
      </div>
      </template>
      基本使用
    • 基本使用-侦听多个数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      <script setup>
      // 1. 从 vue 包中导入 computed 函数
      import { ref, watch } from 'vue';
      const count = ref(0)
      const cp = ref('cp')

      const setCount = () => {
      count.value++
      cp.value = 'cpdd++'
      }

      // 2. 执行 watch 函数传入要侦听的响应式数据(ref对象)和回调函数
      watch([count, cp], ([newCount, newCp], [oldCount, oldCp]) => {
      console.log(`count 发生了变化,老值为 ${oldCount},新值为: ${newCount}`);
      console.log(`cp 发生了变化,老值为 ${oldCp},新值为: ${newCp}`);
      })
      </script>


      <template>
      <div class="container">
      <p> 原始响应: {{ count }}</p>
      <p> 原始响应: {{ cp }}</p>
      <button @click="setCount">改变数据</button>
      </div>
      </template>
  • 立即执行

    说明: 在侦听器创建时立即触发回调,响应式数据变化后继续执行回调

    1
    2
    3
    4
    const count = ref(0)
    watch(count,()=>{},{
    immediate: true
    })
  • deep

    默认机制: 通过watch 监听的ref 对象默认是浅层监听的,直接修改嵌套的对象属性不会触发回调执行,需要开启deep 选项

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <script setup>
    // 1. 从 vue 包中导入 computed 函数
    import { ref, watch } from 'vue';

    const state = ref({ count: 0 })
    const changeStateByCount = () => {
    // 直接修改属性 => 不会触发回调
    state.value.count++
    }

    watch(state, () => {
    console.log("发生了变化......");
    })

    </script>


    <template>
    <div class="container">
    <p> 原始响应: {{ state.count }}</p>
    <button @click="changeStateByCount">改变数据</button>
    </div>
    </template>
    1
    2
    3
    4
    5
    6
    7
    watch(state,
    () => {
    console.log("发生了变化......");
    },
    { deep: true }
    )

    深度监听的开启
    VDwtMt.png
  • 精确监听

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <script setup>
    // 1. 从 vue 包中导入 computed 函数
    import { ref, watch } from 'vue';

    const state = ref({ count: 0, name: 'zs' })

    const changeCount = () => {
    // 直接修改属性 => 不会触发回调
    state.value.count++
    }
    const changeName = () => {
    // 直接修改属性 => 不会触发回调
    state.value.name = 'lisi'
    }

    // TODO 实现精确监听
    watch(
    () => state.value.name, // 精确监听的字段
    () => {
    console.log("name 发生了变化...."); // 回调
    }
    )
    </script>


    <template>
    <div class="container">
    <p> 原始响应: {{ state.count }}</p>
    <p> 原始响应: {{ state.name }}</p>
    <button @click="changeCount">改变 Count</button>
    <button @click="changeName">改变 Name</button>
    </div>
    </template>
  • 总结

    • 作为watch 函数的第一个参数,ref 对象需要添加.value 吗?

      不需要,watch 会自动读取

    • watch 只能侦听单个数据吗?

      单个或者多个

    • 不开启deep,直接修改嵌套属性能触发回调吗?

      不能,默认是浅层监听

    • 不开启deep,想在某个层次比较深的属性变化时执行回调该怎么做?

      可以把第一个参数写成函数的写法,返回要监听的具体属性

生命周期函数
  • vue2vue3对比

    选项式(vue2) API(vue3)
    beforeCreate/create setup
    beforeMount onBeforeMount
    mounted onMounted
    beforeUpdate onBeforeUpdate
    updated onUpdated
    beforeUnmount onBeforeUnmount
    unmounted onUnmount
  • 生命周期函数的基本使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script setup>
    // 1. 从 vue 包中导入 onMounted 函数
    import { onMounted } from 'vue';

    // 2. 执行函数, 传入回调
    onMounted(() => {
    // 自定义逻辑
    console.log("onMounted.....");
    })
    </script>
  • 生命周期函数的多次执行

    生命周期函数是可以执行多次的,多次执行时传入的回调会在时机成熟时依次执行

  • 总结

    • 组合式API 中生命周期函数的格式是什么?

      on + 生命周期名字

    • 组合式API 中可以使用onCreated 吗?

      没有这个钩子函数,直接写入到setup

    • 组合式API 中组件卸载完毕时执行哪个函数

      onUnmounted

父子组件通信
Props
  • 通信结果

    父子组件通信
    VDNibc.png
  • 实现

    • 父组件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      <script setup>
      // 1. 导入子组件
      import User from '@/components/User.vue'

      </script>


      <template>
      <div class="container">
      Father Component
      <!-- 2. 对子组件绑定属性和值 -->
      <user
      message="father data to son"
      count="10">
      </user>
      </div>
      </template>

      <style>
      .container {
      border: 2px solid red;
      padding: 15px;
      font-family: "ER Kurier 1251";
      width: 40%;
      }
      </style>
    • 子组件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      <script setup>
      import {ref} from "vue";

      // 3. 通过 defineProps "编译器宏",接受父组件传递的数据
      defineProps({
      message: String,
      count: Number
      })
      </script>


      <template>
      <div class="user">
      Son Component
      <p>
      {{message}}
      </p>
      <button>{{ count }}</button>
      </div>
      </template>

      <style>
      .user {
      border: 2px solid #000;
      padding: 5px;
      }
      </style>
      • 子组件修改父组件的数据

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        <script setup>
        import {ref} from "vue";

        // 3. 通过 defineProps "编译器宏",接受父组件传递的数据
        const props = defineProps({
        message: String,
        count: Number
        })
        let num = ref(props.count)

        const setCount = ()=>{
        num.value++
        }
        </script>


        <template>
        <div class="user">
        Son Component
        <p>
        {{message}}
        </p>
        <button @click="setCount">{{ num }}</button>
        </div>
        </template>

        <style>
        .user {
        border: 2px solid #000;
        padding: 5px;
        }
        </style>
Emit
  • 父组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <script setup>
    // 1. 引入子组件
    import User from '@/components/User.vue'
    import {ref} from "vue";

    let msg = ref('')
    const getMessage = (data) => {
    console.log(data)
    msg.value = data
    }
    </script>


    <template>
    <div class="container">
    Father Component => {{ msg }}
    <!-- 绑定自定义事件 -->
    <user @get-message="getMessage"></user>
    </div>
    </template>

    <style>
    .container {
    padding: 15px;
    border: 2px solid red;
    font-family: "ER Kurier 1251";
    }
    </style>
  • 子组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <script setup>
    import {ref} from "vue";

    let count = ref(100)

    // 通过 defineEmits 编译器宏生成 emit 方法
    const emit = defineEmits(['get-message'])

    const sendMsg = () => {
    // 触发自定义事件
    emit('get-message', 'this is send son data to father....')
    }
    </script>


    <template>
    <div class="user">
    Son Component
    <button @click="sendMsg">发送数据</button>
    </div>
    </template>

    <style>
    .user {
    padding: 5px;
    border: 2px solid #000;
    }
    </style>
    • 渲染

      未获取数据前 触发自定义事件并渲染
组件通信总结
  • 父传子

    1. 父传子的过程中通过什么方式接受props

      definePros({属性名: 类型})

    2. setup 语法糖中如何使用父组件传过来的数据

      const props = defineProps({属性名: 类型})

  • 子传父

    • 子传父的过程中通过什么方式得到emit 方法

      defineEmits(['事件名称'])

模板引用
  • 概念: 通过ref 标识获取真实的dom对象或者组件实例对象

  • 具体使用

    1. 调用ref 函数生成一个ref 对象

    2. 通过ref 标识绑定ref 对象到标签

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      <script setup>
      import {onMounted, ref} from "vue";

      // 1. 获取 ref 对象
      const h1Ref = ref(null)

      // 组件挂载完毕之后才能获取对象
      onMounted(() => {
      console.log(h1Ref.value)
      })
      </script>


      <template>
      <div class="container">
      <h1 ref="h1Ref">测试 ref 的使用</h1>
      Father Component
      </div>
      </template>

      <style>
      .container {
      padding: 15px;
      line-height: 2;
      border: 2px solid red;
      font-family: "ER Kurier 1251";
      }
      </style>
  • 组件实例

    • 子组件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      <script setup>
      import {ref} from "vue";

      const msg = ref('点击获取修改后的数据...')
      const changeMsg = () => {
      msg.value = '修改后的新数据'
      }
      </script>


      <template>
      <div class="user">
      Son Component
      <button @click="changeMsg">{{ msg }}</button>
      </div>
      </template>

      <style>
      .user {
      padding: 5px;
      line-height: 2;
      border: 2px solid #000;
      }
      </style>
    • 父组件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      <script setup>
      import {onMounted, ref} from "vue";
      import User from "@/components/User.vue";

      // 1. 获取 ref 对象
      const compRef = ref(null)


      // 组件挂载完毕之后才能获取对象
      onMounted(() => {
      console.log(compRef.value)
      })
      </script>


      <template>
      <div class="container">
      Father Component
      <user ref="compRef"></user>
      </div>
      </template>

      <style>
      .container {
      padding: 15px;
      line-height: 2;
      border: 2px solid red;
      font-family: "ER Kurier 1251";
      }
      </style>
      问题
      VDv1Hw.png
    • 获取不到子组件内部的msg 的问题说明

      默认情况下在script setup> 语法糖下组件内部的属性和方法是不开放给父组件访问的,可以通过defineExpose 编译宏指定那些属性和方法允许访问

  • 总结

    • 获取模板引用的时机是什么?

      组件挂载完毕

    • defineExpose 编译宏的作用是什么

      显示暴露组件内部的属性和方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      <!-- 组件内 -->
      <script setup>
      import {ref} from "vue";

      const msg = ref('点击获取修改后的数据...')
      const changeMsg = () => {
      msg.value = '修改后的新数据'
      }

      // 显示的暴露出去
      defineExpose({
      msg,
      changeMsg
      })
      </script>

      1
      2
      3
      4
      5
      6
      7
      <!-- 暴露出去后的使用方式 -->
      // 组件挂载完毕之后才能获取对象
      onMounted(() => {
      console.log('aaa')
      // 调用函数
      compRef.value.changeMsg()
      })
provide 和 inject
  • 跨组件数据传递

    跨组件数据传递
    VDJSno.png
  • provide 和 inject 作用: 顶层组件向任意的组件传递数据和方法,实现跨组件通信

  • 跨层传递普通数据

    • 实现步骤

      1. 顶层组件通过provide 函数提供数据
      2. 底层组件通过inject 函数获取数据
    • 细节实现

      • App.vue(顶层组件)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        <script setup>
        import RoomMsgItem from "@/components/RoomMsgItem.vue";
        import {provide, ref} from "vue";

        // 传递静态数据
        provide('data-key', 'this is root page data....')
        // 传递响应式数据
        const count = ref(0)
        provide('count-key', count)
        setTimeout(() => {
        count.value = 100
        }, 3000)
        </script>


        <template>
        <div class="container">
        顶层组件
        <room-msg-item/>
        </div>
        </template>

        <style>
        .container {
        padding: 15px;
        line-height: 2;
        border: 2px solid red;
        font-family: "ER Kurier 1251";
        }
        </style>
      • RoomMsgItem(中间组件)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        <script setup>
        import RoomMsgComment from "@/components/RoomMsgComment.vue";
        </script>

        <template>
        <div class="room-msg-item">
        中间组件
        <room-msg-comment/>
        </div>
        </template>


        <style scoped>
        .room-msg-item {
        padding: 20px;
        border: 1px solid green;
        }
        </style>
      • RoomMsgComment(最内部组件)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        <script setup>
        import {inject} from "vue";

        const roomData = inject('data-key')
        const countData = inject('count-key')
        </script>

        <template>
        <div class="comment">
        底层组件
        <div class="content">
        <div>来自顶层组件中的数据为: {{ roomData}}</div>
        <div>来自顶层组件中的响应式数据为: {{ countData }} </div>
        </div>
        </div>
        </template>

        <style scoped>
        .comment{
        padding: 20px;
        border: 1px solid blue;
        }
        .content{
        padding: 10px;
        }
        </style>
        跨组件数据传递
        VDJTxa.png
  • 跨层传递方法

    顶层组件可以向底层组件传递方法, 底层组件调用方法修改顶层组件中的数据

    1
    2
    3
    4
    5
    6
    // 顶层组件传递方法
    const setCount = () => {
    count.value = 101;
    }
    provide('setCount-key', setCount)

    1
    2
    3
    4
    5
    6
    7
    8
    // 内部组件调用
    <script setup>
    const setCountData = inject('setCount-key')
    </script>

    <template>
    <button @click="setCountData"> 修改顶层组数据 </button>
    </template>
  • 总结

    • provide inject 的作用是什么

      跨组件通信

    • 如何在传递的过程中保持数据响应式

      第二个参数传递ref 对象

    • 底层组件想要通知顶层组件做修改,如何做

      传递方法,底层组件调用方法

    • 一个组件书只有一个顶层或底层组件吗

      相对概念,存在多个顶层和底层关系

Pinia

基本概念
  • 是什么

    Pinia vue 的专属最新的状态管理库,Vuex 状态管理工具的替代品

优势
  • 优势

    • 提供更加简单的API(去掉了 mutation)
    • 提供符合组合式风格的API(和 Vue3新语法统一)
    • 去掉了modules 的概念,每一个store 都是一个独立的模块
    • 搭配TypeScript 一起使用提供可靠的类型推断
  • 安装

    1
    npm install pinia
安装和基础配置
  • 使用

    • 配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      import {createApp} from 'vue'

      // 1. 导入 createPinia
      import {createPinia} from 'pinia'

      // 2. 执行方法得到实例
      const pina = createPinia()
      const app = createApp(App)
      // 3. 把 pinia 实例加入到 app 应用中
      app.use(pina)

      app.mount('#app')
    • 案例: 使用Pinia 实现计数器案例

      • 创建文件

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        import {defineStore} from "pinia";
        import {ref} from "vue";

        // counter: 是一个标识(自定义的,任意的)
        export const useCounterStore = defineStore('counter', () => {
        // 数据(state)
        const count = ref(0)
        // 修改数据的方法
        const increment = () => {
        count.value++
        }
        // 以对象形式返回
        return {count, increment}
        })
      • 使用

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        <script setup>
        // 1. 导入 useCounterStore 方法
        import {useCounterStore} from "@/store/counters";

        // 2. 执行方法得到 counterStore 对象
        const counterStore = useCounterStore()
        </script>


        <template>
        <div class="container">
        // 调用它的方法: counterStore.increment
        <button @click="counterStore.increment">{{ counterStore.count }}</button>
        </div>
        </template>
Getters实现
  • getters 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import {defineStore} from "pinia";
    import {computed, ref} from "vue";

    export const useCounterStore = defineStore('counter', () => {
    // 数据(state)
    const count = ref(0)
    // 修改数据的方法(action 同步+异步的方式)
    const increment = () => {
    count.value++
    }
    // getter 的定义
    const doubleCount = computed(() => count.value * 2)

    // 以对象形式返回
    return {count, increment,doubleCount}
    })
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <script setup>
    // 1. 导入 useCounterStore 方法
    import {useCounterStore} from "@/store/counters";

    // 2. 执行方法得到 counterStore 对象
    const counterStore = useCounterStore()
    console.log(counterStore)
    </script>


    <template>
    <div class="container">
    <button @click="counterStore.increment">{{ counterStore.count }}</button>
    <!-- 使用 getter -->
    {{ counterStore.doubleCount }}
    </div>
    </template>

    <style>
    .container {
    padding: 15px;
    line-height: 2;
    border: 2px solid red;
    font-family: "ER Kurier 1251";
    }
    </style>
异步 Action
  • action 如何实现异步

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import {defineStore} from "pinia";
    import {computed, ref} from "vue";
    import axios from "axios";

    const API_URL = 'http://geek.itheima.net/v1_0/channels'
    export const useCounterStore = defineStore('counter', () => {
    // 准备数据
    const list = ref([])
    // 异步action
    const loadList = async () => {
    const {data: res} = await axios.get(API_URL)
    list.value = res.data.channels
    }
    // 以对象形式返回
    return {list, loadList}
    })
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <script setup>
    // 1. 导入 useCounterStore 方法
    import {useCounterStore} from "@/store/counters";
    import {onMounted} from "vue";

    // 2. 执行方法得到 counterStore 对象
    const counterStore = useCounterStore()
    onMounted(() => {
    counterStore.loadList()
    })
    </script>


    <template>
    <div class="container">
    <ul>
    <li v-for="item in counterStore.list">
    {{ item.name }}
    </li>
    </ul>
    </div>
    </template>

    <style>
    .container {
    padding: 15px;
    line-height: 2;
    border: 2px solid red;
    font-family: "ER Kurier 1251";
    }
    </style>
storeToRefs
  • storeToRefs

    使用storeToRefs 函数可以辅助保持数据(state + getter)的响应式解构

  • 丢失响应式问题

    1
    2
    3
    4
    5
    6
    // 直接解构赋值(响应式丢失)
    const {count} = counterStore

    // 响应式丢失,数据无法发生改变
    <button @click="counterStore.increment">{{ count }}</button>

    • 如何解决

      1
      2
      // 通过 storeToRefs 包裹即可
      const {count} = storeToRefs(counterStore)
  • 方法的解构

    1
    2
    // 方法直接从原来的 counterStore 中解构赋值
    const { increment } = counterStore
总结
  • Pinia 是用来做什么的?

    集中状态管理工具,新一代的vuex

  • Pinia 中还需要mutation 吗?

    不需要,action 既支持同步也支持异步

  • Pinia 如何实现getter?

    computed 计算属性函数

  • Pinia 产生的Store 如何解构赋值数据保持响应式

    storeToRefs

Vue-Rabbit

  • 项目初始化

    初始化
    VU7xIo.png
  • 目录调整

    目录调整
  • 配置别名路径联想提示

    • 作用

      在编写代码的过程中,一旦输入 @/,Vscode 会立刻联想出 src下的所有子目录和文件,统一文件路径访问不会出错

    • 具体配置

      1. 在项目的根目录下新增jsconfig.json 文件

      2. 添加json 格式的配置项

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        {
        "compilerOptions": {
        "baseUrl": "./",
        "paths": {
        "@/*": [
        "src/*"
        ]
        }
        }
        }

Element-UI Plus

  • 安装

    1
    2
    3
    4
    5
    6
    7
    8
    # NPM
    $ npm install element-plus --save

    # Yarn
    $ yarn add element-plus

    # pnpm
    $ pnpm install element-plus
  • 按需自动导入配置

    1
    npm install -D unplugin-vue-components unplugin-auto-import
  • vite 中添加如下配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    import {fileURLToPath, URL} from "node:url";

    import {defineConfig} from "vite";
    import vue from "@vitejs/plugin-vue";

    // TODO: 配置Element-UI 按需自动导入
    import AutoImport from "unplugin-auto-import/vite";
    import Components from "unplugin-vue-components/vite";
    import {ElementPlusResolver} from "unplugin-vue-components/resolvers";

    // https://vitejs.dev/config/
    export default defineConfig({
    plugins: [
    vue(),
    // TODO: 添加 element-ui的插件
    AutoImport({
    resolvers: [ElementPlusResolver()],
    }),
    Components({
    resolvers: [ElementPlusResolver()],
    }),
    ],
    resolve: {
    alias: {
    "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
    },
    });

  • 主题定制

    • 如何定制:scss变量替换方案

      • 安装依赖

        1
        npm i sass -D
      • 创建文件

        1
        styles/element/index.scss
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        /* 只需要重写你需要的即可 */
        @forward 'element-plus/theme-chalk/src/common/var.scss' with (
        $colors: (
        'primary': (
        // 主色
        'base': #27ba9b,
        ),
        'success': (
        // 成功色
        'base': #1dc779,
        ),
        'warning': (
        // 警告色
        'base': #ffb302,
        ),
        'danger': (
        // 危险色
        'base': #e26237,
        ),
        'error': (
        // 错误色
        'base': #cf4444,
        ),
        )
        );
      • 通知Element 采用scss 语言,导入定制scss 文件覆盖

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        import {fileURLToPath, URL} from "node:url";

        import {defineConfig} from "vite";
        import vue from "@vitejs/plugin-vue";

        // 配置Element-UI 按需自动导入
        import AutoImport from "unplugin-auto-import/vite";
        import Components from "unplugin-vue-components/vite";
        import {ElementPlusResolver} from "unplugin-vue-components/resolvers";

        // https://vitejs.dev/config/
        export default defineConfig({
        plugins: [
        vue(),
        AutoImport({
        resolvers: [ElementPlusResolver()],
        }),
        Components({
        // 1. 配置 elementPlus 采用 sass 样式配色系统
        resolvers: [ElementPlusResolver({importStyle: "sass"})],
        }),
        ],
        resolve: {
        alias: {
        "@": fileURLToPath(new URL("./src", import.meta.url)),
        },
        },
        css: {
        preprocessorOptions: {
        scss: {
        // 自动导入定制化样式文件进行样式覆盖
        additionalData: `@use "@/styles/element/index.scss" as *;`,
        }
        }
        }
        });

  • 如何验证成功替换主题色

    将导入的测试按钮如何发现色系已更改则为替换成功

Axios

  • 安装

    1
    npm i axios
  • 配置基础实例(统一接口配置)

    统一接口配置
  • 基础封装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import axios from 'axios'

    // 创建axios实例
    const httpInstance = axios.create({
    baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
    timeout: 5000
    })
    // 拦截器

    // 请求拦截器
    httpInstance.interceptors.request.use(config => {
    return config;
    }, error => {
    Promise.reject(error)
    })
    // 响应拦截器
    httpInstance.interceptors.response.use(
    res => res.data,
    error => {
    return Promise.reject(error)
    })
    export default httpInstance;
    1
    2
    3
    4
    5
    6
    7
    8
    // 封装请求
    import http from "@/utils/http";

    export function getCategory() {
    return http({
    url: 'home/category/head'
    })
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 使用该请求
    <script setup>
    import {RouterView} from 'vue-router'
    import {getCategory} from '@/apis/test/testHttp'

    getCategory().then(res => {
    console.log(res)
    })
    </script>

路由设计

  • 一级路由与二级路由

    路由设计原则:找内容切换的区域,如果是页面整体切换,则为一级路由

  • 路由文件默认内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import { createRouter, createWebHistory } from 'vue-router'

    const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
    {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutView.vue')
    }
    ]
    })

    export default router

  • 总结

    • 路由设计的依据是什么?

      依据: 内容切换的方式

    • 默认二级路由如何进行设置?

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      // path 配置项置空
      routes: [
      {
      path: '/',
      component: Layout,
      children: [
      {
      // TODO: 小技巧,默认为空是,会自动显示二级路由
      path: "",
      component: Home
      },
      {
      path: "category",
      component: Category
      }
      ]
      },
      {
      path: '/login',
      component: Login
      }
      ]

静态资源初始化

  • 图片资源和样式资源
    • 资源说明
      1. 实际工作中的图片资源通常由UI 设计师提供,常见的图片格式有png,svg 等都是由UI 切图交给前端
      2. 样式资源通常是指项目初始化的时候进行样式重置,常见的比如开源的normalize.css 或者手写
    • 资源操作
      1. 图片资源-把images 文件夹放到assets 目录系啊
      2. 样式资源-把common.scss 文件放到style 目录下

scss-自动导入

  • 为什么需要自动导入

    在项目里一些组件共享的色值会以scss 变量的方式统一放到一个名为var.scss 的文件中,正常组件中使用,需要先导入 scss 文件,再使用内部的变量,比较繁琐,自动导入可以免去手动导入的步骤,直接使用内部的变量

  • 配置

    • 定义一些色值变量

      1
      2
      3
      4
      5
      $xtxColor: #27ba9b;
      $helpColor: #e26237;
      $sucColor: #1dc779;
      $warnColor: #ffb302;
      $priceColor: #cf4444;
    • vite.config.js 中做出配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      css: {
      preprocessorOptions: {
      scss: {
      // 自动导入定制化样式文件进行样式覆盖
      additionalData:
      `@use "@/styles/element/index.scss" as *;
      @use "@/styles/var.scss" as *;
      `,
      }
      }
      }

吸顶导航

  • 吸顶交互: 浏览器再上下滚动的过程中,如果距离顶部的滚动距离大于78px,吸顶导航显示,小于78px 隐藏

  • 实现核心思路分析

    核心思路分析
    VUTBIH.png
  • vueuse

    1
    npm i @vueuse/core
    • 使用

      1
      2
      3
      4
      5
      6
      7
      <script setup>
      // 1. 导入
      import {useScroll} from "@vueuse/core";

      const {y} = useScroll(window)
      </script>

Pinia-优化重复请求

  • 结论: 两个导航中的列表是完全一致的,但是要发送两次网络请求,存在浪费。通过Pinia 集中管理数据,再把数据给组件使用

  • 如何优化

    优化分析
  • 定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import {ref} from 'vue'
    import {defineStore} from 'pinia'
    import {getCategoryAPI} from '@/apis/layout'

    export const useCategoryStore = defineStore('category', () => {
    // 存储列表(state)
    const categoryList = ref([])

    // 发送请求: getCategory 再次使用函数,可以对发送请求前后的数据进行处理(异步 action)
    const getCategory = async () => {
    const res = await getCategoryAPI()
    // 数据绑定
    categoryList.value = res.result
    }

    return {categoryList, getCategory}
    })

    使用的时候只在Layout组件触发 action

Home

  • 页面解构分析

    页面解构分析
  • 组件封装

    • 组件封装解决了什么问题?

      • 复用问题
      • 业务维护问题
    • 组件封装的核心思路

      把可复用的解构只写一次,可能发生变化的部分抽象成组件参数(props/插槽)

    • 实现步骤

      1. 不做任何抽象,准备静态模板
      2. 抽象可变的部分
        • 主标题和副标题是纯文本,可以抽象成prop 传入
        • 主体内容是复杂的模板,抽象成插槽传入
    • 总结

      • 纯展示类组件通用封装思路总结

        • 搭建纯静态的部分,不管可变的部分

        • 抽象可变的部分为组件参数

          非复杂的模板抽象成props,复杂的解构模板抽象为插槽

图片懒加载指令实现

  • 场景和指令用法

    • 场景

      场景: 电商网站的首页通常会很长,用户不一定能访问到页面靠下面的图片,这类图片通过懒加载优化手段可以做到, 只有进入视口区域才发送图片请求

    • 指令用法

      1
      <img v-img-lazy="item.picture">

      在图片img 身上绑定指令,该图片只有在正式进入到视口区域时才会发送图片网络请求

  • 核心原理:

    VnG3CL.png
  • 插件定义与使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 定义懒加载插件 【src/directives/index.js】
    import {useIntersectionObserver} from "@vueuse/core";

    export const lazyPlugin = {
    install(app) {
    // 自定义全局指令
    app.directive('img-lazy', {
    mounted(el, binding) {
    // el: 指定绑定那个元素的 img
    // binding: binding.value 指令等于好后面绑定的表达式的值,图片 url
    // vueuse插件中的一个函数获取视口
    const {stop} = useIntersectionObserver(
    el,
    ([{isIntersecting}]) => {
    if (isIntersecting) {
    // 进入视口区域
    el.src = binding.value
    // 在监听的图片第一次完成加载之后就停止监听,避免内存浪费
    stop()
    }
    },
    )
    }
    })
    }
    }
    1
    2
    3
    4
    // main.js 全局指令注册
    import {lazyPlugin} from "@/directives";
    app.use(lazyPlugin)

激活状态显示

  • 是针对RouterLink 组件默认支持激活样式显示的类名,只需要给active-class 属性设置对应的类名即可

  • 使用

    1
    <RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
  • 类定义

    1
    2
    3
    4
    5
    // 激活时使用的类名
    .active {
    color: $xtxColor;
    border-bottom: 1px solid $xtxColor;
    }

路由缓存

  • 什么是路由缓存问题

    使用带有参数的路由时需要注意的是,当用户从/users/johnny 导航到/users/jolyne 时,相同的组件实例将被重复使用。因为两个路由都渲染同一个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用。

  • 问题复现

    一级分类的怯寒正好满足上面的条件,组件实例复用, 导致分类数据无法更新

  • 解决问题的思路

    1. 思路一: 让组件实例不复用,强制销毁创建
    2. 思路二: 监听路由变化,变化之后执行数据更新操作
    • 方案一: 给router-view 添加key

      以当前路由完成路径为key 的值,router-view 组件绑定

      1
      2
      <!-- 二级路由出口: 添加 key 破坏复用机制 强制销毁重建 -->
      <RouterView :key="$route.fullPath"/>
    • 方案二: 使用beforeRouterUpdate 导航钩子

      beforeRouterUpdate 钩子函数可以在每次路由更新之前执行,在回调中执行需要数据更新的业务逻辑即可

      1
      2
      3
      4
      5
      6
      7
      8
      import { onBeforeRouteUpdate } from 'vue-router'


      // 目标: 路由参数变化的时候,可以把分类数据接口重新发送
      onBeforeRouteUpdate((to) => {
      // 存在问题: 使用最新的路与参数请求最新的分类数据
      getCategory(to.params.id)
      })
  • 总结

    • 路由缓存问题产生的原因是什么?

      路由只有参数变化时,会复用组件实例

    • 两种方案都可以解决路由缓存问题,如何选择?

      • 在意性能问题时,选择onBeforeRouteUpdate,精细化控制
      • 不在意性能问题时,选择key,简单粗暴

使用逻辑函数拆分业务

  • 概念

    基于逻辑函数拆分业务是指把同一个组件中独立的业务代码通过函数做封装处理,提升代码的可维护性

    使用逻辑函数拆分业务
    VnShEc.png
  • 具体的实现步骤

    1. 按照业务声明以use 打头的逻辑函数

    2. 独立的业务逻辑封装到各个函数内部

    3. 函数内部把组件中需要用到的数据或者方法return 出去

    4. 组件中调用函数把数据或者方法组合回来使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      import {onMounted, ref} from "vue";
      import {getBannerAPI} from "@/apis/home";

      // 1.按照业务声明以`use`打头的逻辑函数
      export function useBanner() {
      const bannerList = ref([])
      const getBanner = async () => {
      const res = await getBannerAPI({
      distributionSite: '2'
      });
      bannerList.value = res.result
      }
      onMounted(() => getBanner())
      return {
      // 3. 函数内部把组件中需要用到的数据或者方法`return`出去
      bannerList
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      import {onMounted, ref} from "vue";
      import {onBeforeRouteUpdate, useRoute} from "vue-router";
      import {findTopCategoryAPI} from "@/apis/category";

      // 2. 把`独立的业务逻辑`封装到各个函数内部
      export function useCategory() {
      const categoryData = ref({})
      const route = useRoute()

      const getCategory = async (id = route.params.id) => {
      // 如何在setup中获取路由参数 useRoute() -> route 等价于this.$route
      const res = await findTopCategoryAPI(id)
      categoryData.value = res.result
      }
      onMounted(() => getCategory(route.params.id))
      // 目标: 路由参数变化的时候,可以把分类数据接口重新发送
      onBeforeRouteUpdate((to) => {
      // 存在问题: 使用最新的路与参数请求最新的分类数据
      getCategory(to.params.id)
      })

      return {categoryData}
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <script setup>

      import GoodsItem from "@/views/Home/components/GoodsItem.vue";
      import {useCategory} from './composeable/useCategory'
      import {useBanner} from './composeable/useBanner'

      // 4. 在`组件中调用函数`把数据或者方法组合回来使用
      const {categoryData} = useCategory()
      const {bannerList} = useBanner()

      </script>

列表的无限加载

  • 核心实现逻辑

    使用elementuiPlus 提供的v-infinite-scroll 指令监听是否满足触底条件,满足加载条件时让页数参数加一获取下一页数据,做新老数据拼接渲染

  • 实现步骤

    分析
    VnoOOt.png
  • 详细使用在elementuiPlus 官网

定制路由scrollBehavior

  • 在不同路由切换的时候,可以自动滚动到页面的顶部,而不是停留在原先的位置

    问题复现 滚动条未还原
    VnJ0lt.png VnJCgJ.png
  • 解决方案

    vue-router 支持scrollBehavior 配置项,可以指定路由切换的滚动位置

  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    // 路由滚动行为定制
    scrollBehavior() {
    return {
    top: 0
    }
    },
    // 路由映射
    ...
    }