# VUE3

# 概述

Vue 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。官网为: https://cn.vuejs.org/

Vue 为渐进式 JavaScript 框架

  • 渐进式:只需要导入需要功能的部分
  • 非渐进式:需要导入全部内容

Vue 的两个核心功能:

  • 声明式渲染:Vue 基于标准 HTML 拓展了一套模板语法,使得我们可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。
  • 响应性:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM

# 非工程化实现

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <!-- 这里也可以用浏览器打开连接,然后将获得的文本单独保存进入一个vue.js的文件,导入vue.js文件即可 -->
        <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
        <div id="app">
            <!-- 给style属性绑定colorStyle数据 -->
            <!-- <!--swig0--> -->
           <h1 v-bind:style="colorStyle"><!--swig1--></h1>
           <!-- v-text设置双标签中的文本 -->
           <p v-text="article"></p>
           <!-- 给type属性绑定inputType数据 -->
           <input v-bind:type ="inputType" value="helloVue3"> <br>
           <!-- 给按钮单击事件绑定函数 -->
           <button  @click="sayHello()">hello</button>
        </div>
        <script>
            //组合api
            const app = Vue.createApp({
                // 在setup内部自由声明数据和方法即可!最终返回!
                setup(){
                    //定义数据
                    //在VUE中实现DOM的思路是: 通过修改修数据而影响页面元素
                    // vue3中,数据默认不是响应式的,需要加ref或者reactive处理,后面会详细讲解
                    let inputType ='text'
                    let headline ='hello vue3'
                    let article ='vue is awesome'
                    let colorStyle ={'color':'red'}
                    // 定义函数
                    let sayHello =()=>{
                        alert("hello Vue")
                    }
                    //在setup函数中,return返回的数据和函数可以在html使用
                    return {
                       inputType,
                       headline,
                       article,
                       colorStyle,
                       sayHello
                    }
                }
            });
            //挂载到视图
            app.mount("#app");  // #后跟随id值
        </script>
    </body>
</html>
  1. 框架文件需要自行下载和导入
  2. app 的实现有语法冗余

# 通过 Vite 实现工程化

Vite + npm = Maven

# 操作

image

# 创建项目

使用命令行创建工程

  • 在磁盘的合适位置上,创建一个空目录用于存储多个前端项目
  • 用 vscode 打开该目录
  • 在 vocode 中打开命令行运行如下命令
npm create vite@latest
  • 第一次使用 vite 时会提示下载 vite,输入 y 回车即可,下次使用 vite 就不会出现了

image

  • 注意: 选择 vue+JavaScript 选项即可

image

# 安装依赖

  • 此时只是在 package.json 中含有依赖信息,但是还没有下载,还要手动下载
  • cd 进入刚刚创建的项目目录
  • npm install 命令安装基础依赖
cd ./vue3-demo1
npm install

image

# 启动项目

研发模式运行

npm run dev

image

# 可执行的命令

通过查看 package.json 可以获得可执行的命令

{
  "name": "vite-project",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.4.31"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.5",
    "vite": "^5.3.4"
  }
}

# 停止项目

Ctrl + C

# 目录结构

image

  • node_modules:存放下载的依赖文件

  • public/ :用于存放公共资源,如 HTML 文件、图像、字体等,这些资源会被直接复制到构建出的目标目录中

  • src/ :存放项目的源代码,包括 JavaScript、CSS、Vue 组件、图像和字体等资源。开发过程中,这些文件会被 Vite 实时编译和处理,并在浏览器中进行实时预览和调试

    • assets/ :用于存放一些项目中使用的静态资源,如图片、字体、样式文件等
    • components/ :用于存放组件相关的文件。组件是代码复用的一种方式,用于抽象出一个可复用的 UI 部件,方便在不同的场景中进行重复使用
    • layouts/ :用于存放页面级别的组件文件,通常是路由对应的组件文件。在这个目录下,可以创建对应的文件夹,用于存储不同的页面组件
    • plugins/ :用于存放 Vite 插件相关的文件,可以按需加载不同的插件来实现不同的功能,如自动化测试、代码压缩等
    • router/ :用于存放 Vue.js 的路由配置文件,负责管理视图和 URL 之间的映射关系,方便实现页面之间的跳转和数据传递
    • store/ :用于存放 Vuex 状态管理相关的文件,负责管理应用程序中的数据和状态,方便统一管理和共享数据,提高开发效率
    • utils/ :用于存放一些通用的工具函数,如日期处理函数、字符串操作函数等
  • vite.config.jsVite 的配置文件,可以通过该文件配置项目的参数、插件、打包优化等。该文件可以使用 CommonJS 或 ES6 模块的语法进行配置

  • package.json:标准的 Node.js 项目配置文件,包含了项目的基本信息和依赖关系。其中可以通过 scripts 字段定义几个命令,如 dev、build、serve 等,用于启动开发、构建和启动本地服务器等操作

Vite 项目的入口为 src/main.js 文件,这是 Vue.js 应用程序的启动文件,也是整个前端应用程序的入口文件。在该文件中,通常会引入 Vue.js 及其相关插件和组件,同时会创建 Vue 实例,挂载到 HTML 页面上指定的 DOM 元素中

# 默认 npm scripts

  • 在安装了 Vite 的项目中,可以在 npm scripts 中使用 vite​ 可执行文件,或者直接使用 npx vite​ 运行它。下面是通过脚手架创建的 Vite 项目中默认的 npm scripts:(package.json)
{
  "scripts": {
    "dev": "vite", // 启动开发服务器,别名:`vite dev`,`vite serve`
    "build": "vite build", // 为生产环境构建产物
    "preview": "vite preview" // 本地预览生产构建产物
  }
}

# vite.config.js 设置端口号

//修改vite项目配置文件 vite.config.js
export default defineConfig({
  plugins: [vue()],
  server:{
    port:3000
  }
})

# 单文件组件(SFC)

# VUE 的组件

  • 一个页面作为整体,是由多个部分组成的,每个部分在这里就可以理解为一个组件
  • 每个.vue 文件就可以理解为一个组件,多个.vue 文件可以构成一个整体页面
  • 组件化给我们带来的另一个好处就是组件的复用和维护非常的方便

image

# .vue 文件

  • 传统的页面有.html 文件.css 文件和.js 文件三个文件组成(多文件组件)

  • vue 将这文件合并成一个.vue 文件(Single-File Component,简称 SFC,单文件组件)

  • .vue 文件对 js/css/html 统一封装,这是 VUE 中的概念 该文件由三个部分组成 <script> <template> <style>

    • template 标签 代表组件的html部分代码 代替传统的.html 文件
    • script 标签 代表组件的js代码 代替传统的.js 文件
    • style 标签 代表组件的css样式代码 代替传统的.css 文件

工程化 VUE 项目组织组件

  • index.html是项目的入口,其中 <div id ='app'></div>​ 是用于挂载所有组建的元素
  • index.html 中的 script 标签引入了一个main.js文件,具体的挂载过程在 main.js 中执行
  • main.js是 vue 工程中非常重要的文件,他决定这项目使用哪些依赖,导入的第一个组件
  • App.vue是 vue 中的核心组件,所有的其他组件都要通过该组件进行导入,该组件通过路由可以控制页面的切换

image
注:

  • 语法上,要求 template 只能有一个一级子标签;但是也不会报错,最好加上 div 进行包裹
<script>
    //存储vue页面逻辑js代码
</script>
<template>
    <!-- 页面的样式的是html代码-->
</template>
<style scoped>
    /** 存储的是css代码! <style scoped> 是 Vue.js 单文件组件中用于设置组件样式的一种方式。
    它的含义是将样式局限在当前组件中,不对全局样式造成影响。 */
</style>

# 样式导入

  1. 在.vue 文件中的<style>

  2. 将 css 样式保存到独立的.css 文件中,哪个.vue 文件需要,就在哪里导入即可

    导入方式

  3. <script>中导入

    <script setup>
    import './style/test.css';
    </script>
  4. <style>中导入

    <style scoped>
    @import './style/test.css';
    </style>
  5. 如果某个样式需要在全局使用,则在 main.js 中引入

    import { createApp } from "vue";
    import "./style.css";
    import "./style/test.css"; //此处插入
    import App from "./App.vue";
    createApp(App).mount("#app");

# 响应式和非响应式

响应式数据

在数据变化时,VUE 框架会将变量最新的值更新到 DOM 树中,页面数据就是实时更新的

非响应式数据

在数据变化时,VUE 框架不会将变量最新的值更新到 DOM 树中,页面数据不是实时更新的

  • VUE2 中,数据不做特殊处理,默认就是响应式的

  • VUE3 中,数据要经过 ref/reactive 函数的处理才是响应式的

    • ref/reactive 函数是 VUE 框架提供的方法,需要导入使用

# ref 处理

  • ref 处理的响应式数据,在操作时:

    • <script>中:操作 ref 的响应式数据需要通过.value​ 的形式操作
    • <template>中:操作 ref 的响应式数据无需通过上述形式操作
<script type="module">
import { ref } from "vue";
export default {
    setup() {
        // 定义要展示到html上的一些数据
        let counter = ref(0);
        // 绑定按钮事件
        function counterIncr() {
            counter.value++;
        }
        function counterDecr() {
            counter.value--;
        }
        function showCounter() {
            alert(counter.value);
        }
        return { counter, counterIncr, counterDecr, showCounter };
    },
};
</script>
<template>
    <div>
        <button @click="counterIncr()">+</button>
        <span v-text="counter"></span>
        <button @click="counterDecr()">-</button>
        <button @click="showCounter()">show</button>
    </div>
</template>
<style scoped></style>

# setup 关键字

不使用 setup 关键字
需要在内部书写export default​/setup()​/return{}

<script type="module">
export default {
    setup() {
        return { };
    },
};

使用 setup 关键字

只需要在<script>后添加setup语法糖,就可以省略上述需要再内部书写的内容

<script setup>
import { ref } from "vue";
</script>

# 视图渲染

# 模板语法

Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。
所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。

# 文本渲染

# 插值表达式

插值表达式:最基本的数据类型绑定形式是文本差值,使用双大括号语法:
{{数据名字/函数/对象调用API}}

  • 插值表达式是将数据渲染到元素的指定位置的手段之一
  • 插值表达式不绝对依赖标签,没有标签也可以单独使用,其位置相对自由
  • 插值表达式中支持 javascript 的运算表达式
  • 插值表达式中也支持函数的调用
<script setup>
let msg = "hello vue3";
let getMsg = () => {
    return "hello vue3 message";
};
let age = 19;
let bee = "b e e";
let carts = [
    { name: "Cola", price: 3, number: 10 },
    { name: "Tomato", price: 6, number: 3 },
];
// 获取商品总金额的一个方法
function compute() {
    let count = 0;
    for (let index in carts) {
        count += carts[index].price * carts[index].number;
    }
    return count;
}
</script>
<template>
    <div>
        <!-- 绑定数据到标签中 -->
        <h1><!--swig3--></h1>
        <!-- 不依赖标签 -->
        msg: <!--swig4--> <br />
        <!-- 可调用函数,将函数的返回值进行渲染 -->
        getMsg: <!--swig5--> <br />
        <!-- 支持一些常见的运算符 -->
        Is adult?: <!--swig6--> <br />
        <!-- 支持对象调用一些API -->
        反转:<!--swig7--> <br />
        购物车总金额:<!--swig8--> <br />
        购物车总金额:<!--swig9-->
    </div>
</template>
<style scoped></style>

image

# v-text/v-html

  • v-*** 这种写法的方式使用的是 vue 的命令
  • v-*** 的命令必须依赖元素,并且要写在元素的开始标签
  • v-***指令支持 ES6 中的字符串模板
  • 插值表达式中支持 javascript 的运算表达式
  • 插值表达式中也支持函数的调用
  • v-text 可以将数据渲染成双标签中间的文本,但是不识别 html元素结构的文本
  • v-html 可以将数据渲染成双标签中间的文本,识别 html元素结构的文本
<script setup>
let msg = "hello vue3";
let haha = "hh";
let msg2 = `hello ${haha}`;
let age = 19;
let bee = "b e e";
function fun() {
    return "abc";
}
let redMsg = "<font color='red'>msg</font>";
</script>
<template>
    <div>
        <span v-text="msg"></span> <br />
        <!-- 支持模板字符串 -->
        <span v-text="msg2"></span> <br />
        <!-- 可以直接写入模版字符串 -->
        <span v-text="`你好 ${haha}`"></span> <br />
        <!-- 支持常见运算符 -->
        <span v-text="age >= 18 ? '成年' : '未成年'"></span> <br />
        <!-- 支持常见对象的API调用 -->
        <span v-text="bee.split(' ').reverse().join('-')"></span> <br />
        <!-- 支持函数的调用-->
        <span v-text="fun()"></span> <br />
        <span v-text="redMsg"></span> <br />
        <span v-html="redMsg"></span> <br />
    </div>
</template>
<style scoped></style>

image

# Attribute 属性渲染

使用v-bind​ 进行绑定

  • 由于插值表达式不能直接放在标签的属性中,所有要渲染元素的属性就应该使用 v-bind
  • v-bind 可以用于渲染任何元素的属性,语法为 v-bind:属性名='数据名'​, 可以简写为 :属性名='数据名'
<script setup>
const data = {
    name:'尚硅谷',
    url:"http://www.atguigu.com",
    logo:"http://www.atguigu.com/images/index_new/logo.png"
  }
</script>
<template>
    <div>
        <a v-bind:href="data.url">
        <img v-bind:src="data.logo" v-bind:title="data.name">
    </a>
    </div>
</template>
<style scoped></style>

# 事件绑定

使用 v-on​ 来监听 DOM 事件,并在事件触发时执行对应的 Vue 的 JavaScript 代码。

  • 用法:v-on:click="handler"​ 或简写为 @click="handler"

  • vue 中的事件名=原生事件名去掉on​ 前缀 如:onClick --> click

    • onclick ondbclick onblur onfocus -> click dbclick blur focus
  • handler 的值可以是方法事件处理器,也可以是内联事件处理器

  • 绑定事件时,可以通过一些绑定的修饰符,常见的事件修饰符如下

    • .once:只触发一次事件。[重点]
    • .prevent:阻止默认事件。[重点]
    • .stop:阻止事件冒泡。
    • .capture:使用事件捕获模式而不是冒泡模式。
    • .self:只在事件发送者自身触发时才触发事件。
<script setup>
import { ref } from "vue";
function fun1() {
    alert("hi");
}
function fun2(event) {
    let flag = confirm("确定要访问吗");
    if (!flag) {
        // 原生js编码方式阻止组件的默认行为
        event.preventDefault(); // 需要传入event
    }
}
function fun3() {
    alert("超链接被点击了");
}
let counter = ref(0);
</script>
<template>
    <div>
        <!-- 事件的绑定函数 -->
        <button v-on:click="fun1()">hello</button> <br />
        <!-- 内联事件处理器,需要注意ref处理的变量在template中不需要用.value -->
        <button v-on:click="counter++">+</button> <br />
        <!-- 事件的修饰符 .once 事件只绑定一次 -->
        <button v-on:click.once="counter++">++</button> <br />
        <!--swig10-->
        <!-- 事件的修饰符 .prevent 阻止组件的默认行为,但是会一直阻止,不会提供选项 -->
        <a href="http://www.atguigu.com" v-on:click="fun2($event)">atguigu</a>
        <br />
        <a href="http://www.atguigu.com" v-on:click.prevent="fun3()">atguigu</a>
    </div>
</template>
<style scoped></style>

# 响应式

此处的响应式是指 : 数据模型发生变化时 ,自动更新 DOM 树内容,页面上显示的内容会进行同步变化,vue3 的数据模型不是自动响应式的,需要我们做一些特殊的处理

  1. ref():适合单个变量

    1. <script>中操作 ref 响应式数据要通过.value
    2. <template>中就不需要,直接使用
      <script setup>
      import { ref } from "vue";
      let counter = ref(10);
      function incr() {
          counter.value++;
      }
      </script>
      <template>
          <div>
              <button @click="incr()">+</button>
              <button @click="counter--">-</button>
              <!--swig11-->
          </div>
      </template>
      <style scoped></style>
  2. reactive():适合对象

    1. 直接通过对象名.属性值​ 就可以访问,不需要.value

      <script setup>
      import { reactive } from "vue";
      let person = reactive({
          name: "Exusiai",
          age: 18,
      });
      function incr() {
          person.age++;
      }
      </script>
      <template>
          <div>
              <button @click="incr()">+</button>
              <button @click="person.age--">-</button>
              <!--swig12-->
          </div>
      </template>
      <style scoped></style>

# 对比

  • 使用 ref​ 适用于以下开发场景:

    • 包装基本类型数据:ref​ 主要用于包装基本类型数据(如字符串、数字等),即只有一个值的数据,如果你想监听这个值的变化,用 ref​ 最为方便。在组件中使用时也很常见。
    • 访问方式简单:ref​ 对象在访问时与普通的基本类型值没有太大区别,只需要通过 .value​ 访问其实际值即可。
  • 使用 reactive​ 适用于以下开发场景:

    • 包装复杂对象:reactive​ 可以将一个普通对象转化为响应式对象,这样在数据变化时会自动更新界面,特别适用于处理复杂对象或者数据结构。
    • 需要递归监听的属性:使用 reactive​ 可以递归追踪所有响应式对象内部的变化,从而保证界面的自动更新。
  • 综上所述,ref​ 适用与简单情形下的数据双向绑定,对于只有一个字符等基本类型数据或自定义组件等情况,建议可以使用 ref​;而对于对象、函数等较为复杂的数据结构,以及需要递归监听的属性变化,建议使用 reactive​。当然,在实际项目中根据需求灵活选择也是十分必要的。

# toRef()/toRefs()

  • toRef() 基于 reactive 响应式对象上的一个属性,创建一个对应的 ref 响应式数据。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
  • toRefs() 将一个响应式对象多个属性转换为一个多个 ref 数据,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。其实就是同时使用多个 toRef()
<script setup>
import { ref, reactive, toRef, toRefs } from "vue";
let person = reactive({
    name: "Exusiai",
    age: 18,
});
let personAge = toRef(person, "age");
function incr() {
    person.age++;
}
function incrRefAge() {
    personAge.value++;
}
function incrRefsAge() {
    age.value++;
}
let { name, age } = toRefs(person);
</script>
<template>
    <div>
        <button @click="incr()">+</button>
        <button @click="incrRefAge()">+</button>
        <button @click="incrRefsAge()">+</button>
        personAge: <!--swig13-->
        person.age: <!--swig14-->
        age: <!--swig15-->
    </div>
</template>
<style scoped></style>

# 条件渲染

# v-if

  • v-if='表达式'​ 只会在指令的表达式返回真值时才被渲染
  • 也可以使用 v-else​ 为 v-if​ 添加一个“else 区块”。
  • 一个 v-else​ 元素必须跟在一个 v-if​ 元素后面,否则它将不会被识别。
<script setup>
import {ref} from 'vue';
let flag = ref(true);
</script>
<template>
    <div>
        <span v-if="flag">hello vue!</span>
        <span v-else>bye vue!</span>
        <button @click="flag = !flag">Switch</button>
    </div>
</template>
<style scoped></style>

# v-show

  • 另一个可以用来按条件显示一个元素的指令是 v-show​。其用法基本一样
  • 不同之处在于 v-show​ 会在 DOM 渲染中保留该元素,仅切换了该元素上名为 display​ 的 CSS 属性;而v-if​ 会使元素不在 DOM 树中
  • v-show​ 不支持在 <template>​ 元素上使用,也不能和 v-else​ 搭配使用。
<script setup>
import {ref} from 'vue';
let flag = ref(true);
</script>
<template>
    <div>
        <span v-show="flag">hello world!</span>
        <button @click="flag = !flag">Switch</button>
    </div>
</template>
<style scoped></style>

# 对比

  • v-if​ 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
  • v-if​ 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
  • 相比之下,v-show​ 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display​ 属性会被切换。
  • 总的来说,v-if​ 有更高的切换开销,而 v-show​ 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show​ 较好;如果在运行时绑定条件很少改变,则 v-if​ 会更合适。

# 列表渲染

使用v-for​ 基于一个数组渲染一个列表

  • v-for​ 指令的值需要使用 item in items​ 形式的特殊语法,其中 items​ 是源数据的数组,而 item​ 是迭代项的别名
  • v-for​ 块中可以完整地访问父作用域内的属性和变量。v-for​ 也支持使用可选的第二个参数表示当前项的位置索引。
<script setup>
import { ref, reactive } from "vue";
let items = reactive([
    { id: "i1", name: "Exusiai" },
    { id: "i2", name: "Texas" },
    { id: "i3", name: "Skadi" },
]);
let appendix = ref("No.");
</script>
<template>
    <div>
        <ur>
            <!-- v-bind:key可以不写 -->
            <li v-for="(item, index) in items" v-bind:key="item.id">
                <!--swig16--><!--swig17--> <!--swig18-->
            </li>
        </ur>
    </div>
</template>
<style scoped></style>

# 练习:购物车

<script setup>
import { ref, reactive } from "vue";
let carts = reactive([
    {
        name: "Cola",
        price: 3,
        number: 10,
    },
    {
        name: "Toufu",
        price: 6,
        number: 9,
    },
]);
function compute() {
    let count = 0;
    for (let index in carts) {
        count += carts[index].price * carts[index].number;
    }
    return count;
}
function removeCart(index) {
    carts.splice(index, 1);
}
function clearCart() {
    carts.splice(0, carts.length);
    // 不能直接指向一个新的空数组,会导致循环出错
    // carts = []  x
    // carts = reactive([]) x
}
</script>
<template>
    <div>
        <table>
            <thead>
                <tr>
                    <th>序号</th>
                    <th>商品名</th>
                    <th>价格</th>
                    <th>数量</th>
                    <th>小计</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody v-if="carts.length > 0">
                <!-- 有数据显示-->
                <tr v-for="(cart, index) in carts" :key="index">
                    <td v-text="index + 1"></td>
                    <td v-text="cart.name"></td>
                    <td v-text="cart.price"></td>
                    <td v-text="cart.number"></td>
                    <td v-text="cart.price * cart.number"></td>
                    <td>
                        <button @click="removeCart(index)">删除</button>
                    </td>
                </tr>
                购物车总金额:
                <!--swig19-->

            </tbody>
            <tbody v-else>
                购物车为空
            </tbody>
        </table>
        <button @click="clearCart()">一键清空购物车</button>
    </div>
</template>
<style scoped></style>

# 双向绑定

  • 单向绑定: 响应式数据的变化更新 dom 树,但是 dom 树上用户的操作造成的数据改变不会同步更新到响应式数据

  • 双向绑定: 响应式数据的变化更新 dom 树,但是 dom 树上用户的操作造成的数据改变同步更新到响应式数据,也称呼为收集表单信息的命令

    • 用户通过表单标签才能够输入数据,所以双向绑定都是应用到表单标签上的,其他标签不行
    • v-model 专门用于双向绑定表单标签的 value 属性,语法为 v-model:value=''​,可以简写为 v-model=''
    • v-model 还可以用于各种不同类型的输入,<textarea>​、<select>​ 元素。

# 单项绑定

<script setup>
import { ref } from "vue";
let message = ref("Exusiai")
</script>
<template>
    <div>
        <input type="text" :value="message"> <br>
        <!--swig20-->
    </div>
</template>
<style scoped></style>

image
改变输入框中的数据不会影响响应数据

# 双向绑定

<script setup>
import { ref } from "vue";
let message = ref("Exusiai")
</script>
<template>
    <div>
        <input type="text" v-model="message"> <br>
        <!--swig21-->
    </div>
</template>
<style scoped></style>

image

# 应用场景

<script setup>
import { ref, reactive } from "vue";
let user = reactive({
    username: "",
    userPwd: "",
    gender:"",
    intro:"",
    pro:"",
});
let hbs = ref([]);
function clearForm() {
    // 不能使用指向新对象,只能一个一个删除或重置
    user.username = "";
    user.userPwd = "";
    user.gender = "";
    user.intro = "";
    user.pro = "",
    hbs.value.splice(0, hbs.value.length)  // 因为hbs是由ref()转换的,所以hbs需要加.value
}
</script>
<template>
    <div>
        <input type="text" v-model="user.username" /> <br />
        <input type="password" v-model="user.userPwd" /> <br />
        <!-- 多选框 -->
        爱好:
<input type="checkbox" v-model="hbs" value="sing" />
<input type="checkbox" v-model="hbs" value="dance" />
        rap <input type="checkbox" v-model="hbs" value="rap" /> <br />
        <!-- 单选框 -->
        性别:
<input type="radio" name="gender" v-model="user.gender" value="male" />
<input type="radio" name="gender" v-model="user.gender" value="female" />
        <br />
        简介:<textarea v-model="user.intro"></textarea> <br />
        籍贯:
        <select v-model="user.pro">
            <option value="1"></option>
            <option value="2"></option>
            <option value="3"></option>
        </select> <br />
        <button @click="clearForm()">清空表单</button>
        <!--swig22--> <br />
        <!--swig23--> <br />
    </div>
</template>
<style scoped></style>

# 属性计算

模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑

  1. 通过方法返回数据,每使用一次,都要执行一次
  2. 通过计算属性获得属性,每次使用时如果数据源没有被修改,则直接使用上一次的结果

image

  • 我们在这里定义了一个计算属性 bookMessage​。computed()​ 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 bookMessage.value​ 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value​。
  • Vue 的计算属性会自动追踪响应式依赖。它会检测到 bookMessage​ 依赖于 author.books​,所以当 author.books​ 改变时,任何依赖于 bookMessage​ 的绑定都会同时更新。

计算属性缓存 vs 方法

  • 若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books​ 不改变,无论多少次访问 bookMessage​ 都会立即返回先前的计算结果!

# 数据监听器

计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数:

  • watch 主要用于以下场景:

    • 当数据发生变化时需要执行相应的操作
    • 监听数据变化,当满足一定条件时触发相应操作
    • 在异步操作前或操作后需要执行相应的操作

但是可以通过双向绑定来实现,应用场景不多

# watch

<script setup>
import { ref, reactive, watch } from "vue";
let fullname = ref("");
let firstname = ref("");
let lastname = reactive({
    name: "",
});
/* 监听ref响应式数据 */
watch(firstname, (newValue, oldValue) => {
    console.log(`${oldValue}变为${newValue}`);
    fullname.value = newValue + lastname.name;
});
/* 监听reactive中的一个数据 */
watch(
    () => lastname.name,
    (newValue, oldValue) => {
        console.log(`${oldValue}变为${newValue}`);
        fullname.value = firstname.value + newValue;
    }
);
/* 监听reactive对象数据 */
watch(
    () => lastname,
    (newValue, oldValue) => {
        console.log(`${oldValue}变为${newValue}`);
        fullname.value = firstname.value + newValue.name;
    },
    { deep: true, immediate: true } // deep表示实时监测 immediate表示是否在刚访问页面时就执行
);
</script>
<template>
    <div>
<input type="text" v-model="firstname" /><input
            type="text"
            v-model="lastname.name"
        />
        <span>姓名<!--swig24--></span>
    </div>
</template>
<style scoped></style>

# watchEffect

<script setup>
import { ref, reactive, watchEffect } from "vue";
let fullname = ref("");
let firstname = ref("");
let lastname = reactive({
    name: "",
});
/*
 * 任何的响应式数据都可以直接进行监听,无需作为参数进行传递
 * 注意,当函数中没有对被监听数据进行使用时,只会进行一次调用
 */
watchEffect(() => {
    // 当firstname发生改变时,就会调用一次方法
    console.log(firstname.value)
    // 但是此时没有使用fullname和lastname,当它们两个发生改变时,该方法不会被调用
    // fullname.value = firstname.value + lastname.name;
});
</script>
<template>
    <div>
<input type="text" v-model="firstname" />
        <input type="text" v-model="lastname.name" />
        <span>姓名<!--swig25--></span>
    </div>
</template>
<style scoped></style>

# VUE 生命周期

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

# 图示

image

# 生命周期钩子

# beforeCreate

在组件实例初始化完成之后立即调用
image

  • 在实例初始化完成并且 props 被解析后立即调用。
  • 接着 props 会被定义成响应式属性,data()​ 和 computed​ 等选项也开始进行处理。
  • 注意,组合式 API 中的 setup()​ 钩子会在所有选项式 API 钩子之前调用,beforeCreate()​ 也不例外。

选项式:不在<script>中写入 setup,而是使用setup(){}​ 的方式

# created

在组件实例处理完所有与状态相关的选项后调用。当这个钩子被调用时,以下内容已经设置完成:

  • 响应式数据
  • 计算属性
  • 方法
  • 监听器
    然而,此时挂载阶段还未开始,因此 $el​ 属性仍不可用

# beforeMount

在组件被挂载之前调用。
image
当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。
这个钩子在服务端渲染时不会被调用。

# mounted

在组件被挂载之后调用
组件在以下情况下被视为已挂载:

  • 所有同步子组件都已经被挂载。(不包含异步组件或 <Suspense>​ 树内的组件)
  • 其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。
    这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于确保 DOM 相关代码仅在客户端被调用。
    这个钩子在服务端渲染时不会被调用。

# beforeUpdate

在组件即将因为一个响应式状态变更(如双向绑定、监听器)而更新其 DOM 树之前调用
这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。
这个钩子在服务端渲染时不会被调用。

# updated

在组件因为一个响应式状态变更而更新其 DOM 树之后调用
这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果需要在某个特定的状态更改后访问更新后的 DOM,使用 nextTick() 作为替代
注意:不要打 updated 钩子中更改组件的状态,否则可能导致无限的更新循环

# beforeUnmount

在一个组件实例被卸载之前调用
image
当这个钩子被调用时,组件实例依然还保有全部的功能。
这个钩子在服务端渲染时不会被调用

# unmounted

在一个组件实例被卸载之后调用(比如切换.vue 文件,原来的文件相当于被卸载了)
一个组件在以下情况下被视为已卸载:

  • 其所有子组件都已经被卸载。
  • 所有相关的响应式作用 (渲染作用以及 setup()​ 时创建的计算属性和侦听器) 都已经停止。
    可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。
    这个钩子在服务端渲染时不会被调用。

# 钩子函数

使用组合式VUE 可以使用的 API:

API 作用
onMounted() 注册一个回调函数,在组件挂载完成后执行
onBeforeMount() 注册一个钩子,在组件被挂载之前被调用
onUpdated() 注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用
onBeforeUpdate() 注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用
onUnmounted() 注册一个回调函数,在组件实例被卸载之后调用
onBeforeUnmount() 注册一个钩子,在组件实例被卸载之前调用
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated } from "vue";
// 挂载之前
onBeforeMount(() => {
    console.log("========onBeforeMount========");
    console.log(document.getElementById("s1"));
});
// 挂载完成
onMounted(() => {
    console.log("========onMounted========");
    console.log(document.getElementById("s1"));
});
// 更新之前
onBeforeUpdate(() => {
    console.log("========onBeforeUpdate========");
    let element = document.getElementById("s1");
    console.log(message.value, element.innerText);
});
// 更新完成
onUpdated(() => {
    console.log("========onUpdated========");
    let element = document.getElementById("s1");
    console.log(message.value, element.innerText);
});
let message = ref(0);
</script>
<template>
    <div>
        <span id="s1"><!--swig26--></span>
        <button @click="message++">+</button>
    </div>
</template>
<style scoped></style>

image

# VUE 组件

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。组件就是实现应用中局部功能代码和资源的集合!在实际应用中,组件常常被组织成层层嵌套的树状结构:
image这与嵌套 HTML 元素的方式类似,VUE 实现了自己的组件模型,可以在每个组件内封装自定义内容与逻辑

传统方式编写应用
image
组件方式编写应用:
image

  • 组件化:对 js/css/html 统一封装,这是 VUE 中的概念
  • 模块化:对 js 的统一封装,这是 ES6 中的概念
  • 组件化中:对 js 部分代码的处理使用 ES6 中的模块化

# 组件开发案例

案例需求: 创建一个页面,包含头部和菜单以及内容显示区域,每个区域使用独立组建!
1686885192862

1 准备 vue 项目

npm create vite
cd vite项目
npm install

2 安装相关依赖

npm install sass
npm install bootstrap

3 创建子组件 在 src/components 文件下 vscode 需要安装 Vetur 插件,这样 vue 文件有快捷提示

  • Header.vue
<script setup type="module">
</script>
<template>
    <div>
        欢迎: xx <a href="#">退出登录</a>
    </div>
</template>
<style>
</style>
  • Navigator.vue
<script setup type="module">
</script>
<template>
    <!-- 推荐写一个根标签-->
    <div>
       <ul>
          <li>学员管理</li>
          <li>图书管理</li>
          <li>请假管理</li>
          <li>考试管理</li>
          <li>讲师管理</li>
       </ul>
    </div>
</template>
<style>
</style>
  • Content.vue
<script setup type="module">
</script>
<template>
    <div>
        展示的主要内容!
    </div>
</template>
<style>
</style>
  • App.vue 入口组件 App 引入组件
<script setup>
    import Header  from './components/Header.vue'
    import Navigator  from './components/Navigator.vue'
    import Content  from './components/Content.vue'
</script>
<template>
  <div>
     <Header class="header"></Header>
     <Navigator class="navigator"></Navigator>
     <Content class="content"></Content>
  </div>
</template>
<style scoped>
    .header{
       height: 80px;
       border: 1px solid red;
    }
    .navigator{
      width: 15%;
      height: 800px;
      display: inline-block;
      border: 1px blue solid;
      float: left;
    }
    .content{
      width: 83%;
      height: 800px;
      display: inline-block;
      border: 1px goldenrod solid;
      float: right;
    }
</style>

4 启动测试

npm run dev

image

# 参数传递

# 父传子

image
Vue3 中父组件向子组件传值可以通过 props 进行,具体操作如下:

  1. 首先,在父组件中定义需要传递给子组件的值
  2. 接着,在父组件的模板中引入子组件,同时在引入子组件的标签中添加 props 属性并为其设置需要传递的值。
  3. 在 Vue3 中,父组件通过 props 传递给子组件的值是响应式的。也就是说,如果在父组件中的传递的值发生了改变,子组件中的值也会相应地更新

# 子传父

image

# 兄弟传参

image
通过一个父传子和一个子传父实现

<script setup>
/* 引入编写的组件 */
import { ref } from "vue";
import Header from "./components/Header.vue";
import Navigator from "./components/Navigator.vue";
import Content from "./components/Content.vue";
let menu = ref("");
function receiver(data) {
    menu.value = data;
}
</script>
<template>
    <div>
        <!--swig27-->
        <Header class="header"> </Header>
		<!-- 接收来自子组件的信息@SonFunctionName="ParentFunctionName"不带() -->
        <Navigator @sendMenu="receiver" class="navigator"></Navigator>
		<!-- 向子组件发送内容v-bind:message:"name" -->
        <Content class="content" :message="menu"></Content>
    </div>
</template>
<style scoped>
.header {
    height: 80px;
    border: 1px solid rgb(27, 148, 218);
}
.navigator {
    width: 15%;
    height: 300px;
    border: 1px solid salmon;
    float: left;
}
.content {
    width: 84%;
    height: 300px;
    border: 1px solid blue;
    float: right;
}
</style>

传出子

<script setup>
// 接收父组件的参数
import { defineProps } from "vue";
defineProps = {
    message: String,
};
</script>
<template>
    <div>
        <!--swig28-->
    </div>
</template>
<style scoped></style>

传入子

<script setup>
// 向父组件发送参数
// defineEmits用于定义向父组件提交数据的事件以及正式地提交数据
import { defineEmits } from "vue";
// 定义一个向父组件提交数据的事件,事件名称自定义
const emits = defineEmits(["sendMenu"]); // 本质上是一个方法,只不过JS中方法以对象的形式存在
// 提交数据的方法
function send(data) {
    emits("sendMenu", data);
}
</script>
<template>
    <div>
        <ul>
            <li @click="send('学员管理')">学员管理</li>
            <li @click="send('图书管理')">图书管理</li>
            <li @click="send('请假管理')">请假管理</li>
            <li @click="send('考试管理')">考试管理</li>
            <li @click="send('讲师管理')">讲师管理</li>
        </ul>
    </div>
</template>
<style scoped></style>

# 路由机制 router

# 概述

  • 定义:路由就是根据不同的 URL 地址展示不同的内容或页面。
  • 通俗理解:路由就像是一个地图,我们要去不同的地方,需要通过不同的路线进行导航。

image

# 作用

  • 单页应用程序(SPA)中,路由可以实现不同视图之间的无刷新切换,提升用户体验;
  • 路由还可以实现页面的认证和权限控制,保护用户的隐私和安全;
  • 路由还可以利用浏览器的前进与后退,帮助用户更好地回到之前访问过的页面。

# 案例

npm create vite //创建项目cd 项目文件夹 //进入项目文件夹
npm install //安装项目需求依赖
npm install vue-router@4 --save //安装全局的vue-router 4版本

App.vue

一个视图上可以同时存在多个 router-view,每个 router-view 都快可以设置专门展示哪个组件,通过 name 属性设置
但是一般一个.vue 文件中只会使用一个 router-view 标签

<script setup>
</script>
<template>
    <div>
      <h1>App页面</h1>
      <hr/>
        <!-- 路由的连接 -->
        <router-link to="/">home页</router-link> <br>
        <router-link to="/list">list页</router-link> <br>
        <router-link to="/add">add页</router-link> <br>
        <router-link to="/update">update页</router-link> <br>
      <hr/>
      <!-- 路由连接对应视图的展示位置 -->
      <hr>  <!-- 相当于name="default" -->
      默认展示位置:<router-view></router-view>
      <hr>
      Home视图展示:<router-view name="homeView"></router-view>
      <hr>
      List视图展示:<router-view name="listView"></router-view>
      <hr>
      Add视图展示:<router-view name="addView"></router-view>
      <hr>
      Update视图展示:<router-view name="updateView"></router-view>
    </div>
</template>
<style scoped>
</style>

src/routers/router.js

import {createRouter,createWebHashHistory} from 'vue-router'
// 导入vue组件
import Home from '../components/Home.vue'
import List from '../components/List.vue'
import Add from '../components/Add.vue'
import Update from '../components/Update.vue'
// 创建路由对象,声明路由规则
const router = createRouter({
    //createWebHashHistory() 是 Vue.js 基于 hash 模式创建路由的工厂函数。在使用这种模式下,路由信息保存在 URL 的 hash 中,
    //使用 createWebHashHistory() 方法,可以创建一个路由历史记录对象,用于管理应用程序的路由。在 Vue.js 应用中,
    //通常使用该方法来创建路由的历史记录对象。
    //就是路由中缓存历史记录的对象,vue-router提供
    history: createWebHashHistory(),
    routes:[
        {
            path:'/',
            /*
                component指定组件在默认的路由视图位置展示
                components:Home
                components指定组件在name为某个值的路由视图位置展示
                components:{
                    default:Home,// 默认路由视图位置
                    homeView:Home// name为homeView的路由视图位置
                }
            */
            components:{
                default:Home,
                homeView:Home
            }
        },
        {
            path:'/list',
            component: List
        },
        {
            path:'/add',
            component: Add
        },
        {
            path:'/update',
            component: Update
        },
    ]
})
// 对外暴露路由对象
export default router;

注意:之后实践的时候发现全写 components 可能会失效,建议写成现在这样的形式

main.js

引入 router 配置

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
//导入router模块
import router from './routers/router.js'
let app = createApp(App)
//绑定路由对象
app.use(router)
//挂在视图
app.mount("#app")

image

# 路由重定向

重定向作用:将一个路由重定向到另一个路由上
如:访问/list 和/showAll 都定向到 List.vue

# 编程式路由(userRouter)

普通路由

<template>
    <div>
        <!-- 声明式路由 -->
        <router-link to="/home">home</router-link> <br />
        <router-link to="/list">list</router-link> <br />
        <router-link to="/add">add</router-link> <br />
        <router-link to="/update">update</router-link>
        <hr />
        <router-view></router-view>
    </div>
</template>

编程式路由

<script setup>
import { useRouter } from "vue-router";
import { ref } from "vue";
const router = useRouter();
function showList() {
    // 编程式路由实现页面跳转
    // router.push("/list");
    // 也可以传入一个对象
    router.push({ path: "/list" });
}
let myPath = ref("");
function goMyPage() {
    router.push({ path: myPath.value });
}
</script>
<template>
    <div>
        <!-- 编程式路由 -->
        <!-- 通过函数进行跳转 -->
        <button @click="showList()">list</button>
        <hr />
        <!-- 通过输入进行自由跳转 -->
        <button @click="goMyPage()">Go</button
        ><input type="text" v-model="myPath" />
        <hr />
        <router-view></router-view>
    </div>
</template>
<style scoped></style>
  • 通过 useRouter,动态决定向那个组件切换的路由
  • 在 Vue 3 和 Vue Router 4 中,可以使用 useRouter()​ 来实现动态路由(编程式路由)
  • 这里的 useRouter()​ 方法返回的是一个 router 对象,可以用它来做如导航到新页面、返回上一页面等操作

# 路由传参

# 路径参数

  • 在路径中使用一个动态字段来实现,称为路径参数

    • 例如: 查看数据详情 /showDetail/1​ ,1​ 就是要查看详情的 id,可以动态添值!

# 案例

router.js

import { createRouter, createWebHashHistory } from "vue-router";
import ShowDetail from "../components/ShowDetail.vue";
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: "/showdetail/:id/:language",  // :占位符
            component: ShowDetail,
        },
    ],
});
export default router;

showDetail.vue

<script setup>
// 接收传递过来的路径参数
// useRoute()用来接收参数
import { useRoute } from "vue-router";
import { ref, onUpdated } from "vue";
let route = useRoute();
let languageId = ref(0);
let languageName = ref("");
// route.params 表示路径参数
// 如果不使用生命周期,则页面只会更新一次,这样就可以在每次接收路径参数后进行页面更新
onUpdated(() => {
    languageId.value = route.params.id;
    languageName.value = route.params.language;
})
</script>
<template>
    <div>
        <h1>接收路径参数</h1>
        <h2><!--swig29--></h2>
        <h2><!--swig30--></h2>
    </div>
</template>
<style scoped></style>

App.vue

<script setup>
import { useRouter } from "vue-router";
import { ref } from "vue";
const router = useRouter();
function showDetail(id, language) {
    router.push(`/showDetail/${id}/${language}`);
}
</script>
<template>
    <div>
        <router-link to="/showdetail/1/Java">声明式路由路径传参</router-link>
        <br />
        <button @click="showDetail(2, 'C++')">编程式路由路径传参</button>
        <hr />
        <router-view></router-view>
    </div>
</template>
<style scoped></style>

# 键值对参数

  • 类似与 get 请求通过 url 传参,数据是键值对形式的

    • 例如: 查看数据详情/showDetail?hid=1​,hid=1​ 就是要传递的键值对参数
    • 在 Vue 3 和 Vue Router 4 中,你可以使用 useRoute()​ 这个函数从 Vue 的组合式 API 中获取路由对象。
    • useRoute()​ 方法返回的是当前的 router 对象,你可以用它来获取关于当前路由的信息,如当前的路径、查询参数等。

注意:编程式路由使用useRouter()​;路由传参使用useRoute()​。不是打错了!

# 案例

router.js

import { createRouter, createWebHashHistory } from "vue-router";
import ShowKeyValue from "../components/ShowKeyValue.vue";
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: "/ShowKeyValue",
            component: ShowKeyValue,
        },
    ],
});
export default router;

ShowKeyValue.vue

<script setup>
import { useRoute } from "vue-router";
import { ref, onUpdated } from "vue";
let languageId = ref(0);
let languageName = ref("");
let route = useRoute();
onUpdated(() => {
    languageId.value = route.query.id; // 需要与传入的键名对应
    languageName.value = route.query.language;
});
// route.query 表示键值对参数
</script>
<template>
    <div>
        <h2><!--swig31--></h2>
        <h2><!--swig32--></h2>
    </div>
</template>
<style scoped></style>

App.vue

<script setup>
import { useRouter } from "vue-router";
import { ref } from "vue";
const router = useRouter();
function showKeyValue(pamram_id, pamram_language) {
    router.push(`/ShowKeyValue?id=${pamram_id}&language=${pamram_language}`);
    // router.push({path:'/showKeyValue', query:{id:pamram_id, language:pamram_language}});
}
</script>
<template>
    <div>
        <router-link to="/ShowKeyValue?id=1&language=Java">声明式路由键值对传参</router-link>
        <br />
        <router-link v-bind:to="{path:'/showKeyValue', query:{id:2, language:'C++'}}">声明式路由键值对传参(传对象)</router-link>
        <br />
        <button @click="showKeyValue(3, 'Python')">编程式路由键值对传参</button>
        <hr />
        <router-view></router-view>
    </div>
</template>
<style scoped></style>

# 路由守卫

路由守卫是用于在路由切换期间进行一些特定任务的回调函数。路由守卫可以用于许多任务,例如验证用户是否已登录、在路由切换前提供确认提示、请求数据等

  1. 全局前置守卫:在路由切换前被调用,可以用于验证用户是否已登录、中断导航、请求数据等。
  2. 全局后置守卫:在路由切换之后被调用,可以用于处理数据、操作 DOM 、记录日志等。

守卫代码的位置: 在 router.js 中

import { createRouter, createWebHashHistory } from "vue-router";
const router = createRouter({
    history: createWebHashHistory(),
    routes: []
});
// 设置全局前置守卫
// 每次路由切换页面前,都会执行beforeEach()中的回调函数
router.beforeEach((to, from, next) => {
    /*  from 表示上一个页面,从哪里来
     to 表示下一个页面,到哪里去
     next 表示放行的方法,只有执行了该方法,才会放行路由
        next() 放行
        next("/路径") 路由重定向,容易导致死循环,要放在if中
     */
    console.log("beforeEach");
    console.log("from: " + from.path);
    console.log("to: " + to.path);
    next();
});
// 设置全局后置守卫
// 每次路由切换页面后,都会执行afterEach()中的回调函数
router.afterEach((to, from) => {
    console.log("afterEach");
});
export default router;

# 案例

让用户只有在登录后才能进入 home 页,否则进入 login 页

Login.vue

<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
let router = useRouter();
let username = ref("");
let password = ref("");
function login() {
    if (username.value == "root" && password.value == "123456") {
        // 将用户名保存在浏览器上,这样在home页也可以使用用户名信息了
        window.sessionStorage.setItem("username", username.value);
        // 路由跳转 /home
        router.push("/home");
    } else {
        alert("登录失败");
    }
}
</script>
<template>
    <div>
        <h1>Login</h1>
        <span>账号:</span> <input type="text" v-model="username" /> <br />
        <span>密码:</span> <input type="password" v-model="password" /> <br />
        <button @click="login()">登录</button>
    </div>
</template>
<style scoped></style>

Home.vue

<script setup>
import {useRouter} from 'vue-router';
let router = useRouter();
// 通过浏览器获取用户名
let username = window.sessionStorage.getItem("username");
function logout() {
    // 清除seesionStorage中的用户信息
    window.sessionStorage.removeItem("username");
    // 跳转回login页面
    router.push("/login");
}
</script>
<template>
    <div>
        <h1>Home</h1>
        <span>欢迎<!--swig33-->登录</span> <br>
        <button @click="logout()">退出登录</button>
    </div>
</template>
<style scoped></style>

router.js

import { createRouter, createWebHashHistory } from "vue-router";
import Home from "../components/Home.vue";
import Login from "../components/Login.vue";
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: "/home",
            component: Home,
        },
        {
            path: "/login",
            component: Login,
        },
    ],
});
// 通过路由的前置守卫校验登录,防止用户直接访问home页
router.beforeEach((to, from, next) => {
    if (to.path == "/login") {
        // 判断如果要去登录页面,直接放行
        next();
    } else {
        // 如果是其他资源,都需要先判断有无登录。如果登录了正常进行访问;如果没有登录需要重定向到login页面
        const username = sessionStorage.getItem("username");
        if (null != username) {
            next();
        } else {
            next("/login");
        }
    }
});
export default router;

# 数据交互 axios

# promise

# 普通函数和回调函数

普通函数:正常调用的函数,一般函数执行完毕后才会继续执行下一行代码

<script>
    let fun1 = () =>{
        console.log("fun1 invoked")
    }
    // 调用函数
    fun1()
    // 函数执行完毕,继续执行后续代码
    console.log("other code processon")
</script>

回调函数:一种基于事件的自动调用函数,表示未来才会执行的一些功能,后续代码不会等待该函数执行完毕就开始执行了

<script>
    // 设置一个2000毫秒后会执行一次的定时任务
    setTimeout(function (){
        console.log("setTimeout invoked")
    },2000)
    console.log("other code processon")
</script>

# 概述

前端中的异步编程技术,类似 Java 中的多线程+线程结果回调!

  • Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise​ 对象。
  • 所谓Promise​,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise​ 对象有以下两个特点:

  1. Promise 对象代表一个异步操作,有三种状态:

    1. Pending​(进行中):其它代码继续执行
    2. Resolved​(已完成,又称 Fulfilled):准备成功时处理的方案
    3. Rejected​(已失败):准备失败时处理的方案

    只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise​ 这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从Pending​ 变为Resolved​ 和从Pending​ 变为Rejected​。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果

# 使用

# 基本用法

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <script>
            /*
            resolve 函数,在回调函数中如果调用resolve方法,promise状态会由pending转换为resolved
            reject 函数,在回调函数中如果调用reject方法,promise状态会由pending转换为rejected
            两个变量随意命名,但是默认为这两个
			可以看成开启了一个多线程,括号内的代码和括号外的代码同步运行
        */
            let promise = new Promise(function (resolve, reject) {
                // 这个函数就是回调函数
                console.log("function invoked");
                resolve("Link start!");  // 如果发生状态转换,就不会再变化了
                // reject("Damn");  // 可以传递参数
            });
            console.log("other code1"); // 不会等待回调函数执行完毕
            promise.then(
                function (value) {
                    // promise转换为resolved状态时会执行的函数
                    console.log("promise success: " + value);
                },
                function (value) {
                    // promise转换为rejected状态时会执行的函数
                    console.log("promise fail: " + value);
                }
            ); // 等待promise对象状态发生改变时才会执行的代码
            console.log("other code2");
        </script>
    </head>
    <body></body>
</html>

# catch()

.then()​ 返回的还是一个 promise,可以使用链式编程进行连接,实现一个类似 try{}catch{}的功能

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <script>
            /*
            resolve 函数,在回调函数中如果调用resolve方法,promise状态会由pending转换为resolved
            reject 函数,在回调函数中如果调用reject方法,promise状态会由pending转换为rejected
            两个变量随意命名,但是默认为这两个
			可以看成开启了一个多线程,括号内的代码和括号外的代码同步运行
        */
            let promise = new Promise(function (resolve, reject) {
                // 这个函数就是回调函数
                console.log("function invoked");
                resolve("Link start!"); // 如果发生状态转换,就不会再变化了
                // reject("Damn");  // 可以传递参数
                // throw new Error("error message");
            });
            console.log("other code1"); // 不会等待回调函数执行完毕
            // 等待promise对象状态发生改变时才会执行的代码
            promise
                .then(function (value) {
                    // promise转换为resolved状态时会执行的函数
                    console.log("promise success: " + value);
                })
                .catch(function (value) {
                    // 当promise状态是rejected或者promise出现异常时会执行的函数,此时上方reject对应的方法可以删去
                    console.log("promise catch: " + value);
                });
            console.log("other code2");
        </script>
    </head>
    <body></body>
</html>

# async/await

# async

可以使用简洁的语法获得 promise 对象

  1. async 标识函数后,async 函数的返回值会变成一个promise对象
  2. 如果函数内部返回的数据是一个非 promise 对象,async 函数的结果会返回一个成功状态 promise 对象
  3. 如果函数内部返回的是一个 promise 对象,则 async 函数返回的状态与结果由该对象决定
  4. 如果函数内部抛出的是一个异常,则 async 函数返回的是一个失败的 promise 对象
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <script>
            // let fun1 = async () => {};
            async function fun1() {
                // return 10;  // 方法如果正常return结果,返回状态为resolved的promise
                // throw new Error("Wrong!");  // 如果方法中出现了异常,返回状态为rejected的promise
                let promise = Promise.resolve("Link5");  // Promise.reject("Link3")
                return promise;  // 如果方法返回一个promise,则根据promise的状态来决定返回值的状态
            }
            let promise = fun1();
            promise
                .then(function (value) {
                    console.log("success: " + value);
                })
                .catch(function (value) {
                    console.log("fail: " + value);
                });
        </script>
    </head>
    <body></body>
</html>

# await

  1. await 右侧的表达式一般为一个promise对象,但是也可以是一个其他值
  2. 如果表达式是 promise 对象,await 返回的是promise 成功的值
  3. await 会等右边的promise 对象执行结束,然后再获取结果,后续代码(在该 async function 内)也会等待await 的执行,在该方法外的仍然在运行
  4. 如果表达式是其他值,则直接返回该值
  5. await必须在 async 函数中,但是 async 函数中可以没有 await
  6. 如果 await 右边的 promise 失败了,就会抛出异常,需要通过 try ... catch 捕获处理
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
        <script>
            async function fun1() {
                return 10;
            }
            async function fun2() {
                // let res = await fun1();
                let res;
                try {
                    res = await Promise.reject("Wrong!");
                } catch (e) {
                    console.log("catch got: " + e);
                }
                console.log("await got: " + res);
            }
            fun2();
        </script>
    </head>
    <body></body>
</html>

image

# Ajax 与 Axios

Ajax

  • AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。
  • AJAX 不是新的编程语言,而是一种使用现有标准的新方法。
  • AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。
  • AJAX 不需要任何浏览器插件,但需要用户允许 JavaScript 在浏览器上执行。
  • XMLHttpRequest 只是实现 Ajax 的一种方式。

image

Axios

  • Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和 node.js 中)。在服务端它使用原生 node.js http​ 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。它有如下特性

    • 从浏览器创建 XMLHttpRequests
    • 从 node.js 创建 http 请求
    • 支持 Promise API
    • 拦截请求和响应
    • 转换请求和响应数据
    • 取消请求
    • 自动转换 JSON 数据
    • 客户端支持防御XSRF

# 案例:请求后端随机数据进行展示

# 案例需求

1 案例需求:请求后台获取随机土味情话

# 请求的 url

https://api.uomg.com/api/rand.qinghua?format=json    或者使用  http://forum.atguigu.cn/api/rand.qinghua?format=json

# 请求的方式

GET/POST

# 数据返回的格式

{"code":1,"content":"我努力不是为了你而是因为你。"}

# 准备项目

npm create vite
npm install
/*npm install vue-router@4 --save
npm install pinia */

# 安装 axios

npm install axios

# 设计页面(App.Vue)

<script setup type="module">
  import axios from 'axios'
  import { onMounted,reactive } from 'vue';
  let jsonData =reactive({code:1,content:'我努力不是为了你而是因为你'})
  let getLoveMessage =()=>{
    axios({
      method:"post", // 请求方式
      url:"https://api.uomg.com/api/rand.qinghua?format=json",  // 请求的url
      data:{ // 当请求方式为post时,data下的数据以JSON串放入请求体,否则以key=value形式放url后
        username:"123456"
      }
    }).then( function (response){//响应成功时要执行的函数
      console.log(response)
      Object.assign(jsonData,response.data)
    }).catch(function (error){// 响应失败时要执行的函数
      console.log(error)
    })
  }
  /* 通过onMounted生命周期,自动加载一次 */
  onMounted(()=>{
    getLoveMessage()
  })
</script>
<template>
    <div>
      <h1>今日土味情话:<!--swig34--></h1>
      <button  @click="getLoveMessage">获取今日土味情话</button>
    </div>
</template>
<style scoped>
</style>

# 启动测试

npm run dev

# 异步响应的数据结构

  • 响应的数据是经过包装返回的!一个请求的响应包含以下信息。
{
  // `data` 由服务器提供的响应
  data: {},
  // `status` 来自服务器响应的 HTTP 状态码
  status: 200,
  // `statusText` 来自服务器响应的 HTTP 状态信息
  statusText: 'OK',
  // `headers` 是服务器响应头
  // 所有的 header 名称都是小写,而且可以使用方括号语法访问
  // 例如: `response.headers['content-type']`
  headers: {},
  // `config` 是 `axios` 请求的配置信息
  config: {},
  // `request` 是生成此响应的请求
  // 在node.js中它是最后一个ClientRequest实例 (in redirects),
  // 在浏览器中则是 XMLHttpRequest 实例
  request: {}
}
  • then 取值
then(function (response) {
    console.log(response.data);
    console.log(response.status);
    console.log(response.statusText);
    console.log(response.headers);
    console.log(response.config);
});

# 通过 async 和 await 处理异步请求

<script setup type="module">
  import axios from 'axios'
  import { onMounted,reactive } from 'vue';
  let jsonData =reactive({code:1,content:'我努力不是为了你而是因为你'})
  let getLoveWords = async ()=>{
    return await axios({
      method:"post",
      url:"https://api.uomg.com/api/rand.qinghua?format=json",
      data:{
        username:"123456"
      }
    })
  }
  let getLoveMessage =()=>{
   	 let {data}  = await getLoveWords()
     Object.assign(message,data)
  }
  /* 通过onMounted生命周期,自动加载一次 */
  onMounted(()=>{
    getLoveMessage()
  })
</script>
<template>
    <div>
      <h1>今日土味情话:<!--swig35--></h1>
      <button  @click="getLoveMessage">获取今日土味情话</button>
    </div>
</template>
<style scoped>
</style>

# axios 在发送异步请求时的可选配置:

{
  // `url` 是用于请求的服务器 URL
  url: '/user',
  // `method` 是创建请求时使用的方法
  method: 'get', // 默认值
  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  baseURL: 'https://some-domain.com/api/',
  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  // 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
  // 你可以修改请求头。
  transformRequest: [function (data, headers) {
    // 对发送的 data 进行任意转换处理
    return data;
  }],
  // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
  transformResponse: [function (data) {
    // 对接收的 data 进行任意转换处理
    return data;
  }],
  // 自定义请求头
  headers: {'X-Requested-With': 'XMLHttpRequest'},
  // `params` 是与请求一起发送的 URL 参数
  // 必须是一个简单对象或 URLSearchParams 对象
  params: {
    ID: 12345
  },
  // `paramsSerializer`是可选方法,主要用于序列化`params`
  // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
  paramsSerializer: function (params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  },
  // `data` 是作为请求体被发送的数据
  // 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
  // 在没有设置 `transformRequest` 时,则必须是以下类型之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属: FormData, File, Blob
  // - Node 专属: Stream, Buffer
  data: {
    firstName: 'Fred'
  },
  // 发送请求体数据的可选语法
  // 请求方式 post
  // 只有 value 会被发送,key 则不会
  data: 'Country=Brasil&City=Belo Horizonte',
  // `timeout` 指定请求超时的毫秒数。
  // 如果请求时间超过 `timeout` 的值,则请求会被中断
  timeout: 1000, // 默认值是 `0` (永不超时)
  // `withCredentials` 表示跨域请求时是否需要使用凭证
  withCredentials: false, // default
  // `adapter` 允许自定义处理请求,这使测试更加容易。
  // 返回一个 promise 并提供一个有效的响应 (参见 lib/adapters/README.md)。
  adapter: function (config) {
    /* ... */
  },
  // `auth` HTTP Basic Auth
  auth: {
    username: 'janedoe',
    password: 's00pers3cret'
  },
  // `responseType` 表示浏览器将要响应的数据类型
  // 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
  // 浏览器专属:'blob'
  responseType: 'json', // 默认值
  // `responseEncoding` 表示用于解码响应的编码 (Node.js 专属)
  // 注意:忽略 `responseType` 的值为 'stream',或者是客户端请求
  // Note: Ignored for `responseType` of 'stream' or client-side requests
  responseEncoding: 'utf8', // 默认值
  // `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
  xsrfCookieName: 'XSRF-TOKEN', // 默认值
  // `xsrfHeaderName` 是带有 xsrf token 值的http 请求头名称
  xsrfHeaderName: 'X-XSRF-TOKEN', // 默认值
  // `onUploadProgress` 允许为上传处理进度事件
  // 浏览器专属
  onUploadProgress: function (progressEvent) {
    // 处理原生进度事件
  },
  // `onDownloadProgress` 允许为下载处理进度事件
  // 浏览器专属
  onDownloadProgress: function (progressEvent) {
    // 处理原生进度事件
  },
  // `maxContentLength` 定义了node.js中允许的HTTP响应内容的最大字节数
  maxContentLength: 2000,
  // `maxBodyLength`(仅Node)定义允许的http请求内容的最大字节数
  maxBodyLength: 2000,
  // `validateStatus` 定义了对于给定的 HTTP状态码是 resolve 还是 reject promise。
  // 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
  // 则promise 将会 resolved,否则是 rejected。
  validateStatus: function (status) {
    return status >= 200 && status < 300; // 默认值
  },
  // `maxRedirects` 定义了在node.js中要遵循的最大重定向数。
  // 如果设置为0,则不会进行重定向
  maxRedirects: 5, // 默认值
  // `socketPath` 定义了在node.js中使用的UNIX套接字。
  // e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
  // 只能指定 `socketPath` 或 `proxy` 。
  // 若都指定,这使用 `socketPath` 。
  socketPath: null, // default
  // `httpAgent` and `httpsAgent` define a custom agent to be used when performing http
  // and https requests, respectively, in node.js. This allows options to be added like
  // `keepAlive` that are not enabled by default.
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),
  // `proxy` 定义了代理服务器的主机名,端口和协议。
  // 您可以使用常规的`http_proxy` 和 `https_proxy` 环境变量。
  // 使用 `false` 可以禁用代理功能,同时环境变量也会被忽略。
  // `auth`表示应使用HTTP Basic auth连接到代理,并且提供凭据。
  // 这将设置一个 `Proxy-Authorization` 请求头,它会覆盖 `headers` 中已存在的自定义 `Proxy-Authorization` 请求头。
  // 如果代理服务器使用 HTTPS,则必须设置 protocol 为`https`
  proxy: {
    protocol: 'https',
    host: '127.0.0.1',
    port: 9000,
    auth: {
      username: 'mikeymike',
      password: 'rapunz3l'
    }
  },
  // see https://axios-http.com/zh/docs/cancellation
  cancelToken: new CancelToken(function (cancel) {
  }),
  // `decompress` indicates whether or not the response body should be decompressed
  // automatically. If set to `true` will also remove the 'content-encoding' header
  // from the responses objects of all decompressed responses
  // - Node only (XHR cannot turn off decompression)
  decompress: true // 默认值
}

  1. GET:即使使用了 data,数据也不会放入 data 中;需要使用params​ 将数据以键值对形式放在 url 后
  2. POST:如果有使用 data,则数据会以 JSON 形式放入请求体(也可以使用 params,同样也是放在 url 之后)
# get 配置
function getLoveMessage() {
    let promise = axios({
        method: "get",
        url: "https://api.uomg.com/api/rand.qinghua",
        params: { format: "json", username: "Exusiai" },
    });

image
get 请求:数据以键值对形式在路径中传递

# post 配置
function getLoveMessage() {
    let promise = axios({
        method: "post",
        url: "https://api.uomg.com/api/rand.qinghua",
        data: { format: "json", username: "Exusiai" },
    });

image
post 请求:数据以 JSON 串形式传递

# get()/post()
# get()
<script setup>
import axios from "axios"; // 在源码中使用了默认导出的形式,所以不加{}
import { ref, reactive } from "vue";
let message = ref("");
let objMessage = reactive({
    code: "",
    content: "",
});
function getLoveWords() {
    // 发送GET请求的方式
    // axios.get(url, {其他信息}); url必填,其他可不填
    return axios.get("https://api.uomg.com/api/rand.qinghua", {
        params: {
            format: "json",
            username: "Exusiai",
        },
        header: {
            Accept: "application/json, text/plain, */*",
        },
    });
}
async function getLoveMessage() {
    // 简化1
    let response = await getLoveWords();
    message.value = response.data.content;
    // 简化2
    let { data } = await getLoveWords(); // 解构表达式
    Object.assign(objMessage, data);
}
</script>
<template>
    <div>
        <h1><!--swig36--></h1>
        <hr />
        <h1 v-text="objMessage.content"></h1>
        <button @click="getLoveMessage()">Change</button>
    </div>
</template>
<style scoped></style>
# post()
<script setup>
import axios from "axios"; // 在源码中使用了默认导出的形式,所以不加{}
import { ref, reactive } from "vue";
let message = ref("");
let objMessage = reactive({
    code: "",
    content: "",
});
function getLoveWords() {
    // POST请求
    // axios.post(url, {要放入请求体中的JSON串}, {请求的其他信息})  url必填,其余两项可不填
	// 最简形式
    // return axios.post("https://api.uomg.com/api/rand.qinghua?format=json");
    return axios.post(
        "https://api.uomg.com/api/rand.qinghua",
        { username: "八奈见", userpwd: "杏菜" },
        {
            params: {
                format: "json",
            },
            header: {
                Accept: "application/json, text/plain, text/html, */*",
            },
        }
    );
}
async function getLoveMessage() {
    // 简化1
    let response = await getLoveWords();
    message.value = response.data.content;
    // 简化2
    let { data } = await getLoveWords(); // 解构表达式
    Object.assign(objMessage, data);
}
</script>
<template>
    <div>
        <h1><!--swig37--></h1>
        <hr />
        <h1 v-text="objMessage.content"></h1>
        <button @click="getLoveMessage()">Change</button>
    </div>
</template>
<style scoped></style>

# 拦截器

如果想在 axios 发送请求之前,或者是数据响应回来在执行 then 方法之前做一些额外的工作,可以通过拦截器完成
image

# 分离实例对象

axios.js

import axios from "axios";
// 使用axios函数创建一个可以发送请求的实例对象
const instance = axios.create({});
// 使用默认导出暴露instance
export default instance;

App.vue

<script setup>
import request from "./axios.js";
</script>

# 配置拦截器

axios.js

import axios from "axios";
// 使用axios函数创建一个可以发送请求的实例对象
const instance = axios.create({
    // 请求的基础路径:之后的任何请求都会在URL前面将下面的baseURL进行拼接
    baseURL: "https://api.uomg.com/",
    // 超时时间
    timeout: 10000,
});
// 设置请求拦截器
instance.interceptors.request.use(
    // 请求之前,设置请求信息的方法
    (config) => {
        console.log("请求前拦截器");
        config.headers.Accept = "application/json, text/plain, text/html, */*";
        // 设置完毕之后,必须返回config
        return config;
    },
    // 请求之前,拦截器拦截到异常方法
    (error) => {
        console.log("请求前拦截器拦截到异常方法");
        console.log("错误信息:" + error);
        // 返回一个失败状态的promise
        return Promise.reject("Something wrong!");
    }
);
// 设置响应拦截器
instance.interceptors.response.use(
    // 响应状态码为200要执行的方法
    (response) => {
        // 处理响应数据
        console.log("response success");
        console.log(response);
        // 最后要返回response
        return response; // 和App.vue中的response是同一个
    },
    (error) => {
        console.log("response fail:");
        console.log(error);
        // 最后一定要响应一个promise
        return Promise.reject("Something wrong!");
    }
);
// 使用默认导出暴露instance
export default instance;

# 跨域

# 概述

同源策略(Sameoriginpolicy)是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的 javascript 脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol)主机(host)端口号

前后端分离模式下,客户端请求前端服务器获取视图资源,然后客户端自行向后端服务器获取数据资源,前端服务器的 协议,IP 和端口和后端服务器很可能是不一样的,这样就产生了跨域

1683364198087

# 处理

前端项目代理模式处理

1683365066926

后端跨域过滤器方式处理

1683364436315

  • CrosFilter 过滤器
package com.atguigu.schedule.filter;
import com.atguigu.schedule.common.Result;
import com.atguigu.schedule.util.WebUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter("/*")
public class CrosFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        System.out.println(request.getMethod());
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT,OPTIONS, DELETE, HEAD");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "access-control-allow-origin, authority, content-type, version-info, X-Requested-With");
        // 如果是跨域预检请求,则直接在此响应200业务码
        if(request.getMethod().equalsIgnoreCase("OPTIONS")){
            WebUtil.writeJson(response, Result.ok(null));
        }else{
            // 非预检请求,放行即可
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
}

使用框架,直接用一个@CrossOrigin 就可以解决跨域问题了

# 状态管理 Pinia

# 概述

如何实现多个组件之间的数据传递?

  1. 组件传参
  2. 路由传参
  3. 通过 pinia 状态管理定义共享数据

image
Pinia 数据无法进行持久化
Pinia 结合 sessionStorage 和 localStorage 实现持久化

当我们有多个组件共享一个共同的状态(数据源) 时,多个视图可能都依赖于同一份状态。来自不同视图的交互也可能需要更改同一份状态。虽然我们的手动状态管理解决方案(props,组件间通信,模块化)在简单的场景中已经足够了,但是在大规模的生产应用中还有很多其他事项需要考虑:

  • 更强的团队协作约定
  • 与 Vue DevTools 集成,包括时间轴、组件内部审查和时间旅行调试
  • 模块热更新 (HMR)
  • 服务端渲染支持

Pinia 就是一个实现了上述需求的状态管理库

# 基本用法

1 准备 vite 项目

npm create vite
npm install
npm install vue-router@4 --save

2 安装 pinia

npm install pinia

3 定义 pinia store 对象 src/store/store.js,推荐这么命名不是强制

// 定义共享的Pinia数据
import { defineStore } from "pinia";
// 定义数据并且对外暴露
// store就是定义共享状态的包装对象
// 内部包含四个属性: id 唯一标识 state 完整类型推理,推荐使用箭头函数 存放的数据 getters 类似属性计算,存储放对数据
// 操作的方法  actions 存储数据的复杂业务逻辑方法
// 理解: store类似Java中的实体类, id就是类名, state 就是装数据值的属性  getters就是get方法,actions就是对数据操作的其他方法
export const definedPerson = defineStore({
    id: "personPinia", // 当前数据的id,必须全局唯一
    state: () => {
        // 状态,其实就是响应式数据
        return {
            username: "Exusiai",
            age: 18,
            hobbies: ["Eat", "Sleep", "Work"],
        };
    },
    getters: {
        // 专门定义一个获得数据或者是使用数据计算结果的一些函数,getters中的方法可以当做属性值方式使用
        // 这里的函数只能使用数据,不能修改数据(数据对象为state)
        getAge() {
            return this.age;
        },
        getFavorate() {
            return this.favorate;
        },
    },
    actions: {
        // 专门定义一些修改数据的函数
        doubleAge() {
            this.age = this.age * 2;
        },
    },
});

4 在 main.js 配置 pinia 组件到 vue 中

import { createApp } from "vue";
import router from "./routers/router.js";
import App from "./App.vue";
// 开启全局的Pinia功能
import { createPinia } from "pinia";
let pinia = createPinia();
let app = createApp(App);
app.use(router);
// 使用Pinia数据
app.use(pinia);
app.mount("#app");

5 Operate.vue 中操作 Pinia 数据

<script setup>
// 导入Pinia定义的数据
// 导入自己写的数据
import { definedPerson } from "../store/store.js";
let person = definedPerson(); // JS中函数也是一个对象
</script>
<template>
    <div>
        <h1>修改Pinia数据</h1>
        <span>名字:</span>
        <input type="text" v-model="person.username" /> <br />
        <span>年龄:</span>
        <input type="text" v-model="person.age" /> <br />
        <span><!--swig38--></span> <br />
        <span>爱好:</span>
        <span>Eat</span>
        <input type="checkbox" value="Eat" v-model="person.hobbies" />
        <span>Sleep</span>
        <input type="checkbox" value="Sleep" v-model="person.hobbies" />
        <span>Work</span>
        <input type="checkbox" value="Work" v-model="person.hobbies" />
        <span>Drink</span>
        <input type="checkbox" value="Drink" v-model="person.hobbies" /> <br />
        <button @click="person.doubleAge()">年龄加倍</button>
        <!-- 恢复默认值 -->
        <button @click="person.$reset()">重置</button>
        <!-- 一次性修改多个属性值 -->
        <button
            @click="
                person.$patch({
                    username: 'Skadi',
                    age: 32,
                    hobbies: ['Swim', 'Run'],
                })
            "
        >
            Switch
        </button>
        <hr />
        <span>person: <!--swig39--></span>
    </div>
</template>
<style scoped></style>

6 List.vue 中展示 Pinia 数据

<script setup>
import { definedPerson } from "../store/store.js";
let person = definedPerson();
</script>
<template>
    <div>
        <h1>展示Pinia数据</h1>
        读取姓名:<!--swig40--> <br />
        读取年龄:<!--swig41--> <br />
        通过get年龄:<!--swig42--> <br />
        爱好数量:<!--swig43--> <br />
        所有的爱好:
        <ul>
            <li
                v-for="(hobby, index) in person.hobbies"
                :key="index"
                v-text="hobby"
            ></li>
        </ul>
    </div>
</template>
<style scoped></style>

7 定义组件路由 router.js

// 导入路由创建的相关方法
import { createRouter, createWebHashHistory } from "vue-router";
// 导入vue组件
import List from "../components/List.vue";
import Operate from "../components/Operate.vue";
// 创建路由对象,声明路由规则
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: "/opearte",
            component: Operate,
        },
        {
            path: "/list",
            component: List,
        },
    ],
});
// 对外暴露路由对象
export default router;

8 App.vue 中通过路由切换组件

<script setup></script>
<template>
    <div>
        <router-link to="/operate">operate</router-link>
        <br />
        <router-link to="/list">list</router-link>
        <hr />
        <router-view></router-view>
    </div>
</template>
<style scoped></style>

9 启动测试

npm run dev

# 其它细节

# State

State (状态) 在大多数情况下,都是 store 的核心。人们通常会先定义能代表他们 APP 的 state。在 Pinia 中,state 被定义为一个返回初始状态的函数。

store.js

import {defineStore} from 'pinia'
export const definedPerson = defineStore('personPinia',
    {
        state:()=>{
            return {
                username:'',
                age:0,
                hobbies:['唱歌','跳舞']
            }
        },
        getters:{
            getHobbiesCount(){
                return this.hobbies.length
            },
            getAge(){
                return this.age
            }
        },
        actions:{
            doubleAge(){
                this.age=this.age*2
            }
        }
    }
)

Operate.vue

<script setup type="module">
import { ref } from "vue";
import { definedPerson } from "../store/store";
// 读取存储的数据
let person = definedPerson();
let hobby = ref("");
let addHobby = () => {
    console.log(hobby.value);
    person.hobbies.push(hobby.value);
};
// 监听状态
person.$subscribe((mutation, state) => {
    console.log("---subscribe---");
    /*
        mutation.storeId
            person.$id一样
        mutation.payload
            传递给 cartStore.$patch() 的补丁对象。
        state 数据状态,其实是一个代理
        */
    console.log(mutation);
    console.log(mutation.type);
    console.log(mutation.payload);
    console.log(mutation.storeId);
    console.log(person.$id);
    // 数据 其实是一个代理对象
    console.log(state);
});
</script>
<template>
    <div>
        <h1>operate视图,用户操作Pinia中的数据</h1>
        请输入姓名:<input type="text" v-model="person.username" /> <br />
        请输入年龄:<input type="text" v-model="person.age" /> <br />
        请增加爱好:
        <input type="checkbox" value="吃饭" v-model="person.hobbies" /> 吃饭
        <input type="checkbox" value="睡觉" v-model="person.hobbies" /> 睡觉
        <input type="checkbox" value="打豆豆" v-model="person.hobbies" /> 打豆豆
        <br />
        <input type="text" @change="addHobby" v-model="hobby" /> <br />
        <!-- 事件中调用person的doubleAge()方法 -->
        <button @click="person.doubleAge()">年龄加倍</button> <br />
        <!-- 事件中调用pinia提供的$reset()方法恢复数据的默认值 -->
        <button @click="person.$reset()">恢复默认值</button> <br />
        <!-- 事件中调用$patch方法一次性修改多个属性值 -->
        <button
            @click="
                person.$patch({
                    username: '奥特曼',
                    age: 100,
                    hobbies: ['晒太阳', '打怪兽'],
                })
            "
        >
            变身奥特曼
        </button>
        <br />
        person:<!--swig44-->
    </div>
</template>
<style scoped></style>

# Getter

Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore()​ 中的 getters​ 属性来定义它们。推荐使用箭头函数,并且它将接收 state​ 作为第一个参数:

export const useStore = defineStore('main', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
})

# Action

Action 相当于组件中的 method。它们可以通过 defineStore()​ 中的 actions​ 属性来定义,并且它们也是定义业务逻辑的完美选择。类似 getter,action 也可通过 this​ 访问整个 store 实例,并支持完整的类型标注(以及自动补全)。不同的是,action​ 可以是异步的,你可以在它们里面 await​ 调用任何 API,以及其他 action!

export const useCounterStore = defineStore('main', {
  state: () => ({
    count: 0,
  }),
  actions: {
    increment() {
      this.count++
    },
    randomizeCounter() {
      this.count = Math.round(100 * Math.random())
    },
  },
})

# Element-plus 组件库

# Element-plus 介绍

Element Plus 是一套基于 Vue 3 的开源 UI 组件库,是由饿了么前端团队开发的升级版本 Element UI。Element Plus 提供了丰富的 UI 组件、易于使用的 API 接口和灵活的主题定制功能,可以帮助开发者快速构建高质量的 Web 应用程序。

  • Element Plus 支持按需加载,且不依赖于任何第三方 CSS 库,它可以轻松地集成到任何 Vue.js 项目中。Element Plus 的文档十分清晰,提供了各种组件的使用方法和示例代码,方便开发者快速上手。
  • Element Plus 目前已经推出了大量的常用 UI 组件,如按钮、表单、表格、对话框、选项卡等,此外还提供了一些高级组件,如日期选择器、时间选择器、级联选择器、滑块、颜色选择器等。这些组件具有一致的设计和可靠的代码质量,可以为开发者提供稳定的使用体验。
  • 与 Element UI 相比,Element Plus 采用了现代化的技术架构和更加先进的设计理念,同时具备更好的性能和更好的兼容性。Element Plus 的更新迭代也更加频繁,可以为开发者提供更好的使用体验和更多的功能特性。
  • Element Plus 可以在支持 ES2018ResizeObserver 的浏览器上运行。 如果您确实需要支持旧版本的浏览器,请自行添加 Babel 和相应的 Polyfill
  • 官网https://element-plus.gitee.io/zh-CN/
  • 由于 Vue 3 不再支持 IE11,Element Plus 也不再支持 IE 浏览器。

# Element-plus 入门案例

1 准备 vite 项目

注意要使用 TypeScript

npm create vite  // 注意选择 vue+typeScript
npm install
npm install vue-router@4 --save
npm install pinia
npm install axios

2 安装 element-plus

npm install element-plus

3 完整引入 element-plus

  • main.js
import { createApp } from 'vue'
//导入element-plus相关内容
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')

4 入门案例

  • App.vue
<script setup>
  import { ref } from 'vue'
  const value = ref(true)
</script>
<template>
  <div>
    <!-- 直接使用element-plus组件即可 -->
    <el-button>按钮</el-button>
    <br>
    <el-switch
      v-model="value"
      size="large"
      active-text="Open"
      inactive-text="Close"
    />
    <br />
    <el-switch v-model="value" active-text="Open" inactive-text="Close" />
    <br />
    <el-switch
      v-model="value"
      size="small"
      active-text="Open"
      inactive-text="Close"
    />
  </div>
</template>
<style scoped>
</style>

5 启动测试

npm run dev

# Element-plus 常用组件

Element Plus