Vue


Vue

前端工程化

前端工程化是使用软件工程的方法来单独解决前端的开发流程中模块化、组件化、规范化、自动化的问题,其主要目的是为了提高效率和降低成本。前端工程化是将项目中前端相关的代码剥离出来,形成一个独立的工程。使用相关的专门的技术来实现前端代码的“四化”,这也是前后端分离。

前端工程化相关技术栈:

  • ECMAScript6:VUE3 中大量使用 ES6 语法。
  • Nodejs:前端项目运行环境。
  • npm:依赖下载工具。
  • vite:前端项目构建工具。
  • VUE3:优秀的渐进式前端框架。
  • router:通过路由实现页面切换。
  • pinia:通过状态管理实现组件数据传递。
  • axios:ajax 异步请求封装技术实现前后端数据交互。
  • Element-plus:可以提供丰富的快速构建网页的组件仓库。

Node.js 简介

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,可以使原本运行在浏览器上的 JavaScript 运行在服务端。使用 Node.js 可以方便地开发服务器端应用程序,如 Web 应用、API、后端服务,还可以通过 Node.js 构建命令行工具等。使得 JavaScript 从脚本语言成为一种全栈语言。

相比于传统的服务端语言(如 PHP、Java、Python 等),Node.js 具有以下特点:

  • 单线程,但是采用了事件驱动、异步 IO 模型,可以处理高并发请求。
  • 轻量级,V8 引擎使得 Node.js 的运行速度很快。
  • 模块化,Node.js 内置了大量模块,同时也可以通过第三方模块扩展功能。
  • 跨平台,可以在 Windows、Linux、Mac 等多种平台下运行。

下载地址戳我跳转。

npm 的配置和使用

NPM 全称 Node Package Manager,是 Node.js 包管理工具,是全球最大的模块生态系统,里面所有的模块都是开源免费的,也是 Node.js 的包管理工具,相当于后端的 Maven。是前端框架的下载工具,也是前端项目的管理工具。

npm 的配置:

  • 在命令行工具中使用 npm config set registry https://registry.npmmirror.com命令可以将下载源从国外的中央仓库转移到国内的阿里镜像仓库,提高下载速度。
  • 使用 npm config set registry https://registry.npmjs.org/ 命令可以将下载源恢复回中央仓库。
  • 使用 npm config set prefix "路径名"命令可以将下载地址转移到自定义磁盘路径中。(默认为:C:\Users\40200\AppData\Roaming\npm)

npm 的使用:

  • 查看所有依赖网址。
  • 使用 npm init 为文件配置 npm 初始化,使用 npm init -y 可以跳过所有可选项的设置,使用默认值。
  • 使用 npm install 包名 或者 npm install 包名@版本号安装依赖到当前项目中。
  • 使用npm install -g 包名安装全局依赖。(一般不会把所有依赖直接下载到全局依赖文件夹中,都是根据项目需求在项目所在文件夹中下载依赖,实在是需要所有项目共用的依赖才下载到全局依赖里)
  • 使用 npm install按照已有的配置信息安装所缺少的依赖。(上传项目文件时,我们一般不会将所需要的组件全部上传,这会占用很多空间,一般是上传配置文件,然后根据配置文件再去下载相对应所需要的组件)
  • 使用 npm uninstall 包名 卸载依赖。
  • 使用 npm ls 查看当前项目有哪些依赖。
  • 使用 npm ls -g 查看全局依赖。
  • 使用 npm run script字段命令可以让运行记录在 package.json文件中 script 字段的命令内容。

在 VsCode 中,使用终端窗口 cd 项目名 进入所对应的项目文件夹后,就可以直接在终端窗口使用上述 npm 命令(如果报错,可以以管理员身份启动 VsCode)。

ECMA6Script

ECMA6Script,简称ES6,是 JavaScript 语言的一次重大更新。ES6 带来了大量的新特性,包括箭头函数、模板字符串、let 和 const 关键字、结构、默认参数值、模块系统等等,大大提升了 JavaScript 的开发体验。由于 VUE3 中大量使用了 ES6 的语法,所以 ES6 成为了学习 VUE3 的门槛之一。

变量和模板字符串

ES6 新增了 let 和 const用来声明变量,使用的细节上也存在诸多差异。

  • let 不能重复声明。
  • let 有块级作用域,非函数的花括号遇见 let 会有块级作用域,也就是说只能在花括号里面访问。
  • let 不会预解析进行变量提升。
  • let 定义的全局变量不会作为 windows 属性。
  • const 就是不可修改的 let,类似于 final 修饰的变量。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>let and const</title>
    <script>
        //let不能重复声明
        var i = 10;
        var i = "string";   //可以重复声明

        let j = 10;
        //let j = "zhansan";    无法重复声明


        //let有块级作用域,非函数的花括号遇见let会有块级作用域,也就是说只能在花括号里面访问
        {
            var k = 20;
            let l = 20;
        }
        console.log(k); //var可以在花括号外面访问
        console.log(l); //let无法在花括号外面访问


        //let不会预解析进行变量提升
        console.log(a); //undefined
        var a = 3;  

        console.log(b); //error
        let b = 3;


        //let定义的全局变量不会作为windows属性
        var c = 10; //c会变成window对象的属性
        let d = 10; //d不会变成window对象的属性
        console.log(window.c);
        console.log(window.d);


        //const就是不可修改的let    类似于final修饰的变量
        const e = 10;
        e = 20; //不允许

        const teachers = ["张老师", "李老师", "王老师"];
        //const内容不可以修改,但是数组比较特殊,可以利用push增加数据
        //换句话讲,对于数组,只要保证指针的指向不变,指针指向的值是可以改变的
        teachers.push("陈老师");
    </script>
</head>
<body>
</body>
</html>

模板字符串主要解决字符串换行和字符串拼接的问题。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>let and const</title>
    <script>
        /* 
            js中传统的""和''对于字符串换行和拼接比较繁琐
            模板字符串可以比较方便地解决上述问题
        */

        //传统多行写法
        let info = "<ul>" + 
                       "<li></li>" + 
                       "<li></li>" +
                   "</ul>";


        var city = "北京";
        //使用模板字符串,利用``来表示字符串,利用${}进行变量的拼接
        let str = `<ul>
                       <li>${city}</li>
                       <li></li>
                   </ul>`;
    </script>
</head>
<body>
</body>
</html>

解构表达式

ES6 的解构表达式是一种方便的语法,可以快速将数组或对象中的值差分并赋值给变量。解构赋值的语法使用花括号{}表示对象,方括号[]表示数组。通过解构赋值,函数更方便进行参数接受等。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>解构表达式</title>
    <script>
        //传统的数组声明和取元素
        let arr = [11, 22, 33];
        console.log(arr[0]);

        //使用解构表达式取出数组中的元素
        let [a, b, c, d, e = 5] = [1, 2, 3, 4];
        console.log(a, b, c, d, e);



        //传统的对象声明和取属性
        let person = {
            name:"zhangsan",
            age:18
        }
        console.log(person.name, person.age);

        //使用解构表达式获取对象的属性值
        let {name, age} = person;
        console.log(age, name);

        

        //传统的方法声明和取出元素
        function showArr(arr) {
            console.log(arr[0], arr[1], arr[2]);
        }
        //解构表达式应用在方法的参数列表
        function showArr1([a, b, c]) {
            console.log(a, b, c);
        }
    </script>
</head>
<body>   
</body>
</html>

链判断

如果读取对象内部的某个属性,往往需要判断一下,属性的上层对象是否存在。比如,读取message.body.user.firstName(message 可能是后端返回出来的一个 json 数据)这个属性,安全的写法如下:

let message = null

// 在读数据的时候需要判空
let content = (message &&
               message.body &&
               message.body.user && 
               message.body.user.firstName) || 'default'
console.log(content)

上述通过各种层级的判空进行空值防护,但是在 ES6 中,可以使用链判断来简化上述代码:

// 使用?进行空判断,如果前面的链判断为空,使用或运算赋值默认值
let content = message?.body?.user?.firstName || 'default'
console.log(content)

箭头函数

语法类似于 Java 中的 Lambda 表达式。

基本语法如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>箭头函数</title>
    <script>
        //传统函数声明
        let fun1 = function () {}
        //箭头函数声明
        let fun2 = () => {}

        //如果参数里面有且仅有一个参数,小括号可以省略不写
        let fun3 = (x) => { return x + 1; }
        let fun4 = x => { return x + 1; }

        //如果方法体中只有一行代码,方法体花括号可以不写
        let fun5 = x => { console.log(x); }
        let fun6 = x => console.log(x);

        //方法体中,有且仅有一行return代码,那么{}和return都可以省略不写
        let fun7 = x => { return x + 1; }
        let fun8 = x => x + 1;
    </script>
</head>
<body>
</body>
</html>

箭头函数中的 this关键字问题:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>箭头函数</title>
    <script>

        /* 
            箭头函数没有自己的this
            箭头函数的this是外层上下文环境中的this
        */

        var name = "lisi";

        let person = {
            name:"zhangsan",
            showName: function () { 
                console.log(this.name); 
            },
            viewName: () => console.log(this.name)  
            /* 
                对于箭头函数viewName
                当中的this指的是外层上下文环境中的this
                其外层上下文环境是window对象
                故this.name指的是window对象当中的name
            */
        }
        person.showName();  //zhansan
        person.viewName();  //lisi

    </script>
</head>
<body>
</body>
</html>

rest 和 spread

rest 参数,在形参上和 Java 中的可变参数几乎一样。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>rest and spread</title>
    <script>

        //rest参数(类似于Java中的可变参数)
        let fun1 = (...args) => {console.log(args);}

        fun1(1, 2, 3, 4);


        //spread是rest在实参上的使用
        let arr = [1, 2, 3];
        let fun2 = (a, b, c) => console.log(a, b, c);   
        fun2(arr);  //直接填入参数,会默认给第一个参数赋值:[1, 2, 3] undefined undefined
        fun2(...arr);   //填入rest,会把arr中的数据分配给参数列表中的参数: 1 2 3

        //快速合并数组
        let a = [1, 2, 3];
        let b = [4, 5, 6];
        let c = [7, 8, 9];
        let d = [...a, ...b, ...c]; //let d = [1, 2, 3, 4, 5, 6, 7, 8, 9];
        console.log(d);

        //快速合并对象
        let person1 = {
            name:"zhangsan"
        }
        let person2 = {
            age:18
        }
        let person3 = {...person1, ...person2};

    </script>
</head>
<body>
</body>
</html>

对象的创建

ES6 新增了对象创建的语法糖,支持了 class、extends、constructor 等关键字,让 ES6 的语法和面向对象的语法更加接近。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>类和对象</title>
    <script>

        class Person {

            //属性(加上#号可以将变量变为私有,并且#也作为变量名的一部分)
            #name;
            #age;

            //构造器
            constructor(name, age) {
                this.#name = name;  //注意带上#,否则会多创建一个name出来
                this.#age = age;
            }

            //getter setter
            get name() {
                return this.#name;
            }
            set name(name) {
                this.#name = name;
            }

            get age() {
                return this.#age;
            }
            set age(age) {
                this.#age = age;
            }

            //成员方法
            eat(food) {
                console.log(`${this.#age}岁的${this.#name}正在吃${food}`);
            }
            //静态方法
            static sum(a, b) {
                return a + b;
            }

        }

        let person = new Person("zhangsan", 18);

        console.log(person);

        person.eat("火锅");

        console.log(Person.sum(1, 2));

        console.log(person.age);    //这个时候无法访问

        class Student extends Person {  //继承
            score;
            constructor(name, age, score) {
                super(name, age);   //super可以调用父类的构造
                this.score = score;
            }
            study() {
                console.log(`${this.age}岁的${this.name}正在学习,考了${this.score}分`);
            }
        }
        
        let stu = new Student("lisi", 19, 60);
        stu.study();


    </script>
</head>
<body>
</body>
</html>

对象的拷贝

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>拷贝问题</title>
    <script>
        let arr = [1, 2, 3];
        let person = {name:"zhangsan", age:18};
        //浅拷贝
        let arr2 = arr;
        let person2 = person;
        console.log(arr2);  //1 2 3
        console.log(person2);   //age = 18

        arr[1] = 3;
        person2.age = 19;
        //只拷贝了地址,并没有真正拷贝内容,arr的改变也会导致arr2的改变
        console.log(arr2);  //1 3 3
        console.log(person2);   //age = 19


        //深拷贝
        let arr3 = [...arr];
        let person3 = {...person};

        //如果是对象,深拷贝可以考虑json转换
        let person4 = JSON.parse(JSON.stringify(person));
        
    </script>
</head>
<body>
</body>
</html>

模块化处理

模块化是一种组织和管理前端代码的方式,将代码拆分成小的模块单元,使得代码更易于维护、扩展和复用。它包括了定义、导出、导入以及管理模块的方法和规范。

ES6 模块化的几种暴露和导入方式:

  1. 分别导出。
  2. 统一导出。
  3. 默认导出。

无论是哪种方式导出,导出的都是一个对象,导出的内容都可以理解为是向这个对象中添加属性或者方法。此外,当 html 文件在导入 js 文件时,需要引入 type="module"使得浏览器可以支持模块化解析。

三种导出语法可以混用,但是考虑到混用后难以区分语法,故一般我们还是尽可能只用一种导出方式。

分别导出

module.js 文件如下:

//使用export关键字将变量或者方法导出
export const PI = 3.14;

const PI2 = 3.14;

export function sum (a, b) {
    return a + b;
}

export class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    sayHello() {
        console.log(`hello, my name is ${this.name}, I'm ${this.age} years old.`);
    }
}

app.js 文件如下:

//导入module.js文件
/* 
    * 表示module.js中的所有成员
    无论何种方式导入,导入的内容都会被当成一个对象来处理
    故我们应该使用一个对象来接收
    相对路径中的./不能少
    使用as来给对象起别名
*/
import * as m1 from './module.js'   

console.log(m1.PI);
console.log(m1.PI2);    //没有export,故PI2的访问结果是undefined

console.log(m1.sum(1, 2));

let person = new m1.Person("zhangsan", 18);
person.sayHello();

index.html 文件如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>index</title>
    <!-- 引入app.js文件,使用type关键字让浏览器支持模块化组件解析 -->
    <script src="./app.js" type="module"></script>
</head>
<body>
</body>
</html>

统一导出

module.js 文件如下:

const PI = 3.14;

const PI2 = 3.14;

function sum (a, b) {
    return a + b;
}

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    sayHello() {
        console.log(`hello, my name is ${this.name}, I'm ${this.age} years old.`);
    }
}

//最末尾利用export关键字统一暴露
export {PI, sum, Person};

app.js 文件如下:

//利用解构表达式导入
import {PI as pi, sum, Person} from './module.js'

//使用的时候就无需用对象调用
console.log(pi);    //利用as关键字起别名
console.log(sum(1, 2));

let person = new Person("zhangsan", 18);
person.sayHello();

index.html 文件同上。

默认导出

module.js 文件如下:

const PI = 3.14;

const PI2 = 3.14;

function sum (a, b) {
    return a + b;
}

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    sayHello() {
        console.log(`hello, my name is ${this.name}, I'm ${this.age} years old.`);
    }
}

//默认导出,在一个js中只能有一个默认导出
export default sum;

app.js 文件如下:

//利用解构表达式导入
import * as m1 from './module.js'

//默认导出,使用default关键字调用
console.log(m1.default(1, 2)); 

//或者使用别名的方式导入也可以
//别名关键字导入
import {default as add} from './module.js'
//别名省略关键字导入
import add from './module.js'

console.log(add(1, 2));

index.html 文件同上。

Vue3 简介

Vue (作者:尤雨溪)是一款构建用户界面的 JavaScript 框架。它基于 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。官网戳我。

Vue 的两个核心功能:

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

使用 Vue 的基础思路是:数据发生改变时,数据所绑定的位置对应的 DOM 树内容自动发生改变。

Vite

Vite 可以帮助我们生成一个基础的工程化的 VUE3 项目,而且还给我们提供了其他支撑文件,帮助我们进行项目管理。

使用 Vite 创建工程化前端项目:

  1. 在 VsCode 中运行 npm create vite给项目创建 Vite 环境。
  2. 进入创建之后的项目(使用 cd 项目名 命令进入),使用 npm install 给项目下载缺少的依赖。
  3. 使用 npm run dev 进入研发模式运行。

Vite + Vue3 项目的目录结构

.vscode:使用 VsCode 开发时的相关驱动。

node_modules:存放依赖的目录。

public:存放公共资源,如 HTML 文件、图象、字体等,这些资源会被直接赋值到构建出的目标目录中。可以直接通过网页访问到。

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

  1. assets:存放静态资源,如图片、字体、样式文件等。
  2. components:用于存放组件相关的文件,用于抽象出可以复用的 UI 部件,方便在不同场景中使用。
  3. layouts:用于存放布局组件的文件,通常负责整个应用程序的整体布局,如头部、底部、导航菜单等。
  4. pages:用于存放页面界别的组件文件。
  5. plugins:用于存放 Vite 相关插件。
  6. router:用于存放路由配置文件,负责管理视图和 URL 之间的映射关系,方便实现页面之间的跳转和数据传递。
  7. store:用于存放 Vuex 状态管理相关文件。
  8. utils:用于存放一些通用的工具函数,如日期处理函数,字符串操作函数等。

vite.config.js 文件:Vite 的配置文件,可以通过该文件配置项目的参数、插件、打包优化等。

package.json 文件:标准的 Node.js 项目配置文件,包含了项目的基本信息和依赖关系。

Vite 项目的入口为 src / main.js 文件,这是 Vue.js 应用程序的启动文件,也是整个前端应用程序的入口文件。

在 vite.config.js 中可以配置端口号:

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

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  // server:{
  //   port:3000
  // }
})

Vite + Vue3 项目组件(SFC 入门)

多个页面相同的地方,我们可以单独拿出来,形成一个“组件”,以提高代码的复用性。一旦我们需要使用相关组件,我们便可以直接引入,不需要再去重新写代码了。

一个组件的开发传统上需要使用 HTML、CSS、JS 三种代码,意味着有三种文件需要维护。当然,把 CSS、JS 全部塞入一个 HTML 文件中也是可以。但是可想而知,以上两种方法都会导致文件的维护困难。而 VUE3 框架中通过 .vue 文件来管理组件,使得管理维护更加方便。而这种管理组件形式也被称作单文件组件(Single-File Component,简称 SFC)。

接下来我们探讨 Vue 工程文件之间的关系。

index.html 文件如下,这是基础的页面:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue</title>
  </head>
  <body>
    <div id="app"></div> <!-- main.js的内容会挂载到这个div里 -->
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

可以看到,这个页面在 script 标签中导入了另一个文件 main.js :

//从框架中导入一个createApp的函数
import { createApp } from 'vue'
//导入全局css样式文件,该文件中的样式会作用到所有的.vue元素上
import './style.css'
//导入了一个App.vue的文件,并起了一个别名叫App
import App from './App.vue'

//let app = createApp(App)    使用导入的app组件生成一个对象
//app.mount('#app') 将app对象挂载到id值为app的元素上
createApp(App).mount('#app')

而当中的 App.vue 文件便是我们上述所讲的单文件组件,所以,相当于在 js 文件中导入组件的文件,然后用这个文件创建一个组件对象,最后再让这个组件对象作用到我们想要作用的页面元素上去。

App.vue 框架如下:

<!-- .vue文件把三种代码结合到一起,放入一个文件中 -->

<script setup>
<!-- 引入js代码 -->
</script>

<template>
<!-- 引入html代码 -->
</template>

<style scoped>
/* 引入css代码 */
</style>

语法上,要求 template 标签中只能有一个一级子标签,虽然不遵循这个语法也可以正常工作,但是为了规范还是建议在多个标签需要使用时,外层加上一个 div 标签包裹,保证只有一个一级子标签。

值得注意的是,组件文件当中也可以引入其他组件文件,当这么处理的时候,引入外部组件文件时起的别名可以直接当作标签来使用。

Vue3 的视图渲染技术

Vue3 关于 CSS 样式的导入

<script setup>
/* 
  css样式在.vue文件中的导入方式
    1.写在.vue文件中的style标签中
    2.将css样式保存到独立的css文件中,哪个文件需要就在哪里导入即可
      可以在script标签中导入 语法:import 相对路径
      也可以在style标签中导入 语法:@import 相对路径
    3.如果某个样式要在所有的.vue文件中生效,可以在main.js中导入        
*/
import "./style/test.css"
</script>

<template>
  <div>
    <span class="s1">你好</span><br>
    <span class="s2">hello</span>
  </div>
</template>

<style scoped>
.s1 {
  color:rgb(59, 129, 190);
  font-size:40px;
}
/* @import './style.css/test.css' */
</style>

响应式数据和 setup 语法糖

<script setup>  //使用setup可以不需写其他冗余setup操作
/* 
  响应式数据:在数据变化时,vue框架会将变量最新的值更新到dom树中,页面的数据就是实时更新的
  非响应式数据:在数据变化时,vue框架不会将数据更新到dom树中,页面的数据就不会更新

  在vue2框架中,数据不做特殊处理,默认就是响应式的
  在vue3框架中,数据要经过ref/reactive函数处理,才是响应式的
    ref/reactive函数是vue框架中给我们提供的方法,导入进来即可使用

  ref处理的响应式数据在操作时需要注意
    在script标签中操作ref的响应式数据需要通过.value的形式操作
    在template标签中操作ref响应式数据无需使用.value操作
*/
  import {ref, reactive} from 'vue'

  //定义一些要展示到html上的一些数据  变量/对象
  let counter = ref(10) //ref(10) 返回一个对象,其中value属性为10

  //让变量自增的方法
  function counterIncr() {
    ++counter.value
  }

  //让变量自减的方法
  function counterDecr() {
    --counter.value
  }

</script>


<template>
<div>
  <button @click="counterIncr">+</button>
  <span v-text="counter"></span>
  <button @click="counterDecr">-</button>
</div>
</template>


<style scoped>
</style>

插值表达式

<!-- 
  插值表达式:将变量关联到元素上的一种比较方便的语法
  语法:{{ 数据名字/函数/对象调用API }}
    插值表达式不依赖标签,没有标签时是可以单独使用的
    插值表达式是可以调用函数的,将函数的返回值渲染到指定位置
    插值表达式是支持一些常见的运算符
    插值表达式中还支持对象调用API
-->
<script setup>
  //定义一些常见数据
  let msg = "hello vue3"

  let getMsg = () => {
    return "hello vue3"
  }

  let age = 19

  let bee = "蜜 蜂"
</script>

<template>
<div>
  <!-- 将数据绑定到下面的元素上展示 -->
  <!-- 将msg放入到h1标签中展示 -->
  <h1>{{ msg }}</h1><br>

  <!-- 插值表达式调用函数 -->
  msg 的值为 {{ getMsg() }}<br>

  <!-- 插值表达式使用运算符 -->
  年龄:{{ age }}, 是否成年: {{ age > 18 ? "是":"否" }}<br>

  <!-- 插值表达式支持对象调用API -->
  {{ bee.split(" ").reverse().toString().replace(",", " ") }}
</div>
</template>

<style scoped>
</style>

文本渲染命令

<script setup>
/* 
  v-text  不能将html结构的文本进行识别
  v-html  可以识别html结构的文本

  {{   }} 插值表达式
  v-*** vue指令

  命令必须依赖标签,在开始标签中使用  
*/

  let msg = "hello vue3"
  
  let haha = "哈哈"
  let msg2 = `hello${haha}`

  let age = 19

  let bee = "蜜 蜂"

  function getMsg() {
    return msg
  }

  let fontMsg = '<font color="red">你好</font>'

</script>

<template>
<div>
  <h1 v-text="msg"></h1>

  <!-- vue指令支持模板字符串 -->
  <h1 v-text="msg2"></h1>
  <h1 v-text="`hello ${msg2}`"></h1>

  <!-- 命令中也支持一些常见的运算符 -->
  <h1 v-text="age >= 18 ? `成年`: `未成年`"></h1>

  <!-- 命令中也支持一些API调用 -->
  <h1 v-text="bee.split(' ').reverse().join(' ')"></h1>

  <!-- 命令中也支持函数的调用 -->
  <h1 v-text="getMsg()"></h1>

  <!-- v-text无法识别html结构文本 -->
  <h1 v-text="fontMsg"></h1>
  <h1 v-html="fontMsg"></h1>
</div>
</template>

<style scoped>
</style>

属性渲染命令

<script setup>
/* 
  属性渲染命令
  v-bind 将数据绑定到元素的属性上
  v-bind:属性名="数据名"
  可以将v-bind省略不写
*/

  let imgUrl = "https://github.com/n70huihui/Blog_Photo/blob/main/Elieen.png"

  const data = {
    logo:"https://github.com/n70huihui/Blog_Photo/blob/main/Elieen.png",
    name:"Elieen",
    url:"https://space.bilibili.com/672342685?spm_id_from=333.999.0.0"
  }
</script>

<template>
<div>
  <a v-bind:href="data.url">
    <img v-bind:src="imgUrl" v-bind:title="data.name">
  </a>

  <!-- 属性渲染的简写形式 -->
  <img :src="data.logo">
</div>
</template>

<style scoped>
</style>

事件渲染命令

<script setup>
/* 
  v-on:事件名称="函数名()"
  可以简写为
  @事件名="函数名"

  原生js的事件名是on***形式的 onclick ondbclick onblur onfocuse
  vue中的事件名在原生js的基础上去掉on
*/
  import {ref} from 'vue'

  function fun1() {
    alert("hi")
  }

  let counter = ref(1)
  function fun2() {
    counter.value++
  }

  function fun3(event) {
    let flag = confirm("确定要访问目标链接吗")
    if(!flag) {
      //原生js阻止组件的默认行为
      event.preventDefault()
    }
  }

  function fun4() {
    alert("超链接被点击了")
  }
</script>

<template>
<div>
  <!-- 事件的绑定 -->
  <button v-on:click="fun1()">hello</button><br>

  <!-- 内联事件处理器 -->
  <button v-on:click="fun2()">+</button>
  <button v-on:click="counter++">+</button> <!-- 这里不需要加.value -->
  {{ counter }}

  <!-- 事件的修饰符 .once事件只绑定一次,意味着这个事件只会触发一次 -->
  <button v-on:click.once="counter++">+</button><br>

  <!-- prevent修饰符,阻止组件的默认行为 -->
  <a href="https://blog.hnuxcc21.cn/" v-on:click="fun3($event)" target="_blank">热心市民灰灰</a><br>
  <a href="https://blog.hnuxcc21.cn/" v-on:click.prevent="fun4()" target="_blank">热心市民灰灰</a>

</div>
</template>

<style scoped>

</style>

响应式数据的处理方式

<script setup>
  /* 
    让一个数据转换成响应式数据的方式有两种
      1.ref函数 适合单个变量
        在script标签中操作数值,需要使用.value
        如果在template标签中,则不需要
      2.reactive函数  适合对象
        无论是在哪个标签中,都直接使用对象名.属性名对属性进行操作即可

    拓展:
      toRef函数 将reactive响应式数据中的某个属性转换为ref响应式数据
      toRefs函数  同时将reactive响应式数据中的多个属性转换为ref响应式数据
    
  */
  import { ref, reactive } from 'vue'

  //ref处理单个值
  let counter = ref(10)

  function incr() {
    counter.value++
  }

  //reactive处理单个对象
  let person = reactive({
    name:"zhangsan",
    age:18
  })

  function incrAge() {
    person.age++
  }
</script>

<template>
<div>
  <button @click="incr()">+</button>
  <span v-text="counter"></span>
  <button @click="counter--">-</button>
  <hr>
  <button @click="incrAge()">+</button>
  <span v-text="person.age"></span>
  <button @click="person.age--">-</button>
</div>
</template>

<style scoped>
</style>

条件渲染

<script setup>
/* 
  v-if 使得当表达式为真时,内容才显示
  v-else 自动和前一个v-if作取反操作

  v-show  数据为true,元素则展示在页面上,否则不展示

  v-if 是通过将元素移除出dom树使得页面不展示
  v-show 是通过将元素隐藏起来使得页面不展示,元素仍在dom树中
  
  v-if有更高的切换开销,而v-show有更高的初始渲染开销
  如果需要频繁切换,建议使用v-show,如果运行时绑定条件很少改变,可以使用v-if
*/
import { ref } from 'vue'
let flag = ref(true)

</script>

<template>
<div>
  <h1 v-if="flag">vue is awesome!</h1>
  <h1 v-else>oh no</h1><br>
  <h1 v-show="flag">hahaha</h1>
  <button @click="flag = !flag">点我</button>
</div>
</template>

<style scoped>
</style>

列表渲染

<script setup>
  /* 
    v-for 渲染列表
    语法类似于forEach,用一个li就可以渲染多个li
  */
  import { reactive, ref } from "vue";

  let items = reactive([
    {id:"item1", message:"薯片"},
    {id:"item2", message:"可乐"},
    {id:"item3", message:"炸鸡"}
  ])

  let pro = ref("产品")

</script>

<template>
<div>
  <ul>
    <li v-for="(item, index) in items" v-bind:key="index.id">{{ pro + index + item.message }}</li>
  </ul>
</div>
</template>

<style scoped>
</style>

综合小练习

<script setup>
  /* 
    v-for 渲染列表
  */
  import { reactive } from "vue";

  let carts = reactive([
    {name:"可乐", price:3 , number:10},
    {name:"薯片", price:6 , number:2},
    {name:"炸鸡", price:12 , number:1},
  ])

  function getCartSum() {
    let sum = 0
    for(let index in carts) {
      sum += (carts[index].price * carts[index].number)
    }
    return sum
  }

  //从购物车中移除记录
  function removeCart(index) {
    carts.splice(index, 1)
  }

  //清空购物车
  function clearCart() {
    carts.splice(0, carts.length)
  }

</script>

<template>
<div>

  <h1>您的购物车如下:</h1>
  <table border="1px">
    <thead>
      <tr>
        <th>序号</th>
        <th>商品名称</th>
        <th>价格</th>
        <th>数量</th>
        <th>小计</th>
        <th>操作</th>
      </tr>
    </thead>

    <tbody>
      <!-- 利用v-for迭代渲染列表 -->
      <tr v-for="(cart, index) in carts" v-bind: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.number * cart.price"></td>
        <!-- 调用splice函数删除数据 -->
        <td><button @click="removeCart(index)">删除</button></td>
      </tr>
    </tbody>
  </table>
  <!-- 使用条件渲染判断购物车情况 -->
  <div v-if="carts.length" v-text="'购物车总金额:' + getCartSum() + '元'"></div>
  <div v-else>购物车已被清空</div>
  <!-- 调用splice清空购物车 -->
  <button @click="clearCart()">一键清空购物车</button>
</div>
</template>

<style scoped>
</style>

双向绑定

<script setup>
/* 
  单向绑定v-bind  响应时数据发生变化会更新dom树 用户的操作如果造成页面内容的改变不会影响响应式数据
  双向绑定v-model  页面上的数据由于用户的操作造成了改变,也会同步修改对应的响应式数据
  双向绑定一般用于表单标签,故双向绑定也可称作收集表单信息命令
*/
  import { ref } from 'vue'

  let message = ref("zhangsan")
  let message1 = ref("lisi")

</script>

<template>
<div>
  <!-- 单向绑定 -->
  <input type="text" v-bind:value="message"><br><!-- 这里改变,不会影响下方message的改变 -->
  {{ message }} <br>

  <!-- 双向绑定 -->
  <input type="text" v-model="message1"><br><!-- 这里改变,下方message1同样会改变 -->
  {{ message1 }}
</div>
</template>

<style scoped>
</style>

双向绑定在不同标签上的使用:

<script setup>
/* 
  单向绑定v-bind  响应时数据发生变化会更新dom树 用户的操作如果造成页面内容的改变不会影响响应式数据
  双向绑定v-model  页面上的数据由于用户的操作造成了改变,也会同步修改对应的响应式数据
  双向绑定一般用于表单标签,故双向绑定也可称作收集表单信息命令
*/
  import { ref, reactive } from 'vue'

  let user = reactive({
    username:"",
    userPwd:"",
    intro:"",
    origin:""
  })

  let hbs = ref([])

  function clearForm() {
    user.username = ""
    user.userPwd = ""
    user.intro = ""
    user.origin = ""
    hbs.value.splice(0, hbs.value.length)
  }
</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"><br>
  跳 <input type="checkbox" v-model="hbs" value="dance"><br>
  rap <input type="checkbox" v-model="hbs" value="rap"><br>
  简介:
  <textarea v-model="user.intro"></textarea><br>
  籍贯:
  <select v-model="user.origin">
    <option value="北京">北京</option>
    <option value="南京">南京</option>
    <option value="西京">西京</option>    
    <option value="东京">东京</option>    
  </select><br>
  <button @click="clearForm()">清空</button><br>
  {{ user }} <br>
  {{ hbs }}
</div>
</template>

<style scoped>
</style>

属性计算

<script setup>
/* 
  计算属性
    computed(填入方法,可用箭头函数形式)

  通过方法返回数据:每使用一次,都会执行一次
  通过计算属性返回数据:如果和上次使用时数据没有变化,则直接使用上一次的数据
*/

  import { reactive, computed } from "vue";

  const author = reactive({
    name:"zhangsan",
    books:[
      "java-从入门到精通",
      "算法导论",
      "MySQL从入门到精通"
    ]
  })

  let hasBooks = computed(()=>{
    return author.books.length > 0 ? '是' : '否'
  })

</script>

<template>
<div>
  <p>作者:{{ author.name }}</p>
  是否出版过图书: {{ hasBooks }}
</div>
</template>

<style scoped>
</style>

数据监听器

watch 函数监听:

<script setup>
  /* 
    利用watch函数监听数据的变化
    利用watch函数监听ref响应式数据  语法: watch(监听的数据, 执行的函数)
    利用watch函数监听reactive响应式数据  语法:watch(函数返回要监听的响应式数据, 执行的函数)
    利用watch函数深度监听整个对象  语法:watch(函数返回要监听的对象, 执行的函数, {deep:true})
    让watch函数在页面打开时先执行一次  语法:watch(..., ..., {deep:true, immediate:true})
  */

  import { ref, reactive, watch } from 'vue'

  let fullname = ref("")

  let firstname = ref("")

  let lastname = reactive({
    name:""
  })

  //watch函数监听ref数据
  watch(firstname, (newValue, oldValue)=>{
    fullname.value = newValue + lastname.name
    //console.log(oldValue + ">" + newValue)
  }) 

  //watch函数监听reactive数据
  watch(()=>{return lastname.name}, (newValue, oldValue)=>{
    fullname.value = firstname.value + newValue
    //console.log(oldValue + ">" + newValue)
  })

  //watch函数深度监听整个对象
  watch(()=>{return lastname}, (newValue, oldValue)=>{
    fullname.value = firstname.value + lastname.name
  }, {deep:true})
</script>

<template>
<div>
  姓氏: <input type="text" v-model="firstname"> <br>
  名字: <input type="text" v-model="lastname.name"> <br>
  全名: {{ fullname }}
</div>
</template>

<style scoped>
</style>

watchEffect 函数监听:

<script setup>
  /* 
    利用watchEffect可以同时监听ref和reactive数据
    任何一个响应式数据发生变化,watchEffect都可以监听到
  */

  import { ref, reactive, watchEffect } from 'vue'

  let fullname = ref("")

  let firstname = ref("")

  let lastname = reactive({
    name:""
  })

  watchEffect(()=>{
    fullname.value = firstname.value + lastname.name
  })
</script>

<template>
<div>
  姓氏: <input type="text" v-model="firstname"> <br>
  名字: <input type="text" v-model="lastname.name"> <br>
  全名: {{ fullname }}
</div>
</template>

<style scoped>
</style>

Vue 生命周期

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

浏览器解析的是 index.html 文件,当中只有一个 <div id="app"></div>。我们通过编辑 App.vue 文件来为 div 挂载组件。App.vue 文件内容会先被实例化成一个对象,当中包含了我们编写的属性和方法。在实例化成对象前后分别调用 beforeCreate 和 created 两个钩子。我们知道,index.html文件对应有一棵 DOM 树,而因为我们编写的 App.vue 文件当中实际上也包含了 html 代码,故也有一棵 DOM 树。接下来要做的,就是把 App.vue 文件中的 DOM 树挂载到 index.html 文件的 DOM 树上,挂载前后分别调用 beforeMount 和 mounted 两个钩子。当使用双向绑定等操作使得 DOM 树发生更新时,前后分别会调用 beforeUpdate 和 updated 两个钩子。当卸载部分 DOM 子树时,前后需要调用 beforeUnmount 和 unmounted 两个钩子。

实际开发当中,在进行页面更新的时候可能会使用到生命周期钩子。

组件拼接页面

在其他 .vue 文件中写好组件后,将组件导入到 App.vue 文件中进行组合,组合时可以直接使用标签名:

<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>
/* 此处定义相关css样式 */
</style>

组件传参问题

组件传参有三种方式:父传子,子传父,兄弟传参。其中,兄弟传参是由一次子传父和一次父传子组合而成,这里我们演示兄弟传参。

首先,子传父(相当于儿子发生了一个事件,然后父亲去捕捉这个事件):

<script setup>
    /* 向父组件发送参数 */

    //导入defineEmits方法
    //该方法用于定义向父组件提交数据的事件以及正式提交数据
    import { defineEmits } from 'vue';

    //定义一个向父组件提交数据的事件数组,事件名称自定义
    const emits = defineEmits(["sendMenu"])

    //提交数据的方法
    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>

父组件接收子组件的传参并传递给另一个子组件:

<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("")

  /* receiver方法把子组件的参数赋值给父组件的值 */
  function receiver(data) {
    menu.value = data
  }
</script>

<template>
<div>
<!-- 利用标签名直接使用文件 -->
  <Header class="header"></Header>
  <!-- 调用receiver方法接收子组件的传参 -->
  <Navigator @sendMenu="receiver" class="navigator"></Navigator>
  <!-- 父组件给子组件传参,利用v-bind命令 -->
  <Content class="content" :message="menu"></Content>
</div>
</template>

<style scoped>
/* css样式 */
</style>

另一个子组件接收父组件的传参:

<script setup>
    /* 接受父组件的参数 */
    //导入defineProps方法
    import { defineProps } from 'vue'

    //在definProps方法中明确数据的属性,这里是使用对象
    defineProps({
        message:String
    })
    
    //let props = defineProps(['message']),这里是使用数组

</script>

<template>
<div>
    <!-- 使用父组件传递过来的参数 -->
    {{ message }}
    <!-- {{ props.message }} -->
</div>
</template>

<style scoped>
</style>

插槽

插槽是父组件给子组件传值的一种方式,通常用来传递页面模板。

父组件需要往子组件中利用template标签绑定插槽:

<script setup>
import Son from "./Son.vue";
</script>

<template>
  <h2>Father</h2>
  <Son>
    <template v-slot:default>
      <button>click me</button>
    </template>
  </Son>
</template>

<style scoped>

</style>

子组件使用slot标签使用插槽,到时候这个插槽的位置会被替换成父组件传递过来的页面模板:

<script setup>

</script>

<template>
  <slot name="default">
    <h3>插槽默认值</h3>
  </slot>
</template>

<style scoped>

</style>

Vue3 的路由机制

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

在 Vue3 中,我们通过路由来实现页面的切换,而页面的切换往往伴随着组件的切换,即我们可以通过路由定义路径和组件之间的关系。

使用路由需要在原本项目的基础上安装一个路由依赖,使用 npm install vue-router安装路由。

路由的基本使用

首先,需要在 src 文件夹下再创建一个 routers 文件夹,专门用来存放编写路由相关的 js 文件。

接着,在routers文件夹下创建 router.js 文件,编写路由相关代码:

/* 导入.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'

/* 导入创建路由对象需要使用的函数 */
import { createRouter, createWebHashHistory } from 'vue-router'

//创建一个路由对象
const router = createRouter({
    //history属性用于记录路由的历史
    history:createWebHashHistory(),

    //routes数组用于定义多个不同的路径和组件之间的对应关系
    routes:[
        {path:"/", component:Home},  //刚进入页面时也映射到Home
        {path:"/home", component:Home},
        {path:"/add", component:Add},
        {path:"/list", component:List},
        {path:"/update/:id", component:Update} //使用:id进行参数占位
    ]
})

//向外暴露router
export { router }

在 main.js 中配置路由相关信息:

import { createApp } from 'vue'

import App from './App.vue'

//在整个App.vue中可以使用路由
import { router } from './routers/router.js'

//应用路由
createApp(App).use(router).mount('#app')

最后在 App.vue 中使用 router-view 和 router-link 标签实现页面的切换:

<script setup>
/* 
  使用路由实现页面的切换,需要使用router-view和router-link标签
*/
</script>

<template>
<div>
  <h1>hello</h1><br>
  <!-- 使用router-link实现页面切换 -->
  <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/11">update页</router-link><br>
  <!--
    在update组件中,使用{{ $route.params }}展示传递过来的参数json
  -->
    
  <hr>
    
  <!-- 该标签会被替换成具体的.vue文件 -->
  <router-view></router-view>
    
</div>
</template>

<style scoped>
</style>

此外,我们还可以指定 router-view 和具体 .vue 文件的对应关系,实际操作的时候,需要在 router-view 标签中写入 name 属性,并且在 router.js 文件中将 component 属性重写为 components 属性:

<script setup>
/* 
  使用路由实现页面的切换,需要使用router-view和router-link标签

  一个视图上是可以存在多个router-view的
  每个router-view都可以设置专门用来展示哪个组件
  但是一般来说,一个.vue文件中只需要使用一个router-view标签即可
*/
</script>

<template>
<div>
  <h1>hello</h1><br>
  <!-- 使用router-link实现页面切换 -->
  <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><br>
  <hr>
  <!-- 该标签会被替换成具体的.vue文件 -->
  default<router-view></router-view>
  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><hr>
</div>
</template>

<style scoped>
</style>
/* 导入.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'

/* 导入创建路由对象需要使用的函数 */
import { createRouter, createWebHashHistory } from 'vue-router'

//创建一个路由对象
const router = createRouter({
    //history属性用于记录路由的历史
    history:createWebHashHistory(),

    //routes数组用于定义多个不同的路径和组件之间的对应关系
    routes:[
        {path:"/", components:{default:Home, homeView:Home}},  //利用default进行不含name属性的映射
        {path:"/home", components:{homeView:Home}},
        {path:"/add", components:{addView:Add}},
        {path:"/list", components:{listView:List}},
        {path:"/update", components:{updateView:Update}},
        {path:"/showAll", components:{listView:List}}
    ]
})

//向外暴露router
export {router}

嵌套路由

当点击一个按钮后,跳转到另一个页面,另一个页面中也有嵌套组件,这个时候就需要用到嵌套路由。

import Email from "../components/Email.vue";
import Hello from "../components/Hello.vue";
import Info from "../components/Info.vue";

import { createRouter, createWebHistory } from "vue-router";

const router = createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/hello/:id',
            component: Hello,
            redirect: '/info',	// 重定向,默认重定向到这里
            // 使用children参数声明子路由
            children: [
                {
                    path: 'info',
                    component: Info
                },
                {
                    path: 'email',
                    component: Email
                }
            ]
        }
    ]
})

export { router }
<script setup>
</script>

<template>
<h2>Hello {{ $route.params.id }} </h2><br>
  <!-- 子组件需要把路径写完整 -->
  <router-link to="/hello/zhangsan/info">个人信息</router-link><br>
  <router-link to="/hello/zhangsan/email">电子邮箱</router-link><hr>
  <div>
    <router-view></router-view>
  </div>

</template>

<style scoped>
</style>

编程式路由

<script setup>
    //导入方法
    import { useRouter } from 'vue-router';
    import { ref } from 'vue'

    //调用方法返回路由对象
    const router = useRouter()  

    function showList() {
        //编程式路由实现页面跳转
        //router.push({path:"/list"})
        router.push("/list")
    }

    let myPage = ref("home")

    function goMyPage() {
        router.push("/" + myPage.value)
    }

</script>

<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><br>
    <hr>
    <!-- 编程式路由 -->
    <button @click="showList()">list</button> <br>
    <!-- 结合响应式数据进行页面跳转 -->
    <button @click="goMyPage()">Go</button> <input type="text" v-model="myPage">

    <hr>
    <router-view></router-view>
</div>
</template>

<style scoped>
</style>

路由传参

路由中的参数有:

  1. 形如 .../showDetail?key1=value1&key2=value2... 的键值对参数。
  2. 形如 .../showDetail/value1/value2/value3... 的路径参数。

路径参数

App.vue 文件如下,使用路径参数的方法编写 url,确定参数的值:

<script setup>
    import { useRouter } from 'vue-router';

    const router = useRouter()

    function showDetail(id, language) {
        router.push("/showDetail/" + id + "/" + language)
    }
    
    // 或者使用对象来进行编程式路由跳转
    function showDetail2(id, language) {
        router.push({
            name: 'showDetail',
            params: {
                id: id,
                language: language
            }
        })
    }
</script>

<template>
<div>
    <!-- router-link传参时带上参数 -->
    <router-link to="/showDetail/1/java">声明式路由传参</router-link>
    <button @click="showDetail(2, 'PHP')">编程式路由路径传参</button>
    <hr>
    <router-view></router-view>
</div>
</template>

<style scoped>
</style>

router.js 文件在 path 属性中加入参数的名称:

//导入组件
import ShowDetail from '../components/ShowDetail.vue'

//导入创建路由的相关方法
import { createRouter, createWebHashHistory } from 'vue-router'

//创建路由对象
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        //在指定url时加上参数,需要加:
        {path:"/showDetail/:id/:language", component:ShowDetail}
    ]
})

//对外暴露
export { router }

ShowDetail.vue 文件利用 useRoute 接收传递过来的参数:

<script setup>
    /* 
        接收传递过来的路径参数useRoute 
        route.params 表示路径参数
        route.query 表示键值对参数
    */
    //导入useRoute方法接收参数
    import { useRoute, onUpdated } from 'vue-router';
    import { ref } from 'vue'

    let route = useRoute()

    let id = ref(route.params.id)
    let language = ref(route.params.language)
    
    //利用onUpdated更新页面数据
    onUpdated(()=>{
        id.value = route.params.id
        language.value = route.params.language
    })

</script>

<template>
<div>
    <h1>ShowDetail接收路径参数</h1>
    <div v-text="id + '.' + language + '是世界上最好的语言'"></div>
</div>
</template>

<style scoped>
</style>

键值对参数

App.vue 文件如下,需要确定键值对的键和值:

<script setup>
    import { useRouter } from 'vue-router';

    const router = useRouter()

    function showDetail2(id, language) {
        router.push(`/showDetail2?id=${id}&language=${language}`)
        //router.push({path:'/showDetail2', query:{id:id, language:language}})
    }
    
</script>

<template>
<div>
    <!-- 键值对参数 -->
    <router-link to="/showDetail2?id=1&language=java">声明式路由键值对参数</router-link>
    <!-- routerlink的另一种写法,使得参数更加灵活 -->
    <router-link :to="{path:'/showDetail2', query:{id:3, language:'C++'}}"></router-link>
    <button @click="showDetail2(2, 'PHP')">编程式路由键值对参数</button>
    <hr>
    <router-view></router-view>
</div>
</template>

<style scoped>
</style>

router.js 文件中指定映射路径即可:

//导入组件
import ShowDetail2 from '../components/ShowDetail2.vue'

//导入创建路由的相关方法
import { createRouter, createWebHashHistory } from 'vue-router'

//创建路由对象
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {path:"/showDetail2", component:ShowDetail2}
    ]
})

//对外暴露
export { router }

showDetail2.vue 文件中需要使用 query 来接收参数:

<script setup>
    /* 
        接收传递过来的路径参数useRoute 
        route.params 表示路径参数
        route.query 表示键值对参数
    */
    //导入useRoute方法接收参数
    import { useRoute } from 'vue-router';
    import { ref, onUpdated } from 'vue'

    let route = useRoute()

    let id = ref(route.query.id)
    let language = ref(route.query.language)

    onUpdated(()=>{
        id.value = route.query.id
        language.value = route.query.language
    })

</script>

<template>
<div>
    <h1>ShowDetail接收键值对参数</h1>
    <div v-text="id + '.' + language + '是世界上最好的语言'"></div>
</div>
</template>

<style scoped>
</style>

路由守卫

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

Vue3 中提供了以下几种路由守卫:

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

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

router.js 文件如下:

//导入组件
import Home from '../components/Home.vue'
import Add from '../components/Add.vue'
import List from '../components/List.vue'
import Update from '../components/Update.vue'

//导入创建路由的相关方法
import { createRouter, createWebHashHistory } from 'vue-router'

//创建路由对象
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {path:"/", component:Home},
        {path:"/home", component:Home},
        {path:"/list", component:List},
        {path:"/add", component:Add},
        {path:"/update", component:Update},
    ]
})

//设置全局前置守卫
//beforeEach方法  每次路由切换页面前都会调用
router.beforeEach((to, from, next)=>{
    /* 
        from 表示上一个页面
        to 表示下一个页面
        next 表示放行的方法,只有执行了该方法才会放行路由
            next() 表示放行
            next("/其他路由路径") 表示路由的重定向 

        next的路由重定向可能会造成路由死循环的情况
        (假设我现在要访问hello页面,然后路由再把页面定位到hello页,就造成了死循环)
        使用的时候需要小心谨慎
        建议在if分支下编写next的路由重定向
    */

    console.log("beforeEach")
    console.log(from.path + " -> " + to.path)
    next()  //放行
})

//设置全局后置守卫
//afterEach方法  每次路由切换页面后都会调用
router.afterEach((to, from)=>{
    console.log("afterEach")
})

//对外暴露
export { router }

路由练习

以下是利用路由守卫进行登录判断的操作,登录以后才可以进入 home,否则必须进入 login。可以参考后端的检测登录方式,我们把用户的登录信息保存在浏览器的 session 会话当中(sessionStorage),然后通过查看 sessionStorage 中的用户信息存在情况来判定用户是否登录。

App.vue 文件如下:

<script setup>
</script>

<template>
<div>
  <router-view></router-view>
</div>
</template>

<style scoped>
</style>

main.js 文件如下:

import { createApp } from 'vue'

import { router } from './routers/router.js'

import App from './App.vue'

createApp(App).use(router).mount('#app')

router.js 文件如下:

import Home from '../components/Home.vue'
import Login from '../components/Login.vue'

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

const router = createRouter({
    history: createWebHashHistory(),
    routes:[
        {path:"/", component:Login},
        {path:"/home", component:Home},
        {path:"/login", component:Login}
    ]
})

//通过路由前置守卫控制登录
router.beforeEach((to, from, next)=>{
    if(to.path === "/login") {
        //判断如果要是去登录视图,直接放行
        next()
    }
    else {
        //如果是去home,那么需要在登录之后才放行
        const username = window.sessionStorage.getItem("username")
        if(null != username) {
            next()
        } else {
            //登录后就放行
            next("/login")
        }
    }
})

export { router }

Home.vue 文件如下:

<script setup>
    import { useRouter } from 'vue-router'

    let username = window.sessionStorage.getItem("username")

    const router = useRouter()

    function logout() {
        //清除sessionStorage中的用户登录信息
        window.sessionStorage.removeItem("username")
        //跳转页面
        router.push("/login")
    }
</script>

<template>
<div>
    <h1>欢迎{{ username }}登录</h1>
    <button @click="logout()">退出登录</button>
</div>
</template>

<style scoped>
</style>

Login.vue 文件如下:

<script setup>
    import { ref } from 'vue'
    import { useRouter } from 'vue-router';

    let username = ref("")
    let password = ref("")

    const router = useRouter()

    function login() {
        //获取用户名和密码并校验
        //为了方便处理,这里用户名和密码写死
        if(username.value === "root" && password.value === "123") {
            //将用户名保存到浏览器上    sessionStorage localStorage
            window.sessionStorage.setItem("username", username.value)
            //跳转页面
            router.push("/home")
        }
        else {
            alert("登录失败")
        }
    }
</script>

<template>
<div>
    账号: <input type="text" v-model="username"> <br>
    密码: <input type="password" v-model="password"> <br>
    <button @click="login()">登录</button>
</div>
</template>

<style scoped>
</style>

Promise

普通函数和回调函数

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>promise</title>
    <script>
        function fun1() {
            console.log("fun1 invoked")
        }
        console.log("code1 invoked")
        //函数的调用
        fun1()
        console.log("code2 invoked")
    </script>
</head>
<body>
</body>
</html>

回调函数:一些特殊的函数,表示未来才会执行的一些功能,后续代码不会等待该函数执行完毕就开始执行了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>promise</title>
    <script>
        //setTimeout方法参数列表中的函数便是一个回调函数
        //这个函数的后续代码不会等到该函数执行完后才执行
        console.log("code1 invoked")
        setTimeout(()=>console.log("fun2 invoked"), 2000)
        console.log("code2 invoked")
    </script>
</head>
<body>
</body>
</html>

总的来说,回调函数是一种未来会执行的函数,回调函数以外的代码会继续执行。回调函数有三种状态,一种是进行中,一种是成功,另一种是失败。不管回调函数最后执行成功与否,我们都需要准备对应的解决方案。

我们可以利用 promise 将一个函数转换为一个回调函数,并进行响应的处理工作。

Promise 简介

Promise 是前端中的异步编程技术,类似 Java 中的多线程 + 线程结果回调。它是异步编程的一种解决方案,ES6 将其写入了语言标准之中,统一了用法,原生提供了 promise 对象。

Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)、Rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。一旦状态改变,就不会再变,任何时候都可以得到这个结果,又称状态凝固。

基本语法

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>promise2</title>
    <script>
        //创建promise对象,传入函数,转化为回调函数
        let promise = new Promise((resolve, reject)=>{
            /* 
                这里的代码便是回调函数,其中:
                resolve是函数   在回调函数中如果调用该方法,状态会变为成功
                reject也是函数  在回调函数中如果调用该方法,状态会变为失败
                
                let promise = Promise.resolve("haha")	快速产生一个成功状态的promise
            */
            console.log("function invoked")
            //调用resolve或者reject方法的时候还可以传参
            resolve("haha")
            //reject()
        })

        console.log("other code1")

        //then当中的代码会专门等待promise的回调函数状态发生改变后才执行 
        promise.then(
            //如果resolve方法传参了,可以在这里接收参数
            (data)=>{   //第一个方法参数是promise转化为成功状态后执行的函数
                console.log("promise success")
                console.log(data)
            },
            ()=>{   //第二个方法参数是promise转化为失败状态后执行的函数
                console.log("promise fail")
            }
        ).catch((data)=>{
            //then也会返回一个promise,而catch函数当中的方法是当promise成功失败状态出现异常时执行
            //可以将promise处理失败的代码放在catch当中
            //并且catch同样可以接收promise传过来的参数
        })

        console.log("other code2")
        
    </script>
</head>
<body>
</body>
</html>

async 和 await 的使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>async</title>
    <script>
    
        /* async 帮助我们使用简洁的语法获得一个promise对象
            原生写法: let promise = new Promise((resolve, reject)=>{})
            async写法: async function 函数名() {}
                1.async 用于标识函数的,标识这个函数是回调函数 
                2.方法如果正常return结果,promise状态就是resolve,即状态成功
                3.方法中如果出现异常,则状态失败
                4.async函数返回的结果如果是一个promise,则状态由内部的promise决定
        */
     
        async function fun1() {
            //return 10
            throw new Error("something wrong")
        }

        let promise = fun1()

        promise.then(
            function(value) {   //返回值在这里接收
                console.log("success: " + value)
            }
        ).catch(
            function() {
                console.log("fail")
            }
        )

    </script>
</head>
<body>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>async</title>
    <script>
    
        /* await 帮助我们获取promise成功状态的返回值的关键字
            1.await右边如果是一个普通值,则直接返回该值 let res = await "张三"
            2.如果右边是一个promise,则返回promise成功状态的结果 
                let res = await Promise.resovle("aaa")
                let res = "aaa"
            3.如果右边是一个失败状态的promise,那么await会直接抛异常
            4.await关键字必须在async修饰的函数中使用,async函数中可以没有await
            5.await后边的代码会等待await执行完毕继续运行
        */
    
        async function fun1() {
            return 10
        }

        async function fun2() {
            let promise = fun1()    //fun1被async修饰,返回一个promise对象
            let res = await promise //await必须在async修饰下的函数中使用

            //awati代替then拿到promise的返回值,是10
            console.log("await got " + res)
        }

        //调用fun2函数
        fun2()

    </script>
</head>
<body>  
</body>
</html>

Axios

项目正式编写前需要运行 npm install axios 安装 Axios 依赖。

基本使用

随机土味情话网站:https://api.uomg.com/api/rand.qinghua?format=json

API 测试网站:https://httpbin.org

接下来使用 Axios 请求后台获取土味情话并打印到网页上。

<script setup>
  //默认导入axios
  import axios from 'axios'
  import { ref } from 'vue'

  let message = ref("")

  //使用axios发送请求给后台获取信息
  async function change() {
    /* 
      设置请求参数
        1.请求的资源(url) url
        2.请求的方式  method
        3.请求的参数(键值对类型、json类型...) data
          如果请求方式是get,且使用params,则params中的数据会以键值对的形式放在url后
          如果请求方式是post,且使用data,则data中的数据会以json串的形式放入请求体中
    */
    let promise = axios({
      method:"get",
      url:"https://api.uomg.com/api/rand.qinghua?format=json",
      data:{},
      parmas:{username:"zhangsan"}
    })

    //使用await接收promise的返回值
    let res = await promise
    console.log(res)

    /* 
      res响应结果对象
        data 服务端响应回来的数据
        status 响应状态码 200
        statusText 响应状态描述 OK
        headers 本次响应的所有响应头
        config 本次请求的配置信息
        request 本次请求发送时所使用的XMLHttpRequest对象
    */

    message.value = res.data.content
  } 
</script>

<template>
<div>
  <h1>{{ message }}</h1>    
  <button @click="change()">刷新</button>
</div>
</template>

<style scoped>
</style>

get 和 post 方法

<script setup>
  //默认导入axios
  import axios from 'axios'
  import { ref } from 'vue'

  let message = ref("")

  function init() {
    //发送get请求的方法
    //axios.get(url, {请求的配置信息})
    //axios.get(url, {params:{键值对参数}, header:{设置一些特殊的请求头}})
    return axios.get(
      "https://api.uomg.com/api/rand.qinghua?format=json", {
        params: {
          username:"zhangsan",
          userPwd:"123"
        },
        headers:{
          Accept:"application/json, text/plain, text/html, */*"
        }
      }
    )
  }

  function init2() {
    //发送post请求的方法
    //axios.post(url, {要放入请求体中的json串}, {请求的配置信息})
    return axios.post(
      "https://api.uomg.com/api/rand.qinghua", 
      {
        username:"lisi",
        userPwd:"456"
      },
      {
        params:{format:"json"},
        headers:{Accept:"application/json, text/plain, text/html, */*"}
      }
    )
  }

  async function change() {
    //let promise = init()
    //let res = await promise
    //let data = res.data

    //使用解构表达式直接拿出data属性
    let { data } = await init()
    message.value = data.content
  }
    
  // 另一种写法(常用)
  const userRegisterService = (registerData) => {
    // 借助于UrlSearchParams完成传递
    const params = new URLSearchParams()
    for (let key in registerData) {
        params.append(key, registerData[key])
    }
    return instance.post('/user/register', params)
  }
</script>

<template>
<div>
  <h1>{{ message }}</h1>    
  <button @click="change()">刷新</button>
</div>
</template>

<style scoped>
</style>

基本配置

axios 是用于异步请求的。其中,我们需要编写请求地址,但是我们发现网址的前半段是重复的,所以我们可以将这一部分抽取出来,配置到 axios 中。

创建 axios.js 文件:

import axios from 'axios'

//使用axios函数创建一个可以发送请求的实例对象
const instance = axios.create({
    //设置请求的基础路径,以后的请求会在前面自动拼上这一串
    baseURL:"https://api.uomg.com",
    //超时时间,如果超过这个时间服务器没响应,则报错
    timeout:10000
})

//对外暴露instance,在App.vue中利用instance调用get、post方法
export { instance }

请求和响应拦截器

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

当浏览器利用 axios 发送请求到服务端时,会经过请求拦截器,请求拦截器中有两个函数。其中一个是当请求无误时用来配置请求的信息的,另一个时当请求有误时返回异常的。

当服务端处理完信息把结果响应给客户但时,要经过响应拦截器,响应拦截器中也有两个函数。其中一个处理状态码为 200 时要执行的函数,另一个处理状态码不是 200 时要执行的函数。

创建新的 request.js 文件,在里面配置拦截器数据:

import axios from 'axios'

const instance = axios.create({
    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("请求前拦截器异常方法")
        //返回失败状态的promise
        return Promise.reject("something wrong")
    }
)

//设置响应拦截器,并设置其中两个函数
instance.interceptors.response.use(
    (response)=>{
        //处理响应数据,最终需要返回response
        console.log("响应前的拦截器")
        return response
    },
    (error)=>{
        console.log("响应失败的拦截器")
        console.log(error)
        //最终需要返回响应失败的promise
        return Promise.reject("something wrong")
    }
)

export { instance }

浏览器同源禁止策略

当前端服务器和后端服务器不相同,并且前端需要发送请求给后端,让后端响应数据时,对于浏览器而言,请求来自于前端服务器,数据来自于后端服务器,浏览器会认为该数据不安全,会报错,这个就叫浏览器同源禁止策略。

一种解决方案是利用代理模式,即让前端服务器自己去后端服务器拉取数据然后再传递给浏览器。这样做的优点是代码稳健性强,但缺点便是会给前端服务器造成太大压力,性能较差。

另一种解决方案是让浏览器先给后端服务器发出一个预检请求,确认可以安全跨域。当后端向浏览器响应回可以跨域的信息时,客户端再正式发一个请求过去拿去数据。值得注意的是,预检请求在第一次发出之后可以不在接下来的一段时间重复发送,我们可以设置一个时间,超过了这个时间之后再去发一次预检请求。这样一来,我们需要在浏览器和后端服务器之间放置一个跨域过滤器,用于判断请求信息是否为跨域请求。

以下代码了解即可,实际开发当中跨域问题的处理在框架里只需要写注解即可。

package cn.hnu.schedule.filter;

import cn.hnu.schedule.common.Result;
import cn.hnu.schedule.util.WebUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
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;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        //设置响应头
        //允许任何域进行跨域请求
        response.setHeader("Access-Control-Allow-Origin", "*");
        //允许POST, GET等请求方式进行跨域
        response.setHeader("Access-Control-Allow-Methods",
                "POST, GET, PUT, OPTIONS, DELETE, HEAD");
        //预检请求的有效时间是3600s
        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")) {
            //向浏览器写出json串
            WebUtil.writeJson(response, Result.ok(null));
        } else {
            //非预检请求,放行即可
            filterChain.doFilter(servletRequest, servletResponse);
        }

    }
}

Pinia

当我们有多个组件共享一个共同状态(数据源)时,多个视图可能都依赖于同一份状态。来自不同视图的交互也可能需要更改同一份状态。虽然我们的手动状态管理解决方案(prop,组件间通信,模块化)在简单场景已经足够了,但是在大规模的生产应用中还有很多其他事项需要考虑:例如更强的团队协作约定,于 Vue DevTools 集成,包括时间轴,组件内部审查和时间旅行调试,模块热更新,服务端渲染支持等。Pinia 就是一个实现了上述需求的状态管理库,由 Vue 核心团队维护,对 Vue2 和 Vue3 可用。

在实际开发当中,我们可以利用 Pinia 定义多个公共的共享数据,默认均为响应式数据,这样一来,不同组件就可以使用这些响应式数据了(有点类似于后端的 Session 域对象)。但是光靠 Pinia 本身是无法对数据进行持久化存储的,意味着假设利用 Pinia 进行存储,在浏览器上刷新之后这些存储的数据就会被清空掉。故我们还需要结合 SessionStorage 和 LocalStorage 解决上述无法持久化存储的问题。

基本使用

开始创建项目之前我们需要执行 npm install pinia 命令安装对应依赖。

在 src 中创建一个 store 目录,然后创建一个 store.js 文件:

/* 定义共享的pinia数据 */
import { defineStore } from 'pinia'

//定义一个person对外共享
export const definedPerson = defineStore({
    id:"personPinia",  //当前数据的id必须全局唯一,意味着其他store文件里面不能有id重复
    state:()=>{//表示状态,其实就是响应式数据,return里面的数据才是最终要使用的数据
        return {username:"zhangsan", age:18, hobbies:["唱歌", "跳舞"]}
    },   
    getters:{
        //类似于javabean中的getter,不过最终是以属性值的方式呈现出来
        getAge:(state)=>{return state.age},	//如果是箭头函数,无法使用this的情况下可以使用state
        getHobbyCount() {return this.hobbies.length}
    },
    actions:{
        //类似于javabean中的setter
        doubleAge() {this.age *= 2}
    }
})

在 main.js 中需要使用 pinia:

import { createApp } from 'vue'

import App from './App.vue'

import { router } from './routers/router.js'

//开启全局的pinia功能
import { createPinia } from 'pinia'
let pinia = createPinia()

//使用pinia
createApp(App).use(router).use(pinia).mount('#app')

在组件 .vue 文件中使用 pinia 中定义的数据:

<script setup>
  //导入pinia数据
  import { definedPerson } from '../store/store.js'
  //获得共享数据
  let person = definedPerson()
</script>

<template>
<div>
    <h1>用于修改pinia数据</h1>
    {{ person }}
</div>
</template>

<style scoped>
</style>

setup 语法糖

上面的基本写法真的是太折磨人了,写起来很别扭,所以,利用 setup 语法糖可以让代码更加简洁。在 setup 语法糖中:ref 就是 state 属性;computed 计算属性就是 getters;function 就是 actions。

import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const useMoneyStore = defineStore('money', ()=> {
    // 属性
    const money = ref(100)
    // getters
    const rmb = computed(() => money.value)
    const usd = computed(() => money.value * 0.14)
    const eur = computed(() => money.value * 0.13)

    // setters
    const win = (arg) => {
        money.value += arg
    }
    const pay = (arg) => {
        money.value -= arg
    }

    // 返回给外界使用
    return {money , rmb, usd, eur, win, pay}
}, 
{
    persist: true	// 持久化存储(见pinia持久化一小节)
})

在路由文件中使用

在路由文件中使用 Pinia,我们需要对外提供一个 pinia 对象,然后使用这个对象在路由文件中创建对应的共享数据。

在 src 目录中创建 pinia.js 文件,对外暴露 pinia 对象:

//开启pinia
import { createPinia } from 'pinia'

let pinia = createPinia()

export default pinia

路由文件中使用 pinia 对象来创建共享数据:

//...

//导入pinia相关文件和函数
import pinia from '../pinia.js'
import { defineUser } from '../store/userStore.js'

//创建共享数据时需要额外填入pinia对象
let sysUser = defineUser(pinia)

const router = createRouter({
   //...
})
export { router }

常见 API

<script setup>
  //导入pinia数据
  import { definedPerson } from '../store/store.js'
  //获得共享数据
  let person = definedPerson()
</script>

<template>
<div>
    <h1>用于修改pinia数据</h1>
    名字: <input type="text" v-model="person.username"><br>
    年龄: <input type="text" v-model="person.age"><br>
    爱好: <br>
        <input type="checkbox" value="吃饭" v-model="person.hobbies">吃饭<br>
        <input type="checkbox" value="睡觉" v-model="person.hobbies">睡觉<br>
        <input type="checkbox" value="唱歌" v-model="person.hobbies">唱歌<br>
        <input type="checkbox" value="跳舞" v-model="person.hobbies">跳舞<br>
        <input type="checkbox" value="rap" v-model="person.hobbies">rap<br>
    <button @click="person.doubleAge()">年龄加倍</button>
    <!-- 调用$reset方法恢复默认值 -->
    <button @click="person.$reset()">恢复默认值</button>
    <!-- 调用$patch方法一次性修改多个属性值 -->
    <button @click="person.$patch({username:'热心市民灰灰', age:19, hobbies:['写代码']})">变身</button>
    <hr>
    {{ person }}
</div>
</template>

<style scoped>
</style>

pinia 持久化

Pinia 默认是内存存储,当刷新浏览器的时候会丢失数据。我们可以使用插件 Persist 将 pinia 中的数据持久化的存储起来。

安装 Persist 插件:npm install pinia-persistedstate-plugin。

接下来,在 pinia 中使用插件(main.js 文件中进行编码):

import { createPersistedState } from "pinia-persistedstate-plugin"

const pinia = createPinia()
const persist = createPersistedState()

pinia.use(persist)
app.use(pinia)

然后定义状态 store 时指定持久化配置参数:

export const useTokenStore = defineStore('token', ()=> {
    // 定义状态内容
    // 响应式变量
    const token = ref('')

    // setters
    const setToken = (newToken) => {
        token.value = newToken
    }
    const removeToken = () => {
        token.value = ''
    }

    return {token, setToken, removeToken}
}, 
{
    persist: true	// 持久化存储
})

Element-plus

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

Element Plus 目前已经推出了大量的常用 UI 组件,如按钮、表单、表格、对话框、选项卡等,此外还提供了一些高级组件,如日期选择器、时间选择器、级联选择器、滑块、颜色选择器等。这些组件具有一致的设计和可靠的代码质量,可以为开发者提供稳定的使用体验。官网戳我。

入门案例

在使用前需要利用 npm install element-plus 命令安装对应依赖。

main.ts 配置如下:

import { createApp } from 'vue'

//导入element-plus相关内容
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

import App from './App.vue'

//使用ElementPlus
createApp(App).use(ElementPlus).mount('#app')

接下来,在组件文件中,我们只需要去网站上面复制相关源代码即可。较常用的组件包括但不限于:按钮、图标、提示框、导航、标签页、输入框、单选框、复选框、下拉框、日期选择器、表单、对话框、分页、表格……

其他组件库

Ant Designed Vue:https://www.antdv.com/

Naive UI:www.naiveui.com

Apache ECharts

Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。

官方地址戳我。

前后端分离总结

初始环境准备

在 src 目录下,asserts 目录是用来存放静态资源的(包括 css 样式,所需要的图片资源等);新建 api 目录用来封装各种请求函数;新建 util 目录用来存放 reqeust.js(配置 axios 的拦截器);新建 views 目录用来存放各种页面。

表单提交

前端经常使用到的就是表单提交,在使用 Element-Plus 组件的前提下,用 el-form 进行表单处理。在使用表单的时候,需要结合接口文档,先声明数据模型。例如,注册页面的所需要的数据模型如下:

// 定义数据模型
const registerData = ref({
  username: '',
  password: '',
  rePassword: ''
})

然后不要忘了根据数据模型中的各种参数,进行表单参数校验:

// 自定义校验规则
const validatePass = (rule, value, callback) => {
  if (value === '') {
    callback(new Error('请再次输入密码'))
  }
  else if (value !== registerData.value.password) {
    callback(new Error('两次密码输入不一致'))
  }
  else {
    callback()
  }
}

// 定义表单校验规则
const rules = {
  username: [
    {required: true, message: '请输入用户名', trigger: 'blur'},
    {min: 5, max: 16, message: '长度为5-16非空字符', trigger: 'blur'}
  ],
  password: [
    {required: true, message: '请输入密码', trigger: 'blur'},
    {min: 5, max: 16, message: '长度为5-16非空字符', trigger: 'blur'}
  ],
  rePassword: [
    {validator: validatePass, trigger: 'blur'}
  ]
}

跨域问题

由于浏览器的同源策略限制,向不同源(不同协议、不同域名、不同端口)发送 ajax 请求失败。前端服务端口设置在 5173,后端服务端口设置在 8080。当浏览器启动服务时,先向 5173 请求一个注册页面,当我们点击注册按钮时,5173 端口发送 ajax 请求向 8080 请求服务,这个时候对于浏览器来讲就是不同源了。

跨域问题一般使用配置代理解决。即我们发送请求依旧是向 5173,然后再让 5173 把请求转发到 8080。注意:跨域是针对浏览器而言的,只有浏览器才有同源禁止策略。

在 reqeust.js 中更改 baseURL 地址:

const instance = axios.create({
    // baseURL: 'http://localhost:8023',
    // 更改baseURL
    baseURL: '/api',
    timeout: 10000
})

然后更改 vite.config.js 文件进行请求转发:

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    proxy: {
      // 获取路径中包含/api的请求
      '/api': {
        target: 'http://localhost:8023',  // 后台服务所在的源
        changeOrigin: true,               // 修改源
        rewrite: (path) => path.replace(/^\/api/, '') // 将/api替换为空字符串
      }
    }
  }
})

优化 axios 拦截器

我们在实现各种服务接口的时候,我们经常需要去处理响应回来的数据。并且,在发送请求的时候,我们常常需要携带 token。结合上述的跨域问题,我们不妨直接配置一个 axios 拦截器,帮助我们进行数据的处理:

request.js 如下:

import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useTokenStore } from "@/stores/token.js";

import router from '@/router/router.js'	// 注意这里的导入方式,不能使用useRouter

const instance = axios.create({
    // baseURL: 'http://localhost:8023',
    // 更改baseURL
    baseURL: '/api',
    timeout: 10000
})

// 设置响应拦截器
instance.interceptors.response.use(
    // 判断业务状态码
    response => {
        
        // 如果响应是文件流,直接返回响应
        if (response.config.responseType === 'blob') {
            return response;
        }
        
        // 操作成功
        if (response.data.code === 0) {
            return response.data
        }

        // 操作失败
        // alert(response.data.message ? response.data.message : '服务异常')
        ElMessage({
            message: response.data.message ? response.data.message : '服务异常',
            type: 'error',
            plain: true,
            showClose: true
        })
        return Promise.reject(response.data)
    },
    error => {
        // 判断响应状态码如果为401,则证明未登录
        if (error.response.status === 401) {
            ElMessage({
                message: '请先登录',
                type: 'error',
                plain: true,
                showClose: true
            })
            router.push('/login')
        }
        else {
            ElMessage({
                message: '服务异常',
                type: 'error',
                plain: true,
                showClose: true
            })
        }
        return Promise.reject(error)
    }
)

// 设置请求拦截器
instance.interceptors.request.use(
    // 请求前的回调
    (config) => {
        // 添加token
        const tokenStore = useTokenStore()
        if (tokenStore.token) { // 如果有token则添加token
            config.headers.Authorization = tokenStore.token
        }
        return config
    },
    // 请求错误的回调
    (error) => {
        Promise.reject(error)
    }
)

export default instance

需要准备好 pinia 状态管理库,对 token 进行管理:

import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const useTokenStore = defineStore('token', ()=> {
    // 定义状态内容
    // 响应式变量
    const token = ref('')

    // setters
    const setToken = (newToken) => {
        token.value = newToken
    }
    const removeToken = () => {
        token.value = ''
    }

    return {token, setToken, removeToken}
})

服务接口函数只需要接收然后弹出提示即可:

const tokenStore = useTokenStore()
// ---登录表单---
// 复用注册表单的数据模型
const login = async () => {
  let result = await userLoginService(registerData.value)
  // 设置token
  tokenStore.setToken(result.data)
  // alert(result.message ? result.message : '登录成功')
  ElMessage({
    message: result.message ? result.message : '登录成功',
    type: 'success',
    plain: true,
    showClose: true
  })
  router.push('/')
}

而其他的请求也仅仅只是直接发送即可,没必要再额外向请求头中添加 token 信息了:

// 文章分类列表查询
export const articleCategoryListService = () => {
    return instance.get('/category')
}

文章作者: 热心市民灰灰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 热心市民灰灰 !
  目录