Vue3+TS+Vite+Pinia最全总结

VUE3介绍

Vue3+TS+Vite+Pinia学习总结

vue2和vue3之间的区别

  1. 因为需要遍历data对象上所有属性,所以如果data对象属性结构嵌套很深,就会存在性能问题。
  2. 因为需要遍历属性,所有需要提前知道对象上有哪些属性,才能将其转化为getter和setter,所以vue2中无法将data新增的属性转为响应式,只能通过vue提供的vue.set或者this.$set向data中嵌套的对象新增响应式属性,而这种方式并不能添加根级别的响应式属性。
  3. 不能通过下表或者length属性响应式地改变数组,而是必须用数组的方法push,pop,shift,unshift,splice来响应式的改变数组。
  4. 在vue3中使用proxy这个特性,替换了Object.defineProperty重构响应式系统,使用Proxy优势,直接可以监听数组类型的变化,监听的目标为对象本身,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升,可以拦截apply,ownKeys,has等13种方法,而Object.defineProperty不行。直接实现对象属性的新增删除。

Vue3新特性

向下兼容

vue3支持大多数vue2的特性,vue2添加Object数据需this.$set或Vue.set,删除需要Vue.delete或者可能得使用其他得一些办法来解决这个问题。vue3添加Obect数据删除都是响应式。

Composition Api

a. setup函数是一个新的组件选项,作为CompositionApi的入口点。
b. 接受两个参数props和context(attrs,slots,emit)
       1. attrs:值为对象,包含组件外部传递过来,但没有在props配置中声明的属性,相当于vue2种的this.$attrs。

       2. slots:收到的插槽内容,相当于this.$slots。

       3. emit:分发自定义的事件的函数,相当于this.$emit。
执行时机:setup处于beforeCreate生命周期钩子之前的函数
返回值:
      1. 返回一个对象,对象中的属性,方法,在模板中均可以直接使用
      2. 返回一个渲染函数,子当以渲染内容代替模板

Ref和Reactive都是用来创建响应式对象的

ref
  1. ref使用的时候需要.value。
  2. 生命基本数据类型时使用Object.definePropety的原型对象中的get set方法来修改数据实现响应式。
  3. 声明Object类型时内部通过reactive来转为代理对象。
  4. template模板中不需要.value。
reactive
  1. 用来定义Object类型数据。
  2. 操作数据与读取数据都不需要.value。
  3. 通过使用proxy来实现响应式,并通过reflect操作元素对象内部的数据。

生命周期钩子

setup执行时机在beforeCreate之前执行,将beforeDestroy改名为beforeUnmount,destroyed改名为unmounted

钩子说明
setup()开始创建组件之前,在beforeCreate之前执行,创建的是data,method。
onBeforeMount()组件挂载到节点上之前执行的函数
onMounted()组件挂载完成后执行的函数
onBeforeUpdate()组件更新之前执行的函数。
onUpdated()组件更新完成之后执行的函数
onBeforeUnmount()组件卸载之前执行的函数。
onUnmounted()组件卸载完成后执行的函数。
onActivated()若组件实例是 缓存树的一部分,当组件被插入到 DOM 中时调用。
onDeactivated()若组件实例是 缓存树的一部分,比如从 A 组件,切换到 B 组件,A 组件消失时执行。
onErrorCaptured()在捕获了后代组件传递的错误时调用。

watch

watch可以接收三个参数,第一个是要监听的对象,第二个是数据处理变化,第三个是配置项(options),其中options:immediate:true刚一进去就监听一次。

六、setup语法糖

  1. <script setup>
  2. 更少的模板语法
  3. 组件只需要引入不需要注册,属性和方法也不用返回,setup函数也不需要,export default不用写,数据,计算属性和方法,自定义指令都是在template中直接可获取。
  4. 可以直接写await 组件的setup会自动生成 async setup
    defineProps:用来接收父组件传来的值
    propsdefineEmits:用来声明触发的事件表

模板指令

  1. v-model用法更改,在一个组件上支持双向绑定多个属性,例如v-model:forst-name=“first” v-model:last-name="lase"还有子组件defineProps([‘’]),defineEmits([‘’])用来接收和响应事件来更新数据。
  2. 移除keyCode,v-on的修饰符,同事也不再支持config.keyCode。
  3. 移除过滤器vue3认为这个有实现成本,官方目前建议用方法调用或计算属性。
  4. v-if和v-for的优先级对比,V2版本中在一个元素上同时使用v-if和v-for时,v-for会优先作用。V3版本中v-if总是优先于v-for生效。
  5. v-bind合并行为,在V2中如果一个元素同时定义了v-bind="object"和一个相同的单独的property,那么这个单独的property总是会覆盖Object中的绑定,单独的属性覆盖v-bind,V3中这,如果一个元素同时定义了v-bind="object"和一个单独的property,那么声明绑定的顺序决定了它们如何合并。

一、vue3模板语法

创建工程

  1. 在电脑上创建存放vue3学习代码的文件夹: vue3-study。
  2. 使用vscode打开创建的vue3-01-study
  3. 打开vue3-study文件夹,运行终端,进行项目初始化,执行" npm init "回车即可,会生成package.json文件。
  4. 安装vue模块npm install vue@3.2.47,安装完成后在package.json中的dependencies会增加相关依赖信息。
  5. 在vue-3-01-study中创建一个01-helloworld.html文件

编写HTML页面

  1. 采用 <script> 标签引入 Vue 3 库
  2. 定义一个根节点元素 <div id="app">
  3. vue.global.js中会暴露出全局对象 Vue所有顶层 API 都以属性的形式暴露在了全局的 Vue 对象上。
  4. 从 Vue 对象中解构出 createApp 函数,用于实例化 Vue 应用程序:const { createApp } =Vue;
  5. mount 函数:用于挂载应用,指定被 Vue 管理的 Dom 节点入口,节点必须是一个普通的 HTML标签节点,一般是 div。参数值:可以是 DOM 元素或 CSS 选择器字符串。注意:参数值不要指定html 或 body。 注意: .mount() 函数应该始终在整个应用配置和资源注册完成后被调用。data 必须是一个函数:指定初始化数据,在 Vue 所管理的 Dom 节点下,可通过模板语法来进行使用。
  6. 标签体显示数据:{{}}
  7. 表单元素双向绑定数据:"v-model"
    代码:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
    <!-- 1.定义根节点元素 -->
    <div id="app">
      <p>Hellow Word,{{msg}}</p>
      <input type="text" v-model="msg">
    </div>
</body>
<script src="./node_modules/vue/dist/vue.global.js"></script>
<script type="text/javascript">
  //Vue是vue.global.js中暴露出来的全局对象,所有顶层API都以属性的形式暴露在了全局的vue对象上
  const {createApp} = Vue
  //每个Vue应用都是通过createApp函数创建一个新的应用实例
  const app = createApp({
    data(){
      return {
        msg:'hellow Vue3'
      }
    }
  }).mount('#app')
  // mount挂在应用节点,mount函数的参数值是dom元素或者css选择器字符串,不要指定html或body,注意".mount"函数应该始终在整个用用配置和支援注册完成后被调用
  console.log("app",app)
</script>
</html>

直接运行html文件:
Vue3+TS+Vite+Pinia入门到高级学习

分析MVVM模型

什么是MVVM模型?

    MVVM是Model-view-viewModel的缩写,它是一种软件架构风格,其中model为模型,数据对象,view为视图,视图模板页面,viewModel为视图模型,其实本质上就是vue实例。
    把需要改变视图的数据初始化到vue中,然后再通过修改Vue中的数据,从而实现对视图的更新,按照vue的特定语法进行声明开发,就可以实现对应功能,不需要直接操作Dom元素,JQuery就是,需要手动去操作DOM才能实现对应功能。
Vue3+TS+Vite+Pinia入门到高级学习

模板数据绑定渲染

可生成动态的HTML页面,页面中使用嵌入Vue.js语法可动态生成

  1. {{}} 双大括号文本绑定。
  2. v-标签属性名,以v-开头用于标签属性绑定,成为指令。

双大括号语法{{}}

vue3-01-study下创建一个02-模板数据绑定渲染.html

格式{{表达式}}
作用使用在标签体中用于获取数据可以使用JavaScript表达式

代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
    <!-- 1.定义根节点元素 -->
    <div id="app">
      <p>Hellow Word,{{msg}}</p>
      <h2>1、双大括号输出文本内容</h2>
      <p>普通文本:{{ name }}</p>
      <p>JS表达式1{{ salary + 10 }}</p>
      <p>JS表达式2{{ Math.abs(salary) }}</p>
    </div>
</body>
<script src="./node_modules/vue/dist/vue.global.js"></script>
<script type="text/javascript">
  //Vue是vue.global.js中暴露出来的全局对象,所有顶层API都以属性的形式暴露在了全局的vue对象上
  const {createApp} = Vue
  //每个Vue应用都是通过createApp函数创建一个新的应用实例
  const app = createApp({
    data(){
      return {
        msg:'hellow Vue3',
        salary:-100,
      }
    }
  }).mount('#app')
  // mount挂在应用节点,mount函数的参数值是dom元素或者css选择器字符串,不要指定html或body,注意".mount"函数应该始终在整个用用配置和支援注册完成后被调用
  console.log("app",app)
</script>
</html>

运行后:
Vue3+TS+Vite+Pinia入门到高级学习

文本显示指令v-text和v-cloak

使用说明
v-text等价于{{}}用于显示内容,且会覆盖元素中所有现有的内容。区别在于{{}}会有闪烁双大括号表达式问题,v-text不会闪烁。例:

v-cloak用于吟唱未完成编译的DOM模板,默认一开始被Vue管理的模板是隐藏的,当Vue解析处理完DOM模板之后,会自动把这个样式去除,然后就显示出来。如果还想用{{}}又不想又闪烁问题,则使用v-cloak处理,1. 添加一个属性选择器[v-cloak]{display:none}。2.在vue管理的模板入口节点上作用v-cloak,指令也可以做用到子元素上

Vue3+TS+Vite+Pinia入门到高级学习

输出HTML指令 v-html

v-html
  1. 如果是HTML格式数据,双大括号会将数据解释为普通文本,为了输出真正的 HTML,你需要使用 v-html 指令。
  2. v-html 的内容直接作为普通 HTML 插入—— Vue 模板语法不会被解析的。
  3. Vue 为了防止 XSS 攻击,在此指令上做了安全处理,当发现输出内容有Vue 模板语法、script 标签等,则不被解析。

注意:
<style scoped> msg: <p class="name" > 张三</p>,在单文件组件, <scoped> 样式将不会作用于 v-html 里的内容,因为 HTML 内容不会被 Vue 的模板编译器解析。 如果你想让 v-html 的内容也支持
scoped CSS,你可以使用 CSS modules 或使用一个额外的全局<style>元素。
代码

<div id="app">
      <h2>3、v-html 指令输出真正的 HTML 内容</h2>
      <p>双大括号:{{ contentHtml }}</p>
      <!-- 指令的值不需要使用双大括号获取,直接写获取的属性名,错误写法:v-html="{{contentHtml}}" -->
      <p>v-html指令:<span v-html="contentHtml"></span></p>
    </div>
<script src="./node_modules/vue/dist/vue.global.js"></script>
<script type="text/javascript">
  //Vue是vue.global.js中暴露出来的全局对象,所有顶层API都以属性的形式暴露在了全局的vue对象上
  const {createApp} = Vue
  //每个Vue应用都是通过createApp函数创建一个新的应用实例
  const app = createApp({
    data(){
      return {
        contentHtml: '<span style="color:red">红色HTML代码渲染<script>alert("hello vue")'
      }
    }
  }).mount('#app')
  // mount挂在应用节点,mount函数的参数值是dom元素或者css选择器字符串,不要指定html或body,注意".mount"函数应该始终在整个用用配置和支援注册完成后被调用
  console.log("app",app)
</script>

例图:
Vue3+TS+Vite+Pinia入门到高级学习

一次性插值v-once

v-once:一次性插值,当后面数据更新后,视图数据不会更新。
<h2>4、v-once 一次性插值</h2><p v-once> 这个将不会改变: {{ name }} </p>

属性动态绑定:v-bind
完整格式:v-bind:元素的属性名=“xxx”
缩写格式::元素的属性名=“xxx”
作用:将数据动态绑定到指定元素上,活绑定自组建的Prop属性,书写格式一致 修饰符

  1. .camel:将短横线命名的attribute转变为驼峰式明明。由于HTML特性是不区分大小写的,.camel修饰符允许在使用DOM模板时将v-bind属性名称驼峰化,
  2. .attr:强制绑定为DOMattribute
  3. .prop:强制绑定为DOMproprty

class与style属性绑定v-bind

通过class和sty指定样式式数据绑定的一个常见需求。他们都是元素需求,都用v-bind处理,其中表达式结果的类型可以是:字符串,对象或数组。
语法格式: v-bind:class='表达式’或:class=‘表达式’

  1. class的表达式可以为
    字符串:class="active"
    对象:class="{active:'isActive',error:hasError}"
    数组:class="['active','error']" 要加上单引号,不然式获取data中的值

  2. style的表达式支持绑定对象和数组
    :style=“{color:activeColor,fontSize:fibtSize+‘px’}”,注意,对象中的value值activeColor和fontSize是data中的属性。

事件处理指令 v-on

v-on给元素绑定事件监听器,如点击事件onclick,输入框失去事件onblur等,当用于普通元素,只监听原生DOM事件,当用于自定义元素组件,则监听子组件触发的自定义事件。
完整格式:v-on:时间名=“函数名” 或 v-on:时间名=“函数名(参数)” 如:v-on:blur="doNumBlur"、v-on:click="add('hello')"
缩写格式:@时间名="函数名" 或 @事件名="函数名(参数...)"
注意,@后面没有冒号,如@blur,@click
event:函数中的默认形参,代表原生DOM事件,当调用的函数有多个参数传入时,需要使用原生DOM事件,则通过$event作为实参传入
作用:用来将i安亭DOM事件。

事件修饰符:

功能说明
.stop阻止单机事件继续传播 event.stopPropagation()
.prevent阻止时间默认行为event.preventDefault()
once点击事件将只会触发一次

按键修饰符

格式:v-on:keyup.按键名 或者 @keyup.按键名
常用按键名:enter,table,delete,esc,space,up,down,left,right

表单数据双向绑定 v-model

单向绑定:数据变视图变,视图变数据不变,数据不变,上面的都是单向绑定。
<input :value="name" > <span>数据绑定:{{name}}</span>
双向绑定:数据变,视图变,视图变,数据便,表单输入框的内容同步给javaScript中相应的变量,手动连接值绑定和更改事件监听器可能会很麻烦。
<input :value="name" @input="event => name = event.target.value"> <span>数据绑定:{{name}}</span>
使用 v-model 双向绑定,可以简化上面操作。v-model 指令用于表单数据双向绑定,针对以下类型:text 文本,textarea 多行文本,radio 单选按钮,checkbox 复选框,select 下拉框
代码例子:

<body>
  <div id="app">
    <form action="#" @submit.prevent="submitForm">
      姓名(文本)<input v-model="name" name="name" type="text">
      <br><br>
      性别(单选按钮)<input v-model="sex" name="sex" type="radio" :value="0"/><input v-model="sex" name="sex" type="radio" :value="1"/><br><br>
      技能(多选框)<input v-model="skills" type="checkbox" name="skills" value="java">Java开发
      <input v-model="skills" type="checkbox" name="skills" value="vue">Vue.js开发
      <input v-model="skills" type="checkbox" name="skills" value="python">Python开发
      <br><br>
      城市(下拉框)<select name="city" v-model="city">
      <option value="bj">北京</option>
      <option value="sz">深圳</option>
      <option value="nc">南昌</option>
      </select>
      <br><br>
      说明(多行文本)<textarea v-model="remark" name="remark" cols="30" rows="5"></textarea>
      <br><br>
      <button type="submit" >提交</button>
      </form>
      </div>
  </div>
</body>

<script src="./node_modules/vue/dist/vue.global.js"></script>
<script type="text/javascript">
  const { createApp } = Vue;
  // 创建应用实例
  const app = createApp({
    data() {
      return {
        name: "",
        sex: 0, //默认选中:女
        skills: ["vue"], //默认勾选:vue.js开发
        city: "sz", //默认选中:深圳
        remark: "",
      };
    },
    methods: {
      // 是个对象,不要写小括号
      submitForm: function () {
        // 发送ajax请求
        alert(
          this.name +
            "," +
            this.sex +
            "," +
            this.skills +
            "," +
            this.city +
            "," +
            this.remark
        );
      },
    },
  }).mount("#app");
</script>

效果:
Vue3+TS+Vite+Pinia入门到高级学习

v-model修饰符

v-modle.lazy:默认情况下, v-model 会在每次 input 事件后更新数据。你可以添加 lazy 修饰符来改为在每次
change 事件后更新数据。
v-modle.number:自动将输入值转成数字(Vue底层会通过parseInt转数字,不能转数字返回原始值).number 修饰符会在输入框有 type=“number” 时自动启用。

二、Vite构建项目

单文件组件(SFC)

大多数vue项目中,使用一种类似HTML格式的文件来书写Vue组件,目前可以简单的理解为:一个Vue组件对应的是一个html文件,他被称为单文件组件,也被称为*.vue文件,英文single-File Components 缩写为SFC。
简单理解,vue的单文件组件会将一个组件的逻辑,模板,和样式封装在同一个文件里。每一个*.vue文件主要有三种顶层语言模块构成<script> 、 <template> 和 <style>

  1. 每个 *.vue 文件最多可以包含一个顶层 <template> 块。
  2. 每个 *.vue 文件最多可以包含一个 <script> 块。
  3. 每个 *.vue 文件最多可以包含一个 <script setup>
  4. 每个 *.vue 文件可以包含多个 <style> 标签。 scoped 限制当前定义的样式只在当前组件有效。

单文件组件的格式示例:

<script>
	// JS 逻辑代码
	export default {
	  data() {
	    return {
	      count: 0,
	    };
	  },
	};
	</script>
	<template>
	  <!-- HTML 模板代码 -->
	  <button @click="count++">Count is: {{ count }}</button>
	</template>
	<style scoped>
	/* CSS 样式代码 */
	button {
	  font-weight: bold;
	}
</style>

为什么要使用单文件组件SFC,及其优点

  1. 使用熟悉的HTML,css,和JavaScript语法编写模块化的组件。
  2. 在使用组合式API时语法更简单。
  3. 让本来就强相关的关注点自然内聚。
  4. 预编译模板,避免运行时的编译开销。
  5. 组件作用域的CSS
  6. 通过交叉分析模板和逻辑代码能进行更多编译时优化。
  7. 更好的IDE支持,提供自动补全和对模板中表达式的类型检查。
  8. 开箱即用的模块热更新(HMR)支持。
    需要使用SFC必须使用构建工具,Vite是Vue官方提供的Vue构建工具,内置了Vue项目脚手架,直接使用Vite很方便的构建Vue单页面应用。

Vite介绍

Vue3+TS+Vite+Pinia入门到高级学习
Vite(法语意为 “快速的”,发音 /vit/ ,发音同 “veet”)是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

  1. 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
  2. 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态
    资源。

Vite的优点

  1. 急速的服务启动:使用原生的ESM文件,ems标准通过import,export语法实现模块变量的导入和导出。
  2. 轻量快速的热重加载,无论应用程序大小如何,都始终快速的模块热替换。
  3. 对TypeScript,JSX,CSS等支持开箱即用。
  4. 灵活的API和完整的TypeScript类型。

Vite创建第一个Vue项目

  1. 打开窗口命令,cd到要创建项目的文件夹
  2. 在命令行中运行npm init vue@3.6.0
    Vue3+TS+Vite+Pinia入门到高级学习
  3. 项目创建后,通过以下步骤安装依赖并启动开发服务器
	# 进入到项目目录
	# cd <your-project-name>
	cd vue3-02-vite
	# 安装依赖
	npm install
	# 启动项目
	npm run dev

Vue3+TS+Vite+Pinia入门到高级学习
效果:
Vue3+TS+Vite+Pinia入门到高级学习

Vite脚本项目结构

不同版本的vite创建出来的项目,可能目录文件会有所差距,但是没关系的,没有目录和文件你手动创建出来即可。

|-- .vscode: vscode工具相关配置
| |-- extensions.json
|-- node_modules: 存放下载依赖的文件夹
|-- public: 存放不会变动静态的文件,打包时不会被编译
| |-- favicon.ico: 在浏览器上显示的图标
|-- src: 源码文件夹
| |-- App.vue: 应用根主组件
| |-- main.ts: 应用入口JS文件
| |-- components: Vue 子组件及其相关资源文件夹
| |-- assets: 静态文件,会进行编译压缩,如css/js/图标等
|-- .gitignore: Git 版本管制忽略的配置
|-- env.d.ts: 针对环境变量配置,如:声明类型可类型检查&代码提示
(.env.development、.env.production)
|-- index.html: 主页面入口文件
|-- package-lock.json: 用于记录实际安装的各个包的具体来源和版本号等,其他人在 npm install 项目时大家的
依赖能保证一致
|-- package.json: 项目基本信息,包依赖配置信息等
|-- README.md: 项目描述说明文件
|-- tsconfig.config.json: TypeScript 相关配置文件(在tsconfig.json中被引用了)
|-- tsconfig.json: TypeScript 相关配置文件
|-- vite.config.ts: vite 核心配置文件

解决 main.ts 中 import App from ‘./App.vue’ 有红线问题,原因是typescript只能理解.ts文件,无法理解.vue文件,在env.d.ts文件生命*.vue文件是component类型。
Vue3+TS+Vite+Pinia入门到高级学习
Vue3+TS+Vite+Pinia入门到高级学习

项目运行流程分析

  1. http://127.0.0.1:5173/请求到了项目根目录下的index.html页面
  2. index.html页面中,指定渲染出口,并引入了/src/main.ts文件。
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
  1. main.ts入口文件代码
a. 通过vue导出createApp方法,用来创建一个应用实例。
b. 导入应用根组件App.vue
c. 挂在节点#app`(index.html中的id="app")`
d. 最终将app.vue组件代码,在index.html中的`<div id="app"> `渲染出口 `<div>` 中进行渲染。
e. 最后将 App.vue 组件页面效果渲染到浏览器

三、选项式和组合式API语法

创建新的vite项目

  1. 执行 npm init vue@latest 创建项目 vue3-03-vite-api (创建时选择 TypeScript)存放学习Vue3API代码。
  2. cd 进入项目目录,安装依赖 npm i ,启动项目 npm run dev。
  3. 保留 App.vue 组件,其他 .vue 后缀的组件文件全部删除。
  4. 将 App.vue 组件只保留三大顶级元素,多余代码都删除。
<script>
</script>
<template>
<div>hello</div>
</template>
<style scoped>
</style>

比较选项式和组合式API

通过计算器案例,演示不同风格的API编写方式。

选项式

前面知识我们一直使用的是选项式 API。 选项式 API 我们可以用包含多个选项的对象来描述组件的逻辑,例如 data 、 methods
和 computed 等选项。 通过 选项 所定义的属性都会暴露在函数内部的this上,它会指向当前的组件实例。 在<temlate></temlate>模板代码中可以省略 this ,直接写变量名{{ count }}或者方法名@ click="add"

Vue3+TS+Vite+Pinia入门到高级学习

<template>
  <div>
    <div>count值为:{{ count }}</div>
    <button @click="add">点击+1</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    add() {
      // 通过 `this.变量名` 来操作当前组件中 data 选项的变量
      this.count++;
    },
    add2() {
      // 通过 `this.方法名` 调用当前组件的方法
      this.add();
    },
  },
};
</script>
<style scoped>
</style>
组合式 API (Composition API)-setup()

组合式 API 是 Vue3 推出的全新特性,目前 Vue 2.7 也已兼容支持。

先简单介绍下 setup 函数:

  1. setup 函数是在组件中使用组合式 API 的入口
  2. setup 函数中没有 this
  3. setup 函数只会在组件初始化的时候执行一次

组合式 API 我们可以使用导入的 API 函数来描述组件逻辑,如: const { createApp } = Vue 前面html用的也是组合式 API。

  1. 使用组合式API + setup 选项
    在模板中访问从 ref 声明的响应式变量,不需要 .value ,直接引用导出的属性名即可,因为Vue会自动浅层解包。
<template>
  <div>
    <!--不需要 `.value `,直接引用导出的属性名即可,因为Vue会自动浅层解包-->
    <div>count值为:{{ count }}</div>
    <button @click="add">点击+1</button>
  </div>
</template>
<script>
// 组合式API
import { ref } from "vue";
export default {
  // `setup` 是一个专门用于组合式 API 的特殊钩子函数
  setup() {
    // 通过 ref 定义响应式变量,ref参数是初始值
    const count = ref(0);
    // 定义方法
    function add() {
      // 要获取通过ref声明的变量值,需要后面加上 `.value`,即 count.vulue
      count.value++;
    }
    // 返回一个对象,返回值会暴露给模板和其他的选项式 API 钩子
    // return {count: count, add: add}
    return { count, add };
  },
};
</script>
<style scoped>
</style> 
  1. 在 setup() 函数中手动暴露大量的状态和方法非常繁琐,幸运的是,我们可以通过使用构建工具来简化该操作;
    在单文件组件(SFC)中,组合式 API 通常会与 <script setup> 搭配使用,使用它可以大幅度地简化代码。
    这个 setup 属性是一个标识,会告诉 Vue 需要在编译时进行一些处理,比如: <script setup> 中的导入和顶层变量/函数都能够在模板中直接使用
    我们基本上都会在组合式 API 中使用单文件组件 +<script setup>的语法,因为大多数 Vue 开发者
    都会这样使用。
<template>
  <div>
    <!--不需要 `.value `,直接引用导出的属性名即可,因为Vue会自动浅层解包-->
    <div>count值为:{{ count }}</div>
    <button @click="add">点击+1</button>
  </div>
</template>
<script setup>
//  组合式API + setup 属性
import { ref } from "vue";
// 通过 ref 定义响应式变量,ref参数是初始值
const count = ref(0);
// 定义方法
function add() {
  // 要获取通过ref声明的变量值,需要后面加上 `.value`,即 count.vulue
  count.value++;
}
// 不需要导出变量和方法了
</script>
<style scoped>
</style> 

Vue3+TS+Vite+Pinia入门到高级学习

如何选择组合式和选项式风格
  1. 在学习的过程中:推荐采用更易于自己理解的风格。再强调一下,大部分的核心概念在这两种风格之间都是通用的。熟悉了一种风格以后,你也能够很快地理解另一种风格。
  2. 在生产项目中:当你不需要使用构建工具,或者打算主要在低复杂度的场景中使用 Vue,例如渐进增强的应用场景,推荐采用选项式 API。当你打算用 Vue 构建完整的单页应用,推荐采用组合式 API + 单文件组件 .vue。

Vue3新项目毫无疑问用 组合式API

data 选项声明状态-选项式API

在选项式API中,会用 data 选项来声明组件的响应式状态,此选项的值必须为是返回一个对象的函数。
Vue 在创建组件实例的时候会调用此函数,并将函数返回的对象用响应式系统进行包装。
此对象的所有顶层属性都会被代理到组件实例 (即方法和生命周期钩子中的 this ) 上。

  1. data选项返回对象中的属性名:不能以 $ 和 _ 开头。Vue 在组件实例上暴露的内置 API 使用 $ 作为前缀。它同时也为内部属性保留 _ 前缀。因此,你应该避免在顶层 data 上使用任何以这些字符作前缀的属性。
export default {
  data() {
    return {
      // 属性名不能以 `$` 和 `_` 开头
      msg: "提示内容",
      staff: {
        // 对象
        id: 1,
        name: "张三",
        hobbies: ["篮球", "足球"],
      },
    };
  },
  methods: {
    say() {
      this.msg = "陪你学习,伴你梦想";
    },
  },
};
reactive 函数声明状态-组合式API
  1. 使用 reactive() 函数创建一个响应式对象或数组。
  2. 仅对对象类型有效(对象、数组和 MapSet 这样的集合类型),而对 string numberboolean 这样的 原始类型 无效。
  3. reactive() 返回的是一个原始对象的 Proxy 代理对象,其行为表现与一般对象相似;不同之处在于Vue 能够跟踪对响应式对象属性的访问与更改操作。
<template>
  <div>ID{{ state.id }},名称:{{ state.name }},爱好:{{ bobbies }}</div>
</template>
<script setup>
	import { reactive } from "vue";
	// reactive函数的参数值为对象或数组
	const state = reactive({
	  id: 1,
	  name: "张三",
	});
	// 数组
	const bobbies = reactive(["篮球", "足球"]);
	// 响应式对象其实是代理对象 Proxy,其行为表现与一般对象相似。
	// 不同之处在于 Vue 能够跟踪对响应式对象属性的访问与更改操作
	bobbies.push("台球");
	console.log("state", state, bobbies);
</script>
<style scoped>
</style>
  1. reactive() 返回的是一个原始对象的 代理对象 Proxy,它和原始对象是不相等的。只有代理对象是响应式的,更改原始对象不会触发更新。
<script setup>
	import { reactive } from "vue";
	// reactive函数的参数值为对象或数组
	const state = reactive({
	  id: 1,
	  name: "张三",
	});
	// 数组
	const bobbies = reactive(["篮球", "足球"]);
	// 响应式对象其实是代理对象 Proxy,其行为表现与一般对象相似。
	// 不同之处在于 Vue 能够跟踪对响应式对象属性的访问与更改操作
	bobbies.push("台球");
	console.log("state", state, bobbies);
	const obj = { salary: 10000 };
	const state2 = reactive(obj);
	// 代理对象和原始对象不是全等的:false
	console.log("是否为同一对象", obj === state2);
	// 在同一个对象上调用 reactive() 会返回相同的代理:true
	console.log("同一个对象返回相同的代理", reactive(obj) === state2);
</script>
<template>
  <div>
    ID{{ state.id }},名称:{{ state.name }},工资:{{ state2.salary }}
    <button @click="add">点击工资+1</button>
  </div>
</template>
<style scoped>
</style>

Vue3+TS+Vite+Pinia入门到高级学习

reactive + TypeScript 标注类型

TypeScript 是一种基于 JavaScript 构建的强类型编程语言 。

  1. <script>根元素上添加属性 lang="ts" 来声明代码块采用 TypeScript语法 :<script setup lang="ts">
  2. reactive() 会根据参数中声明的属性,自动推导出数据类型。
<script setup lang="ts">
	import { reactive } from 'vue';
	// 推导得到的类型:{ id: number, name: string }
	const user = reactive({ id: 10, name: 'zhangsan' });
</script>
  1. 显式地标注一个 reactive 变量的类型,我们可以使用接口声明类型
<script setup lang="ts">
	import { reactive } from 'vue';
	// TS + reactive 声明属性
	// 推导得到的类型:{ id: number, name: string }
	//const user = reactive({ id: 10, name: 'zhangsan' })
	// 使用接口声明类型
	interface User {
		id: number,
		name: string
	}
	// 显式地标注一个 reactive 变量的类型
	const user: User = reactive({
		id: 10,
		name: 'zhangsan'
	});
	console.log('user', user);
</script>
ref 函数声明状态-组合式API

reactive() 只能声明对象类型(对象,数组,Map,Set等),不能声明原始类型(stringnumberboolean 等)。
reactive() 的种种限制归根结底是:因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。 为此,Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref:ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象

<script setup>
	import { ref } from "vue";
	// ref 可以声明任意类型的响应式属性
	const count = ref(0);
	// 数组
	const user = ref({
	  id: 1,
	  name: "哈哈",
	});
	const hobbies = ref(["篮球", "足球"]);
	// ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象:
	console.log("count", count, user, hobbies); // RefImpl { value: 0 }
	// 获取值 .value
	console.log(count.value, user.value, hobbies.value); // 0
	function add() {
	  // 操作值,一样要 .value
	  count.value++;
	  user.value.id = 2;
	  hobbies.value.push("跑步");
	}
</script>
<template>
  <div>
    <!-- 不用 .value,因为Vue会自动解包 -->
    数量:{{ count }},用户: {{ user }},爱好:{{ hobbies }}
    <button @click="add">新增</button>
  </div>
</template>
<style scoped>
</style>
ref + TypeScript 标注类型

<script> 根元素上添加属性 lang="ts" 来声明代码块采用 TypeScript语法 :<script setup lang="ts">,ref() 会根据参数中声明的属性,自动推导出数据类型。

<!-- 不要少了 lang="ts" -->
<script setup lang="ts">
import { ref } from 'vue';
// 自动推导出的类型:Ref<number>
const age = ref(18);
// 会有ts错误提示:TS Error: Type 'string' is not assignable to type 'number'.
age.value = '20';
</script>

手动显式地为 ref 内的值指定一个更复杂的类型,可以通过使用 Ref (大写字母 R )指定类型。

<!-- 不要少了 lang="ts" -->
<script setup lang="ts">
	import { ref } from 'vue';
	// 要加 `type` 表示导入 Ref 接口类型
	import type { Ref } from 'vue';
	// 手动指定类型
	const age: Ref<number | string> = ref(18);
	age.value = '20'; // 成功,无ts错误提示
</script>

在调用 ref() 时传入一个泛型参数,来覆盖默认的推导行为:

<!-- 不要少了 lang="ts" -->
<script setup lang="ts">
	import { ref } from 'vue';
	// 得到的类型:Ref<number | string>
	const age = ref<number | string>('18');
	age.value = 2020 // 成功,无ts错误提示
</script>

如果你指定了一个泛型参数但没有给出初始值,那么最后得到的就将是一个包含 undefined 的联合类型:

<!-- 不要少了 lang="ts" -->
<script setup lang="ts">
	import { ref } from 'vue';
	// 推导得到的类型:Ref<number | undefined>
	const age = ref<number>();
	console.log('age', age.value); // undefined
</script>
$ref 响应性语法糖 (实验性功能)

value 则感觉很繁琐,并且一不小心就很容易漏掉 .value 。使用 $ref 响应式的变量,可以像普通变量那样被访问和重新赋值,但这些操作在编译后都会变为带 .value 的ref。

响应性语法糖目前是一个实验性功能(具体设计在最终定稿前仍可能发生变化),它是组合式 API 特有的功能,且必须通过构建步骤使用。
响应性语法糖目前默认是关闭状态,需要你显式选择启用。
支持的版本(package.json中可查看:

  1. vue 版本大于等于 3.2.25 ,小于等于 Vue 3.3 ,将在 3.4 及以上版本中被移除
  2. @vitejs/plugin-vue 版本大于等于 2.0.0

每一个会返回 ref 的响应式 API 都有一个相对应的、以 $ 为前缀的宏函数。包括以下这些 API:

  1. ref -> $ref
  2. computed -> $computed
  3. shallowRef -> $shallowRef
  4. customRef -> $customRef
  5. toRef -> $toRef
  6. 当启用响应性语法糖时,这些宏函数都是全局可用的、无需手动导入。

但如果你想让它更明显,你也可以选择从 vue/macros 中引入:

import { $ref } from 'vue/macros'
let count = $ref(0)

使用步骤:
vite.config.ts 文件中启用对语法糖的支持:

	plugins: [
		vue({
			// 开启对语法糖的支持 $ref
			reactivityTransform: true
		})
	],

代码实现:

<script setup>
	// 在 `vite.config.ts` 文件中启用对语法糖的支持:reactivityTransform: true
	let count = $ref(0);
	console.log('count', count); // 0
	function add() {
	count++;
	}
	</script>
	<template>
	<button @click="add">{{ count }}</button>
</template>
shallowReactive 和 shallowRef 浅层响应式对象
  1. reactive() 和 ref() 是深层响应式对象:也就是声明的对象属性不管是多少层级的,会深度监听其所有属性,从而所有属性都是响应式的。

  2. shallowReactive() 是浅层响应式对象:一个浅层响应式对象里,只有根级别的属性是响应式的,嵌套子属性不是响应式的。shallowReactive注意:如果只修改了子属性的值不会响应式,但是一旦修改了根属性值,对应所有属性的新值都会更新到视图中。

  3. shallowReactive()应用场景:我们可以把需要展现在视图层的数据,放置在第一层。而把内部数据放置第二层及以下。

  4. shallowRef():是浅层响应式对象,只处理原始类型的响应式,对象类型是浅层监听不进行响应式处理。不是监听状态的第一层数据的变化, 而是监听 .value 的变化。

    state.value.name = 'xx' //不会被监听到,视图不更新
    state.value = {name: 'xx'} //才会被监听到,视图会更新
    

5.shallowRef()应用场景:如果有一个对象数据,当修改该对象中的属性值不进行响应式更新视图,而是希望当生成新对象来替换旧对象时才进行响应式,则使用shallowRef。

<script setup>
import { shallowReactive, shallowRef } from "vue";
/**
 * shallowReactive 和 shallowRef 是浅层响应式监听
 * shallowReactive:
 * 只监听第一层数据变化,也就是只有根级别的属性是响应式的,嵌套子属性不是响应式的。
 * (注意:如果只修改了子属性的值不会响应式,但是一旦修改了根属性值,
 * 对应所有属性的新值都会更新到视图中。)
 * shallowRef:
 * 对象类型才能浅层监听,对原始类型都是响应式;
 * 不是监听状态的第一层数据的变化, 而是监听 .value 的变化
 * 如 state.value.name = 'xx' 不会被监听到;
 * 如:state.value = {name: 'xx'} 才会被监听到;
 */
// 浅层响应式:只对根属性是响应式的
const shallowState = shallowReactive({
  id: 1,
  name: "张三",
  car: {
    price: 100000,
    color: "red",
  },
});
function testShallowReactive() {
  // 直接改非第一层数据,视图不更新非响应式的
  shallowState.car.price++;
  shallowState.car.color = "yellow";
  // 当修改了第一层数据,也修改了其他层数据,此时会将所有数据都更新到视图
  // 因为当改为第一层会触发此状态的监听器,从而将此状态的所有数据全部更新到视图
  //shallowState.id++;
}
// shallowRef 对原始类型都是响应式
const count = shallowRef(0);
// 浅层监听不是监听状态的第一层数据的变化;而是监听 .value 的变化,如:state.value = {}
const shallowRefState = shallowRef({
  id: 1,
  name: "李四",
  mobile: {
    name: "华为meta60",
    price: 7888,
  },
});
function testShallowRef() {
  console.log("testShallowRef");
  // 原始类型是响应式的
  count.value++;
  // 修改 shallowRefState.value 属性的对象值无响应式
  //shallowRefState.value.id++;
  //shallowRefState.value.mobile.price = '5999';
  // 重新向.value赋值一个全新的的对象,响应式生效
  shallowRefState.value = {
    id: 11,
    name: "meng",
    mobile: {
      name: "赵四",
      price: 5888,
    },
  };
}
</script>
<template>
  <div>
    <p>{{ shallowState.id }} == {{ shallowState.car }}</p>
    <button @click="testShallowReactive">测试shallowReactive浅层响应</button>
    <p>count:{{ count }}</p>
    <p>{{ shallowRefState.id }} == {{ shallowRefState.mobile }}</p>
    <button @click="testShallowRef">测试shallowRef浅层响应</button>
  </div>
</template>
<style scoped>
</style>

效果:
Vue3+TS+Vite+Pinia总结

只读代理 - readonly和shallowReadonly
  1. readonly() 深层次只读代理

readonly() 接收一个对象 (不论是响应式对象还是普通对象) 或是一个 ref,返回一个原值的深层次只读代理。是深层次只读代理:对象的任何层级的属性都是只读的,不可修改的。它的 ref() 解包行为与 reactive() 相同,但解包得到的值是只读的。

<script setup lang="ts">
import { reactive, readonly } from "vue";
/**
 * readonly() 接受一个对象 (不论是响应式还是普通的对象) 或是一个 ref,返回一个原值的只读代理。
 */
const original = reactive({
  count: 0,
  user: {
    name: "张三",
    age: 18,
  },
});
const copy = readonly(original);
function add() {
  // 源属性可修改
  original.count++;
  // 更改该只读副本将会失败,并会得到一个警告
  copy.count++; // [Vue warn] Set operation on key "count" failed: target is readonly
  copy.user.age++; // 深层次只读代理,不可修改
}
</script>
<template>
  <div>
    <span>count:{{ copy.count }}</span>
    <button @click="add">新增</button>
  </div>
</template>
<style scoped>
</style>

Vue3+TS+Vite+Pinia学习总结

  1. shallowReadonly() 浅层次只读代理
  • shallowReadonly() 浅层级只读代理:只有根层级的属性变为了只读,子层级的属性可修改。
  • 谨慎使用:shallowReadonly() 浅层次只读应该只用于组件中的根级状态。请避免将其嵌套在深层次的响应式对象中,因为它创建的树具有不一致的响应行为,这可能很难理解和调试。
import { reactive, shallowReadonly } from 'vue';
const original = reactive({
	count: 0,
	user: {
		name: '赵四',
		age: 18
	}
});
/**
* `shallowReadonly()` 浅层级只读代理:只有根层级的属性变为了只读
*/
const shallowCopy = shallowReadonly(original);
// 得到一个警告,对象的根属性 count 不允许修改
// shallowCopy.count++;
shallowCopy.user.age++; // 修改成功
响应式API-工具函数
  1. isRef()
    检查某个值是否为 ref。常用于条件判断中,返回值布尔类型:truereffalse不是ref

    <script setup>
    	import { ref, isRef, unref } from 'vue';
    	const count = ref(0);
    	// `isRef` 判断是否为 ref,从而是否.value获取值
    	if (isRef(count)) {
    		console.log('是ref', count.value);
    	} else {
    		console.log('非ref', count);
    	}
    </script>
    
  2. unref()
    如果参数是 ref,则返回内部值,否则返回参数本身。等价于 isRef(count) ? count.value : count注意:r 是小写的

// unref(注意:r是小写),参数是ref则会返回.value的值,否则直接返回值本身
//testUnref,接受一个参数x,该参数可以是number类型或者是一个Ref<number>类型的值。
function testUnref(x: number | Ref<number>) {
	// 保证val是具体的值,直接用于逻辑处理
	const val = unref(x);
	console.log('unref', val); // 1
}
<button @click="testUnref(ref(1))">测试unref函数</button>
  1. toRef()
    针对响应式对象的某个属性,创建一个对应的ref,这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然:修改ref的值将更新原属性的值。
import { reactive, toRef } from 'vue';

// `toRef` 将响应对象的某个属性,创建一个对应的ref
const state = reactive({
	name: '张三',
	age: 6
});

// 针对 name 属性创建一个对应的 ref
const ageRef = toRef(state, 'age');

// 更改该 ref 会更新源属性
ageRef.value++;
console.log('state.age', state.age) // 7

// 更改源属性也会更新该 ref
state.age++;
console.log('ageRef', ageRef.value); // 8

//即使源属性当前不存在, toRef() 也会返回一个可用的 ref。
//这让它在处理子组件的可选 props 的时候格外实用,不然为空逻辑处理时可能报错。
const other = toRef(state, 'other');
console.log('other', other);
  1. toRefs()
    将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

toRefs 是创建响应式对象的每个属性的 ref , toRef 是创建响应式对象中的单个属性的 ref 。
toRefs 在调用时只会为源对象上的属性创建 ref。如果要为可能还不存在的属性创建 ref,请改用toRef 。

import { reactive, toRefs } from 'vue';
const user = reactive({
	name: '张三',
	salary: 10000
});
// `toRefs` 是创建响应式对象的每个属性的 `ref`。
const userRefs = toRefs(user);
// userRefs 的类型:{ name: Ref<string>, salary: Ref<number> }
// 这个 ref 和源属性已经双向奔赴了
user.salary++;
// 不要少了.value
console.log('userRefs.salary', userRefs.salary.value) // 10001
// 修改ref值, 源属性也同步了
userRefs.salary.value++;
console.log('user.salary', user.salary) // 10002

toRefs将每个属性转为ref后再进行解构属性,这样可以在模板中直接通过属性名引用

const user = reactive({
  name: 'John Doe',
  salary: 50000,
});
// 直接解构对象属性就不是响应式的
// const {name, salary} = {...user};
// 先将响应式对象user的每个属性转成ref:{ name: Ref<string>, salary: Ref<number> },然后
解构出每个ref属性,
// 在模板中就不需要 {{ user.name }} 获取,直接 {{ name }}获取
const {name, salary} = {...toRefs(user)};
function testToRefs() {
	user.name = '小四';
	user.salary++;
}
<!-- 响应式对象名.属性名 -->
<div>姓名:{{ user.name }},工资:{{ user.salary }} </div>
<!-- 每个属性转成ref后,可直接通过属性名获取 -->
<div>姓名:{{ name }},工资:{{ salary }} </div>
<button @click="testToRefs()">测试toRefs函数</button>
  1. isProxy()
    isProxy() 检查一个对象是否是由 reactive() 、 readonly() 、 shallowReactive() 、 shallowReadonly()创建的代理。

    import { reactive, isProxy } from 'vue';
    const state2 = reactive({
    username: '123456',
    });
    console.log('isProxy', isProxy(state2)); // true
    
  2. isReactive()
    检查一个对象是否是由 reactive() 或 shallowReactive() 创建的代理。

    import { reactive, isReactive } from 'vue';
    const state2 = reactive({
    	username: '123456',
    });
    	console.log('isReactive', isReactive(state2)); // true
    /**
    * isReadonly() 检查传入的值是否为只读对象,
    * 通过 `readonly()` 和 `shallowReadonly()` 创建的代理都是只读的。*/
    const stateCopy = readonly(state2);
    console.log('isReadonly', isReadonly(state2)); // false
    console.log('isReadonly', isReadonly(stateCopy)); // true
    
  3. isReadonly()
    检查传入的值是否为只读对象,通过 readonly() 和 shallowReadonly() 创建的代理都是只读的。
    没有 set 函数的 computed() 计算属性也是只读的,反之则不是只读。

import { reactive, isReactive, isReadonly, readonly, computed } from 'vue';
const state2 = reactive({
	username: '123456',
});
// 深层只读
const stateCopy = readonly(state2);
console.log('isReadonly', isReadonly(state2)); // false
console.log('isReadonly', isReadonly(stateCopy)); // true
// 没有set的计算属性也是只读的,后续会讲
const statusText = computed(() => '张三');
console.log('isReadonly', isReadonly(statusText)); // true
同一组件同时存在两种风格API

在同一组件中,组合式api和选项式api可以同时出现使用。选项式API和组合式API存在相同属性时,引用的是组合式API的属性。两个<script>语言要保持一致,如果指定了 lang="ts" <script lang="ts"> ,另外一个也要指定 <script setup lang="ts"> 。但是不建议这样一起使用,容易造成代码混乱。一般是为了兼容以前vue2的代码才可能会这样子。在vue3强烈建议使用组合式API,可能以后更新把选项式API废除也有可能。

<script lang="ts">
export default {
	data() {
		return {
			message: '选项式API',
			name: '111'
		}
	}
}
</script>
	<!-- 组合式api和选项式api可以同时存在,但是不建议这样作 -->
<script setup lang="ts">
	import { ref } from 'vue';
	// 选项式API和组合式API存在相同属性时,引用的是组合式API的属性
	const message = ref('组合式API');
</script>
<template>
	<div >
		<input v-model="message">
		<input v-model="name">
	</div>
</template>
<style scoped>
</style>

四、计算属性和监听器

计算属性 computed 选项式API

computed 选项定义计算属性,类似于 methods 选项中定义的函数。
计算属性和函数的区别

  1. 函数每次都会执行函数体进行计算。
  2. 计算属性值会基于其响应式依赖缓存,只在相关响应式依赖发生改变时它们才会重新求值。

计算属性-单向绑定(只读)

需求:输入数学与英语分数,采用 methods 与 computed 分别计算出总得分。

<script >
export default {
  data() {
    return {
      mathScore: 80,
      englishScore: 90,
    };
  },
  // methods 选项,用于定义函数
  methods: {
    // 函数只支持单向绑定
    getSumScore() {
      console.info("getSumScore函数被调用");
      // 选项式API调用时都要加上 `this`。减 0 是为了字符串转为数字运算
      return this.mathScore - 0 + (this.englishScore - 0);
    },
  },
  // computed 选项,用于定义计算属性
  computed: {
    // 计算属性默认是get函数仅单向绑定;
    sumScore() {
      console.info("sumScore 计算属性被调用");
      return this.mathScore - 0 + (this.englishScore - 0);
    },
  },
};
</script>
<template>
  <div>
    <div>数学:<input type="text" v-model="mathScore" /></div>
    <div>英语:<input type="text" v-model="englishScore" /></div>
    <div>总分(方法-单向){{ getSumScore() }}</div>
    <div>总分(计算属性-单向)<input type="text" v-model="sumScore" /></div>
  </div>
</template>
<style scoped>
</style>

Vue3+TS+Vite+Pinia学习总结
computed 选项内的计算属性默认是 getter 函数,所以上面只支持单向绑定,当修改数学和英语的数据才会更新总分,而修改总分不会更新数据和英语。

计算属性(双向绑定)

计算属性默认是 getter 函数 ,要实现计算属性双向绑定,需要手动显式声明 getter 和 setter 函数。

<script >
import { ref } from "vue";
export default {
  data() {
    return {
      mathScore: 80,
      englishScore: 90,
    };
  },
  // methods 选项,用于定义函数
  methods: {
    // 函数只支持单向绑定
    getSumScore() {
      console.info("getSumScore函数被调用");
      // 选项式API调用时都要加上 `this`。减 0 是为了字符串转为数字运算
      return this.mathScore - 0 + (this.englishScore - 0);
    },
  },
  // computed 选项,用于定义计算属性
  computed: {
    // 计算属性默认是get函数仅单向绑定;
    sumScore() {
      console.info("sumScore 计算属性被调用");
      return this.mathScore - 0 + (this.englishScore - 0);
    },
    // 计算属性如果要双向绑定,需要手动显式指定get和set函数
    sumScore2: {
      // 要指定对象{},就不是函数了
      // 当get函数体的响应式数据变化后,则会触发更新sumScore2计算属性值,get函数一定要有返回值
      get() {
        console.info("sumScore2 计算属性被调用");
        return this.mathScore - 0 + (this.englishScore - 0);
      },
      // 当sumScore2计算属性变化后,会触发set函数处理
      set(newValue) {
        // value 为更新后的新值
        // 被调用则更新了sumScore2,然后将数学和英语更新为平均分
        const avgScore = newValue / 2;
        this.mathScore = avgScore;
        this.englishScore = avgScore;
      },
    },
  },
};
</script>
<template>
  <div>
    <div>数学:<input type="text" v-model="mathScore" /></div>
    <div>英语:<input type="text" v-model="englishScore" /></div>
    <div>总分(函数-单向){{ getSumScore() }}</div>
    <div>总分(计算属性-单向)<input type="text" v-model="sumScore" /></div>
    <div>总分(计算属性-双向)<input type="text" v-model="sumScore2" /></div>
  </div>
</template>
<style scoped>
</style>

Vue3+TS+Vite+Pinia学习总结

计算属性 computed 组合式API

在组合式API中,计算属性通过 computed 函数进行定义。 声明一个计算属性,调用一次 computed
函数,默认提供了get单向绑定(只读),返回值是一个只读的响应式 ref对象,在setup中需要使用计算属性要使用 计算属性名.value,模板中使用则会自动解包不需要加.value。
要实现计算属性双向绑定,需要手动显式提供计算属性的get和set方法。

<script setup>
// 组合式API,从 vue 中导入 computed 函数
import { ref, computed } from "vue";
const mathScore = ref(88);
const englishScore = ref(99);
// 定义函数,与js中定义函数一样
function getSumScore() {
  return mathScore.value - 0 + (englishScore.value - 0);
}
// 声明一个计算属性,调用一次 computed 函数,默认提供了get单向绑定(只读)
// const sumScore = computed(function () {
// return (mathScore.value - 0) + (englishScore.value - 0);
// });
// // es6 箭头函数简写
// const sumScore = computed(() => {
// return (mathScore.value - 0) + (englishScore.value - 0);
// });
// es6 函数体只有一条语句,且它的返回值作为结果返回,可以简写成如下
const sumScore = computed(() => mathScore.value - 0 + (englishScore.value - 0));
// setup中使用计算属性要:计算属性名.value
console.log("sumScore", sumScore.value);
// 给计算属性提供get和set方法,实现双向绑定
const sumScore2 = computed({
  // get函数体返回的结果就是sumScore2计算属性的值,get函数一定要有返回值
  get() {
    return mathScore.value - 0 + (englishScore.value - 0);
  },
  set(newValue) {
    const avgScore = newValue / 2;
    mathScore.value = avgScore;
    englishScore.value = avgScore;
  },
});
</script>
<template>
  <div>
    <div>数学:<input type="text" v-model="mathScore" /></div>
    <div>英语:<input type="text" v-model="englishScore" /></div>
    <div>总分(函数-单向){{ getSumScore() }}</div>
    <div>总分(计算属性-单向)<input type="text" v-model="sumScore" /></div>
    <div>总分(计算属性-双向)<input type="text" v-model="sumScore2" /></div>
  </div>
</template>
<style scoped>
</style>

效果和以上效果一样

计算属性最佳实战

  1. Getter不应有副作用
    计算属性的 getter 应只做计算,而没有任何其他的”副作用“(例如:不要在 getter 中做异步请求或者更改DOM!),一个计算属性,主要是根据其他值来派生出一个新值,因此 getter 的职责应该仅为计算和返回该值。如果需要根据响应式状态的变更,来创建副作用,可以使用后续我们讲解的 监听器 来创建副作用 避免直接修改计算属性值。

  2. 避免直接修改计算属性值
    从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改,应该更新它所依赖的源状态以触发新的计算。

监听器(侦听器) watch 选项式API

不建议在计算属性的 getter 函数中执行一些”副作用“,而是采用 监听器去执行一些”副作用“。在选项式API中,我们可以使用 watch 选项来监听一个或多个响应式属性,当响应式属性变化时,对应监听器的回调函数会自动调用。通过 watch 选项在每次被监听的响应式属性发生变化时触发。

watch 选项基本使用

用例:监听输入的关键字,如果关键字包含 zhangsan 则模拟请求数据接口获取数据。

<script >
/**
 * 选项式API - watch 选项实现响应式属性的监听
 */
export default {
  data() {
    return {
      keyword: "", // 关键字
      result: "", // 查询结果
    };
  },
  // 监听器,值是个对象{}
  watch: {
    // 指定要监听的:响应式属性,如:当keyword改变时,会触发
    keyword(newVal, oldVal) {
      // 接收两个参数(新值,旧值)
      if (newVal.includes("zhangsan")) {
        // 输入的关键字包含 `zhangsan` 则查询请求结果
        this.searchResult();
      } else {
        this.result = "";
      }
    },
  },
  methods: {
    searchResult() {
      this.result = "查询结果中";
      // 模拟发送请求接口,获取结果
      setTimeout(() => {
        this.result = "hello,zhangsanxuegu!";
      }, 2000);
    },
  },
};
</script>
<template>
  <div>
    <input v-model="keyword" placeholder="请输入带zhangsan的关键字" />
    <div>搜索结果:{{ result }}</div>
  </div>
</template>
<style scoped>
</style>

用例:
Vue3+TS+Vite+Pinia学习总结
监听对象某个属性,注意:用 . 分隔对象属性路径,只能是简单的路径,不支持表达式。

<script >
export default {
	data() {
		return {
			query: {
				username: ''
			}
		}
	},
	// 监听器,值是个对象{}
	watch: {
		// 监听对象某个属性;注意:不要少了引号,只能是简单的路径,不支持表达式。
		'query.username'(newVal) {
			console.log('username', newVal);
		}
	},
}
</script>
<template>
	<div>
		<input v-model="query.username" placeholder="请输入用户名">
	</div>
</template>
<style scoped>
</style>

watch 选项深层监听 deep

watch
默认是浅层监听的:被监听的属性,仅在被重新赋值时,才会触发回调函数,而嵌套属性的变化不会触发。如果想监听所有嵌套的变更,你需要深度监听器:要将函数形式改为对象,handler 为监听的回调函数,再加上 deep: true 开启深度监听。谨慎使用:深度监听需要遍历被监听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

<script >
export default {
	data() {
		return {
			query: {
				username: ''
			}
		}
	},
	// 监听器,值是个对象{}
	watch: {
		/**
		* 监听 query 对象属性
		* 1. 默认是浅层监听,不会监听子属性的改变,所以当query.username改变不会被监听到。
		* 2. 使用 deep 可以深度监听,被监听对象的任意层级子属性改变后都会被监听到
		*/
		// 浅层监听
		// 只会监听到对象自身改变 this.query = {username: 123};
		query(newVal, oldVal) {
			console.log('浅层监听对象query',newVal, oldVal);
		},
		// 深度监听,要将函数形式改为对象,handler 为监听的回调函数,deep: true
		query: {
			handler(newVal, oldVal) { // 监听回调函数名必须是handler
				console.log('深度监听对象query', newVal, oldVal);
			},
			deep: true, // 开启深度监听
		},
	}
}
</script>

watch 即时回调的监听器

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建监听器时,立即执行一遍回调。
比如:我们想请求一些初始数据,然后在相关状态更新时再重新请求数据。我们可以用一个对象来声明监听器,这个对象有 handler 函数和 immediate: true 选项,这样便能强制回调函数初始化立即执行:

watch: {
	// query数据源监听器
	query: {
		handler(newVal, oldVal) { // 监听回调函数名必须是handler
			console.log('深度监听对象query', newVal, oldVal);
		},
		deep: true, // 开启深度监听
		immediate: true, // 强制初始化立即执行回调
	},
}

watch 回调的触发时机 flush

当你更改了响应式状态,它可能会同时触发监听器回调和 Vue 组件更新。默认情况下,监听器回调都会在 Vue 组件更新之前被调用。这意味着你在监听器回调中访问的 DOM 将是被 Vue更新之前的状态。如果想在监听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: ‘post’ 选项(默认是 flush:‘pre’ )

<script >
export default {
  data() {
    return {
      query: {
        username: "",
      },
    };
  },
  // 监听器,值是个对象{}
  watch: {
    // 深度监听,要将函数形式改为对象,handler 为监听的回调函数,deep: true
    query: {
      handler(newVal, oldVal) {
        // 监听回调函数名必须是handler
        // 通过ref指定的元素都会暴露在 `this.$refs` 上,通过它来获取元素后进行操作
        console.log("this.$refs.spanRef", this.$refs.spanRef?.innerHTML);
        console.log("深度监听对象query", newVal, oldVal);
      },
      deep: true, // 开启深度监听
      immediate: true, // 初始化立即执行
      // 默认`pre`:在 Vue 组件更新之前被调用,所以在handler回调中访问的 DOM 将是被 Vue 更新之前的状态。
      // 取值:pre 在组件`更新前`回调,post 在组件`更新后`回调
      flush: "pre",
    },
  },
};
</script>
<template>
  <!-- ref 是一个特殊的属性,类型元素上的id属性,通过ref值直接引用操作此 DOM 元素或组件 -->
  <span ref="spanRef">显示:{{ query.username }}</span>
</template>

Vue3+TS+Vite+Pinia学习总结

监听器 watch() 组合式API

在组合式API中,我们可以使用 watch 函数 在每次响应式状态发生变化时触发回调函数。


监听单个数据源:
	参数一:source 是数据源,可以是以下类型:
		1. 一个函数,且函数要返回一个值
		2. 一个 ref (包括计算属性computed等声明)
		3. 一个响应式对象 reactive等
		4. 或是由以上类型的值组成的数组
	参数二:callback 在数据源(source)发生变化时要调用的回调函数,且回调函数会传递三个参数(新值,旧值,以及一个用于注册副作用清理的回调函数-该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求),一般用接收前两个参数就行了。
	参数三:options 是可选的参数,是一个对象,即:
	{
		immediate?:boolean, // 默认false,true在监听器创建时立即触发回调函数。第一次调用时旧值是
		undefined。
		deep?:boolean, // 默认false,如果数据源是对象,强制深度遍历,以便在深层级变更时触发回调。
		flush?: 'pre' | 'post' // 默认:'pre'在组件`更新前`回调,post 在组件`更新后`回调
	}
function watch<T>(
	source: WatchSource<T>,
	callback: WatchCallback<T>,
	options?: WatchOptions
)
// 监听多个数据源:
function watch<T>(
	// 数据源是数组类型
	sources: WatchSource<T>[],
	// 回调函数接受两个数组,分别对应数据源数组中的新值和旧值。
	callback: WatchCallback<T[]>,
	// 同上面单个数据源一样的
	options?: WatchOptions
)

watch() 函数基本使用

<script setup>
/**
 * 组件式API:
 * 1. watch() 函数实现响应式属性的监听
 */
import { ref, watch } from "vue";
const keyword = ref("");
const result = ref("未查询到");
/**
 * 参数1:监听的响应式属性;
 * - 普通类型的ref直接写变量名,不用加.value
 *
 * 参数2:回调函数,响应式属性值发生变化时触发
 */
watch(keyword, (newVal, oldVal) => {
  if (newVal.includes("wu")) {
    // 输入的关键字包含 `wu` 则查询请求结果
    searchResult();
  } else {
    result.value = "";
  }
});
function searchResult() {
  result.value = "查询结果中";
  // 模拟发送请求接口,获取结果
  setTimeout(() => {
    result.value = "hello,wuyong!";
  }, 2000);
}
</script>
<template>
  <div>
    <input v-model="keyword" placeholder="请输入带wu的关键字" />
    <div>搜索结果:{{ result }}</div>
  </div>
</template>
<style scoped>
</style>

Vue3+TS+Vite+Pinia学习总结

wach() 监听不同类型数据源

监听响应式对象、带返回值的函数、数组方式监听多个数据源。

<script setup>
/**
 * 组件式API:
 * 1. watch() 函数实现响应式属性的监听
 */
import { ref, reactive, watch } from "vue";
const state = reactive({
  query: {
    username: "",
  },
});
const keyword = ref("");
// 监听响应式对象
watch(state.query, (newVal, oldVal) => {
  console.log('state.query', newVal);
});
// 错误,不能直接监听响应式对象的属性值,需要传递一个函数将属性值作为返回值
// watch(state.query.username, (newVal, oldVal) => {
// 改为函数方式,即可监听响应式对象的属性值
watch(
  () => state.query.username,
  (newVal, oldVal) => {
    console.log("state.query.username", newVal);
  }
);
watch(
  keyword,
  (newVal, oldVal) => {
    console.log("keyword【newVal,oldVal】", newVal, oldVal);
  },
  { immediate: true }
);
// 采用数组方式,同时监听多个;
watch(
  [keyword, () => state.query.username],
  ([newKeyword, newUsername], [oldKeyword, oldUsername]) => {
    // 此回调函数的参数使用了解构数组方式不要少了中括号:参数1是监听的每个属性的新值数组,参数2是监听的每个属性的旧值数组;
    console.log(
      "数组方式监听多个",
      newKeyword,
      newUsername,
      oldKeyword,
      oldUsername
    );
  },
  { immediate: true }
);
</script>
<template>
  <div>
    <input v-model="state.query.username" placeholder="请输入用户名" /><br />
    <input v-model="keyword" placeholder="请输入keyWord" />
  </div>
</template>

wach() 深层监听&立即监听&回调触发时机

watch函数的第3个参数为对象,其对象属性有:

deep: true, // 深度监听,多嵌套属性值修改后监听
immediate: true, // 立即监听
flush: ‘pre’ // 默认 pre:在组件 更新前 回调,post: 在组件 更新后 回调

<script setup>
import { ref, watch } from "vue";
// 与模板元素的ref属性进行绑定,通过此ref进行DOM操作等
const spanRef = ref();
// 深度监听、立即监听、回调时机
watch(
  () => state,
  (newVal, oldVal) => {
    console.log("spanRef", spanRef.value?.innerHTML);
    // console.log('监听state', newVal, oldVal)
  },
  {
    deep: true, // 深度监听,多嵌套属性值修改后监听
    immediate: true, // 立即监听
    flush: "pre", // 默认 pre:在组件`更新前`回调,post: 在组件`更新后`回调
  }
);
</script>
<template>
  <div>
    <input v-model="keyword" placeholder="请输入带wu的关键字" />
    <div>搜索结果:{{ result }}</div>
    <input v-model="state.query.username" placeholder="请输入用户名" />
    <span ref="spanRef">{{ state.query.username }}</span>
  </div>
</template>

监听器 watchEffect() 组合式API

watchEffect() 立即运行一个回调函数,会自动监听回调函数中响应式属性,并在响应式属性更改时重新执行。

  1. 会立即执行监听器的回调函数,不用指定 immediate: true
  2. 会自动监听回调函数中使用到的响应式属性,而不会递归地监听所有的属性,没有使用到的不会被监听。
  3. 适用于多个监听依赖项进行业务处理:
    只有一个依赖项的例子来说, watchEffect() 的好处相对较小。但是对于有多个依赖项的监听器来说,使用 watchEffect() 可以消除手动维护依赖列表的负担。
/**
* effect 传递监听触发的函数
* options 选项:
* { flush?: 'pre' | 'post' } // 默认:'pre'在组件`更新前`回调,post 在组件`更新后`回调
*/
function watchEffect(effect, options);

watchEffect() 实现计算购物车的总价

<script setup>
/**
 * 组件式API:watchEffect() 函数实现响应式属性的监听
 * 1. 立即监听
 * 2. 自动监听回调函数中使用到的响应式属性数据;不会深度监听所有的属性,没有使用到的不会被监听
 * 3. 针对同时监听多个依赖项
 */
import { reactive, toRefs, watchEffect, computed } from "vue";
const state = reactive({
  totalMoney: 0, // 总金额
  cartList: [
    // 购物车商品
    { name: "手机", price: 3999, num: 1 },
    { name: "平板", price: 5999, num: 1 },
    { name: "电脑", price: 6999, num: 1 },
  ],
});
// 每个属性转为ref后,再解构出每个ref
const { totalMoney, cartList } = { ...toRefs(state) };
// 自动监听回调函数中使用到的所有属性的值
watchEffect(
  () => {
    console.log("watchEffect被触发");
    state.totalMoney = state.cartList.reduce(
      (prev, curr) => prev + (curr.price || 0) * (curr.num || 0),
      0
    );
  },
  {
    flush: "post", // 'post' 回调中能访问被 Vue 更新之后的 DOM,'pre' (默认) 访问Vue更新前的DOM
  }
);
</script>
<template>
  <div>
    <li v-for="(item, index) in cartList" :key="index">
      <span>{{ item.name }}</span> &nbsp;&nbsp;
      <input v-model="item.price" placeholder="请输入单价" />
      <input v-model="item.num" placeholder="请输入数量" />
    </li>
    <h3>合计总价:{{ totalMoney }}</h3>
  </div>
</template>
<style scoped>
</style>

Vue3+TS+Vite+Pinia学习总结

比较 watch 和 watchEffect

watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:

  1. watch 只监听指定的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。
  2. watch 可手动配置选项来控制:是否立即监听,是否深度监听等,因此我们就能更加精确地控制回调函数的触发时机。
  3. watchEffect 会自动监听回调函数中所有使用到的响应式属性数据。同时监听多个数据源时代码更简洁,但有时其响应性依赖关系会不那么明确。

监听器 watchPostEffect()

watchPostEffect 在Vue更新后的DOM执行监听器回调函数。为了简写 watchEffect() 省去 flush: ‘post’ 。

// 自动监听回调函数中使用到的所有属性的值
watchEffect(() => {
	console.log('watchEffect被触发');
	state.totalMoney = state.cartList.reduce((prev, curr) => prev + (curr.price || 0) *(curr.num || 0), 0);
}, 
{
	flush: 'post' // 'post' 回调中能访问被 Vue 更新之后的 DOM,'pre' (默认) 访问Vue更新前的DOM
});
// 使用 watchPostEffect 简写:回调函数中访问Vue更新后的DOM,
watchPostEffect(() => {
	console.log('watchPostEffect在Vue更新之后执行')
	// vue更新之后执行
	state.totalMoney = state.cartList.reduce((prev, curr) => prev + (curr.price || 0) *(curr.num || 0), 0);
});

五、Vue生命周期函数

Vue 生命周期介绍

每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤。比如设置好数据监听,编译模板,挂载实例到DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。

Vue 3.x 中可以继续使用 Vue 2.x 中的生命周期钩子,但有两个钩子被更名:

  1. beforeDestroy 改名为 beforeUnmount
  2. destroyed 改名为 unmounted

生命周期主要分为四大阶段:初始化阶段、挂载阶段、更新阶段、销毁组件实例

三大阶段选项式API组合式API声明周期钩子说明
初始化阶段beforeCreate 不需要,直接写到setup函数中解析之后,data()computed 等选项处理之前立即调用。setup() 最先被调用,钩子会在所有选项式 API 钩子之前调用,也就是在选项式API的beforeCreate() 前面调用。
初始化阶段created不需要(直接写到setup函数中)created钩子被调用时,会完成的设置有:响应式数据、计算属性、方法和监听器 。
挂载阶段 beforeMountonBeforeMount在组件被挂载之前调用。组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。
挂载阶段mountedonMounted在组件被挂载之后调用。数据和DOM都已被渲染出来。
更新阶段beforeUpdateonBeforeUpdate响应式状态修改,而更新其 DOM 树之前调用。
更新阶段updatedonUpdated响应式状态修改,而更新其 DOM 树之后调用。要在 updated 钩子中更改组件的状态,这可能会导致无限的更新循!
销毁阶段beforeUnmount (Vue2是beforeDestroy )onBeforeUnmount在一个组件实例被卸载之前调用。当这个钩子被调用时,组件实例依然还保有全部的功能。
销毁阶段unmounted(Vue2是 destroyed )onUnmounted在一个组件实例被卸载之后调用。一个组件在以下情况下被视为已卸载:1. 其所有子组件都已经被卸载。2. 所有相关的响应式作用 (渲染作用以及 setup()时创建的计算属性和监听器) 都已经停止。

Vue3+TS+Vite+Pinia学习总结

生命周期钩子示例-选项式API

为了演示组件卸载从而触发生命钩子,我们在 src/components 目录下创建子组件 lifeOption.vue

<script>
/**
 * 选项式API:Vue 生命周期
 */
export default {
  data() {
    return {
      message: "hello, Vue选项式生命钩子",
    };
  },
  beforeCreate() {
    // $el 该组件实例管理的 DOM 根节点,$el 到组件挂载完成 (mounted) 之前都会是空的
    console.log("beforeCreate()", this.$el, this.$data);
  },
  // 已初始化 data 数据,但数据未挂载到模板中
  created() {
    console.log("created()", this.$el, this.$data);
  },
  // 组件被挂载之前调用,已经完成了其响应式状态的设置,但还没有创建 DOM 节点
  beforeMount() {
    console.log("beforeMount()", this.$el, this.$data);
  },
  // 挂载完成,数据和DOM都已被渲染出来
  mounted() {
    console.log("mounted()", this.$el, this.$data);
  },
  // 响应式状态变更,更新 DOM 前调用
  beforeUpdate() {
    // 使用 this.$el.innerHTML 获取更新前的 Dom 模板数据
    console.log("beforeUpdate()", this.$el.innerHTML, this.$data);
  },
  // 响应式状态变更,更新 DOM 后调用
  updated() {
    // data 被 Vue 渲染之后的 Dom 数据模板
    console.log("updated()", this.$el.innerHTML, this.$data);
  },
  // 卸载组件实例前调用
  beforeUnmount() {
    console.log("beforeUnmount()");
  },
  // 卸载组件实例后调用
  unmounted() {
    console.log("unmounted()");
  },
};
</script>
<template>
  <div>
    <span>{{ message }}</span>
  </div>
</template>
<style scoped>
</style>

引入改组件使用:

<script>
// 导入子组件
import LifecycleOptions from "./components/lifeOption.vue";
/**
 * 选项式API:Vue 生命周期
 */
export default {
  components: {
    // 引用子组件
    // LifecycleOptions: LifecycleOptions
    LifecycleOptions, // 简写
  },
  data() {
    return {
      isShow: true,
    };
  },
  methods: {
    handleUnmount() {
      this.isShow = false;
    },
  },
};
</script>
<template>  
  <div>
    <!-- false 会销毁组件 -->
    <LifecycleOptions v-if="isShow" />
    <button @click="handleUnmount">销毁子组件</button>
  </div>
</template>
<style scoped>
</style>

Vue3+TS+Vite+Pinia学习总结

生命周期钩子示例-组合式API

为了演示组件卸载从而触发生命钩子,我们在 src/components 目录下创建子组件 lifeOption.vue

<script setup>
/**
 * 组件式API:vue声明周期
 */
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
} from "vue";
const message = ref("hello,Vue组合式API生命钩子");
const divRef = ref();

// setup替代选项式API的beforCreate,created声明钩子
console.log("setup", divRef.value);

// 组件被挂载之前调用,为创建DOM元素
onBeforeMount(() => {
  console.log("onBeforeMount", divRef.value);
});
//组件被挂载之后调用,已完成数据和DOM渲染
onMounted(() => {
  console.log("onBeforeMount", divRef.value);
});
//响应状态变更,更新DOM前调用
onBeforeUpdate(() => {
  console.log("onBeforeUpdate", divRef.value.innerHTML);
});
// 响应式状态变更,更新 DOM 后调用
onUpdated(() => {
  console.log("onUpdated", divRef.value.innerHTML);
});
// 卸载组件实例前调用
onBeforeUnmount(() => {
  console.log("onBeforeUnmount");
});
// 卸载组件实例后调用
onUnmounted(() => {
  console.log("onUnmounted");
});
</script>
<template>
  <div ref="divRef">
    <span>{{ message }}</span>
  </div>
</template>
<style scoped>
</style>

调用该组件:

<script>
// 导入子组件
import LifecycleOptions from "./components/lifeOption.vue";
import { ref } from "vue";
const isShow = ref(true);
function handleUnmount() {
  isShow.value = false;
}
</script>
<template>
  <LifecycleOptions v-if="isShow" />
  <button @click="handleUnmount">销毁组件</button>
</template>
<style scoped>
</style>

效果:
Vue3+TS+Vite+Pinia学习总结

nextTick 等待 DOM 更新完成后执行

当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
使用 nextTick() 可以在状态改变后立即使用,就是等待 DOM 更新完成后执行相关代码。类似于 updated 生命钩子,更新后dom操作。

  1. 选项式API: nextTick() 函数绑定了组件实例上的,使用 this.$nextTick() 传递一个回调函数作为参
    数。
  2. 组合式API:向 nextTick() 传递一个回调函数作为参数,或者 await 返回的 Promise。
<!--
  nextTick:等待DOM更新完成后,再执行相关代码
-->
<script>
import { nextTick } from "vue";
export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    // 方法体使用了 await,对应方法前要加上 await
    async add() {
      this.count++;
      // DOM 未更新,0
      console.log("DOM 未更新", this.$refs.counterRef.innerHTML);
      // 等待dom更新完成后再执行后面代码,
      // 方式1:使用通过 vue 导入的 nextTick,
      // await nextTick(); // 方法前要加 async
      // console.log('DOM 已更新', this.$refs.counter.innerHTML);
      // 方式2:使用实例上的$nextTick函数,传递一个回调函数,更新完成dom的会自动调用回调函数
      this.$nextTick(() => {
        // DOM 已更新,1
        console.log("DOM 已更新", this.$refs.counterRef.innerHTML);
      });
    },
  },
};
</script>
<!-- 组合式api和选项式api可以同时存在,但是不建议这样作 -->
<script setup>
import { ref, nextTick } from "vue";
const age = ref(100);
const ageRef = ref();
async function sub() {
  age.value--;
  // DOM 未更新,99
  console.log("DOM 未更新", ageRef.value.innerHTML);
  // 使用全局 nextTick 是上面通过 vue 导入的
  // await nextTick(); // 方法前要加 async
  // console.log('DOM 已更新', ageRef.value.innerHTML);
  // 传递回调函数
  nextTick(() => {
    console.log("DOM 已更新", ageRef.value.innerHTML);
  });
}
</script>
<template>
  <button ref="counterRef" @click="add">选项式API{{ count }}</button>
  <button ref="ageRef" @click="sub">组合式API{{ age }}</button>
</template>
<style scoped>
</style>

nextTick 等待 DOM 更新完成后执行

六、自定义指令 v-xxx 和 插件

Vue 内置指令

名称作用
v-text更新元素的文本内容。
v-html内容按普通 HTML 插入渲染, Vue 模板语法是不会被解析的,可防止 XSS 攻击。
v-show根据表达式的真假值,切换元素的 display CSS 属性来显示隐藏元素。
v-clock用于隐藏尚未完成编译的 DOM 模板,解决双大括号闪烁等问题。
v-if根据表达式的真假值,来渲染元素。
v-else前面必须有 v-if 或 v-else-if 。
v-else-if前面必须有 v-if 或 v-else-if 。
v-for遍历的数组或对象
v-on给元素绑定事件监听器。
v-bind动态的绑定一个或多个 attribute,也可以是组件的 prop。
v-model在表单输入元素或组件上创建双向绑定。.lazy - 监听 change 事件而不是 input,.number - 将输入的合法符串转为数字,.trim - 移除输入内容两端空格
v-once一次性插值,当后面数据更新后视图数据不会更新
v-pre元素内具有 v-pre ,所有 Vue 模板语法都会被保留并按原样渲染。会跳过这个元素和它的子元素的编译过程,加快编译。最常见的用例就是 显示原始双大括号标签及内容。例如:网页中的一篇文章,文章内容不需要被 Vue 管理渲染,则可以在此元素上添加 v-pre 忽略文章编译提高性能。
v-memoVue 3.2+版本新增指令,它的值为数组,用于缓存一个节点及子节点,在元素和组件上都可以使
用,只作性能的提升。

自定义指令语法

有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候使用自定义指令更为方便。其他情况下应该尽可能地使用 v-bind 这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。

自定义全局指令

自定义全局指令是在项目的所有组件中都可以使用。

  1. 注册全局指令: 在 main.ts 文件中添加如下代码,注意:注册时,指令名不要带 v-
import { createApp } from 'vue'
import App from './App.vue'

import './assets/main.css'
const app = createApp(App)

// 全局指令
app.directive('指令名',{
  // 在绑定元素的 属性 前,或事件监听器应用前调用
  created(el, binding){},
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding) {},
  //(一般用这个) 在绑定元素的父组件,及他自己的所有子节点都挂载完成后调用
  mounted(el, binding) {
  // 逻辑代码
  },
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding) {},
  // 在绑定元素的父组件,及他自己的所有子节点都更新后调用
  updated(el, binding) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding) {}
})

// 挂载要放到最后
app.mount("#app")

例子:当还未点击页面的其他地方,input元素自动聚焦。

import { createApp } from 'vue'
import App from './App.vue'

import './assets/main.css'
const app = createApp(App)

// 全局指令
app.directive('focus', {
  // 在绑定元素的 属性 前,或事件监听器应用前调用
  created(el, binding) { },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding) { },
  //(一般用这个) 在绑定元素的父组件,及他自己的所有子节点都挂载完成后调用
  // el:v-focus 指令绑定到的dom元素
  // binding:可获取使用了此指令的绑定值 等信息
  mounted(el, binding) {
    // 逻辑代码
    el.focus(); // 自动获取焦点
  },
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding) { },
  // 在绑定元素的父组件,及他自己的所有子节点都更新后调用
  updated(el, binding) { },
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding) { },
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding) { }
})

// 挂载要放到最后
app.mount("#app")

使用指令:

  1. 引用指令时,指令名前面加上v-
  2. 直接在元素上在使用即可:v-指令名="表达式"
<input v-focus>

自定义指令简化形式
自定义指令一般实在mounted和updated上实现相同的行为,除此之外并不需要其他钩子,这种情况下可以直接用一个函数来定义指令。

app.directive('指令名', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
})

按钮级别权限控制,也就是按钮显示或隐藏

// 简写形式: 在 `mounted` 和 `updated` 时都调用
app.directive('auth', (el, binding) => {
	// value 传递的值:v-auth="传递的值"
	const {value} = binding;
	if (value !== 'add:user') {
		// 为了旧版本IE浏览器,不能直接使用el.remove(),要先获取父组节点再删除当前节点
		el.parentNode.removeChild(el);
	}
})

使用:

<!-- 指令值:不能少了单引号,没有单引号就认为变量,要定义对应变量才行 -->
<button v-auth="'add:user'">新增用户</button>

在这里插入图片描述

自定义局部指令

局部指令只能在当前组件中使用,其他组件中无法使用,

  1. 注册局部指令-选项式API,使用 directives 选项,来注册指令
export default {
// 注册局部指令
	directives: {
		'指令名': { // 指令名不要带 v
			mounted(el, binding) {
				// 逻辑代码
			}
		}
	}
}

示例:注册 v-upper-text 指令,将指令中获取到的值,变成大写输出到标签体中,且通过指令来设置字体颜色。

<script>
export default {
  data() {
    return {
      message: "",
    };
  },
  // 注册局部指令
  directives: {
    "upper-text": {
      // 指令名不要带 v-
      // 因为是样式,所以不需要元素插入到DOM中,就好像link引入CSS文件时并不关心元素是否加载
      created(el, binding) {
        el.style.color = "red";
      },
      beforeUpdate(el, binding) {
        // 将在 v-upper-text 指令中获取到的值,变成大写输出到标签体中
        el.innerHTML = binding.value.toUpperCase();
      },
    },
  },
};
</script>
<template>
  <div>
    <!-- 使用指令,指令名前面加 v- -->
    <input v-model="message" />
    输入的内容转成大写:<span v-upper-text="message"></span>
  </div>
</template>

效果:
Vue3+TS+Vite+Pinia最全学习总结
2. 注册局部指令-组合式API
<script setup> 中,任何以 v 开头的驼峰式命名的变量,都可以被用作一个自定义指令。
示例:如果指令值不是 ‘add:staff’ ,按钮就会被移除

<script setup>
/**
 * 组合式:自定义指令
 * 在 <script setup> 中,任何以 v 开头的驼峰式命名的变量,都可以被用作一个自定义指令。
 */
// 声明vPermission变量,对应的就是 v-permission 指令
const vPermission = {
  mounted: (el, binding) => {
    console.log(binding)
    if (binding.value !== "add:staff") {
      el.parentNode.removeChild(el);
    }
  },
};
</script>
<template>
  <div>
    <!-- 如果指令值不是 'add:staff',按钮就会被移除 -->
    <button v-permission="'add:staff'">新增员工</button>
  </div>
</template>

效果:
Vue3+TS+Vite+Pinia最全学习总结

自定义插件

Vue 插件的作用
  1. 插件通常会为 Vue 添加全局功能,一般是添加全局指令、全局实例属性或方法、全局组件等。

向 app.config.globalProperties 中添加一些全局实例属性或方法
通过 app.component() 和app.directive() 注册一到多个全局组件或自定义指令。
通过 app.provide() 使一个资源可被注入进整个应用。

  1. 一个插件有一个公开方法 install() ,通过 install() 方法添加全局功能,方法接收到安装它的应用实例
    app 和额外选项作为参数。
  2. 通过全局方法 app.use() 使用插件。
自定义插件演示-国际化插件
  1. 开发插件, 在 src 目录下创建 plugins 目录,在 plugins 目录建一个 i18n.ts 文件,src/plugins/i18n.ts ,将 I want to learn vue汉化为 我想学习vue
import type {App} from 'vue';

//定义全局插件:中英转换
export default{
  //app式应用实例,options绑定到应用实例时传递的对象
  install:(app:App,options:any)=>{
    console.log("插件选项options:",options)
    // 1.注入全局方法
    app.config.globalProperties.$translate = (key:string) => {
      console.log("插件:注册全局方法生效")
      // 使用key作为索引
      const obj = {a:"我想学vue"} as any
      return obj[key]
    }
    //2. 注入全局指令
    app.directive("my-directive",(el,bindling)=>{
      console.log("插件:注册全局指令生效")
      el.innerHTML = "i18n插件v-my-directive指令:" + options.name
    })
  }
}
  1. 挂载到应用实例app上,要在 main.ts
import { createApp } from 'vue'
import App from './App.vue'
import i18n from './plugins/i18n'
import './assets/main.css'
const app = createApp(App)

// 全局指令
app.directive('focus', {
  // 在绑定元素的 属性 前,或事件监听器应用前调用
  created(el, binding) { },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding) { },
  //(一般用这个) 在绑定元素的父组件,及他自己的所有子节点都挂载完成后调用
  // el:v-focus 指令绑定到的dom元素
  // binding:可获取使用了此指令的绑定值 等信息
  mounted(el, binding) {
    // 逻辑代码
    el.focus(); // 自动获取焦点
  },
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding) { },
  // 在绑定元素的父组件,及他自己的所有子节点都更新后调用
  updated(el, binding) { },
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding) { },
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding) { }
})

app.use(i18n,{name:"汉化插件"})

// 挂载要放到最后
app.mount("#app")

  1. 使用插件,在组件中使用,在选项式API中,使用this获取全局方法变量,在组合式API中,要使用getCurrentInstance()来获取。
<script >
export default {
  mounted() {
    // 在选项式api中,直接this.全局变量/方法
    console.log("onMounted", this.$translate("a"));
  },
};
</script>
<script setup>
// 在 setup 中调用全局方法
import { getCurrentInstance } from "vue";
const instance = getCurrentInstance();
const chinese = instance.appContext.config.globalProperties.$translate("a");
console.log("汉化结果:", chinese);
</script>
<template>
  <div>
    <!-- 在 i18n.ts 插件中定义的全局指令 -->
    <div v-my-directive></div>
    <!-- 调用全局方法,直接方法名即可 -->
    <div>I want to learn vue汉化结果:{{ $translate("a") }}</div>
  </div>
</template>

效果:
Vue3+TS+Vite+Pinia最全学习总结

七、经典实战项目-TodoMVC

效果链接

TodoMVC源码地址:https://github.com/tastejs/todomvc-app-template

git clone https://github.com/tastejs/todomvc-app-template.git

https://todomvc.com/examples/vue/dist/#/

Vite 构建 TodoMVC 项目

基于 Vue3+Vite+TypeScript 来开发 TodoMVC 项目。

初始化 Vite 脚手架项目
  1. 执行命令 npm init vue@3.6.0
    Vue3+TS+Vite+Pinia最全学习总结
  2. 初始化项目后,进入项目目录安装依赖
cd vue3-vite-todomvc
npm install
  1. 启动项目
npm run dev
添加 TodoMVC 模板代码
  1. todomvc-app-template/index.html 页面中的 <section><footer class="info"> 元素体的代码复制到 vue3-vite-todomvc/src/App.vue 文件里的 <template> 元素中。
    Vue3+TS+Vite+Pinia最全学习总结
    Vue3+TS+Vite+Pinia最全学习总结
  2. 安装 TodoMVC 相关依赖
npm install todomvc-app-css@2.4.2 todomvc-common@1.0.5
  1. 在 src/main.ts 文件中,全局方式引入样式
import { createApp } from 'vue'
import App from './App.vue'
import '/node_modules/todomvc-app-css/index.css'
import '/node_modules/todomvc-common/base.css'
createApp(App).mount('#app')

效果:
Vue3+TS+Vite+Pinia最全学习总结

数据列表渲染实战

列表中的记录有3种状态且 <li> 样式不一样:未完成(没有样式)、已完成( .completed )、编辑中( .editing )任务字段 : id (主键) 、 content (内容)、 (状态 :true 已完成, false 未完成 )。无数据.main 和 .footer 标识的标签应该被隐藏 ( v-show ),无数据列表及其下面操作标签都要隐藏。

有数据列表功能实现
<script setup lang="ts">
import { reactive, toRefs } from "vue";
// 声明列表记录的数据类型
interface Todo {
  id: number;
  content: string;
  completed: boolean;
}
interface State {
  todoList: Array<Todo>;
}
const state: State = reactive({
  todoList: [
    {
      id: 1,
      content: "1",
      completed: true,
    },
    {
      id: 2,
      content: "2",
      completed: false,
    },
  ],
});
const { todoList } = { ...toRefs(state) };
</script>
<template>
  <section class="todoapp">
    <header class="header">
      <h1>todos</h1>
      <input class="new-todo" placeholder="What needs to be done?" autofocus />
    </header>
    <!-- This section should be hidden by default and shown when there are todos -->
    <section class="main">
      <input id="toggle-all" class="toggle-all" type="checkbox" />
      <label for="toggle-all">Mark all as complete</label>
      <ul class="todo-list">
        <!-- These are here just to show the structure of the list items -->
        <!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
        <li class="completed" v-for="(item,index) in todoList" :key="item.id" :class="{'completed':item.completed}">
          <div class="view">
            <input class="toggle" type="checkbox" v-model="item.completed" checked />
            <label>{{item.content}}</label>
            <button class="destroy"></button>
          </div>
          <input class="edit" value="Create a TodoMVC template" />
        </li>
      </ul>
    </section>
    <!-- This footer should be hidden by default and shown when there are todos -->
    <footer class="footer">
      <!-- This should be `0 items left` by default -->
      <span class="todo-count"><strong>0</strong> item left</span>
      <!-- Remove this if you don't implement routing -->
      <ul class="filters">
        <li>
          <a class="selected" href="#/">All</a>
        </li>
        <li>
          <a href="#/active">Active</a>
        </li>
        <li>
          <a href="#/completed">Completed</a>
        </li>
      </ul>
      <!-- Hidden if no completed items are left ↓ -->
      <button class="clear-completed">Clear completed</button>
    </footer>
  </section>
  <footer class="info">
    <p>Double-click to edit a todo</p>
    <!-- Remove the below line ↓ -->
    <p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
    <!-- Change this out with your name and url ↓ -->
    <p>Created by <a href="http://todomvc.com">you</a></p>
    <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
  </footer>
</template>

<style scoped>
</style>

效果:
Vue3+TS+Vite+Pinia最全学习总结

无数据隐藏功能

只要判断todoList数组length等于0,则表示没有数据,结合v-show即可。
修改模板中的<section class=main> <footer class="footer">
Vue3+TS+Vite+Pinia最全学习总结

添加任务功能实现

需求:在最上面的文本框中添加新的任务,不允许添加非空数据,按Enter键添任务列表中,并清空文本框。
分析:在添加到列表前,使用.trim()去除空格,如果去除后是空的就不添加,在页面添加Enter按键监听事件,最后像文本框赋空值。

  1. 在文本框中添加v-model.trim="content" 双向绑定且去除空格,且绑定Enter按键监听事件@keyup.enter="addData"

代码:

<script setup lang="ts">
import { reactive, toRefs } from "vue";
// 声明列表记录的数据类型
interface Todo {
  id: number;
  content: string;
  completed: boolean;
}
interface State {
  content: string;
  todoList: Array<Todo>;
}
const state: State = reactive({
  content: "",
  todoList: [
    // {
    //   id: 1,
    //   content: "1",
    //   completed: true,
    // },
    // {
    //   id: 2,
    //   content: "2",
    //   completed: false,
    // },
  ],
});
function addData() {
  //  1.获取文本框输入的数据,手动再次去除空格,防止没有使用v-model.trim
  const content = state.content.trim();

  // 2. 判断数据如果为空,则什么都不做
  if (!content.length) return;

  // 3. 判断如果不为空,则添加到数组中,自动生成ID值
  const id = state.todoList.length + 1;
  state.todoList.push({
    id,
    content,
    completed: false,
  });
  // 4.清空文本框内容
  state.content = "";
}
const { content, todoList } = { ...toRefs(state) };
</script>
<template>
  <section class="todoapp">
    <header class="header">
      <h1>todos</h1>
      <input
        class="new-todo"
        v-model="content"
        @keyup.enter="addData"
        placeholder="What needs to be done?"
        autofocus
      />
    </header>
    <!-- This section should be hidden by default and shown when there are todos -->
    <section class="main" v-show="todoList.length">
      <input id="toggle-all" class="toggle-all" type="checkbox" />
      <label for="toggle-all">Mark all as complete</label>
      <ul class="todo-list">
        <!-- These are here just to show the structure of the list items -->
        <!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
        <li
          class="completed"
          v-for="(item, index) in todoList"
          :key="item.id"
          :class="{ completed: item.completed }"
        >
          <div class="view">
            <input
              class="toggle"
              type="checkbox"
              v-model="item.completed"
              checked
            />
            <label>{{ item.content }}</label>
            <button class="destroy"></button>
          </div>
          <input class="edit" value="Create a TodoMVC template" />
        </li>
      </ul>
    </section>
    <!-- This footer should be hidden by default and shown when there are todos -->
    <footer class="footer" v-show="todoList.length">
      <!-- This should be `0 items left` by default -->
      <span class="todo-count"><strong>0</strong> item left</span>
      <!-- Remove this if you don't implement routing -->
      <ul class="filters">
        <li>
          <a class="selected" href="#/">All</a>
        </li>
        <li>
          <a href="#/active">Active</a>
        </li>
        <li>
          <a href="#/completed">Completed</a>
        </li>
      </ul>
      <!-- Hidden if no completed items are left ↓ -->
      <button class="clear-completed">Clear completed</button>
    </footer>
  </section>
  <footer class="info">
    <p>Double-click to edit a todo</p>
    <!-- Remove the below line ↓ -->
    <p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
    <!-- Change this out with your name and url ↓ -->
    <p>Created by <a href="http://todomvc.com">you</a></p>
    <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
  </footer>
</template>

<style scoped>
</style>

Vue3+TS+Vite+Pinia最全学习总结

显示所有未完成任务数功能实现

需求:在左下角要显示未完成的任务数量,数字是由<strong>标签包装的。还要将item单词多元化(1没有s,其他的数字均有s):0 items,1 item,2 items
分析:当todoList数组中的元素有改变,则重新计算未完成任务数量,可以通过计算属性来获取未完成的任务数量,通过数组函数filter过滤未完成任务,然后进行汇总,当数据为1不显示s,否则显示。

  1. 从 vue 中导入 computed 方法,通过此方法定义一个计算属性 remaining
import { reactive, toRefs,computed } from "vue";
const remaining = computed(()=> state.todoList.filter(item=>!item.completed).length)
  1. 在模板添加剩余任务数remaining,将模板中显示的 item 单词多元化( 1 没有 s , 其他数字均有 s )
    添加 {{ remaining === 1 ? ‘’ : ‘s’ }}
<span class="todo-count"><strong>{{remaining}}</strong> item{{ remaining === 1 ? '' : 's' }} left</span>

Vue3+TS+Vite+Pinia最全学习总结

切换所有任务状态功能实现

需求:点击复选框v后,将所有任务状态标记为复选框相同的状态,当“选中/取消”某个任务后,复选框v也应该同步更新状态。
分析:复选框状态发生变化后,就迭代出每一个任务项,再将复选框状态赋值给每个任务项即可,为复选框绑定一个计算属性,通过这个计算属性的set方法监听复选框更新后就更新任务状态。
当所有未完成任务数remaining为0的时候,表示所有任务都完成了,就可以选中复选框,在复选框绑定的计算属性get方法中判断remaining是否为0,为0则返回true,复选框会自动选中,反之补选中,当remaining发射管变化后,会自动更新复选框状态)。
代码:

const toggleAll = computed({
  get(){
    return remaining.value === 0
  },
  set(newStatus){
    console.log("newStatus",newStatus)
    state.todoList.forEach(item=>item.completed=newStatus)
  }
})

模板代码:

<input id="toggle-all" v-model="toggleAll" class="toggle-all" type="checkbox" />
<label for="toggle-all">Mark all as complete</label>

效果:
Vue3+TS+Vite+Pinia最全学习总结

移除任务项

需求:悬停在某个任务项顶上显示x移除按钮,可点击移除当前任务项。
分析,x移除按钮处添加点击事件,通过数组函数splice移除任务项。

  1. 模板中添加点击事件:@click="removeData(index)"

    <button class="destroy" @click="removeData(index)"></button>
    
  2. 添加函数 removeData , 通过 state.todoList.splice(index, 1) 移除

    function removeData(index: number) {
      // 移除索引为index的一条记录
      state.todoList.splice(index, 1);
    }
    
清除所有已完成任务实现

需求:单机右下角clear completed按钮时候,移除所有已安城任务。当列表中没有已完成的任务时,应该吟唱clear completed按钮。
分析:页面增加点击事件:@click="removeCompleted",添加removeCompleted函数实现过滤处所有未完成的任务项,将过滤出来的未完成数据赋值给items数组,已完成的任务就被删除。
clear completed按钮上使用v-show,当总任务数(items.length)>未完成数(remaining),说明列表中还有已完成数据,则是显示按钮,反之不显示,实现v-show="items.length>remaining"

  1. 模板中添加点击事件:@click="removeCompleted"

    <button class="clear-completed" @click="removeCompleted">Clear completed</button>
    
  2. 添加函数 removeCompleted , 通过数组的 filter 函数过滤出所有未完成的任务项,将过滤出来的未完成数据赋值给 todoList 数组.

    //移除所有已完成任务项
    function removeCompleted() {
      // 过滤出所有未完成的任务(这样已完成的就没有了),然后赋值过滤后的数组即可
      state.todoList = state.todoList.filter((item) => !item.completed);
    }
    
  3. 在模板中使用 v-show="todoList.length > remaining" 进行切换显示/隐藏 Clear completed 按钮.

     <button v-show="todoList.length > remaining" class="clear-completed" @click="removeCompleted">Clear completed</button>
    

效果:
Vue3+TS+Vite+Pinia最全学习总结

编辑任务项

需求1:双击<label>(某个任务项)进入编辑状态,在<li>上通过.editing进行状态切换。
分析1:为label绑定双击事件@dbclick=toEdit(item),当item任务项=== current,当前点击任务项,新定义的响应式属性时,在<li>上就显示.editing样式,格式class={editing:item===current}

需求2:进入编辑状态后,输入框显示原内容,并会自动获取编辑焦点。
分析2:在 <input> 单向绑定输入框的值即可 :value="item.content"
通过自定义指令获取编辑焦点

需求3:输入状态按esc取消编辑,.editing样式应该被移除。
分析3:为 <input> 绑定 Esc 按键事件 @keyup.esc=cancelEdit ,将 current 值变为 null

需求4:按Enter键或市区焦点时,保存改变数据,移除.edition样式。
分析4:添加事件@keyup.enter=finishEdit(item, $event)@blur="finishEdit(item, $event)",通过 $event 变量获取当前输入框的值,使用.trim()去空格后进行判断是否为空,如果为空则清除这条任务,否则修改任务项;添加数据保存任务项中将current值变为 null ,移除 editing 样式。

  1. <label>绑定双击事件 @dblclick="toEdit(item)" , 将迭代出来的每个 item 传入当前行的 toEdit函数中, 在 <li> 上判断是否显示 .editing 样式 :class={editing: item === current}

    <li
     class="completed"
     v-for="(item, index) in todoList"
      :key="item.id"
      :class="{ completed: item.completed,editing:item===current}"
    >
        <div class="view">
          <input
            class="toggle"
            type="checkbox"
            v-model="item.completed"
            checked
          />
          <label  @dblclick="toEdit(item)">{{ item.content }}</label>
          <button class="destroy" @click="removeData(index)"></button>
        </div>
        <input class="edit" value="Create a TodoMVC template" />
      </li>
    
  2. . 在State接口中添加 currentstate 中添加属性 current,添加函数 toEdit(item) 接收到点击的那个 item 后,将它赋值给 current ,那对应的任务项就会进入编辑状态 this.current = item

    interface Todo {
      id: number;
      content: string;
      completed: boolean;
    }
    interface State {
      content: string;
      todoList: Array<Todo>;
      current: Todo | null;
    }
    const state: State = reactive({
      content: "",
      current:null,
      todoList: []
    });
    //双击进入编辑状态
    function toEdit(item:Todo) {
      state.current = item;
    }
    const { content, todoList,current } = { ...toRefs(state) };
    
  3. 编辑窗口显原内容

     <input class="edit" :value="current?.content" />
    

:value=current?.content 的含义是将当前对象(假设它存在且有 content 属性的话)的内容赋值给 value 这个属性或变量。如果 current 不存在或没有 content 属性,则 value 将被赋予 undefined

  1. 取消编辑
    input绑定ESC按键事件@keyup.esc=cancelEdit,将current值变为null,就:class={editing:item===current}不成立了,样式就没有了。
    <input class="edit" :value="item.content" @keyup.esc="cancelEdit" />
    //取消编辑
    function cancelEdiit(){
       // 移除样式
       state.current = null
    }
    
  2. 保存数据
    按Enter键或者失去焦点时,保存改变数据,移除editing样式;

添加事件 @keyup.enter="$event.target.blur()"@blur="finishEdit(item,index, $event)"
注意:回车事件会同时触发失去焦点事件,所以不能写 @keyup.enter=finishEdit(item, index, $event) ,写成这样就会触发两次 finishiEdit 方法,通过@keyup.enter="$event.target.blur()" 回车转向触发失去焦点处理。 通过 $event 变量获取当前输入框的值,使用.trim()去空格后进行判断是否为空,如果为空则 清除这条任务,否则修改任务项; 添加数据保存任务项中 将 current 值变为 null ,移除 editing 样式。

模板代码

 <input
    class="edit"
    :value="current?.content"
    @keyup.esc="cancelEdiit"
    @keyup.enter="$event.target.blur()"
    @blur.stop="finishEdit(item, index, $event)"
  />

逻辑代码

//完成编辑
function finishEdit(item:Todo,index:number,event:any){
  //按esc键退出编辑,会出发失去焦点,调用此方法,直接结束此方法运行
  if(!state.current) return 

  const content = event.target.value.trim();

  //如果为空,则进行删除任务项
  if(!content){
    //重用removeData函数进行删除
    removeData(index);
    return;
  }
  //2.添加数据到任务项
  item.content = content
  //3.移除.editing样式
  state.current = null
}
  1. 获取焦点自定义指令
    刷新页面后,通过自定义全局指令,v-app-focus,内容输入框自动获取焦点,在main.ts中定义全局指令v-app-focus用于新增输入框自动获取焦点。
import { createApp } from 'vue'
import App from './App.vue'

import '/node_modules/todomvc-app-css/index.css'
import '/node_modules/todomvc-common/base.css'

const app = createApp(App)
app.directive('app-focus',(el,binding)=>{
  el.focus()
})
app.mount('#app')

在app.vue中写入自定义指令获取焦点

//自定义局部指令:任何v开头的驼峰式明明的变量都可以用作一个自定义指令,生命vTodoFocus变量,对应的就是v-todo-focus指令
const vTodoFocus = {
  //每个指令的值更新后,会调用此函数
  updated:(el,binding)=>{
    if(binding.value){
      el.focus();
    }
  }
}

模板语法中使用自定义指令:

 <input
   class="new-todo"
   v-app-focus
   v-model="content"
   @keyup.enter="addData"
   placeholder="What needs to be done?"
   autofocus
 />
 <input
   class="edit"
   :value="current?.content"
   @keyup.esc="cancelEdiit"
   @keyup.enter="$event.target.blur()"
   @blur.stop="finishEdit(item, index, $event)"
   v-todo-focus="item===current"
 />

完整代码:

<script setup lang="ts">
import { reactive, toRefs, computed } from "vue";
// 声明列表记录的数据类型
interface Todo {
  id: number;
  content: string;
  completed: boolean;
}
interface State {
  content: string;
  todoList: Array<Todo>;
  current: Todo | null;
}
const state: State = reactive({
  content: "",
  current: null,
  todoList: [
    // {
    //   id: 1,
    //   content: "1",
    //   completed: true,
    // },
    // {
    //   id: 2,
    //   content: "2",
    //   completed: false,
    // },
  ],
});
const remaining = computed(
  () => state.todoList.filter((item) => !item.completed).length
);

const toggleAll = computed({
  get() {
    return remaining.value === 0;
  },
  set(newStatus) {
    console.log("newStatus", newStatus);
    state.todoList.forEach((item) => (item.completed = newStatus));
  },
});
function addData() {
  //  1.获取文本框输入的数据,手动再次去除空格,防止没有使用v-model.trim
  const content = state.content.trim();
  // 2. 判断数据如果为空,则什么都不做
  if (!content.length) return;

  // 3. 判断如果不为空,则添加到数组中,自动生成ID值
  const id = state.todoList.length + 1;
  state.todoList.push({
    id,
    content,
    completed: false,
  });
  // 4.清空文本框内容
  state.content = "";
}
function removeData(index: number) {
  // 移除索引为index的一条记录
  state.todoList.splice(index, 1);
}
//双击进入编辑状态
function toEdit(item: Todo) {
  state.current = item;
}
//移除所有已完成任务项
function removeCompleted() {
  // 过滤出所有未完成的任务(这样已完成的就没有了),然后赋值过滤后的数组即可
  state.todoList = state.todoList.filter((item) => !item.completed);
}
//取消编辑
function cancelEdiit() {
  // 移除样式
  state.current = null;
}
//完成编辑
function finishEdit(item:Todo,index:number,event:any){
  //按esc键退出编辑,会出发失去焦点,调用此方法,直接结束此方法运行
  if(!state.current) return 

  const content = event.target.value.trim();

  //如果为空,则进行删除任务项
  if(!content){
    //重用removeData函数进行删除
    removeData(index);
    return;
  }
  //2.添加数据到任务项
  item.content = content
  //3.移除.editing样式
  state.current = null
}
//自定义局部指令:任何v开头的驼峰式明明的变量都可以用作一个自定义指令,生命vTodoFocus变量,对应的就是v-todo-focus指令
const vTodoFocus = {
  //每个指令的值更新后,会调用此函数
  updated:(el,binding)=>{
    if(binding.value){
      el.focus();
    }
  }
}
const { content, todoList, current } = { ...toRefs(state) };
</script>
<template>
  <section class="todoapp">
    <header class="header">
      <h1>todos</h1>
      <input
        class="new-todo"
        v-app-focus
        v-model="content"
        @keyup.enter="addData"
        placeholder="What needs to be done?"
        autofocus
      />
    </header>
    <!-- This section should be hidden by default and shown when there are todos -->
    <section class="main" v-show="todoList.length">
      <input
        id="toggle-all"
        v-model="toggleAll"
        class="toggle-all"
        type="checkbox"
      />
      <label for="toggle-all">Mark all as complete</label>
      <ul class="todo-list">
        <!-- These are here just to show the structure of the list items -->
        <!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
        <li
          class="completed"
          v-for="(item, index) in todoList"
          :key="item.id"
          :class="{ completed: item.completed, editing: item === current }"
        >
          <div class="view">
            <input
              class="toggle"
              type="checkbox"
              v-model="item.completed"
              checked
            />
            <label @dblclick="toEdit(item)">{{ item.content }}</label>
            <button class="destroy" @click="removeData(index)"></button>
          </div>
          <input
            class="edit"
            :value="current?.content"
            @keyup.esc="cancelEdiit"
            @keyup.enter="$event.target.blur()"
            @blur.stop="finishEdit(item, index, $event)"
            v-todo-focus="item===current"
          />
        </li>
      </ul>
    </section>
    <!-- This footer should be hidden by default and shown when there are todos -->
    <footer class="footer" v-show="todoList.length">
      <!-- This should be `0 items left` by default -->
      <span class="todo-count"
        ><strong>{{ remaining }}</strong> item{{
          remaining === 1 ? "" : "s"
        }}
        left</span
      >
      <!-- Remove this if you don't implement routing -->
      <ul class="filters">
        <li>
          <a class="selected" href="#/">All</a>
        </li>
        <li>
          <a href="#/active">Active</a>
        </li>
        <li>
          <a href="#/completed">Completed</a>
        </li>
      </ul>
      <!-- Hidden if no completed items are left ↓ -->
      <button
        v-show="todoList.length > remaining"
        class="clear-completed"
        @click="removeCompleted"
      >
        Clear completed
      </button>
    </footer>
  </section>
  <footer class="info">
    <p>Double-click to edit a todo</p>
    <!-- Remove the below line ↓ -->
    <p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
    <!-- Change this out with your name and url ↓ -->
    <p>Created by <a href="http://todomvc.com">you</a></p>
    <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
  </footer>
</template>

<style scoped>
</style>

效果:
Vue3+TS+Vite+Pinia最全学习总结

路由状态切换,过滤不同状态数据

需求:根据点击的不同状态( All / Active / Completed ),进行过滤出对应的任务,并进行样式的切换。
分析:

  1. state 中定义变量 filterStatus , 用于接收变化的状态值。
  2. 态值赋值给 filterStatus定义一个计算属性 filterTodoList 用于过滤出目标数据, 用于感知 filterStatus 的状态值变化,当变化后,通过 switch-case +数组的 filter 过滤出目标数据
  3. 在 html 页面中,将 v-for 中之前的 todoList 数组替换为 filterTodoList 迭代出目标数据。
  4. 将被点击状态的 </a> 样式切换为 .select ,通过判断状态值实现,如: filterStatus === 'all'
  1. 在state中定义变量filterStatus,并声明数据类型string,用于接受变化的状态值
	interface State {
	  content: string;
	  todoList: Array<Todo>;
	  current: Todo | null;
	  filterStatus: string;
	}
	const state: State = reactive({
	  filterStatus: "all", //默认全选
	  content: "",
	  current: null,
	  todoList: [],
	});
  1. 通过window.onhashchange监听hash,获取点击的路由hash,#开头,来获取对应的那个状态值,第一次访问页面时,调用一次让状态生效

    //当路由hash值改变后会自动调用此函数
    window.onhashchange = function () {
      console.log("hash改变了", window.location.hash);
      //1. 获取电机的路由hash,当街区的hash不为空返回截取的,为空时返回all
      const hash = window.location.hash.substr(2) || "all";
      //2. 状态一旦改变,将hash赋值给filterStatus,当计算属性filterItems感知到filterStatus变化后,会从新过滤,当filterItems重新过滤处目标数据后,则自动同步更新到视图中
      state.filterStatus = hash;
    };
    // 第一次访问页面时,调用一次让状态生效
    window.onhashchange();
    
  2. 定义一个计算属性 filterTodoList 用于过滤出目标数据, 用于感知 filterStatus 的状态值变化,当变化后,通过 switch-case + filter 过滤出目标数据。

const filterTodoList = computed(() => {
  //state.filterStatus 作为条件,变化后过滤不同数据
  const { filterStatus, todoList } = state;
  switch (filterStatus) {
    case "active": // 过滤出未完成的数据
      return todoList.filter((item) => !item.completed);
      break;
    case "completed": // 过滤出已完成的数据
      return todoList.filter((item) => item.completed);
      break;
    default: // 其他,返回所有数据
      return todoList;
  }
});
  1. 在模板中,将 v-for 中之前的 filterItems 数组替换为 filterTodoList。
<li
  class="completed"
  v-for="(item, index) in filterTodoList"
  :key="item.id"
  :class="{ completed: item.completed, editing: item === current }"
>
  1. 在模板中, 将被点击状态的</a>样式切换为 .select ,通过判断状态值实现。
 <ul class="filters">
        <li>
          <a :class="{selected: filterStatus === 'all'}" href="#/">All</a>
        </li>
        <li>
          <a :class="{selected: filterStatus === 'active'}"  href="#/active">Active</a>
        </li>
        <li>
          <a :class="{selected: filterStatus === 'completed'}" href="#/completed">Completed</a>
        </li>
      </ul>

效果:
Vue3+TS+Vite+Pinia最全学习总结

数据持久化

需求:将所有任务数持久化到localStorage中,它主要用于本地存储数据,localStorage中一般浏览器支持的是5M大小,这个在不同的浏览器中localStorage会有所不同。
分析:使用window.localStorage实例进行保存数据与获取数据

定义itemStorage数据存储对象,里面自定义fetch获取本地数据,save存数据到本地。
修改todoList属性,通过itemStorage.fetch()方法初始化数据。
从vue导入watch方法,用于监听todoList的变化,一旦变化通过itemStorage.save()重新保存数据到本地,因为todoList数组内部是对象,当对象的值发生变化后要被监听到,在选项参数中使用deep:true

代码:

// 本地存储的key
const STORAGE_KEY = "todomvc-data";
import { reactive, toRefs, computed, watch } from "vue";
// 本地存储数据对象
const itemStorage = {
  fetch() {
    return JSON.parse(localStorage.getItem(STORAGE_KEY) || "");
  },
  save(items: any) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
  },
};
}
const state: State = reactive({
  filterStatus: "all", //默认全选
  content: "",
  current: null,
  todoList: itemStorage.fetch(),
});

// 4.  如果 todoList 发生改变,这个回调函数就会运行
watch(
  () => state.todoList,
  (newValue, oldValue) => {
    // 本地进行存储
    itemStorage.save(newValue);
  },
  { deep: true }
); // 发现对象内部值的变化, 要在选项参数中指定 deep: true。

全部代码:

<script setup lang="ts">
import { reactive, toRefs, computed, watch } from "vue";
// 本地存储的key
const STORAGE_KEY = "todomvc-data";
// 本地存储数据对象
const itemStorage = {
  fetch() {
    return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
  },
  save(items: any) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
  },
};
// 声明列表记录的数据类型
interface Todo {
  id: number;
  content: string;
  completed: boolean;
}
interface State {
  content: string;
  todoList: Array<Todo>;
  current: Todo | null;
  filterStatus: string;
}
const state: State = reactive({
  filterStatus: "all", //默认全选
  content: "",
  current: null,
  todoList: itemStorage.fetch(),
});

const remaining = computed(
  () => state.todoList.filter((item) => !item.completed).length
);

const toggleAll = computed({
  get() {
    return remaining.value === 0;
  },
  set(newStatus) {
    console.log("newStatus", newStatus);
    state.todoList.forEach((item) => (item.completed = newStatus));
  },
});
function addData() {
  //  1.获取文本框输入的数据,手动再次去除空格,防止没有使用v-model.trim
  const content = state.content.trim();
  // 2. 判断数据如果为空,则什么都不做
  if (!content.length) return;

  // 3. 判断如果不为空,则添加到数组中,自动生成ID值
  const id = state.todoList.length + 1;
  state.todoList.push({
    id,
    content,
    completed: false,
  });
  // 4.清空文本框内容
  state.content = "";
}
function removeData(index: number) {
  // 移除索引为index的一条记录
  state.todoList.splice(index, 1);
}
//双击进入编辑状态
function toEdit(item: Todo) {
  state.current = item;
}
//移除所有已完成任务项
function removeCompleted() {
  // 过滤出所有未完成的任务(这样已完成的就没有了),然后赋值过滤后的数组即可
  state.todoList = state.todoList.filter((item) => !item.completed);
}
//取消编辑
function cancelEdiit() {
  // 移除样式
  state.current = null;
}
//完成编辑
function finishEdit(item: Todo, index: number, event: any) {
  //按esc键退出编辑,会出发失去焦点,调用此方法,直接结束此方法运行
  if (!state.current) return;

  const content = event.target.value.trim();

  //如果为空,则进行删除任务项
  if (!content) {
    //重用removeData函数进行删除
    removeData(index);
    return;
  }
  //2.添加数据到任务项
  item.content = content;
  //3.移除.editing样式
  state.current = null;
}
//自定义局部指令:任何v开头的驼峰式明明的变量都可以用作一个自定义指令,生命vTodoFocus变量,对应的就是v-todo-focus指令
const vTodoFocus = {
  //每个指令的值更新后,会调用此函数
  updated: (el, binding) => {
    if (binding.value) {
      el.focus();
    }
  },
};
const filterTodoList = computed(() => {
  //state.filterStatus 作为条件,变化后过滤不同数据
  const { filterStatus, todoList } = state;
  switch (filterStatus) {
    case "active": // 过滤出未完成的数据
      return todoList.filter((item) => !item.completed);
      break;
    case "completed": // 过滤出已完成的数据
      return todoList.filter((item) => item.completed);
      break;
    default: // 其他,返回所有数据
      return todoList;
  }
});
//当路由hash值改变后会自动调用此函数
window.onhashchange = function () {
  console.log("hash改变了", window.location.hash);
  //1. 获取电机的路由hash,当街区的hash不为空返回截取的,为空时返回all
  const hash = window.location.hash.substr(2) || "all";
  //2. 状态一旦改变,将hash赋值给filterStatus,当计算属性filterItems感知到filterStatus变化后,会从新过滤,当filterItems重新过滤处目标数据后,则自动同步更新到视图中
  state.filterStatus = hash;
};
// 第一次访问页面时,调用一次让状态生效
window.onhashchange();
const { content, todoList, current, filterStatus } = { ...toRefs(state) };
// 4. +++ 如果 todoList 发生改变,这个回调函数就会运行
watch(
  () => state.todoList,
  (newValue, oldValue) => {
    // 本地进行存储
    itemStorage.save(newValue);
  },
  { deep: true }
); // 发现对象内部值的变化, 要在选项参数中指定 deep: true。
</script>
<template>
  <section class="todoapp">
    <header class="header">
      <h1>todos</h1>
      <input
        class="new-todo"
        v-app-focus
        v-model="content"
        @keyup.enter="addData"
        placeholder="What needs to be done?"
        autofocus
      />
    </header>
    <!-- This section should be hidden by default and shown when there are todos -->
    <section class="main" v-show="todoList.length">
      <input
        id="toggle-all"
        v-model="toggleAll"
        class="toggle-all"
        type="checkbox"
      />
      <label for="toggle-all">Mark all as complete</label>
      <ul class="todo-list">
        <!-- These are here just to show the structure of the list items -->
        <!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
        <li
          class="completed"
          v-for="(item, index) in filterTodoList"
          :key="item.id"
          :class="{ completed: item.completed, editing: item === current }"
        >
          <div class="view">
            <input
              class="toggle"
              type="checkbox"
              v-model="item.completed"
              checked
            />
            <label @dblclick="toEdit(item)">{{ item.content }}</label>
            <button class="destroy" @click="removeData(index)"></button>
          </div>
          <input
            class="edit"
            :value="current?.content"
            @keyup.esc="cancelEdiit"
            @keyup.enter="$event.target.blur()"
            @blur.stop="finishEdit(item, index, $event)"
            v-todo-focus="item === current"
          />
        </li>
      </ul>
    </section>
    <!-- This footer should be hidden by default and shown when there are todos -->
    <footer class="footer" v-show="todoList.length">
      <!-- This should be `0 items left` by default -->
      <span class="todo-count"
        ><strong>{{ remaining }}</strong> item{{
          remaining === 1 ? "" : "s"
        }}
        left</span
      >
      <!-- Remove this if you don't implement routing -->
      <ul class="filters">
        <li>
          <a :class="{ selected: filterStatus === 'all' }" href="#/">All</a>
        </li>
        <li>
          <a :class="{ selected: filterStatus === 'active' }" href="#/active"
            >Active</a
          >
        </li>
        <li>
          <a
            :class="{ selected: filterStatus === 'completed' }"
            href="#/completed"
            >Completed</a
          >
        </li>
      </ul>
      <!-- Hidden if no completed items are left ↓ -->
      <button
        v-show="todoList.length > remaining"
        class="clear-completed"
        @click="removeCompleted"
      >
        Clear completed
      </button>
    </footer>
  </section>
  <footer class="info">
    <p>Double-click to edit a todo</p>
    <!-- Remove the below line ↓ -->
    <p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
    <!-- Change this out with your name and url ↓ -->
    <p>Created by <a href="http://todomvc.com">you</a></p>
    <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
  </footer>
</template>

<style scoped>
</style>

效果:
Vue3+TS+Vite+Pinia最全学习总结

八、Vue 组件化开发-Component

什么是组件

Vue 中的组件化开发就是把网页的重复代码抽取出来 ,封装成一个个可复用的视图组件,然后将这些视图组件拼接到一块就构成了一个完整的系统。这种方式非常灵活,可以极大的提高我们开发和维护的效。通常一套系统会以一棵嵌套的组件树的形式来组织。
Vue3+TS+Vite+Pinia最全学习总结

组件就是对局部视图的封装:
HTML 结构 <template>
CSS 样式 <style>
JavaScript 行为 <script>
data 数据/ ref/ reactive
methods 方法、
computed 计算属性… 等
提高开发效率,增强可维护性,更好的去解决软件上的高耦合、低内聚、无重用的3大代码问题,Vue 中的组件思想借鉴于 React目前主流的前端框架:Angular、React 、Vue 都是组件化开发思想。

组件的基本使用

为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。
有两种组件的注册类型:【全局注册】和【局部注册】

全局组件注册

一般吧网页中特殊的公共部分注册为全局组件,比如选项卡,分页,输入框等,全局组件注册后,可以再次应用的任意组建的模板中使用。全局组件注册后,可以在此应用的任意组件的模板中使用。

简单使用:

app.component('组件名',{
	template: '定义组件模板'data: function(){ //data 选项在组件中必须是一个函数
		return {}
	}
	//其他选项:methods
})

说明:

  1. 组件名:可使用 首字母大写命名(PascalCase)、驼峰命名(camelCase)、短横线命名(kebab-case) 命名方式
  2. template:定义组件的模板
  3. data :在组件中必须是一个函数

示例:创建一个组件,在component文件夹中创建一个全局组件

<body>
  <div id="app">
    <!--.html 文件的代码称为 Dom 模板,下面 Dom模板 引用组件,注意组件名的书写-->
    <!-- 在 Dom 模板中采用 首字母大写命名(PascalCase) 方式,渲染失败-->
    <!-- <ComponentA></ComponentA> -->
    <!-- 在 Dom 模板中采用 驼峰命名(camelCase) 方式,渲染失败 -->
    <!-- <componentA></componentA> -->
    <!-- 在 Dom 模板中采用 短横线命名(kebab-case) 方式,渲染成功 -->
    <component-a></component-a>
  </div>
  <script src="./node_modules/vue/dist/vue.global.js"></script>
  <script type="text/javascript">
    const { createApp } = Vue;
    const app = createApp({});
    /**
     * 全局组件注册:注册之后多处使用
     * - 参数1:组件名,参数2:组件实现对象
     * - 组件名命名支持:首字母大写命名(PascalCase)、驼峰命名(camelCase)、短横线命名(kebab-case)
     */
    app.component("ComponentA", {
      // data 声明响应式状态,必须是函数
      data() {
        return {
          title: "全局组件A",
        };
      },
      // template 选项指定此组件的模板代码
      template: "<h3>Hello,{{ title }}</h3>",
    });
    // 挂载
    app.mount("#app");
  </script>
</body>

局部组件注册

一般把一些非通用部分注册为局部组件。
格式:

1. JS 对象来定义组件:
var ComponentA = { data: function(){}, template: '组件模板A'}
var ComponentB = { data: function(){}, template: '组件模板B'}
2. 使用 components 选项中注册组件:
const { createApp } = Vue;
const app = createApp({
	components: { // 组件选项
	ComponentA: ComponentA // key:组件名,value: 引用的组件对象
	'component-b': ComponentB
	},
	data(){
		return {
		}
	},
	//...其他选项
});
app.mount('#app');

示例:

<body>
  <div id="app">
    <!--通过组件名直接使用, 在 Dom 中只支持短横线 (kebab-case) 形式-->
    <component-b></component-b>
    <!-- <ComponentB></ComponentB> -->
    <!-- <componentB></componentB> -->
    <!-- 不会渲染出 hello -->
    <!-- <component-b /> <span>hello</span> -->
  </div>
  <script src="./node_modules/vue/dist/vue.global.js"></script>
  <script type="text/javascript">
    // 定义局部组件对象
    const ComponentB = {
      template: "<h3>Hello:{{ name }}</h3>",
      data() {
        return {
          name: "局部组件",
        };
      },
    };
    const { createApp } = Vue;
    const app = createApp({
      components: {
        // 局部组件注册选项
        ComponentB, // Es6简写,等价于 ComponentB: ComponentB
      },
    });
    app.mount("#app");
  </script>
</body>

Dom模板和字符串模板

在 .html 文件中,被 Vue 挂载的 HTML 代码,称为 Dom 模板(又称 Html 模板)。

<!-- xxx.html -->
<div id="app">
	Dom 模板:此区域编写的代码都为 Dom 模板代码
</div>

在 Dom 模板中引用组件时,有几点特殊使用要求:

  1. Dom模板中引用组件必须使用 短横线(kebab-case) 引用 ;不能使用 首字母大写(PascalCase)、驼峰(camelCase),因为是由于原生 HTML 解析行为的限制,HTML 标签和属性名是不分大小写的,所以浏览器会把任何大写的字符解释为小写。

如:<ComponentA>会被浏览器解析为<componenta>,导致 vue 中没有对应的componenta组件。 正确写法: <component-a></component-a>

2 .Dom 模板中必须显式地写出关闭标签<compoent-a></component-a>,不能使用直接使用闭合标签<compoent-a />

原因: 由于 HTML 只允许一小部分特殊的元素省略其关闭标签,最常见的就是 <input> 和 <img>
对于其他的元素来说,如果你省略了关闭标签,原生的 HTML 解析器会认为开启的标签永远没有结束。 如: <component-a /><span>hello</span> 解析后渲染结果没有 hello 被覆盖了 因为会解析成: <my-component> <span>hello</span> </my-component> ,会把 </my-component> 闭合 放到最后。

<div id="app">
	<!--正确的:必须使用短横线(kebab-case)引用,且必须显示写关闭标签-->
	<component-a></component-a>
	<!--错误的:不能使用首字母大写(PascalCase)、驼峰(camelCase),不可写闭合标签
	<ComponentA></ComponentA>
	<componentA></componentA>
	<component-a /> <span>hello</span>
	-->
</div>
<script src="./node_modules/vue/dist/vue.global.js"></script>
<script type="text/javascript">
const { createApp } = Vue;
const app = createApp({ });
// 全局组件注册
app.component('ComponentA', {
	template: '<h3>Hello组件A</h3>'
});
app.mount('#app');
</script>
  1. Dom 模板中有元素位置限制,某些 HTML 元素对于放在其中的元素类型有限制。可在这些HTML限用元素上,使用 is="vue:组件名" 属性引用组件。

例如:<ul><table><select> 元素中只能放置特定元素才会显示,一一对应放置 <li><tbody><option> 才能正常显示。
如果把引用自定义组件的代码 <component-a></component-a> 放置到 <ul><table><select>元素中,渲染后的html 代码中自定义组件 <component-a> 的模板代码可能不会放到其元素中。要想让自定义组件 <component-a> 的模板代渲染到其元素中,可以使用 is="vue:组件名" 属性引用组件可解决此问题。

<div id="app">
  <!-- `table` 下放的元素有限制 `tbody`,引用自定义组件代码直接放到table中 -->
  <table>
    <component-a></component-a>
  </table>
  <!-- 上面渲染后的代码:
  <h3>Hello组件A</h3>
  <table></table>
  -->
  <!-- `table` 下放的元素有限制 `tbody`,在限用元素上使用 is="vue:组件名" 来引用组件 -->
  <table>
    <tbody is="vue:component-a"></tbody>
  </table>
  <!-- 上面渲染后的代码:
  <table>
  <h3>Hello组件A</h3>
  </table>
  -->
</div>
什么是字符串模板

在字符串模板中没有以上使用限制。因为字符串模板是要先通过Vue编译的,可以在编译中区分大小写的。

字符串模板中引用组件可以使用首字母大写(PascalCase)、驼峰(camelCase)、短横线(kebab-case)方式。
可以直接使用闭合标签: <ComponentA />,没有HTML限用元素下引用自定义元素限制: <table><component-a></component-a></table>

  1. 在 .html 中 <script type="text/x-template"> 元素中编写的代码,称为字符串模板。
<script type="text/x-template" id="my-component-tempate">
	字符串模板
</script>
<div id="app">
  <my-component>
</div>
  <!--自定义组件模板-->
<script type="text/x-template" id="my-component-tempate">
字符串模板(引用组件没有Dom模板中的限制)
<ComponentA />
</script>
  <script src="./node_modules/vue/dist/vue.global.js"></script>
  <script type="text/javascript">
    // 省略部分代码...
    // 注册全局组件
    app.component('MyComponent', {
      // template选项的值如果以 `#` 开头,它将被用作选择器,并使用所选中元素的 `innerHTML` 作为模  板字符串
      template: '#my-component-tempate' // 值是上面id
    });
    app.component('ComponentA', {
      template: 'Hello组件A'
    });
    app.mount('#app);
  </script>
  1. 在 .html 和 .vue 文件使用 template 选项编写的代码,称为字符串模板。
<div id="app">
	<component-b></component-b>
</div>
<script type="text/javascript">
app.component('ComponentB', {
	// template中的字符串模板(引用组件没有Dom模板中的限制)
	template: '<ComponentA /> <span>梦学谷</span>'
});
</script>

基于Vite使用单文件组件(SFC)

在使用Vue开发项目时,一般使用 .vue 单文件组件(SFC) 来进行编码,所以我们下面就基于Vite使用构建讲解多组件开发。

基于 Vite 注册全局组件

在 main.ts 文件中注册全局组件 ComponentCount ,在此项目的其他任意组件中都可以使用。

import { createApp } from 'vue'
import App from './App.vue'

import './assets/main.css'
const app = createApp(App)
/**
* 全局组件注册:
* 组件名:支持 首字母大写命名(PascalCase)、驼峰命名(camelCase)、短横线命名(kebab-case)
*/
app.component('ComponentCount', {
  template: '<button @click="count++">全局组件:{{count}}</button>',
  // data() { // 选项式API
  // return {
  // count: 0
  // }
  // },
  setup() { // 组合式API
    const count = ref(0);
    return { count }
  },
}
);
app.mount('#app')

在 App.vue 中使用全局组件,直接在<template></template>字符串模板区域引用组件名即可,不需要import导入。

<script setup lang="ts">
</script>

<template>
  <component-count></component-count>
  <componentCount></componentCount>
  <ComponentCount></ComponentCount>
  <ComponentCount />
</template>

<style scoped></style>

结果报错:
Vue3+TS+Vite+Pinia最全学习总结
解决:
vite.config.ts 添加如下配置: 'vue': 'vue/dist/vue.esm-bundler.js'

import { fileURLToPath, URL } from 'node:url'

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

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
      // 指定vue使用运行时即时编译构建版本,就可以使用 template 选项定义字符串模板
      'vue': 'vue/dist/vue.esm-bundler.js', // 默认是:'vue/dist/vue.runtime.esm-bundler.js'
    }
  }
})

效果:Vue3+TS+Vite+Pinia最全学习总结

使用 .vue 单文件组件(SFC)声明组件

由上面可看出其实 Vue 官方是不推荐我们使用 template 选项来定义模板,代码累赘 堆积在一起阅读性差不好维护。
下面使用 .vue 单文件组件(SFC)来声明组件,再将 .vue 文件导入注册为全局组件。

  1. src/compoents 目录下创建compoents01.vue单文件组件
<!-- 选项式API -->
<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>
<!-- 组件式API -->
<script setup>
import { ref } from "vue";
const name = ref('SFC全局组件:');
</script>
<template>
  <button @click="count++">{{ name }}{{ count }}</button>
</template>
<style></style>
  1. main.ts 文件中导入组件,并将其注册为全局组件。
import { createApp, ref } from 'vue'
import App from './App.vue'

import './assets/main.css'
// 第1步:导入组件
import GlbComponent from './components/GlbComponent.vue'
const app = createApp(App)

// 第2步:注册为全局组件
app.component('GlbComponent', GlbComponent);
app.mount('#app')

  1. 在 App.vue 直接使用全局组件
<script setup lang="ts">
</script>

<template>
  <glb-component></glb-component>
  <GlbComponent></GlbComponent>
</template>

<style scoped></style>

效果:Vue3+TS+Vite+Pinia最全学习总结

基于 Vite 注册局部组件
  1. 在 App.vue 中注册局部组件 ComponentA
<script>
export default {
  // 注册局部组件:只在当前组件中使用
  components: {
    // 组件名:组件实例
    'ComponentA': {
      // 需要在 vite.config.ts 配置 'vue': 'vue/dist/vue.esm-bundler.js'
      template: '<h3>局部组件:{{name}} </h3>',
      data() {
        return {
          name: 'hello word Vue3~'
        }
      }
    }
  },
}
</script>
<template>
  <!-- 引用局部组件 -->
  <ComponentA />
</template>
<style></style>
  1. 使用 .vue 单文件组件(SFC)来定义组件,在 src/compoents 目录下创建 MyComponent.vue

第1步:定义组件/components/MyComponent.vue
第2步:导入组件import MyComponent from '@/components/MyComponent.vue';
第3步:注册为局部组件(如果在使用 <script setup> 的单文件组件中,导入的组件可以直接在模板中使用,无需注册)
第4步:模板中使用局部组件(局部组件在哪个文件中注册,则只能在这个文件中使用)

<script>
// 第1步:导入组件
<script>
// 第1步:导入组件
import MyComponent from '@/components/MyComponent.vue';
export default {
  // 注册局部组件:只在当前组件中使用
  components: {
    // 组件名:组件实例
    'ComponentA': {
      // 需要在 vite.config.ts 配置 'vue': 'vue/dist/vue.esm-bundler.js'
      template: '<h3>局部组件:{{name}} </h3>',
      data() {
        return {
          name: 'hello word vue3!'
        }
      }
    },
    // 第2步:注册为局部组件
    MyComponent // 等价 MyComponent: MyComponent
  },
}
</script>
<template>
  <!--3步使用局部组件 -->
  <MyComponent />
</template>
<style></style>

<script setup>中导入 OtherComponent.vue 组件后,在模板中就可使用,不需要注册。

<script setup>
// 第一步:在<script setup>中导入的组件,不需要注册,直接在模板中引用此组件即可
import OtherComponent from '@/components/OtherComponent.vue';
</script>
<template>
  <!-- 第二步: 在<script setup>中导入的组件,不需要注册可直接使用 -->
  <OtherComponent />
</template>
<style></style>
注册组件小结

全局组件和局部组件都推荐 .vue 单文件组件(SFC)方式来定义组件,不推荐使用 template 选项来定义模板。
如果在Vite构建项目中使用了 template 定义模板,需要在 vite.config.ts 中配置 'vue': 'vue/dist/vue.esm-bundler.js' 指定vue使用运行时即时编译构建版本。
<template></template>字符串模板中引用组件:

  1. 可采用首字母大写(PascalCase)、驼峰形式(camelCase)、短横形式(kebab-case) 方式引用。
  2. 推荐使用首字母大写(PascalCase)方式。
  3. 可直接使用闭合标签。

全局组件:

  1. 定义 .vue 单文件组件;
  2. 在 main.ts 中,
  3. import 导入 .vue 单文件组件;
    通过app.component(组件名, 组件实例)函数注册为全局组件;
    强制要求 app.component 函数中每个全局组件名不得重复;
  1. 在其他任意组件中,直接通过注册的组件名直接引用即可,不需要 import 导入。

局部组件:

  1. 定义 .vue 单文件组件;
  2. 在引用组件文件中, import 导入 .vue 单文件组件。如果是 <script> 导入的,则在 components 选项中注册为此文件中的局部组件 如果是 <script setup> 导入的,不需要在 components 选项中注册。
  1. 在引用组件文件中,通过组件名引用组件即可。

Vue 父子组件通信

props / defineProps / withDefaults

组件间通信方式
  1. 子组件接收父组件数据。

    子组件使用选项式API: props 选项
    子组件使用组件式API: defineProps() 函数声明、 withDefaults()设置默认值。
    父组件向子组件传递 props 时,可以使用 kebab-case 横杠 和 camelCase驼峰(在 DOM 模板不支持驼峰)。

  2. 自定义事件

    选项式API: $emit 选项
    组件式API: defineEmits 函数
    在父组件模板中可以使用 kebab-case 横杠 和
    camelCase 驼峰来绑定事件监听器, vue官网也推荐使用 kebab-case 形式。@update-member="updateMember"

  3. 子组件导出状态/方法给父组件操作

    子组件使用选项式API:不需要导出,父组件直接可操作属性和方法。
    data中定义的状态属性、methods中定义的方法等都是公开,父组件可以直接访问到属性/方法 子组件使用 <script setup>
    组合式API时: defineExpose() 导出 子组件是默认关闭的,在 <script setup> 中声明的任何属性、方法等都是不公开,默认父组件是 不可以直接访问;
    可以通过 defineExpose 方法显式指定在 <script> setup> 组件中要暴露出去的属性/方法。

  4. 插槽分发内容: slot

注意:

defineProps() 、 withDefaults() 、 defineEmits() 、 defineExpose() 这些函数必须直接放置在<script setup> 的顶级作用域下,不能在子函数中使用。

子组件 props 选项声明 prop - 选项式API

子组件接收父组件数据,先在子组件声明 prop,父组件通过 v-bind: 组件属性方式动态传递数据。

  1. 使用 props 选项在组件对象中声明组件props,用于接收父组件传递的数据
<script>
export default {
	props: 此处值有以下3种方式,
	data() {
		return {}
	},
	setup(props) {
		// setup() 接收 props 作为第一个参数
		console.log('props', props);
	}
}
</script>

方式1:使用字符串数组来声明 ·,声明的每个 prop 都非必传的(可传也可以不传)

props: ['id','name''isPublished', 'commentIds', 'getEmp']

方式2:使用对象声明 prop ,key 是属性名 和 value 数据类型构造函数(首字母大写,是预期类型的构造函数,比如,如果要求一个 prop 的值是 number 类型,则可使用 Number 构造函数作为其声明的值。)

props: {
	id: Number, // 默认为 非必传
	name: String,
	isPublished: Boolean,
	commentIds: Array,
	getEmp: Function
}

方式3:指定属性名、数据类型、必要性、默认值

props: {
	name: { // 多种约束,使用对象形式
		type: String,
		required: true, // 是否必传,true父组件必须绑定传递
		default: 'mxg'
	}
}

例子:
子组件代码:

<script lang="ts">
export default {
  // 方式1:使用字符串数组来声明 prop ,声明的每个 prop 都非必传的(可传也可以不传)。
  // props: ['id', 'name', 'isPublished', 'commentIds', 'getEmp'],
  // 方式2:使用对象声明 prop ,key 是属性名 和 value 数据类型构造函数或约束对象
  props: {
    id: Number, // 默认为 非必传
    name: {
      type: String, // 数据类型的构造函数
      required: true, // 是否必传,true父组件必须绑定传递
      default: '' // 父组件未绑定此prop,则取当前默认值
    },
    isPublished: Boolean,
    commentIds: Array,
    getEmp: Function
  },
  methods: {
    print() {
      // 通过 this 可访问到 props
      console.log('print', this.id, this.name, this.isPublished);
      // this.getEmp();
      // props 对象方式,默认是非必须,ts会校验类型 getEmp可能未定义
      this.getEmp && this.getEmp();
    }
  },
  setup(props) {
    // setup() 接收 props 作为第一个参数
    console.log(props, props.id, props.name, props.isPublished);
  },
}
</script>
<template>
  <div>
    <h3>子组件</h3>
    <button @click="print">打印props</button>
    <p>
      <!-- 模板中可省略 this ,直接prop名称可获取 -->
      id: {{ id }},name: {{ name }}, {{ isPublished }}
    </p>
  </div>
</template>

父调用代码:

<script setup>
// 第一步:在<script setup>中导入的组件,不需要注册,直接在模板中引用此组件即可
import ChildComponent from '@/components/childComponent.vue';
function getEmpData() {
  console.log('我被打印了');
}
</script>
<template>
  <!-- 第二步: 在<script setup>中导入的组件,不需要注册可直接使用 -->
  <ChildComponent v-bind:id="1" name="吴用" :is-published="true" :comment-ids="[1, 2]" :getEmp="getEmpData" />
</template>
<style></style>

效果:
Vue3+TS+Vite+Pinia最全学习总结

子组件 props 函数声明 prop - 组合式API

<script setup>的单文件组件中,使用 defineProps() 函数来声明props
注释子组件 ChildComponent.vue 代码 <script lang="ts"> export default{ xxx } </script>子组件 ChildComponent.vue 添加如下 <script setup lang="ts"> 代码:
子组件代码:

<script lang="ts" setup>
interface Props{
  id:number,
  name:string,
  isPublished?:boolean, //可选
  commentIds?:number[], //number类型数组
  getEmp?:Function //首字母大写
}
const props = defineProps<Props>();
function print(){
  console.log("print",props.id,props.name,props.isPublished)
  props.getEmp && props.getEmp()
}
</script>
<template>
  <div>
    <h3>子组件</h3>
    <button @click="print">打印props</button>
    <p>
      <!-- 模板中可省略 this ,直接prop名称可获取 -->
      id: {{ id }},name: {{ name }}, {{ isPublished }}
    </p>
  </div>
</template>

父组件调用:

<script setup>
// 第一步:在<script setup>中导入的组件,不需要注册,直接在模板中引用此组件即可
import ChildComponent from '@/components/childComponent.vue';
function getEmpData() {
  console.log('我被打印了');
}
</script>
<template>
  <!-- 第二步: 在<script setup>中导入的组件,不需要注册可直接使用 -->
  <ChildComponent v-bind:id="1" name="吴用" :is-published="true" :comment-ids="[1, 2]" :getEmp="getEmpData" />
</template>
<style></style>

效果:
Vue3+TS+Vite+Pinia最全学习总结

props 解构默认值 withDefaults

当使用 defineProps 的泛型参数声明时,我们失去了为 props 声明默认值的能力。这可以通过 withDefaults 函数解决:

<script lang="ts" setup>
interface Props {
  id: number,
  name: string,
  isPublished?: boolean, //可选
  commentIds?: number[], //number类型数组
  getEmp?: Function //首字母大写
}
// const props = defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
  name: '张三',
  isPublished: false,
  commentIds: () => [2, 3],
  getEmp: () => {
    console.log('默认getEmp')
  }
});
console.log(props.name,props.isPublished,props.commentIds,props.getEmp)
function print() {
  console.log("print", props.id, props.name, props.isPublished)
  props.getEmp && props.getEmp()
}
</script>
<template>
  <div>
    <h3>子组件</h3>
    <button @click="print">打印props</button>
    <p>
      <!-- 模板中可省略 this ,直接prop名称可获取 -->
      id: {{ id }},name: {{ name }}, {{ isPublished }}
    </p>
  </div>
</template>

Vue3+TS+Vite+Pinia最全学习总结

这将被编译为等效的运行时 props default 选项。此外, withDefaults 帮助程序为默认值提供类型检查,并确
保返回的 props 类型删除了已声明默认值的属性的可选标志。

props 数据传递注意事项
  1. props 只用于子组件接收父组件传递的数据
  2. 引用子组件时,组件标签上绑定的所有属性都会认为是传递给子组件的 prop
  3. 子组件在 <template> 模板页面中可以直接引用 prop
  4. 问题:

    a. 如果需要向非子后代传递数据,必须多层逐层传递prop。
    b. 兄弟组件间也不能直接 prop 通信, 必须借助父组件才可以。

组件间通信规则
  1. 不要在子组件中直接修改父组件传递的数据
  2. 数据初始化时,应当看初始化的数据是否用于多个组件中,如果需要被用于多个组件中,则初始化在父组件中;如果只在一个组件中使用,那就初始化在这个要使用的组件中。
  3. 数据初始化在哪个组件, 更新数据的方法(函数)就应该定义在哪个组件。

向子孙后代组件传递数据 provide / inject

prop 逐级透传问题

通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props
想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。
在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:
Vue3+TS+Vite+Pinia最全学习总结
注意,虽然这里的<Footer>组件可能根本不关心这些 props,但为了使 <DeepChild> 能访问到它们,仍然需要定义并向下传递。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”,显然是我们希望尽量避免的情况。
provide inject 可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
Vue3+TS+Vite+Pinia最全学习总结

provide 为后代提供数据

使用 provide() 为组件后代提供数据(注意:必须是后代组件,不能是兄弟组件)

  1. 从 vue 导入 provide 函数

  2. 使用 provide('注入名', 注入值) 函数注入属性/方法。
    注入名:

    • 后代组件会调用 inject 方法,通过注入名来查找期望注入的值。
    • 一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。

    注入值: 值可以是任意类型,包括响应式的状态,函数等。

  3. 在 父组件文件中添加如下代码

    <script setup>
    // 第一步:在<script setup>中导入的组件,不需要注册,直接在模板中引用此组件即可
    import ChildComponent from '@/components/childComponent.vue';
    function getEmpData() {
      console.log('我被打印了');
    }
    import { provide, ref } from 'vue';
    const score = ref(100);
    function updateScore(val) {
      score.value = val;
    }
    // 向后代组件提供数据,参数1:注入名,参数2:任意类型值/方法等
    provide('company', 'mengxuegu'); // 字面值
    provide('score', score); // ref
    provide('updateScore', updateScore); // 方法
    </script>
    <template>
      <!-- 第二步: 在<script setup>中导入的组件,不需要注册可直接使用 -->
      <ChildComponent v-bind:id="1" name="吴用" :is-published="true" :comment-ids="[1, 2]" :getEmp="getEmpData" />
    </template>
    <style></style>
    
Inject 注入值

使用 inject()函数注入上层组件通过 provide函数提供的数据:
如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref对象保持了和供给方的响应性链接。

父组provide件注入值

<script setup>
// 第一步:在<script setup>中导入的组件,不需要注册,直接在模板中引用此组件即可
import ChildComponent from '@/components/childComponent.vue';
function getEmpData() {
  console.log('我被打印了');
}
import { provide, ref } from 'vue';
const score = ref(100);
function updateScore(val) {
  score.value = val;
}
// 向后代组件提供数据,参数1:注入名,参数2:任意类型值/方法等
provide('company', 'wuyong'); // 字面值
provide('score', score); // ref
provide('updateScore', updateScore); // 方法
</script>
<template>
  <!-- 第二步: 在<script setup>中导入的组件,不需要注册可直接使用 -->
  <ChildComponent v-bind:id="1" name="吴用" :is-published="true" :comment-ids="[1, 2]" :getEmp="getEmpData" />
</template>
<style></style>

子组件使用inject获取父provide的传入

<script lang="ts" setup>
import { inject, ref } from 'vue';
interface Props {
  id: number,
  name: string,
  isPublished?: boolean, //可选
  commentIds?: number[], //number类型数组
  getEmp?: Function //首字母大写
}
// const props = defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
  name: '张三',
  isPublished: false,
  commentIds: () => [2, 3],
  getEmp: () => {
    console.log('默认getEmp')
  }
});
console.log(props.name, props.isPublished, props.commentIds, props.getEmp)
function print() {
  console.log("print", props.id, props.name, props.isPublished)
  props.getEmp && props.getEmp()
}
// 后代获取provide传递的数据,参数1:注入时名称, 参数2:默认值,未获取到值取默认值
const company = inject('company', '');
const score = inject('score', ref(0));
const updateScore = inject<Function>('updateScore', () => { });
console.log('company', company);
console.log('score', score.value);
updateScore(88);
</script>
<template>
  <div>
    <h3>子组件</h3>
    <button @click="print">打印props</button>
    <p>
      <!-- 模板中可省略 this ,直接prop名称可获取 -->
      id: {{ id }},name: {{ name }}, {{ isPublished }}
    </p>
  </div>
</template>

Vue3+TS+Vite+Pinia最全学习总结
当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在提供方(provide)组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。

自定义事件 $emit / defineEmits

不要在子组件中直接修改父组件传递的数据,如果要修改可通过自定义事件方式,间接修改父组件数据。子组件需要触发调用父组件方法(函数),可通过自定义事件来实现。
自定义事件:

  1. 在组件的模板表达式中,可以直接使用 $emit(‘事件名’ [, 传递的参数1, …, 参数n]) 函数触发自定义事件。
  2. 选项式API:this.$emit('事件名' [, 传递的参数1, ..., 参数n])函数触发自定义事件。
  3. <script setup> 组件式API:defineEmits()定义事件后,返回一个函数 emit ,通过调用 emit('事件名' [,传递的参数1, ..., 参数n]) 函数来触发自定义事件。
    在父组件模板中可以使用 kebab-case 横杠和 camelCase 驼峰来绑定事件监听器,vue官网也推荐使用 kebab-case 形式。如:@search-goods="searchGoodsHandle"
父组件监听自定义事件

在父组件模板的子组件元素上,通过 v-on@ 监听子组件的自定义事件并绑定处理函数,当子组件触发自定义事件后,就会调用父组件中绑定的事件处理函数。

<template>
	<!--
	在父组件中,通过 v-on 或 @ 绑定子组件的自定义事件在子组件中,当触发 delete-hobby 事件后,就会调用到父组件的 deleteHobby 事件处理函数
	-->
	<HeaderSearch @search-goods="searchGoodsHandle"/>
</template>
<script setup lang="ts">
	// 搜索商品
	function searchGoodsHandle(keyword:string, param2?: string) {
		console.log('父组件监听到search-goods事件', keyword, param2);
	}
</script>
子组件触发自定义事件

在子组件中触发自定义事件,从而调用在父组件中绑定的事件处理函数。

  1. 在组件的模板表达式中,可以直接使用$emit('事件名' [, 传递的参数1, ..., 参数n])函数触发自定义事件,只在模板中使用就不用管 <script xxx> 中使用的是哪种API
    <input @keyup.enter="$emit('searchGoods', keyword, '参数2')" :placeholder="placeholder" vmodel="keyword" type="text" >
    
  2. 选项式 API 中,通过 this.$emit('事件名'[, 传递的参数1, ..., 参数n]) 函数触发自定义事件
    <script lang="ts">
    export default {
      data() {
        return {
          placeholder: '组件自定义事件',
          keyword: ''
        }
      },
      methods: {
        search() {
          // 方式2:在选项式API,使用 this.$emit('事件名'[, 传递的参数1, ..., 参数n]) 触发事件
          this.$emit('searchGoods', this.keyword);
        }
      }
    }
    </script>
    <template>
      <!-- 方式1:模板中直接使用 $emit('事件名'[, 传递的参数1, ..., 参数n]) 触发事件
    <input @keyup.enter="$emit('searchGoods', keyword, '参数2')"
    :placeholder="placeholder" v-model="keyword" type="text" >
    -->
      <input @keyup.enter="search()" :placeholder="placeholder" v-model="keyword" type="text">
    </template>
    
  3. 在组件导出默认对象中 export default {} ,显式地使用了 setup 函数而不是 <script setup> , 则事件需要通过 emits 选项来定义, emit 函数也被暴露在 setup() 的上下文对象上。
    <script lang="ts">
    export default {
      // 方式3:先通过 emits 选项定义事件,然后在 setup 函数中触发事件(setup函数中是没有this的)
      emits: ['searchGoods', 'submit'],
      setup(props, ctx) {
        // ctx.emit触发事件
        ctx.emit('searchGoods', '参数1');
        /*
    	    注意:setup函数没有 this,这里方法不到data选项数据的(this.keyword),
    	    要函数体中ref/reactive等函数定义响应式状态才可以
    	    const keyword = ref('');
    	    return {keyword};
        */
      }
    }
    </script>
    
  4. <script setup> 组件式API中,先通过defineEmits()函数声明事件后会返回一个函数 emit ,通过调用emit 函数来触发自定义事件。
<script setup lang="ts">
import { ref } from 'vue';
const placeholder = ref('vue3学习');
// 双向绑定搜索关键字
const keyword = ref('');
// 第1步:声明触发的事件
// const emit = defineEmits(['searchGoods']);
// 使用·纯类型标注·来声明触发的事件
const emit = defineEmits<{
  (e: 'searchGoods', keyword: string, param2?: string): void
  (e: 'submit', id: number): void
}>();
function search() {
  // 第2步:触发事件,
  emit('searchGoods', keyword.value, '参数2');
}
</script>
<template>
  <div id="header">
    <!-- <input @keyup.enter="search()" :placeholder="placeholder" v-model="keyword" type="text" > -->
    <!-- 使用了defineEmits定义事件后,组件模板中使用返回函数 emit 触发 -->
    <input @keyup.enter="emit('searchGoods', keyword, '参数2')" :placeholder="placeholder" v-model="keyword" type="text">
  </div>
</template>
自定义事件注意事项
  1. 自定义事件只用于子组件向父组件发送消息(数据)。
  2. 隔代组件或兄弟组件间通信此种方式不合适(可以使用provide/inject解决多级组件调用函数问题)。
子组件导出状态/方法给父组件操作 defineExpose()

子组件导出状态/方法,导出后父组件可以操作子组件属性和方法。
子组件使用选项式API:不需要导出,父组件直接可操作属性和方法。data中定义的状态属性、methods中定义的方法等都是公开,父组件可以直接访问到属性/方法。
使用选项式API,不需要额外导出操作代码:

<script lang="ts">
export default {
  props: {
    navList: {
      type: Array<String>,
      required: true,
    }
  },
  data() {
    return {
      navIndex: 0 // 当前选择下标
    }
  },
  methods: {
    clickItem(index: number) {
      // 赋值是一个等号 =
      this.navIndex = index;
    }
  }
}
</script>

在父组件 中,通过直接ref模板引用的方式,可以直接访问子组件实例的data选项状态和方法等。

<script setup lang="ts">
import MainNav from './components/main-nav.vue';
// 导入watchEffect
import { watchEffect } from 'vue';
// 导航组件 ref
const mainNavRef = ref();
watchEffect(() => {
  // 获取当前点击的导航下标
  console.log('点击的导航下标为:', mainNavRef.value?.navIndex);
});
</script>
<template>
  <!-- 导航 -->
  <MainNav ref="mainNavRef" :nav-list="navList" />
</template>

子组件使用 <script setup>组合式API时: defineExpose() 导出子组件是默认关闭的,在 <script setup> 中声明的任何属性、方法等都是不公开,默认父组件是不可以直接访问;可以通过 defineExpose 方法显式指定在 <script setup> 组件中要暴露出去的属性/方法。

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
// 当前轮播图
const current = ref('');
// 导出状态和方法,让父组件可访问
defineExpose({
  current, // 等价于`current:current`,key是父组件引用名,value是子组件的状态名或方法名
  getImageUrl, // 导出方法 `getImageUrl:getImageUrl`
});
// 本地资源动态加载处理如下
function getImageUrl(filename: string) {
  return new URL(`../../../assets/img/${filename}`, import.meta.url).href
}
</script>

在父组件中,通过直接ref模板引用的方式获取到子组件的实例,获取到的实例是 { current: string, getImageUrl: Function } (ref 会和在普通实例中一样被自动解包)。

<script setup lang="ts">
import MainNav from './components/main-nav.vue';
import MainBanner from './components/main-banner.vue';
// 导入 nextTick
import { nextTick } from 'vue';
// 轮播图组件 ref
const mainBannerRef = ref();
nextTick(() => {// 组件渲染完成回调
  console.log('父组件获取轮播图子组件实例', mainBannerRef.value);
  console.log('父组件调用轮播图子组件方法', mainBannerRef.value.getImageUrl('1.jpg'));
  // 注意 子组件ref有父组件获取时会自动解包,不能加.value
  console.log('父组件获取当前轮播图URL:', mainBannerRef.value?.current);
});
</script>
<template>
  <!-- 导航 -->
  <MainNav ref="mainNavRef" :nav-list="navList" />
  <!-- 轮播 -->
  <MainBanner ref="mainBannerRef" />
</template>

插槽 slot

作用: 用于子组件接收父组件传递的 模板内容 , 子组件接收后在对应位置渲染。(而props和自定事件只是传递数据)。

简单插槽

在子组件中定义插槽, 当父组件向指定插槽传递模板内容后,模板内容就会在插槽处渲染出现,父组件没有传递内容则插槽出口不显示内容。

  1. 子组件声明插槽: <slot>

创建 FancyButton.vue

<button class="fancy-btn">
	<!-- <slot> 元素是一个插槽出口,标示了父元素提供的插槽内容将在哪里被渲染。-->
	<slot></slot>
</button>

父组件传递模板内容:引用子组件 <FancyButton> ,并传递模板内容。

<template>
  <FancyButton>
    <!-- 插槽模板内容:替换到FancyButton的slot元素 -->
    Click Me
  </FancyButton>
  <FancyButton>
    <!-- 插槽模板内容:可以是任意合法的模板内容,不局限于文本 -->
    <span style="color:red">点我!</span>
  </FancyButton>
  <FancyButton>
    <!-- 插槽模板内容:插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的
    -->
    {{ message }}
  </FancyButton>
</template>
<script setup lang="ts">
  import FancyButton from "./FancyButton.vue";
  import {ref} from 'vue';
  const message = ref('父组件数据');
</script>

在 组件中导入并引用 <ParentSolt/> 组件,<ParentSolt/> 父组件最终渲染出的 DOM 是这样:

Vue3+TS+Vite+Pinia最全学习总结

<button class="fancy-btn">Click Me</button>
<button class="fancy-btn"><span style="color:red">点我!</span></button>
<button class="fancy-btn">父组件数据</button>
插槽默认内容

插槽默认内容:子组件可以为插槽指定默认内容;指定默认内容后,当父组件没有向插槽提供任何内容时,子组件插槽处渲染默认内容,反之渲染父组件传递的模板内容。

  1. 子组件声明插槽 <DefaultContentButton> (带默认内容的)
    <button class="default-content">
    	<!-- 插槽出口 -->
    	<slot>
    		DefaultContent <!--默认内容-->
    	</slot>
    </button>
    
  2. 父组件使用· <DefaultContentButton> 且没有提供任何插槽内容时,会渲染默认内容:DefaultContent
    <DefaultContentButton></DefaultContentButton>
    
  3. 父组件最终渲染出的 DOM 是这样:
    <button class="default-content">DefaultContent</button>
    
具名插槽

一个组件中可以包含多个插槽出口,在 <slot> 元素有一个属性name,用来给各个插槽分配唯一的 ID,这类带name 的插槽被称为具名插槽。

<div class="container">
<header>
<!-- 标题内容放这里 -->
</header>
<main>
<!-- 主要内容放这里 -->
</main>
<footer>
<!-- 底部内容放这里 -->
</footer>
</div>

对于上面布局场景, <slot> 元素上绑定name属性,给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:

  1. 具名插槽:通过name属性值指定唯一插槽名,父组件通过此名确定此处的模板内容。
  2. 默认插槽,没有提供 name 的 出口会隐式地命名为“default”
<div class="container">
  <header>
	  <!-- name属性值指定唯一插槽名,父组件通过此名确定此处的模板内容 -->
	  <slot name="header"></slot>
  </header>
  <main>
	  <!-- 默认插槽:没有提供 name 的 <slot> 出口会隐式命名为“default-->
	  <slot></slot>
  </main>
  <footer>
	  <slot name="footer"></slot>
	  </footer>
  </div>

父组件 要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template>元素,并将目标插槽的名字传给该指令:

<BaseLayout>
	<!-- v-slot:插槽名 -->
	<template v-slot:header>
		<!-- 将下面模板内容传入子组件的 header 插槽中 -->
		<h1>页面头部区域</h1>
		<h2>梦学谷-mengxuegu.com</h2>
	</template>
	<!-- 默认插槽要显示指定 `default` -->
	<template v-slot:default>
		<p>页面核心区域</p>
		<p>展示您想要展示的内容</p>
	</template>
	<template v-slot:footer>
		<p>页面底部区域</p>
		<p>展示您的联系方式</p>
	</template>
</BaseLayout>

v-slot 有对应的简写#,因此 <template v-slot:header> 可以简写为 <template #header>
注意:默认插槽需要加上 default <template v-slot:default> 可以简写为 <template #default>

<BaseLayout>
	<!-- v-slot:插槽名 -->
	<template #header>
		<!-- 将下面模板内容传入子组件的 header 插槽中 -->
		<h1>页面头部区域</h1>
		<h2>hello Vue3</h2>
	</template>
	<!-- 默认插槽要显示指定 `default`
	<template #defult>
		<p>页面核心区域</p>
		<p>展示您想要展示的内容</p>
	</template>
	-->
	<!-- 隐式的默认插槽 --- start ----
		当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template>
		节点都被隐式地视为默认插槽的内容。所以上面也可以写成:
	-->
	<p>页面核心区域</p>
	<p>展示您想要展示的内容</p>
	<!-- 隐式的默认插槽 --- end ---- -->
	<template #footer>
		<p>页面底部区域</p>
		<p>展示您的联系方式</p>
	</template>
</BaseLayout>

Vue3+TS+Vite+Pinia最全学习总结

默认作用域插槽(子组件插槽向父组件传递数据)

有时候,父组件向子组件的插槽传递模板内容时,传递的模板内容中希望获取到子组件中的一些数据,可以在子组件声明插槽出口的slot元素上类似props绑定数据,绑定的数据可以在父组件向插槽传递模板内容时获取到。
在 元素上绑定要传递的插槽 props

<template>
<div>
	<!-- 插槽出口,在 <slot> 元素上绑定要传递的插槽 props -->
	<slot :count="1" :totalMoney="totalMoney"></slot>
</div>
</template>
<script setup lang="ts">
	import {ref} from 'vue';
	const totalMoney = ref(100);
</script>

父组件获取:当需要接收插槽 props 时,默认插槽和具名插槽的使用方式有一些小区别。
下面展示默认插槽如何接受 props,通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props对象。

<ChildSlotProps v-slot="slotProps">
	<span>数量:{{ slotProps.count }}</span>
	<span>金额:{{ slotProps.totalMoney }}</span>
</ChildSlotProps>

v-slot=“slotProps” 可以和函数的参数类似,可以在 v-slot 中解构对象:

<!-- 解构出 soltProps 对象属性-->
<ChildSlotProps v-slot="{count, totalMoney}">
	<span>数量:{{ count }}</span>
	<span>金额:{{ totalMoney }}</span>
</ChildSlotProps>

Vue3+TS+Vite+Pinia最全学习总结

具名作用域插槽

具名作用域插槽的工作方式与上面也是类似的,插槽 props 可以作为 v-slot 指令的值被访问到:v-slot:name="slotProps"

<BaseLayout>
	<!-- v-slot:插槽名="slotProps" -->
	<template v-slot:header="headerProps">
		{{ headerProps }}
	</template>
	
	<!-- 默认插槽要显示指定 `default` -->
	<template v-slot:default="defaultProps">
		{{ defaultProps }}
	</template>
	
	<template v-slot:footer="footerProps">
		{{ footerProps }}
	</template>
</BaseLayout>

使用“#”缩写

<BaseLayout>
	<template #header="headerProps">
		{{ headerProps }}
	</template>
	<template #default="defaultProps">
		{{ defaultProps }}
	</template>
	<template #footer="footerProps">
		{{ footerProps }}
	</template>
</BaseLayout>

九、内置组件

<Component> 动态切换组件

有些场景会需要在两个组件间来回切换,可以使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。

  • component 是 Vue的内置组件,不需要注册可以直接使用。
  • component :is 的值可以是以下几种:
    1. 被注册的组件名 (字符串)
    2. 导入的组件对象

使用<component>实现一个tab切换。
Vue3+TS+Vite+Pinia最全学习总结
index.vue代码

<script setup lang='ts'>
import HotQuestion from './HotQuestion.vue';
import WaitQuestion from './WaitQuestion.vue';
import NewQuestion from './NewQuestion.vue';
import { shallowRef } from 'vue';
const tabs = [
  { title: '热门回答', comp: HotQuestion },
  { title: '最新问答', comp: NewQuestion },
  { title: '等待回答', comp: WaitQuestion },
];
// 使用 shallowRef(浅层ref)避免组件被深入观察
const currentTab = shallowRef(HotQuestion);
</script>
<template>
  <div class="demo">
    <button v-for="(tab, index) in tabs" :key="index" :class="['tab-button', { active: currentTab === tab.comp }]" @click="currentTab = tab.comp">
      {{ tab.title }}
    </button>
    <component :is="currentTab" class="tab"></component>
  </div>
</template>
<style scoped>
.demo {
  font-family: sans-serif;
  border: 1px solid #eee;
  border-radius: 2px;
  padding: 20px 30px;
  margin-top: 1em;
  margin-bottom: 40px;
  user-select: none;
  overflow-x: auto;
}

.tab-button {
  padding: 6px 10px;
  border-top-left-radius: 3px;
  border-top-right-radius: 3px;
  border: 1px solid #ccc;
  cursor: pointer;
  background: #f0f0f0;
  margin-bottom: -1px;
  margin-right: -1px;
}

.tab-button:hover {
  background: #e0e0e0;
}
.tab-button.active {
  background: greenyellow;
}

.tab {
  border: 1px solid #ccc;
  padding: 10px;
}
</style>

HotQuestion.vue代码

<script setup lang='ts'>
import { ref, reactive } from 'vue'
</script>
<template>
  <div>
    <p>等待回答区域</p>
  </div>
</template>
<style scoped></style>

NewQuestion.vue代码

<script setup lang='ts'>
  import { ref, reactive } from 'vue'
</script>
<template>
  <div>
    <p>最新回答区域</p>
  </div>
</template>
<style scoped></style>

WaitQuestion.vue代码

<script setup lang='ts'>
  import { onMounted, onUnmounted } from 'vue'
  onMounted(() => {
    console.log('热门回答被加载')
  })
  onUnmounted(() => {
    console.log('热门回答被卸载')
  })
</script>
<template>
  <div>
    <p>热门回答区域</p>
  </div>
</template>
<style scoped></style>

观察热门回答组件中的生命周期钩子打印的日志,当使用<component :is="...">来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 <KeepAlive> 组件将被切换掉的组件仍然保持“存活”的状态。

<KeepAlive> 缓存被移除的组件实例

基本使用方式

通过 <component :is="xxx"> 将当前组件被切换后,则当前组件实例会被卸载,从而丢失所有已变化的状态。
缓存好处:如果可以缓存路由组件实例,切换后不用重新加载数据,可以提高用户体验。

  1. 在 src/components/tabs/HotQuestion.vue 添加如下 点击+1代码
<script setup lang='ts'>
import { ref,onMounted, onUnmounted } from 'vue'
onMounted(() => {
  console.log('热门回答被加载')
})
onUnmounted(() => {
  console.log('热门回答被卸载')
})
const count = ref(0)
</script>
<template>
  <div>
    <p>热门回答区域</p>
    <span>count:{{ count }} </span>
    <button @click="count++">点击+1</button>
  </div>
</template>
<style scoped></style>
  1. NewQuestion.vue 最新问答组件中添加一个输入框并绑定状态:
<script setup lang='ts'>
  import { ref, reactive } from 'vue'
  const content = ref('')
</script>
<template>
  <div>
    <p>最新回答区域</p>
    输入内容:<input v-model="content"/>
  </div>
</template>
<style scoped></style>

效果:
Vue3+TS+Vite+Pinia最全学习总结
当 count 状态变化后,点击切换 tab 页,然后回到 热门回答 发现 count 变回了 0,切换回 最新问答 输入框的content值变为空的。如果希望在组件切换后已变化的状态也能被保留,我们可以用Vue的 内置组件 <KeepAlive> 将这些动态组件包装起来:

<template>
  <div class="demo">
    <button v-for="(tab, index) in tabs" :key="index" :class="['tab-button', { active: currentTab === tab.comp }]" @click="currentTab = tab.comp">
      {{ tab.title }}
    </button>
    <KeepAlive>
      <component :is="currentTab" class="tab"></component>
    </KeepAlive>
  </div>
</template>

效果:
Vue3+TS+Vite+Pinia最全学习总结

包含/排除(include 和 exclude)

<KeepAlive> 默认会缓存内部的所有组件实例,但是实际应用场景中并不是所有组件都需要缓存,其中的有些组件可能不需要缓存,但我们可以通过 include exclude prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、正则表达式、包含这两种类型的数组。值匹配的是子组件的 name 选项值,参考 NewQuestion.vue 。

  • include 指定需要缓存的组件
  • exclude 指定不需要缓存的组件
  • 上面两个prop,只要写其中一个 prop 就行,没在prop中指定的组件就是反之意思。
    例如:
<KeepAlive :include="['HotQuestion', 'NewQuestion']">
	<component :is="currentTab" class="tab"></component>
</KeepAlive>

NewQuestion.vue 中指定 name 选项值,指定后include exclude 属性值匹配的是name选项值
Vue3.2.34 或以上的版本中,使用<script setup>的单文件组件会自动根据文件名生成同名的 name 选项值,无需再进行如下手动声明。

<script lang="ts">
export default {
	/**
	* name 组件名选项,
	* 在 <KeepAlive> 的 include/exclude 中指定的就是此name选项值
	* 在 3.2.34 或以上的版本中,使用 <script setup> 的单文件组件会自动根据文件名生成同名的 name
	选项值,无需再进行如下手动声明。
	* 如果手动指定了name选项值,则以指定的为准
	*
	*/
	name: 'NewQuestion'
}
</script>
<script setup lang='ts'>
	import { ref, reactive } from 'vue'
	const content = ref('');
</script>
<template>
	<div>
		<p>最新问答区域</p>
		输入内容:<input v-model="content"/>
	</div>
</template>
<style scoped>
</style>
最大缓存实例数 max

我们可以通过传入 max prop 来限制可被缓存的最大组件实例数。
<KeepAlive>的行为在指定了 max 后:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。
例子:

<KeepAlive :include="['HotQuestion', 'NewQuestion', 'WaitQuestion']" :max="2">
	<component :is="currentTab" class="tab"></component>
</KeepAlive>
缓存实例的生命周期

当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。
一个持续存在的组件可以通过 onActivated()onDeactivated() 注册相应的两个状态的生命周期钩子:

  • 请注意:onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用
  • Vue3+TS+Vite+Pinia最全学习总结

十、路由

什么是路由

Vue Router Vue.js 官方的路由管理器。它和Vue.js的核心深度集成,让构建单页面应用变得非常简单。通过根据不同的请求路径,切换显示不同组件进行渲染页面。

基础路由的使用

  1. 安装路由
    npm install vue-router@4.2.2
    
  2. JavaScript 路由配置
    在 src/main.ts 文件,进行路由配置:
import { createApp } from 'vue'
import App from './App.vue'

// 1. 导入相关路由方法
import { createRouter,createWebHashHistory,createWebHistory} from 'vue-router'

import './assets/main.css'

//2. 定义路由组件,可以是导入的组件
const Home = { template: '<div> Home 组件 </div>' }
const Login = { template: '<div> Login 组件 </div>' }

//3. 配置路由表,当点击特定的url时,显示对应的那个组件。
const router = createRouter({
  history:createWebHashHistory(),
  routes:[
    {path:"/home",component:Home},
    {path:"/login",component:Login},
  ]
})
const app = createApp(App);

// 4. 整个应用支持路由
app.use(router)
app.mount('#app')
  • history: createWebHashHistory()
    路由模式路径带#号,支持所有浏览器,是 Vue Router 提供的一种基于浏览器 URL hash 路由模式,它将路由添加到 URL 中的 hash 中,例如: /#/home/#/about由于 URL 中添加了 hash,因此在搜索引擎的 SEO 优化中存在一些问题。
  • history: createWebHistory()
    路由模式路径不带#号, 只支持 HTML5 标准浏览器,对于老版本的浏览器无法使用。
  1. vite.config.ts 指定对 template选项字符串模板的支持版本
    问题: vite 默认使用的 vue 版本是不对 template 选项的字符串模板的编译支持,导致模板内容无法正常渲染出
    来。
    解决:手动在 vite.config.ts 中配置 vue 运行时即时编译构建版本。
    说明 :如果项目中没有使用 template选项字符串模板,不需要做如下配置。
import { fileURLToPath, URL } from 'node:url'

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

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
      // 指定vue使用运行时即时编译构建版本,就可以使用 template 选项定义字符串模板
      'vue': 'vue/dist/vue.esm-bundler.js',
    }
  }
})
  1. 组件中进行路由切换组件渲染
    在 App.vue 中,添加点击菜单项,渲染对应组件
<script setup lang="ts">
</script>
<template>
  <div>
    <div class="left">
      <p>
      <ul>
        <!-- 方式1: 传统方式 -->
        <li><a href="#/home">Go Home</a></li>
        <li><a href="#/login">Go Login</a></li>
        <!-- 方式2: 官方推荐 -->
        <!-- <router-link> 默认会被渲染成一个 `<a>` 标签, -->
        <!-- 通过传入 `to` 属性指定跳转链接,不用像上面加 `#`-->
        <li><router-link to="/home">Go to Home</router-link></li>
        <li><router-link to="/login">Go to Login</router-link></li>
      </ul>
      </p>
    </div>
    <div class="main">
      <!-- 路由出口: 路由匹配到的组件将渲染在这里 -->
      <router-view></router-view>
    </div>
  </div>
</template>
<style ></style>

效果:
Vue3+TS+Vite+Pinia最全学习总结

基础路由实战-新闻管理案例

案例布局准备
  1. 将 bootstarp模板 下的 style 目录拷贝到/src/assets 目录下,并在在 main.ts 中进行全局导入。
    Vue3+TS+Vite+Pinia最全学习总结
    main.ts全局引入Vue3+TS+Vite+Pinia最全学习总结
    src/components/ 目录下创建layout/AppHeader.vue,头部导航区域代码:
<template>
   <!--头部导航区域-->
   <nav class="navbar navbar-inverse navbar-fixed-top">
      <div class="container-fluid">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="#">VueRouter路由</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
          <ul class="nav navbar-nav navbar-right">
            <li><a href="#">Dashboard</a></li>
            <li><a href="#">Settings</a></li>
            <li><a href="#">Profile</a></li>
            <li><a href="#">Help</a></li>
          </ul>
          <form class="navbar-form navbar-right">
            <input type="text" class="form-control" placeholder="Search...">
          </form>
        </div>
      </div>
    </nav>
    
</template>

<script lang="ts" setup>
</script>
<style scoped>
</style>

src/components/layout/ 目录下创建AppMenu.vue,拷贝index.html中核心区域的 左边菜单栏区域 代码:

<script setup lang='ts'>
</script>
<template>
  <!--左边菜单栏区域-->
  <div class="col-sm-3 col-md-2 sidebar">
    <ul class="nav nav-sidebar">
      <li class="active"><a href="#">首页</a></li>
      <li><a href="#">新闻管理</a></li>
      <li><a href="#">关于我们</a></li>
    </ul>
  </div>
</template>
<style scoped></style>

src/components/layout/ 目录下创建 AppMain.vue ,拷贝 index.html 中核心区域的 右边主页面区域 代码:

<script setup lang='ts'>
</script>
<template>
  <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
    <h1 class="page-header">Dashboard</h1>
    <div class="row placeholders">
      <div class="col-xs-6 col-sm-3 placeholder">
        <img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
          height="200" class="img-responsive" alt="Generic placeholder thumbnail">
        <h4>Label</h4>
        <span class="text-muted">Something else</span>
      </div>
      <div class="col-xs-6 col-sm-3 placeholder">
        <img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
          height="200" class="img-responsive" alt="Generic placeholder thumbnail">
        <h4>Label</h4>
        <span class="text-muted">Something else</span>
      </div>
      <div class="col-xs-6 col-sm-3 placeholder">
        <img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
          height="200" class="img-responsive" alt="Generic placeholder thumbnail">
        <h4>Label</h4>
        <span class="text-muted">Something else</span>
      </div>
      <div class="col-xs-6 col-sm-3 placeholder">
        <img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
          height="200" class="img-responsive" alt="Generic placeholder thumbnail">
        <h4>Label</h4>
        <span class="text-muted">Something else</span>
      </div>
    </div>
  </div>
</template>
<style scoped></style>

src/components/layout/目录下创建 index.vue 布局入口组件 ,将上面3个组件导入并引用。

<script setup lang='ts'>
import AppHeader from './AppHeader.vue';
import AppMenu from './AppMenu.vue';
import AppMain from './AppMain.vue';
</script>
<template>
  <div>
    <!-- 头部区域 -->
    <AppHeader />
    <!--核心区域:分左右两边-->
    <div class="container-fluid">
      <div class="row">
        <!--左边菜单栏区域-->
        <AppMenu />
        <!--右边主页面区域-->
        <AppMain />
      </div>
    </div>
  </div>
</template>
<style scoped></style>

App.vue 中导入并引用布局入口组件 layout/index.vue

<script setup lang="ts">
	import Layout from '@/components/layout/index.vue';
</script>
<template>
	<!-- 项目布局组件 -->
	<Layout />
</template>
<style >
</style>

效果:
Vue3+TS+Vite+Pinia最全学习总结

创建路由组件(视图组件)

在核心区域的 右边主页面区域,当前是首页效果,而其实应该是根据点击左侧菜单(路由地址),动态的渲染对应的路由组件(或称为视图组件),所以我们针对每个菜单项都分别创建一个路由组件:所以我们在 src 目录下创建一个 views 目录,将所有路由组件都放到src/views

  1. 创建首页的路由组件: src/views/home/index.vue ,然后拷贝 AppMain.vue 的模板代码进来
<script setup lang='ts'>
</script>
<template>
  <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
    <h1 class="page-header">Dashboard</h1>
    <div class="row placeholders">
      <div class="col-xs-6 col-sm-3 placeholder">
        <img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
          height="200" class="img-responsive" alt="Generic placeholder thumbnail">
        <h4>Label</h4>
        <span class="text-muted">Something else</span>
      </div>
      <div class="col-xs-6 col-sm-3 placeholder">
        <img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
          height="200" class="img-responsive" alt="Generic placeholder thumbnail">
        <h4>Label</h4>
        <span class="text-muted">Something else</span>
      </div>
      <div class="col-xs-6 col-sm-3 placeholder">
        <img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
          height="200" class="img-responsive" alt="Generic placeholder thumbnail">
        <h4>Label</h4>
        <span class="text-muted">Something else</span>
      </div>
      <div class="col-xs-6 col-sm-3 placeholder">
        <img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
          height="200" class="img-responsive" alt="Generic placeholder thumbnail">
        <h4>Label</h4>
        <span class="text-muted">Something else</span>
      </div>
    </div>
  </div>
</template>
<style scoped></style>
  1. . 创建新闻管理的路由组件 src/views/news/index.vue ,添加新闻管理模板代码
<script setup lang='ts'>
</script>
<template>
  <!--右边主页面区域-->
  <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
    <div class="header clearfix">
      <nav>
        <ul class="nav nav-pills">
          <li class="active"><a href="#">体育</a></li>
          <li><a href="#">科技</a></li>
        </ul>
      </nav>
      <hr>
    </div>
    <!--体育栏目-->
    <div>
      <ul>
        <li>
          <a href="#">世界杯开赛啦</a>
        </li>
        <li>
          <a href="#">NBA开赛倒计时</a>
        </li>
      </ul>
      <!--详情-->
      <div class="jumbotron">
        <h2>世界杯开赛啦</h2>
        <p>世界杯于明晚8点举行开幕式.....</p>
      </div>
    </div>
    <!--科技栏目-->
    <div>
      <ul>
        <li>
          <span>5G时代到来了 </span>
          <button class="btn btn-default btn-xs">查看(Push)</button>&nbsp;
          <button class="btn btn-default btn-xs">查看(replace)</button>
        </li>
        <li>
          <span>互联网大洗牌</span>
          <button class="btn btn-default btn-xs">查看(Push)</button>&nbsp;
          <button class="btn btn-default btn-xs">查看(replace)</button>
        </li>
      </ul>
      <p>
        <button>后退</button>
      </p>
      <!--详情-->
      <div class="jumbotron">
        <h2>世界杯开赛啦</h2>
        <p>世界杯于明晚8点举行开幕式.....</p>
      </div>
    </div>
  </div>
</template>
<style scoped></style>
  1. 创建关于我们的路由组件 src/views/about/index.vue ,添加如下模板代码
<script setup lang='ts'>
</script>
<template>
  <!-- 不要少了css样式,不然下面渲染内容会被遮挡 -->
  <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
    <h2>关于我们</h2>
    <p>vue3学习</p>
  </div>
</template>
<style scoped></style>
路由配置

src 目录下新建router/index.ts路由配置文件

// 1. 导入相关路由方法
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
// 2. 导入路由组件
import News from '../view/news/index.vue';
import Home from '../views/home/index.vue';
import About from '../views/about/index.vue'; // 如果出现红色线,重启vscode可解决
// 3. 配置路由表:当点击特定的 url 时,显示对应的那个组件。
const router = createRouter({
  // hash 模式,路径带#号
  history: createWebHashHistory(), // createWebHistory(),
  routes: [ // 配置每个路由映射一个组件
    {
      path: '/',
      // component: Home
      // 或者动态加载组件,是懒加载在第1次访问时才加载组件
      component: () => import('../views/home/index.vue')
    },
    {
      path: '/news',
      component: News
    },
    {
      path: '/about',
      component: About
    }]
});
// 4. 导出路由对象
export default router;
路由作用到整个应用
import { createApp } from 'vue'
import App from './App.vue'
import './assets/style/bootstrap.min.css'
import './assets/style/dashboard.css'

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

import './assets/main.css'
import router from '@/router/index'; // '@/router'

const app = createApp(App);

// 4. 整个应用支持路由
app.use(router)
app.mount('#app')
修改路由跳转地址

src/components/layout/AppMenu.vue 中修改路由跳转地址

<script setup lang='ts'>
</script>
<template>
  <!--左边菜单栏区域-->
  <div class="col-sm-3 col-md-2 sidebar">
    <ul class="nav nav-sidebar">
      <li class="active"><a href="/">首页</a></li>
      <li><a href="/news">新闻管理</a></li>
      <li><a href="/about">关于我们</a></li>
    </ul>
  </div>
</template>
<style scoped></style>

效果:
Vue3+TS+Vite+Pinia最全学习总结

样式匹配-高亮显示导航

我们会发现不管点击左侧导航哪个链接,当前都是在第一个导航处显示高亮背景。
F12 查看源代码,发现高亮样式在 <li class="active"> 标签上,应该点击哪个作用在哪个上面,
我们接下来就要实现:点击的是哪个菜单,对应这个菜单项才会有亮度背景
vue2 中可在 router-link 组件上使用 tag、exact Prop来实现,但是很遗憾vue3已经废弃了这些Prop已删除 <router-link> 中的 eventtag 、 exact属性。
要实现动态高亮显示菜单项,实际上就是关注对应 class 样式问题,我们通过以下知识点可以实现:

  1. 关注 RouterLink 组件上有哪些 Prop ,参考:https://router.vuejs.org/zh/api/interfaces/RouterLinkProps.html

custom:接收一个布尔值

  • false(默认值):将 转成一个a标签,将其标签体的内容包裹起来,
  • true 不转成a标签,渲染 标签体的内容,并配置 v-slot 获取和跳转链接
  1. 关注 RouterLink 组件上的v-slot指令参考:v-slot 指令接收得到一个对象 RouterLinkOptions
    其中对象的属性有:
  • href:解析后的路由地址。用在 <a>元素的 href 属性值,如:#/ 、#/news、#/
  • route:解析出来的当前路由对象。如:{path:xx, params:xx, children:xx, ....}
  • navigate:是一个函数:触发路由跳转的函数。
  • isAcive:是一个布尔值 true/false,匹配当前访问的链接是否为 当前路由地址 或 当前的子路由地址 :true是匹配成功。
    例如:针对 to="/news" ,所有访问 /news 开头的路由, isActive 值都是 true(/news、/news/sport)
  • isExactActive:是一个布尔值 true/false, 精确匹配 当前访问的链接是否为 当前路由地址 :true是精确匹配成功。
    例如:针对 to="/news",只有访问 /news 路由 isExactActive 值为 true,而访问/news/sport 的isExactActive 值为 false
  1. 重写AppMenu.vue 代码
<script setup lang='ts'>
</script>
<template>
  <!--左边菜单栏区域-->
  <div class="col-sm-3 col-md-2 sidebar">
    <ul class="nav nav-sidebar">
      <router-link to="/" :custom="true" v-slot="{isActive,href}">
        <li :class="{active:isActive}"> <a :href="href">首页</a></li>
      </router-link>
      <router-link to="/about" custom v-slot="{isActive,href}">
        <li :class="{active:isActive}"> <a :href="href">关于我们</a></li>
      </router-link>
      <router-link to="/news" custom v-slot="{isActive,href}">
        <li :class="{active:isActive}"> <a :href="href">新闻管理</a></li>
      </router-link>
    </ul>
  </div>
</template>
<style scoped></style>

效果:Vue3+TS+Vite+Pinia最全学习总结

嵌套路由-新闻管理案例

在这里插入图片描述

新闻管理模块中体育和科技栏目,点击不同栏目下方显示不同内容,将体育和科技为 Sport 和 Tech 两个组件,所以将 news/index.vue 中的模板内容区域分别抽取到 news/sport.vue 和 news/tech.vue 文件中。

  1. 在 src/views/news/ 目录下新建 sport.vue 体育新闻路由组件
<script setup lang='ts'>
</script>
<template>
  <!--体育栏目-->`
  <div>
    <ul>
      <li>
        <a href="#">世界杯开赛啦</a>
      </li>
      <li>
        <a href="#">NBA开赛倒计时</a>
      </li>
    </ul>
    <!--详情-->
    <div class="jumbotron">
      <h2>世界杯开赛啦</h2>
      <p>世界杯于明晚8点举行开幕式.....</p>
    </div>
  </div>
</template>
<style scoped>
</style>
  1. 在 src/views/news/ 目录下新建 tech.vue 体育新闻路由组件
<script setup lang='ts'>
</script>
<template>
  <!--科技栏目-->
  <div>
    <ul>
      <li>
        <span>5G时代到来了 </span>
        <button class="btn btn-default btn-xs">查看(Push)</button>&nbsp;
        <button class="btn btn-default btn-xs">查看(replace)</button>
      </li>
      <li>
        <span>互联网大洗牌</span>
        <button class="btn btn-default btn-xs">查看(Push)</button>&nbsp;
        <button class="btn btn-default btn-xs">查看(replace)</button>
      </li>
    </ul>
    <p>
      <button>后退</button>
    </p>
    <!--详情-->
    <div class="jumbotron">
      <h2>世界杯开赛啦</h2>
      <p>世界杯于明晚8点举行开幕式.....</p>
    </div>
  </div>
</template>
<style scoped></style>
  1. 在 src/router/index.ts 中的 /news 路由下添加 嵌套子路由
// 1. 导入相关路由方法
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
// 2. 导入路由组件
import News from '../view/news/index.vue';
import Home from '../view/home/index.vue';
import About from '../view/about/index.vue'; // 如果出现红色线,重启vscode可解决
// 3. 配置路由表:当点击特定的 url 时,显示对应的那个组件。
const router = createRouter({
  // hash 模式,路径带#号
  history: createWebHashHistory(), // createWebHistory(),
  routes: [ // 配置每个路由映射一个组件
    {
      path: '/',
      // component: Home
      // 或者动态加载组件,是懒加载在第1次访问时才加载组件
      component: () => import('../view/home/index.vue')
    },
    {
      path: '/news',
      component: News,
      children:[
        {
          path:'/news/sport',
          component:()=> import('@/view/news/sport.vue')
        },
        {
          path:'/news/tech',
          component:()=> import('@/view/news/tech.vue')
        }
      ]
    },
    {
      path: '/about',
      component: About
    }]
});
// 4. 导出路由对象
export default router;
  1. 修改src\view\news\index.vue代码
<script setup lang='ts'>
</script>
<template>
  <!--右边主页面区域-->
  <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
    <div class="header clearfix">
      <nav>
        <ul class="nav nav-pills">
          <router-link to="/news/sport" custom v-slot="{ href, isActive }">
            <li :class="{active: isActive}"><a :href="href">体育</a></li>
            </router-link>
            <router-link to="/news/tech" custom v-slot="{ href, isActive }">
            <li :class="{active: isActive}"><a :href="href">科技</a></li>
            </router-link>
        </ul>
      </nav>
      <hr>
    </div>
    <router-view></router-view>
  </div>
</template>
<style scoped></style>

效果:
Vue3+TS+Vite+Pinia最全学习总结

配置默认选中

在 router/index.ts 的 /news 子路由中配置默认重定向(跳转)到体育新闻
方式1: {path: ‘/news’, component: News, redirect: /news/sport’'}
方式2: children: [{ path: ‘’, redirect: ‘/news/sport’}]
Vue3+TS+Vite+Pinia最全学习总结

路由组件传递数据

  1. 路由配置,在路由上加上:id变量占位符。
 path: '/news/sport/detail/:id', // :id 路径变量占位符
  1. 配置跳转地址
 <router-link :to="'/news/sport/detail/'+33">世界杯开赛啦</router-link>
 <!--详情-->
 <router-view></router-view>
  1. 在详情文件中拿到传递过来的数据
<template>
  <div class="jumbotron">
    {{ $route.params }}
  </div>
</template>

实例:实现路由的参数传递,新闻详情页面
1、路由配置path: '/news/sport/detail/:id', // :id 路径变量占位符

    {
      path: '/news',
      component: News,
      redirect: '/news/sport',
      children: [
        {
          path: '/news/sport',
          component: Sport,
          children: [
            {
              path: '/news/sport/detail/:id', // :id 路径变量占位符
              component: () => import('../view/news/sportDetail.vue')
            }
          ]
        },
        {
          path: '/news/tech',
          component: () => import('@/view/news/tech.vue')
        }
      ]
    },

2、创建详情文件并拿到通过路由传递过来的数据sportDetail.vue,通过$route.params获取传递过来的信息

<template>
  <div class="jumbotron">
    {{ $route.params }}
  </div>
</template>

<script lang="ts">
export default {
  created() {
    // 方式1:通过 this.$route 获取参数
    console.log('this.$route.params', this.$route.params.id)
  }
}
</script>

<style></style>

3、配置体育栏目的router-link,指定渲染出口 ,要动态拼接值, 则 to 属性值是 JS 表达式,要写 JS 表达式, 则要使用v-bind方式绑定属性。
注意: + 前面有单引号 ' '
如下代码:在这里插入代码片

方式一、

<script setup lang='ts'>
</script>
<template>
  <!--体育栏目-->
  <div>
    <ul>
      <li style="width: 200px; display: block;">
        <!-- <a href="#">世界杯开赛啦</a> -->
        <router-link :to="'/news/sport/detail/'+33">世界杯开赛啦</router-link>
      </li>
      <li style="width: 200px; display: block; display: block;">
        <router-link :to="'/news/sport/detail/'+45">NBA开赛倒计时</router-link>
      </li>
    </ul>
    <!--详情-->
    <router-view></router-view>
  </div>
</template>
<style scoped>
</style>

方式二、在路由配置表(router/index.ts)的当前路由对象中添加 props: true ,将 route.params 设置为组件的 props

  children: [
        {
          path: '/news/sport',
          component: Sport,
          children: [
            {
              path: '/news/sport/detail/:id', // :id 路径变量占位符
              component: () => import('../view/news/sportDetail.vue'),
              props: true, // 将route.params设置到组件的 props 对象中
            }
          ]
        },
        {
          path: '/news/tech',
          component: () => import('@/view/news/tech.vue')
        }
      ]

通过 props 获取数据

<template>
  <div class="jumbotron">
    {{ $route.params }}
  </div>
</template>

<script lang="ts">
export default {
  // 要在路由配置表的当前路由对象中添加 `props: true`,才会将 route.params 设置为组件的 props,声明与路由参数完全相同的 prop 名
  props:['id'],
  created() {
    // 方式1:通过 this.$route 获取参数
    console.log('this.$route.params',  this.$props.id)
  },
  setup(props){
    console.log("选项式setup函数中获取",props.id)
  }
}
</script>

<style></style>

Vue3+TS+Vite+Pinia最全学习总结

b.组合式API动态获取路由参数值

<template>
  <div class="jumbotron">
    {{ $route.params }}
  </div>
</template>

<script setup lang="ts">
import { useRoute } from 'vue-router';
  //方式一、获取当前路由对象
  const route = useRoute();
  const sportId = route.params.id;
  console.log("route", sportId)
  // 方式二、通过路由的prop传递
  const props = defineProps(['id'])
  console.log('props', props.id)
</script>

<style></style>

效果:
Vue3+TS+Vite+Pinia最全学习总结

监听路由参数变化-查询体育栏目详情数据

问题:点击不同的新闻标题,上面只会获取第一次渲染此路由组件的id值,而不会获取新点击的id值。
解决:通过监听器 watch/watchEffect 来监听路由地址的变化。

watch(() => route.params, (newParams)=>{
	// 1. 获取路由地址上的路径变量id值 (组合式api <scirpt setup>)
	const {id} = newParams as any;
	console.log('watch监听器:id', id);
	// 2. 通过路径变量id值查询新闻详情
	getSportDetailById(id);
}, {immediate: true}); // 立即监听,监听第1次
// 通过id查询详情数据
function getSportDetailById (id:number|string) {
	// 查询出所有新闻
	const portList = getPortList();
	// console.log('portList', portList)
	// 查询为当前路由变量id值的详情数据
	sport.value = portList.find((item:any) => item.id == id);
}

编程式路由导航(JS方式)

声明式与编程式路由
声明式(直接通过<a>标签href指定链接跳转)编程式(采用JS代码链接跳转,如Localhost.href)
<router-link :to="....">router.push(...)
<router-link :to="...." replace>router.replace(...)
编程式路由导航 API

// 选项式API

this.$router.push(path) 相当于点击路由链接(后退1,会返回当前路由界面)
this.$router.replace(path) 用新路由替换当前路由(后退1,不可返回到当前路由界面)
this.$router.back() 后退回上一个记录路由
this.$router.go(n) 参数 n 指定步数
this.$router.go(-1) 后退回上一个记录路由
this.$router.go(1) 向前进下一个记录路由

// 组合式API <script setup>

import { useRouter } from 'vue-router';
const router = useRouter();
router.push(path) 相当于点击路由链接(后退1,会返回当前路由界面)
router.replace(path) 用新路由替换当前路由(后退1,不可返回到当前路由界面)
router.back() 后退回上一个记录路由
router.go(n) 参数 n 指定步数
router.go(-1) 后退回上一个记录路由
router.go(1) 向前进下一个记录路由

命名路由 name

除了path之外,还可以为任何路由提供name。可以通过name进行路由跳转,防止你在url中出现打字错误。

  1. router/index.ts配置
{
	path: 'tech',
	component: () => import('@/views/news/tech.vue'),
	children: [
		{
			path: '/news/tech/detail/:id',
			// name 指定路由名称,通过此名称进行路由跳转,router.push({name:'techDetail',
			params: {id: 1}})
			name: 'techDetail',
			component: () => import('@/views/news/tech-detail.vue'),
		}
	]
}
  1. 在组件中,进行路由跳转:
<!-- 方式4:通过路由名称跳转,在router/index.ts中添加 name: 'techDetail' -->
<button @click="router.push({name: 'techDetail', params: {id: item.id}, query: {count:100}})" class="btn btn-default btn-xs">查看(Push)</button>

或者

<router-link :to="{ name: 'techDetail', params: {id: item.id}, query: {count: 100}}">User</router-link>

路由元信息 meta

有时,你可能希望将任意信息附加到路由上,如:过渡名称、谁可以访问此路由等。
这些事情可以通过在定义路由时,在路由对象中使用 meta 属性来实现,并且它可以通过 $route / route 对象和导航守卫上都被访问到。

  1. router/index.ts 中定义路由的时候你可以这样配置 meta 字段:
{
	path: '/about',
	component: About,
	meta: {isAuth: true} // 指定路由元信息,通过 $route.meta.isAuth 获取
}
  1. 在组件中获取 meta 对象值
<script setup lang='ts'>
	import { useRoute } from 'vue-router';
	const route = useRoute();
	const isAuth = ref(false);
	// 会报红线,ts中使用要增强 RouteMeta 接口中的属性,在 env.d.ts 中配置
	isAuth.value = route.meta.isAuth || false;
</script>

上面 Typescript 中使用 meta 的对象属性会报错,需要增强 RouteMeta 接口中的属性,在 env.d.ts 中配置:

/// <reference types="vite/client" />
import 'vue-router'
declare module 'vue-router' {
	// 针对路由元信息meta对象属性添加类型声明
	interface RouteMeta {
		// ?表示可选的
		isAuth?: boolean;
	}
}
declare module '*.vue' {
	import { Component } from 'vue'
	const component: Component
	export default component
}

缓存路由组件

  1. 默认情况下,当路由组件被切换后组件实例会销毁,当切换回来时实例会重新创建。
  2. 如果可以缓存路由组件实例,切换后不用重新加载数据,可以提高用户体验
实现缓存路由组件和动态过度效果

<keep-alive> 可缓存渲染的路由组件实例

<!-- 路由组件渲染出口, 解构出当前路由组件对象 -->
<router-view v-slot="{ Component }">
  <transition name="fade" mode="out-in"> <!-- 过滤效果 -->
    <!-- 组件缓存(<keep-alive> 标签内不可写注释,会报错)-->
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </transition>
</router-view>

案例演示

  1. src/views/about/index.vue的模板中添加一个input输入框
<script setup lang='ts'>
import { ref } from 'vue';
const name = ref('')
</script>

<template>
  <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
    <!-- 不要少了css样式,不然下面渲染内容会被遮挡 ,注意:注释不要放第一行,不然过渡会空白 -->
    <h2>关于我们</h2>
    <p>梦学谷-陪你学习,伴你梦想!</p>
    <p>www.mengxuegu.com</p>
    <h3>是否有权限访问当前路由组件:{{ $route.meta.isCache }} {{ $route.meta.isCache }}</h3>
    <input v-model="name" type="text">
  </div>
</template>

<style scoped></style>
  1. 输入框输入内容后,来回切换组件,实现输入框内容不会被清空。在 src/components/layout/AppMain.vue 中配置
<script setup lang='ts'>
import { ref, reactive } from 'vue'
</script>

<template>
  <router-view v-slot="{ Component, route }">
    <transition :name="route.meta.transitionName || 'fade'" mode="out-in">
      <keep-alive>
        <component :is="Component" />
      </keep-alive>
    </transition>
  </router-view>
</template>

<style scoped>
/*过渡样式*/
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

/* name='wuyong' 的过滤效果*/
.wuyong-enter-active,
.wuyong-leave-active {
  transition: opacity 2s;
}

.wuyong-enter-from,
.wuyong-leave-to {
  opacity: 0;
}
</style>
  1. 配置router/index.ts文件
    {
      path: '/about',
      component: About,
      meta: {isAuth: true, transitionName: 'wuyong'} // 指定路由元信息,通过 $route.meta.isAuth 获取
    }  

效果:
Vue3+TS+Vite+Pinia最全总结

路由导航守卫

vue-router 路由守卫就是路由跳转时,会触发的钩子函数我们把他称为路由守卫。
vue-router 提供了三种路由守卫:

全局的(执行顺序按下面编号排序)

  1. beforeEach 全局前置守卫(目标路由进入前触发)
  2. beforeResolve 全局解析守卫(目标路由解析前触发,几乎不用)
  3. afterEach 目标路由进入后触发

单个路由独享的

  1. beforeEnter 路由进入之前

组件级的

  1. beforeRouteEnter 选项:路由进入之前调用。 (组合api中没有对应的 onBeforeRouteEnter )
  2. beforeRouteUpdate 选项:路由更新之前调用,针对同一路由的params不同参数变化时调用,如
    /user/:id,/user/1和/user2切换时调用。(组合api中有对应的 onBeforeRouteUpdate )。
  3. beforeRouteLeave 选项:路由离开之前调用。(组合api中有对应的 onBeforeRouteLeave )
全局前置守卫 beforeEach

应用场景:在实际开发项⽬中,我们经常使⽤ vue-router 路由守卫实现⻚⾯的权限校验。⽐如:当⽤户登录之后,我们会把后台返回的token以及⽤户信息保存本地,当⻚⾯进⾏跳转的时候,我们会在路由的全局前置守卫⾥⾯获token,如果token存在,则允许进⼊要跳转的⻚⾯;如果token不存在,则跳转到登录⻚要求用户登录。
使用 router.beforeEach 注册一个全局前置守卫。当一个路由导航触发时,会先调用全局前置守卫方法。全局前置守卫方法接收3个参数:

  • to : 即将要进入的目标路由对象
  • from : 当前导航正要离开的路由对象
  • next : 调用该方法,进入目标路由

如果守卫方法中没有接收第3个参数 next ,则通过 return 返回一个值来进行路由导航处理。


// 1、 通过 return 返回一个值来判断是否继续路由导航(不要接收next参数)
router.beforeEach((to, from) => {
  // 是否已登录
  const token = localStorage.getItem('token');
  console.log(token, 'to', to.path, 'from', from.path)
  if (to.path === '/about') {
    // 中断本次导航
    alert('中断本次导航');
    return false;
  }
  // 用户未登录,且当前不是登录请求(防止无限重定向)
  if (!token && to.path !== '/login') {
    // 将用户重定向到登录页面
    console.log('将用户重定向到登录页面');
    return { path: '/login' };
  }
  // `return true` 或无返回值则表示进入目标路由
  console.log('进入目标路由', to.path);
});

如果守卫方法中有接收第3个参数 next ,则必须通过 next 来进行路由导航处理。

// 2、通过第3个参数 next 来进行路由导航
router.beforeEach((to, from, next) => {
	console.log('router.beforeEach触发');
	// 是否已登录
	const token = localStorage.getItem('token');
	// 未登录,且当前不是登录请求
	if (!token && to.path !== '/login') {
		// 跳转到登录页
		next({path: '/login'});
		// 如果不return,则下面next()也会被调用,导致生产环境会失败,
		// 因为每个导航守卫中只能被调用一次,而这里调用了两次【 调用完 next('/login') 马上又会调用 next() 】。
		return;
	}
	// 进入目标路由(接收了 next 参数,就要调用next)
	console.log('进入目标路由', to.path);
	next();
});

注意:如果在守卫中通过 return 返回一个值,而不是调用 next 方法,方法中必须删除 next 参数。

全局解析守卫 beforeResolve

使用 router.beforeResolve 注册一个全局解析守卫,,它和 router.beforeEach 类似,在每次导航时都会触发,在 router.beforeEach 后面执行。router.beforeResolve 主要用于异步获取数据或执行任何其他操作(如果用户无法进入页面时,你希望额外做一些补偿的操作)的理想位置。

/**
* 路由全局解析守卫:目标路由解析前触发
*/
router.beforeResolve((to, from, next) => {
	console.log('router.beforeResolve触发');
	// 1、没有接收 next 参数,通过 return 进行路由处理;
	/*
	if (to.path == '/about') {
		// 中止导航
		return false;
	}
	*/
	// 2、有接收next参数,必须通过 next 方法进行路由处理
	next();
});
全局后置钩子 afterEach

使用 router.beforeResolve
注册一个全局后置钩子,在目标路由进入后触发。可用于关闭页面进度条等。会接收3个参数:(没有 next )

  • to: 即将要进入的目标路由对象。
  • from: 当前导航正要离开的路由对象。
  • failure:路由跳转失败原因; undefined 则表示跳转成功。
    如 Error: Navigation aborted from “/news/sport” to “/about” via a navigation guard.
/**
* 路由全局后置钩子:目标路由·进入后·触发
* 会接收3个参数:
* to: 即将要进入的目标路由对象
* from: 当前导航正要离开的路由对象
* failure:路由跳转失败原因; `undefined`则表示跳转成功
* 如 `Error: Navigation aborted from "/news/sport" to "/about" via a navigation guard.`
*
*/
router.afterEach((to, from, failure) => {
	console.log('router.afterEach被触发', to.fullPath, failure);
})
路由独享的守卫 beforeEnter

路由配置上定义 beforeEnter 路由独享的守卫,它在路由进入之前被调用

{
	path: '/about',
	component: About,
	meta: {isAuth: true, transitionName: 'mxg'},
	beforeEnter: () => {
		 // 路由进入前被调用
		alert('beforeEnter路由独享的守卫:中断此导航访问')
		return false;
	}
}

也可以将一个函数数组传递给 beforeEnter ,这样方便守卫的处理逻辑在不同的路由中多处复用:

// 导入 to 类型接口
import type { RouteLocationNormalized } from 'vue-router';
// 移除查询参数query
function removeQueryParams(to: RouteLocationNormalized) {
	if (Object.keys(to.query).length){
		return { path: to.path, query: {}, hash: to.hash }
	}
}
// 引用守卫方法 removeQueryParams
{
	path: '/about',
	component: About,
	meta: {isAuth: true, transitionName: 'mxg'},
	beforeEnter: [removeQueryParams] // 数组值,可指定多个函数
},
组件内的守卫

在路由组件内部,可以直接定义路由导航守卫。
组件内的守卫,针对组合式API只有如下守卫:

选项式API组合式API说明
beforeRouteEnter无(可在路由配置中使用路由,独享的守卫 beforeEnter 处理)路由进入之前调用。
beforeRouteUpdateonBeforeRouteUpdate路由更新之前调用。针对同一路由的params不同参数变化时调用,如 /user/:id,/user/1和/user2切换时调用。常用于同一个组件传入不同参数时,展示不同的数据
beforeRouteLeaveonBeforeRouteLeave离开当前路由组件,跳转到别的路由组件时触发该钩子。常用于表单页面,当用户填了一部分内容,需要提醒用户“确定离开是否当前页面吗”

十一、Pinia 状态管理

安装Pinia

cd进入项目根目录,执行安装pinia:

npm install pinia@2.1.4

创建pinia实例并传递给应用

在main.ts中创建pinia实例并传递给应用

import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
// 导入pinia实例方法
import { createPinia } from 'pinia'
// 创建pinia实例并挂载到应用上
const app = createApp(App).use(createPinia())
app.mount('#app')

定义和修改Store状态state

通过 defineStore() 方法定义 Store(全局状态数据),其中参数和返回值说明:

  • 参数1:应用中 Store 的唯一标识字符串Id,不可重复
  • 参数2:接受Option对象或 Setup 函数形式
    返回值:
  • defineStore() 的返回值进行任意命名,但最好使用 store 的名字,同时以 use 开头且以 Store
    尾。
  • use开头是一个符合组合式函数风格的约定。(比如 useUserStoreuseCartStoreuseProductStore )

购物车实例:

  1. 创建购物车数据状态管理文件:/src/stores/cart.ts
import { defineStore } from "pinia";
interface Goods{
  id:number;
  name:string;
  num:number;
}
// 参数二,采用option对象形式
export const useCartStore = defineStore('cart',{
  // state用于定义一个返回初始状态的函数
  state:()=>{
    return {
      name:"wuyong",
      age:18,
      goodsList:[{id:1,name:'手机',num:1}] as Goods[]
    }
  }
})
  1. 组件中获取和修改状态数据,解构出来的state状态,不是响应式的,使用 storeToRefs 方法将状态转成 ref 后,解构出来的状态才是响应式的,通过 store 实例操作状态是 响应式 的,通过 $patch 可同时修改多个状态属性。
<script setup lang="ts">
import { useCartStore } from './stores/cart';
import { storeToRefs } from 'pinia';
const cartStore = useCartStore();
// 通过store实例获取state状态
console.log("获取state状态", cartStore.$state, cartStore.name);
// 直接结构出来state状态,不是响应式的
//let { age } = cartStore;
// 使用storeToRefs方法将状态转成ref后,结构出来的状态是响应式的
const { age } = storeToRefs(cartStore);
console.log("age", age.value)
//修改state状态值
function handleAddAge() {
  // age ++; // 直接解构是`非响应式`的
  // age.value++; // 转成ref是`响应式`的
  // cartStore.age++;

  // 4. 通过 $patch 方法接受一个对象,可同时修改多个状态属性
  // cartStore.$patch({
  //   name: 'wuqi',
  //   age: cartStore.age+1,
  // });

  // $patch 方法接受一个带state参数的回调函数,state来操作状态
  cartStore.$patch((state) => {
    state.name = 'wuqi';
    state.age = 24;
    state.goodsList.push({ id: state.goodsList.length + 1, name: '电脑'+state.goodsList.length + 1, num: 2 });
  })
}
</script>

<template>
  <div>
    <p>用户名:{{ cartStore.name }}</p>
    <p>
      年龄:{{ age }}
      <button @click="handleAddAge">+1</button>
    </p>
    <ul>
      <li v-for="(item, index) in cartStore.goodsList" :key="index">
        商品ID: {{ item.id }},名称:{{ item.name }},数量:{{ item.num }}
      </li>
    </ul>
    <button @click="handleAddAge">年龄+1</button>
  </div>
</template>

<style scoped></style>

Vue3+TS+Vite+Pinia最全总结
3. 重置状态:通过调用 store $reset() 方法,将 state 状态重置为初始值,

<script setup lang="ts">
import { useCartStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
const cartStore = useCartStore();
function handleResetState() {
	// 将 state 状态重置为初始值
	cartStore.$reset();
}
</script>
<template>
<div>
	<button @click="handleResetState">重置状态</button>
</div>
</template>
<style scoped>
</style>

效果:Vue3+TS+Vite+Pinia最全总结
4. 监听state状态变化:通过 store$subscribe() 方法监听 state 及其变化

// 通过 store 的 $subscribe() 方法监听 state 及其变化
cartStore.$subscribe((mutation, state) => {
	// 获取组件标识id(和 cartStore.$id 一样)
	const { storeId } = mutation;
	console.log('storeId', storeId); // 'cart'
	// 每当状态发生变化时,将整个 state 持久化到本地存储。
	localStorage.setItem('cart', JSON.stringify(state))
})

Vue3+TS+Vite+Pinia最全总结

Getter 派生属性

Getter 介绍:
  • 有时候我们需要从 Store 中的 state 中派生出一些状态。例如:基于上面代码,增加一个userType 属性,当 age 值小于18,则 userType 值为 未成年人 ; 大于等 于18 , 则 userType 值为 成年人 。这时我们就需要用到 Getter 为我们解决。
  • Getter 相当于组件中的计算属性(computed)。可以通过 defineStore() 中的 getters 属性来定义它们。 推荐使用箭头函数,并且它将接收 state 作为第一个参数。
  • Getter中通过返回一个函数,该函数可以接受调用方传递任意参数。
Getter 实操作
  1. 在 src/stores/cart.ts 定义一个 userType 派生属性
import { defineStore } from "pinia";
interface Goods{
  id:number;
  name:string;
  num:number;
}
// 参数二,采用option对象形式
export const useCartStore = defineStore('cart',{
  // state用于定义一个返回初始状态的函数
  state:()=>{
    return {
      name:"wuyong",
      age:18,
      goodsList:[{id:1,name:'手机',num:1}] as Goods[]
    }
  },
  //定义派生属性(等同于计算属性,根据state状态值得到新的一个状态值)
  getters:{
    //接受state作为第一个参数,使用建通函数声明:箭头函数方法不能使用this
    userType:(state)=>{
      return state.age<18?'未成年人':'成年人'
    },
    //使用普通函数声明,方法体可以使用this访问整个store实例
    getGoodsById(state){
      //通过this获取上面getter
      console.log("getter普通函数",this.userType);
      return (id:number)=>this.goodsList.find(item=>item.id===id)
    }
  }
})
  1. 在组件中获取Getter
<p>Getter 派生属性:{{ cartStore.userType }}</p>
<p>向 Getter 派生属性传递参数:{{ cartStore.getGoodsById(1) }}</p>

效果:
Vue3+TS+Vite+Pinia最全总结

Action

Action 相当于组件中的 method。它通过 defineStore() 中的 actions 属性来定义,并且它是定义业务逻辑的完美选择。

Action购物车使用实例
  1. 将商品加入购物车 goodsList 状态中
import { defineStore } from "pinia";
interface Goods{
  id:number;
  name:string;
  num:number;
}
// 参数二,采用option对象形式
export const useCartStore = defineStore('cart',{
  // state用于定义一个返回初始状态的函数
  state:()=>{
    return {
      name:"wuyong",
      age:15,
      goodsList:[{id:1,name:'手机',num:1}] as Goods[]
    }
  },
  //定义派生属性(等同于计算属性,根据state状态值得到新的一个状态值)
  getters:{
    //接受state作为第一个参数,使用建通函数声明:箭头函数方法不能使用this
    userType:(state)=>{
      return state.age<18?'未成年人':'成年人'
    },
    //使用普通函数声明,方法体可以使用this访问整个store实例
    getGoodsById(state){
      //通过this获取上面getter
      console.log("getter普通函数",this.userType);
      return (id:number)=>this.goodsList.find(item=>item.id===id)
    }
  },
  // 定义行为,类似method
  actions:{
    //新增商品
    addGoods(goods:Goods){
      if(goods.num){
        //将goods.nums的值除1转成数值
        goods.num = goods.num/1
        //查询购物车中是否存在此商品,存在则数量累加,不存在则追加商品到数组中
        const target = this.goodsList.find(item=>item.id === goods.id)
        if(target){
          target.num+=goods.num
        }else{
          this.goodsList.push(goods)
        }
      }
    }
  }
})
  1. 组件中使用
<script setup lang="ts">
import { useCartStore } from './stores/cart';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
//加入购物车逻辑
const goodsId = ref<number>(1)
const goodsName = ref<string>('')
const goodsNum = ref<number>(1)
function handleAddCart() {
  const goods = { id: goodsId.value, name: goodsName.value, num: goodsNum.value }
  //触发store中的action
  cartStore.addGoods(goods)
}
const cartStore = useCartStore();
// 通过store实例获取state状态
console.log("获取state状态", cartStore.$state, cartStore.name);
// 使用storeToRefs方法将状态转成ref后,结构出来的状态是响应式的
const { age } = storeToRefs(cartStore);
console.log("age", age.value)
//将state状态重置为初始值
function handleResetState() {
  cartStore.$reset();
}
//修改state状态值
function handleAddAge() {
  // $patch 方法接受一个带state参数的回调函数,state来操作状态
  cartStore.$patch((state) => {
    state.name = 'wuqi';
    state.age = ++state.age;
    state.goodsList.push({ id: state.goodsList.length + 1, name: '电脑' + state.goodsList.length + 1, num: 2 });
  })
  cartStore.$subscribe((mutation, state) => {
    console.log("mutation, state", mutation, state)
    // 获取组件标识id(和 cartStore.$id 一样)
    const { storeId } = mutation;
    console.log('storeId', storeId); // 'cart'
    // 每当状态发生变化时,将整个 state 持久化到本地存储。
    localStorage.setItem('cart', JSON.stringify(state))
  })
}
</script>

<template>
  <div>
    <p>用户名:{{ cartStore.name }}</p>
    <p>
      年龄:{{ age }}
    </p>
    <ul>
      <li v-for="(item, index) in cartStore.goodsList" :key="index">
        商品ID: {{ item.id }},名称:{{ item.name }},数量:{{ item.num }}
      </li>
    </ul>
    <input v-model="goodsId" placeholder="商品ID"> &nbsp;
    <input v-model="goodsName" placeholder="商品名称"> &nbsp;
    <input v-model="goodsNum" placeholder="商品数量"> &nbsp;
    <button @click="handleAddCart">加入购物车</button>
    <!-- <p>Getter 派生属性:{{ cartStore.userType }}</p>
    <p>向 Getter 派生属性传递参数:{{ cartStore.getGoodsById(1) }}</p> -->
  </div>
</template>

<style scoped></style>

效果:
Vue3+TS+Vite+Pinia最全总结

Setup Store

上面用的都是选项式 Option Store ,也存在另一种定义语法 Setup Store :与 Vue 组合式 API 的 setup
函数 相似:传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。
在 Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

实例:

  1. 创建src/store/counter.ts
    import {defineStore} from 'pinia'
    import {ref,computed} from 'vue'
    /**
    * defineStore 参数2:采用 Setup 函数形式
    * ref() 就是 state 属性
    * computed() 就是 getters
    * function() 就是 actions
    */
    export const useCounterStore = defineStore('counter',()=>{
      // 定义store的state属性
      const count = ref(0)
      // 定义store的getters
      const doubleCount = computed(()=>{
        count.value*2
      })
      function addCount(){
        count.value++
      }
      return {count,doubleCount,addCount}
    })
    
  2. 组件中调用使用counter.ts
<script setup lang="ts">
import {useCounterStore} from './stores/counter'
const counterStore = useCounterStore()
</script>

<template>
  <h2>Setup Store</h2>
  <p>count:{{ counterStore.count }}</p>
  <button @click="counterStore.addCount">+1</button>
</template>

<style scoped></style>

效果:
Vue3+TS+Vite+Pinia最全总结
----------------vue3学习到此完结----------------

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值