1. 快速入门
1.1. 为什么是vue3?
在企业中,常用的前端框架主要包括 React、Vue.js和 Angular。这几个框架各有特点,但都非常流行,并且得到了广泛的企业应用:
-
Vue.js:
-
- 推荐理由:Vue.js 的设计注重简单性和易用性,对初学者非常友好。它的文档清晰,易于理解,而且中文社区活跃,中文资料丰富,对于中文母语者来说,学习起来会更加便利。在中国企业中更常用。
- 适合人群:适合那些希望快速看到成果、喜欢渐进式学习和逐步深入学习的初学者。
-
React:
-
- 推荐理由:React 是目前就业市场上需求最高的前端框架之一,它强调组件化和函数式编程的概念。学习 React 可以帮助你建立强大的组件设计和状态管理的思维方式。
- 适合人群:适合那些对前端开发有热情、愿意投入时间深入学习并希望在未来有更多就业机会的初学者。
-
Angular:
-
- 推荐理由:Angular 是一个由 Google 支持的完整框架,它提供了端到端的解决方案,包括工具、强大的功能集和最佳实践。学习 Angular 可以让你对前端开发的各个方面有更全面的了解。
- 适合人群:适合那些希望在一个严格和结构化的框架中工作,并且不介意较陡峭学习曲线的初学者。
其中,vue是中国企业主流,react是国外主流。
1.2. 如何学习vue3?
基础知识:
- 确保你熟悉 HTML、CSS 和 JavaScript 基础。
- 确保安装了nodejs,并了解npm的基本操作
- 了解js es6常用语法糖
参考教程:
vue3教程
- 官网:https://vuejs.org/
- 中文官网:https://cn.vuejs.org/
- 中文官网vue3:https://v3.cn.vuejs.org/
- 菜鸟教程:https://www.runoob.com/vue3/vue3-tutorial.html
- W3CSchool: https://www.w3cschool.cn/vuejs3/
视频教程推荐(有时效性,参考最新播放量高的即可)
1.3. vue3包括哪些知识?
1.4. 使用什么软件?
你可以使用vscode编写大多数前端代码,包括vue。但是需要安装插件,如果使用其编写vue3,需要安装:
插件名 | 功能 |
---|---|
Vue office | Vue3.0 语法支持 |
unocss | unocss for vscode |
Iconify IntelliSense | Iconify 预览和搜索 |
Prettier | 代码格式化 |
ESLint | 脚本代码检查 |
git graph | git上传 |
Chinese (Simplified) (简体中文) Language | 翻译为中文 |
Error Lens | 错误提示 |
codegeex或其他ai编码插件 | 代码提示,ai对话,提升效率 |
除了vscode,你还可以使用IDEA,webstorm,HBuilderX等软件。
软件名 | 优 | 劣 |
---|---|---|
vscode | 免费,自定义性强,插件市场丰富 | 自定义太强,对于初学者配置过于麻烦 |
idea | 适合同时学习java后端的全栈学习者,无需安装多余软件,插件市场丰富 | 需要激活 |
webstorm | 和idea是同一家,插件市场丰富 | 需要激活 |
hbuilder | 方便后面继续学习uniapp | 插件少 |
此外,给浏览器安装一个vue.js devtools,方便调试。
1.5. nodejs和npm
1.5.1. 安装Node.js
- 安装了15.0以上的Node.js,如果你还没安装node.js,请自行搜索相关教程进行安装。
- 安装node.js可以使用其自带的npm包管理工具,后续你还会发现其更大的用处。
在命令行下查看node.js版本
node -v
1.5.2. npm包管理工具
常用指令:
# 安装pakage.json中所有依赖
npm install
# 安装指定依赖
npm i 依赖
# 运行脚本
npm run 脚本
依赖可以理解为html阶段导入的第三方包
使用国内镜像
- 直接使用npm下载速度很慢,可以使用镜像进行加速
- 常用国内镜像如下:
npm 官方原始镜像网址是:https://registry.npmjs.org/
淘宝 NPM 镜像:https://www.npmmirror.com/
阿里云 NPM 镜像:https://npm.aliyun.com
腾讯云 NPM 镜像:https://mirrors.cloud.tencent.com/npm/
华为云 NPM 镜像:https://mirrors.huaweicloud.com/repository/npm/
网易 NPM 镜像:https://mirrors.163.com/npm/
中科院大学开源镜像站:http://mirrors.ustc.edu.cn/
清华大学开源镜像站:https://mirrors.tuna.tsinghua.edu.cn/
腾讯,华为,阿里的镜像站基本上比较全
- 使用镜像下载
npm install -g 要下载的依赖 --registry=镜像网址
例如:
npm install -g @vue/cli --registry=https://www.npmmirror.com/
- 或者设置全局镜像,设置后可以直接下载依赖
npm config set registry 镜像地址
例如:
npm config set registry https://www.npmmirror.com/
npm cache clean -f
npm install -g @vue/cli
- 查看镜像是否配置成功
npm config get registry
pnpm
你简单理解pnpm为npm的升级版,npm的语法在pnpm中基本都可以使用,且pnpm有更多简写方法,且打包速度更快
安装(需要管理员权限):
npm install -g pnpm
or
npm i -g pnpm
查看版本
# 查看版本
pnpm -v
# 升级
pnpm add -g pnpm to update
配置镜像源:
# 获取当前配置的镜像地址
pnpm get registry
or
pnpm config get registry
# 设置新的镜像地址
pnpm set registry https://registry.npmmirror.com
常用命令(示例)
# 运行脚本
pnpm run dev
or
pnpm dev
# 安装依赖
pnpm install axios
or
pnpm i axios
1.6. 第一个vue3程序-HelloWorld
- vue.js作为一个js库,可以直接在html中引入使用,也可以创建一个vue工程让webpack等打包工具编译为html
- 实际开发中当然是创建vue工程,直接引入使用只需了解
1.6.1. 直接使用vue*
- 创建.html文件
- 引入vue.js脚本(使用网络CDN或自己下载)
- 在body标签中使用vue语法
hello.html代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World</title>
<!--引入vue.js脚本-->
<script src="https://cdn.staticfile.org/vue/3.0.5/vue.global.js"></script>
<style>
/* 定义根元素的样式 */
#root {}
</style>
</head>
<body>
<div id="root">
</div>
<!-- 在script里写vue代码 -->
<script>
// 创建一个根组件,在vue3中组件就是一个普通的js对象
const Root = {
data() {
return {
msg: '-by FL' //data方法返回的对象,其中的属性会自动添加到组件实例中
}
},
// 在模板中需要通过{{属性名}}的模板语法访问组件中的属性
template: "<h1>Hello World!!!<small>{{msg}}</small></h1>" //定义组件模板,希望在网页中显示什么
}
// 创建vue实例app
const app = Vue.createApp(Root)
// 在使用 mount() 挂载应用时,该组件被用作渲染的起点。
app.mount('#root') //将实例app挂载到页面中的#root元素上
</script>
</body>
</html>
1.6.2. Vue Cli创建项目*
介绍:
-
官方提供构建工具,通常称为脚手架
-
已经停止维护,不推荐使用
-
安装
# 全局安装 (推荐)
npm install -g @vue/cli
# 项目(局部)安装
npm install @vue/cli
## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
## 安装或者升级你的@vue/cli
npm install -g @vue/cli
使用:
- 创建vue create 项目名创建项目
选择第三个,进行手动创建
注意:
- 如果是直接使用命令行,必须使用管理员权限打开
- 如果使用vscode,需要用管理员权限打开,并将shell的当前执行策略改为RemoteSigned
(1)以管理员身份运行VSCode
(2)执行命令:get-ExecutionPolicy(取得shell的当前执行策略)
显示Restricted(表示状态是禁止的),如果显示RemoteSigned就不需要更改
(3)执行命令:set-ExecutionPolicy RemoteSigned
(4)执行命令:get-ExecutionPolicy,显示RemoteSigned
- 选择特点
将Linter取消(按空格取消),初学者不需要,其他依赖可以后面再在package.json添加。
- 选择vue版本(根据需要选择),我们选择vue3
- 选择依赖文件位置(package.json即可)
- 是否要将前面的设置设为快照(N)
- 选择包管理工具(yarn/pnpm/npm)
初学者使用npm就行,后续可以根据自己需求选择。
- 等待创建成功
记住三个指令:
cd 项目名 # 跳转到项目目录下
npm run serve # 运行项目
npm install # 安装依赖
点击查看官方文档
备注:目前vue-cli
已处于维护模式,官方推荐基于 Vite
创建项目。
1.6.3. 基于 vite 创建(推荐)
1.6.3.1. 介绍
vite是新一代前端构建工具,vite的优势如下:
- 启动速度比vue cli快得多
- 对 TypeScript、JSX、CSS 等支持开箱即用。
- 有更多的社区支持(例如很多第三方组件库都有vite相关配置指南)
1.6.3.2. 使用
初学时什么都无需添加
## 创建命令
npm create vite@latest
# 或通过附加的命令行选项直接指定项目名称和你想要使用的模板
npm create vite@latest my-vue-app --template vue
## 具体配置
## 配置项目名称
√ Project name: vue3_test
## 是否添加TypeScript支持
√ Add TypeScript? No
## 是否添加JSX支持
√ Add JSX Support? No
## 是否添加路由环境
√ Add Vue Router for Single Page Application development? No
## 是否添加pinia状态管理环境
√ Add Pinia for state management? No
## 是否添加单元测试
√ Add Vitest for Unit Testing? No
## 是否添加端到端测试方案
√ Add an End-to-End Testing Solution? » No
## 是否添加ESLint语法检查
√ Add ESLint for code quality? No
## 是否添加Prettiert代码格式化
√ Add Prettier for code formatting? No
使用vite创建项目后,必须安装依赖才可以启动:
npm i
vite项目的启动方式默认为:
npm run dev
可以在package.json查看启动脚本:
1.7. HelloWorld项目分析
1.7.1. 项目结构
初始:
完整(仅为推荐结构):
my-vue3-project/
|-- node_modules/ # 存放项目依赖的第三方包
|-- public/ # 存放不会经过Webpack处理的静态资源文件,如index.html、favicon.ico等
| |-- vite.svg # 页面图标(浏览器栏显示的小图标)
|-- src/ # 源代码目录
| |-- assets/ # 存放静态资源,如图片、样式、字体等
| |-- components/ # 存放可复用的Vue组件
| |-- views/ # 存放页面级别的Vue组件
| |-- App.vue # 根组件,所有页面都在这个组件内渲染
| |-- main.js # 入口文件,用于创建Vue实例并挂在到#app
| |-- router/ # 存放Vue Router相关配置
| | |-- index.js # 路由配置文件
| |-- store/ # 存放中状态管理相关文件
| | |-- modules/ # 各个模块的状态
| | |-- index.js # 状态管理配置文件
| |-- api/ # 存放与后端接口交互的代码
| |-- utils/ # 存放工具函数
| |-- styles/ # 存放样式文件,如全局样式、变量等
| |-- layouts/ # 存放布局组件
| |-- types/ # 存放ts类型(如果使用ts)
| |-- services/ # 存放服务层的代码,如API调用
| |-- directives/ # 存放自定义指令
|-- .gitignore # 指定Git将忽略的文件和目录
|-- index.html # 主页面HTML文件
|-- package.json # 项目描述文件,包含依赖、脚本等信息
|-- README.md # 项目说明文档
|-- vite.config.js # vite配置文件(如果使用vite)
其他可以自己添加的文件:
src/constants/:存放常量文件,如API地址、默认配置等。
src/theme/:存放项目的主题样式,如颜色、字体等。src/enums/:存放枚举类型定义。
src/locales/:存放国际化(i18n)的翻译文件。src/tests/:存放单元测试和集成测试文件。
src/vendors/:存放第三方库或插件的定制代码。.env、.env.local等:环境变量配置文件,用于不同环境下的配置管理。
1.7.2. 常用文件
1.7.2.1. App.vue
App.vue 是 Vue 3 项目的根组件,它是整个应用程序的入口组件。通常,你会在这里定义应用的布局和结构。App.vue 文件通常包含三个部分:模板(template)、逻辑(script)和样式(style)。
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Vite + Vue" />
</template>
<style scoped>
.logo {
height: 6em;
padding: 5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
- 模板(template):使用 HTML-like 语法定义组件的结构。在这里,你可以使用 Vue 的指令,如 v-bind、v-model 等。
- 逻辑(script):使用 JavaScript-like 语法,通常在这里导入其他组件,定义组件的数据、方法、计算属性、生命周期钩子等。
- 样式(style):定义组件的样式。你可以使用 CSS,也可以使用预处理器如 SCSS 或 LESS。
1.7.2.2. package.json
package.json 是一个 JSON 文件,位于项目的根目录中,它定义了项目所需的依赖项、脚本、配置和其他元数据。这个文件是由 npm(Node Package Manager)管理的,用于帮助管理和组织项目。
{
"name": "hello",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.10"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.4",
"vite": "^5.4.8"
}
}
- name:项目的名称。
- version:项目的版本号。
- scripts:定义了一组可以运行的 npm 脚本,如 serve 和 build。
- dependencies:项目运行时所需的依赖项。
- devDependencies:项目开发时所需的依赖项,如构建工具、测试框架等
1.7.2.3. main.js
main.js 是 Vue 3 项目的入口文件,它负责创建 Vue 应用实例并挂载到 DOM 元素上。在这个文件中,你会初始化 Vue 应用,配置全局属性,比如全局组件、插件等。
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
- createApp:Vue 3 提供的函数,用于创建一个新的 Vue 应用实例。
- App:导入的 App.vue 根组件。
- router:Vue Router 实例,用于管理应用的路由。.use(router) 是用来安装 Vue Router 插件的。
- mount:将 Vue 应用实例挂载到 HTML 中的一个元素上,这里是 #app。
2. Vue基础
2.1. vue标签
2.1.1. 模板<template>
- Vue 3 中的
<template>
标签是一个用于包裹多个子节点的容器,它本身不会渲染任何 DOM 元素,而是用于组织和管理组件的结构。在 Vue 3 中,<template>
标签提供了一些新的特性和改进,使得它在构建复杂的组件时更加灵活和高效。 - 在vue3中,只有
<template>
标签是必须的,其他标签可以省略 - 在
<template>
标签内部,必须有一个且只能有一个根元素。这意味着你不能有多个并列的顶级元素。 - 在
<template>
标签内部,你可以使用任何的html标签和自定义标签(即组件)
2.1.1.1. 模板语法
在 <template>
标签中,可以使用多种模板语法,这里只介绍最常用的插值语法{{}}
,其他模板语法后面介绍。
下面示例中,使用{{ }}
双大括号来绑定了文本内容。
<template>
<div>{{ message }}</div>
</template>
<script setup>
const message = 'Hello, Vue 3!'
</script>
2.1.1.2. 响应式状态
- 在vue中,我们如果想要一个变量变化后重新渲染,就不能使用普通变量,而是使用响应式状态
- 当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。
<script setup>
const count =0
function increment() {
count++
console.log(count)//count变化,而视图不会重新渲染
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
console.log(count)//count变化,而视图会重新渲染
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
2.1.2. 逻辑script
用于定义组件的脚本部分。它通常包含以下内容:
- 导出默认对象: 使用 export default 语句来定义组件的选项,如 data, methods, computed, props, setup 等。
- 定义组件数据: 通过 data 函数返回一个对象,该对象包含所有组件实例的数据。
- 声明方法: 在 methods 对象中定义函数,这些函数可以操作数据并处理用户输入。
- 计算属性: computed 用于声明那些其值取决于其他数据属性的响应式数据。
- 生命周期钩子: Vue 提供了一系列生命周期钩子函数,如 mounted, created, updated 等,用于在组件的不同生命周期阶段执行代码。
2.1.2.1. 两种API
Vue.js 3.0 引入了许多新特性和优化,其中包括对 Composition API (组合式 API)的添加,它作为新的核心特性与原有的 Options API(选项式 API ) 一起工作。
- 选项式 API 是 Vue.js 传统的组件写法,适合简单的组件和初学者。选项式 API 的优点是结构清晰,易于理解和上手。但是,当组件变得复杂时,相关的逻辑会分散在不同的选项中,使得代码组织和重用变得困难。
- 组合式 API 是 Vue 3 引入的新特性,提供了更灵活的代码组织和重用方式,可以更容易地将相关的逻辑组合在一起,并且可以跨组件重用这些逻辑。这对于编写大型和复杂的组件尤其有用。
开发者可以根据自己的需求选择使用 Options API 或 Composition API,两者可以共存于同一个组件中。这使得开发者可以渐进式地升级现有项目到 Vue 3,同时享受 Composition API 带来的灵活性。
2.1.2.2. 选项式API*
选项式 API 是 Vue.js 从一开始就有的组件写法。在这种写法中,你通过填充 (options) 对象的方式来描述组件的逻辑,这个对象包含了如 data, methods, props 等属性和生命周期钩子。每个属性都对应组件的一个方面。
选项式 API在vue2,vue1版本常用,vue3基本不用。我们只需了解,根据工作或学习要求使用vue2是再复习一下即可。
2.1.2.2.1. 属性
一个组件的代码被组织为一系列选项,如 data、methods、computed、watch、lifecycle hooks 等。每个选项都有其特定的用途
data
- data是一个函数,返回一个对象,该对象包含Vue实例的所有响应式数据。该对象会被vue所代理。
- Vue会自动将data中的属性转换为响应式的,当数据变化时,视图会自动更新。
- data的属性必须通过return语句返回
示例(在App.vue添加以下代码):
<template>
<h1>{{msg}}</h1>
<hr />
<h2>全民制作人大家好,我是{{stu.name}},喜欢{{stu.hobby.hobby1}},{{stu.hobby.hobby2}},{{stu.hobby.hobby3}},{{stu.hobby.hobby4}}
</h2>
</template>
<script>
export default {
data() {
return {
msg: "两年半",
stu: {
name: "cxk",
age: 18,
gender: "女",
hobby: {
hobby1: "唱",
hobby2: "跳",
hobby3: "rap",
hobby4: "篮球",
}
}
}
}
}
</script>
<style>
</style>
效果如下:
methods
- methods包含一系列方法,这些方法可以在模板中通过指令如v-on来调用,也可以在组件的实例中通过this来调用。
- 这些方法中的this上下文指向当前的Vue实例。
<template>
<h1>{{ count }}</h1>
<hr />
<!-- 在模板中绑定事件处理器时,如果方法后面跟有括号,该表达式会被当作 JavaScript 表达式计算,这意味着 increament() 会被立即调用,而不是作为一个函数被引用 -->
<button @click="increament">+1</button>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increament() {
console.log(this.count) // 使用 this 引用 count
this.count++; // 使用 this 更新 count 的值
}
}
}
</script>
<style>
</style>
computed
计算属性和侦听器都是响应式数据变化的利器,但它们的使用场景有所不同。
- 计算属性适用于同步操作,当需要根据一个或多个数据属性计算出一个新值时,计算属性是最佳选择。
- 而侦听器适用于异步操作或复杂逻辑,当你需要在数据变化时执行副作用(如Ajax请求、数据存储等)时,侦听器更为合适。
- computed包含一系列计算属性,这些属性是基于它们的依赖进行缓存的。
- 只有当依赖的数据发生变化时,计算属性会重新计算,即computed会对数据进行缓存。而method每次组件重新渲染都会调用。
- 计算属性默认只有getter,但也可以提供setter。尽量只使用computed读取属性(getter),不要用它修改属性(setter),例如像method中修改count的值。
- 计算属性是通过函数返回结果的,所以在computed中可以添加各种逻辑。这是computed与data最大的不同。
<template>
<h1>你的成绩是{{ count }},{{rank}}</h1>
<hr />
<!-- 在模板中绑定事件处理器时,如果方法后面跟有括号,该表达式会被当作 JavaScript 表达式计算,这意味着 increament() 会被立即调用,而不是作为一个函数被引用 -->
<button @click="increament">+10</button>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increament() {
console.log(this.count)
this.count += 10;
}
},
computed: {
rank() {
if (this.count >= 80) {
return "优秀";
} else if (this.count >= 60) {
return "及格";
} else {
return "不及格"
}
}
}
}
</script>
<style>
</style>
侦听器
侦听器
用于观察和响应Vue实例上的数据变动。当你有一些数据需要随着其它数据变动而变动时,或者需要在数据变化时执行异步或开销较大的操作时,侦听器非常有用。
基本用法:
export default {
data() {
return {
question: '',
answer: 'I cannot give you an answer until you ask a question!'
};
},
watch: {
// 监听question的变化
question(newQuestion, oldQuestion) {
if (newQuestion.indexOf('?') > -1) {
this.getAnswer();
}
}
},
methods: {
getAnswer() {
// 异步操作或开销较大的操作
}
}
};
深度侦听和立即执行:
export default {
data() {
return {
obj: {
key: 'value'
}
};
},
watch: {
obj: {
//使用deep: true可以侦听对象内部值的变化,immediate: true可以让侦听器立即执行一次。
deep: true, // 深度侦听
immediate: true, // 立即执行
handler(newValue, oldValue) {
// 处理变化
}
}
}
};
- props:
- props用于定义组件可以接收的外部参数。
- 父组件可以通过属性的方式将数据传递给子组件。
- emits:
- emits是一个包含所有父组件可以监听的事件名称的数组。
- 它用于定义组件可以触发的事件,以便父组件可以监听这些事件。
2.1.2.2.2. 生命周期钩子
- 生命周期钩子(如 created、mounted、updated、destroyed)用于在组件的不同生命周期阶段执行代码。
- 这些钩子函数在组件的不同生命周期阶段被调用,用于执行特定的操作。
常用生命周期钩子:
-
beforeMount:
-
- 在组件挂载到DOM之前调用。此时,模板已经编译完成,但是尚未挂载到页面上。
- 可以在这个钩子中进行一些数据请求或者最后的准备工作。
-
mounted:
-
- 在组件挂载到DOM之后调用。此时,组件已经创建好了DOM结构,可以访问到DOM元素。
- 可以在这个钩子中执行依赖于DOM的操作,如集成第三方库或初始化页面。
-
beforeUpdate:
-
- 在组件数据更新之前调用,但是仅仅在数据变化时调用。
- 可以在这个钩子中执行一些逻辑,如手动移除已添加的事件监听器。
-
updated:
-
- 在组件数据更新之后调用,同样仅仅在数据变化时调用。
- 可以在这个钩子中执行依赖于更新后的DOM的操作。
-
beforeUnmount:
-
- 在组件卸载之前调用,此时实例仍然完全可用。
- 可以在这个钩子中执行一些清理工作,如取消定时器、解绑事件监听器。
-
unmounted:
-
- 在组件卸载之后调用。当这个钩子被调用时,组件的所有指令都被解绑,所有的事件监听器都被移除。
- 可以在这个钩子中执行最终的清理工作。
<template>
<div id="app">
{{ message }}
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello, Vue!'
};
},
beforeMount() {
console.log('beforeMount: 组件挂载之前');
},
mounted() {
console.log('mounted: 组件挂载之后');
},
beforeUpdate() {
console.log('beforeUpdate: 数据更新之前');
},
updated() {
console.log('updated: 数据更新之后');
},
beforeUnmount() {
console.log('beforeUnmount: 组件卸载之前');
},
unmounted() {
console.log('unmounted: 组件卸载之后');
}
};
</script>
2.1.2.3. 组合式API
- 组合式API (Composition API)是我们使用vue3主要的API风格。
- 组合式 API 的核心是 setup 函数,它在组件创建之前执行,允许开发者定义组件的响应式数据和函数。setup 函数返回的对象中的属性和方法可以在组件的模板中使用。
- 组合式 API 的优点是可以按逻辑组织代码,而不是按选项类型。这有助于提高代码的可读性和可维护性,尤其是在处理复杂组件时。
- 它还提供了更好的类型支持,使得与TypeScript集成更加容易。
- Composition API 提供了一系列生命周期钩子函数,如 onMounted, onUpdated 等,它们与 Options API 中的生命周期钩子相对应。
2.1.2.3.1. 传统的 setup 选项
-
setup是一个新的组件选项,它是一个在组件创建之前执行的函数。
-
在setup中,你可以定义响应式数据、计算属性、侦听器、生命周期钩子等。
-
setup函数中定义的属性和函数可以在模板中直接使用。
-
优势:
-
- 明确返回值:setup 函数需要明确返回一个对象,这个对象中的属性和方法将会暴露给模板使用。这种方式使得数据的来源和使用更加清晰。
- 更好的类型支持:对于使用 TypeScript 的开发者来说,这种方式可以提供更好的类型支持,因为返回对象的类型可以很容易地被推导和验证。
-
劣势:
-
- 稍微繁琐:需要显式地定义和返回所有需要在模板中使用的属性和方法,对于简单的组件来说,这可能显得有些繁琐。
<template>
<h1>{{msg}}</h1>
<hr />
<h2>全民制作人大家好,我是{{stu.name}},喜欢{{stu.hobby.hobby1}},{{stu.hobby.hobby2}},{{stu.hobby.hobby3}},{{stu.hobby.hobby4}}
</h2>
</template>
<script>
import {
reactive
} from "vue"
export default {
setup() {
let msg = "两年半";
let stu = reactive({
name: "cxk",
age: 18,
gender: "女",
hobby: {
hobby1: "唱",
hobby2: "跳",
hobby3: "rap",
hobby4: "篮球",
}
});
return {//需要通过return暴露,否则外部无法访问
msg,
stu
}
}
}
</script>
2.1.2.3.2. <script setup>
{{msg}}
全民制作人大家好,我是{{stu.name}},喜欢{{stu.hobby.hobby1}},{{stu.hobby.hobby2}},{{stu.hobby.hobby3}},{{stu.hobby.hobby4}}
```2.1.2.3.3. 响应式代理
-
响应式代理是 Vue 3 中实现响应式数据的基础,它通过代理对象的方式,使得数据的读取和修改能够触发视图的自动更新,从而实现了数据与界面的双向绑定。
-
reactive()
-
- 作用:定义一个响应式对象(基本类型不要用它,要用ref,否则报错)
- **语法:**let 响应式对象= reactive(源对象)。
- **返回值:**一个Proxy的实例对象,简称:响应式对象。
- **注意点:**reactive定义的响应式数据是“深层次”的。
<template>
<div class="person">
<h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
<h2>游戏列表:</h2>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
<h2>测试:{{obj.a.b.c.d}}</h2>
<button @click="changeCarPrice">修改汽车价格</button>
<button @click="changeFirstGame">修改第一游戏</button>
<button @click="test">测试</button>
</div>
</template>
<script setup name="Person">
import { reactive } from 'vue'
// 数据
let car = reactive({ brand: '奔驰', price: 100 })
let games = reactive([
{ id: 'ahsgdyfa01', name: '英雄联盟' },
{ id: 'ahsgdyfa02', name: '王者荣耀' },
{ id: 'ahsgdyfa03', name: '原神' }
])
let obj = reactive({
a:{
b:{
c:{
d:666
}
}
}
})
function changeCarPrice() {
car.price += 10
}
function changeFirstGame() {
games[0].name = '流星蝴蝶剑'
}
function test(){
obj.a.b.c.d = 999
}
</script>
-
ref()
: -
- **作用:**定义响应式变量。
- 语法:
let xxx = ref(初始值)
。 - **返回值:**一个
RefImpl
的实例对象,简称ref对象
或ref
,ref
对象的value
属性是响应式的。 - 注意点:
-
-
- JS中操作数据需要:xxx.value,但模板中不需要.value,直接使用即可。
- 对于let name = ref(‘张三’)来说,name不是响应式的,name.value是响应式的。
- ref接收的数据可以是:基本类型、对象类型。
-
<template>
<div class="person">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">年龄+1</button>
<button @click="showTel">点我查看联系方式</button>
</div>
</template>
<script setup name="Person">
import {ref} from 'vue'
// name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的。
let name = ref('张三')
let age = ref(18)
// tel就是一个普通的字符串,不是响应式的
let tel = '13888888888'
function changeName(){
// JS中操作ref对象时候需要.value
name.value = '李四'
console.log(name.value)
// 注意:name不是响应式的,name.value是响应式的,所以如下代码并不会引起页面的更新。
// name = ref('zhang-san')
}
function changeAge(){
// JS中操作ref对象时候需要.value
age.value += 1
console.log(age.value)
}
function showTel(){
alert(tel)
}
</script>
- 使用原则:
- 若需要一个基本类型的响应式数据,必须使用
ref
。 - 若需要一个响应式对象,层级不深,
ref
、reactive
都可以。 - 若需要一个响应式对象,且层级较深,推荐使用
reactive
。
-
toRefs 与 toRef
-
- 作用:将一个响应式对象中的每一个属性,转换为ref对象。
- 备注:toRefs与toRef功能一致,但toRefs可以批量转换。
<template>
<div class="person">
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<h2>性别:{{person.gender}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeGender">修改性别</button>
</div>
</template>
<script setup name="Person">
import {ref,reactive,toRefs,toRef} from 'vue'
// 数据
let person = reactive({name:'张三', age:18, gender:'男'})
// 通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
let {name,gender} = toRefs(person)
// 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力
let age = toRef(person,'age')
// 方法
function changeName(){
name.value += '~'
}
function changeAge(){
age.value += 1
}
function changeGender(){
gender.value = '女'
}
</script>
2.1.2.3.4. 计算属性
作用:根据已有数据计算出新数据(和Vue2中的computed作用一致)。
<template>
<div class="person">
姓:<input type="text" v-model="firstName"> <br>
名:<input type="text" v-model="lastName"> <br>
全名:<span>{{fullName}}</span> <br>
<button @click="changeFullName">全名改为:li-si</button>
</div>
</template>
<script setup name="Person">
import {ref,computed} from 'vue'
let firstName = ref('zhang')
let lastName = ref('san')
// 计算属性——只读取,不修改
/* let fullName = computed(()=>{
return firstName.value + '-' + lastName.value
}) */
// 计算属性——既读取又修改
let fullName = computed({
// 读取
get(){
return firstName.value + '-' + lastName.value
},
// 修改
set(val){
console.log('有人修改了fullName',val)
firstName.value = val.split('-')[0]
lastName.value = val.split('-')[1]
}
})
function changeFullName(){
fullName.value = 'li-si'
}
</script>
2.1.2.3.5. 监听器
- 作用:监视数据的变化(和
Vue2
中的watch
作用一致) - 特点:
Vue3
中的watch
只能监视以下四种数据:
ref
定义的数据。reactive
定义的数据。- 函数返回一个值(
getter
函数)。 - 一个包含上述内容的数组。
- 监视ref定义的【基本类型】数据:直接写数据名即可,监视的是其value值的改变。
<template>
<div class="person">
<h1>情况一:监视【ref】定义的【基本类型】数据</h1>
<h2>当前求和为:{{sum}}</h2>
<button @click="changeSum">点我sum+1</button>
</div>
</template>
<script setup>
import {ref,watch} from 'vue'
// 数据
let sum = ref(0)
// 方法
function changeSum(){
sum.value += 1
}
// 监视,情况一:监视【ref】定义的【基本类型】数据
const stopWatch = watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
if(newValue >= 10){
stopWatch()
}
})
</script>
- 监视
ref
定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。注意:
- 若修改的是
ref
定义的对象中的属性,newValue
和oldValue
都是新值,因为它们是同一个对象。 - 若修改整个
ref
定义的对象,newValue
是新值,oldValue
是旧值,因为不是同一个对象了。
<template>
<div class="person">
<h1>情况二:监视【ref】定义的【对象类型】数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changePerson">修改整个人</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,watch} from 'vue'
// 数据
let person = ref({
name:'张三',
age:18
})
// 方法
function changeName(){
person.value.name += '~'
}
function changeAge(){
person.value.age += 1
}
function changePerson(){
person.value = {name:'李四',age:90}
}
/*
监视,情况一:监视【ref】定义的【对象类型】数据,监视的是对象的地址值,若想监视对象内部属性的变化,需要手动开启深度监视
watch的第一个参数是:被监视的数据
watch的第二个参数是:监视的回调
watch的第三个参数是:配置对象(deep、immediate等等.....)
*/
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
},{deep:true})
</script>
- 监视reactive定义的【对象类型】数据,且默认开启了深度监视。
<template>
<div class="person">
<h1>情况三:监视【reactive】定义的【对象类型】数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changePerson">修改整个人</button>
<hr>
<h2>测试:{{obj.a.b.c}}</h2>
<button @click="test">修改obj.a.b.c</button>
</div>
</template>
<script setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18
})
let obj = reactive({
a:{
b:{
c:666
}
}
})
// 方法
function changeName(){
person.name += '~'
}
function changeAge(){
person.age += 1
}
function changePerson(){
Object.assign(person,{name:'李四',age:80})
}
function test(){
obj.a.b.c = 888
}
// 监视,情况三:监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
})
watch(obj,(newValue,oldValue)=>{
console.log('Obj变化了',newValue,oldValue)
})
</script>
- 监视ref或reactive定义的【对象类型】数据中的某个属性,注意点如下:
-
- 若该属性值不是【对象类型】,需要写成函数形式。
- 若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。结论:监视的要是对象里的属性,那么最好写函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。
<template>
<div class="person">
<h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeC1">修改第一台车</button>
<button @click="changeC2">修改第二台车</button>
<button @click="changeCar">修改整个车</button>
</div>
</template>
<script setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18,
car:{
c1:'奔驰',
c2:'宝马'
}
})
// 方法
function changeName(){
person.name += '~'
}
function changeAge(){
person.age += 1
}
function changeC1(){
person.car.c1 = '奥迪'
}
function changeC2(){
person.car.c2 = '大众'
}
function changeCar(){
person.car = {c1:'雅迪',c2:'爱玛'}
}
// 监视,情况四:监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式
/* watch(()=> person.name,(newValue,oldValue)=>{
console.log('person.name变化了',newValue,oldValue)
}) */
// 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数
watch(()=>person.car,(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
watchEffect
- 官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。
- watch对比watchEffect
-
- 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
- watch:要明确指出监视的数据
- watchEffect:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。
<template>
<div class="person">
<h1>需求:水温达到50℃,或水位达到20cm,则联系服务器</h1>
<h2 id="demo">水温:{{temp}}</h2>
<h2>水位:{{height}}</h2>
<button @click="changePrice">水温+1</button>
<button @click="changeSum">水位+10</button>
</div>
</template>
<script setup name="Person">
import {ref,watch,watchEffect} from 'vue'
// 数据
let temp = ref(0)
let height = ref(0)
// 方法
function changePrice(){
temp.value += 10
}
function changeSum(){
height.value += 1
}
// 用watch实现,需要明确的指出要监视:temp、height
watch([temp,height],(value)=>{
// 从value中获取最新的temp值、height值
const [newTemp,newHeight] = value
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if(newTemp >= 50 || newHeight >= 20){
console.log('联系服务器')
}
})
// 用watchEffect实现,不用
const stopWtach = watchEffect(()=>{
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if(temp.value >= 50 || height.value >= 20){
console.log(document.getElementById('demo')?.innerText)
console.log('联系服务器')
}
// 水温达到100,或水位达到50,取消监视
if(temp.value === 100 || height.value === 50){
console.log('清理了')
stopWtach()
}
})
</script>
2.1.3. 3. 样式<style>
我们可以在vue的 <style>
标签中,编写css样式,与html+css一致。如果你想让css更优雅,可以使用scss或less等预编译器。scss或less等预编译器不难学习,可以看看我后续的笔记。链接
2.1.3.1. <style scoped>
- 如果使用
<style>
,设置的是全局样式。为了防止样式冲突,我们通常使用<style scoped>
。 - 如果想在
<style scoped>
中设置全局样式,可以使用:deep(过滤器)
设置,例如:#app :deep(h2)
就是设置app下的所有h2 - 你还可以使用
:global(过滤器)
设置全局样式 - 你也可以在根组件中既使用
<style>
又使用<style scoped>
示例1(使用style):
<!-- App.vue -->
<template>
<A />
</template>
<script setup>
import A from './components/A.vue'
</script>
<style>
.sa {
color: blue;
background-color: #bfa;
}
</style>
<!-- A.vue -->
<script setup>
import {
ref
}
from 'vue'
const url = 'https://vuejs.org/'
const imgSrc = '/images/kun.png' //使用/public/images/kun.png无法访问,因为images前的/就是根目录public
const attrs = ref({
src: '/images/kun.png',
alt: 'kun plus',
style: 'width: 200px;'
})
</script>
<template>
<h2 class="sa">超链接:<a v-bind:href="url">Visit Vue.js</a></h2>
<img :=attrs />
</template>
示例2(使用style scoped):
<!-- App.vue -->
<template>
<A />
</template>
<script setup>
import A from './components/A.vue'
</script>
<style scope>
.sa {
color: blue;
background-color: #bfa;
}
</style>
<!-- A.vue -->
<script setup>
import {
ref
}
from 'vue'
const url = 'https://vuejs.org/'
const imgSrc = '/images/kun.png' //使用/public/images/kun.png无法访问,因为images前的/就是根目录public
const attrs = ref({
src: '/images/kun.png',
alt: 'kun plus',
style: 'width: 200px;'
})
</script>
<template>
<h2 class="sa">超链接:<a v-bind:href="url">Visit Vue.js</a></h2>
<img :=attrs />
</template>
- 注意,如果示例2
<template>
中只有一个根(标签),那么它也会变色。
当我们在组件中使用scoped样式时,vue会自动为组件中的所有元素生成一个随机的属性形如:data-v-7a7a37b1
生成后,所有的选择器都会在最后添加一个[data-v-7a7a37b1]
h1一>h1[data-v-7a7a37b11
.box1一>.box1[data-v-7a7a37b1]
注意:
- 随机生成的属性,除了会添加到当前组件内的所有元素上也会添加到当前组件引入的其他组件的根元素上
- 这样设计是为了,可以通过父组件来为子组件设置一些样式
2.1.3.2. <style module>
在 Vue 3 中,<style module>
是一种特殊的样式封装方式,它允许你将 CSS 类名模块化,以避免全局样式的冲突。当你在一个 <style>
标签上使用 module
属性时,所有的 CSS 类名都只在当前组件内部有效,并且每个类名都会被编译成一个唯一的字符串,以避免在不同的组件或页面中重复。使用 <style module>
的好处包括:
- 局部作用域:样式只在当前组件内部有效,不会影响到其他组件或全局样式。
- 避免样式冲突:由于类名是唯一的,因此不会出现不同组件间样式相互覆盖的问题。
- 易于维护:模块化的样式使得组件更加自包含,便于理解和维护。
- 更好的重用性:可以在不同的组件中重用相同的 CSS 类名,而不用担心冲突。
- 动态样式:可以通过 JavaScript 动态绑定样式类,实现更加灵活的样式管理。使用
<style module>
的基本语法如下:
<template>
<div :class="$style.red">This text is red.</div>
</template>
<style module>
.red {
color: red;
}
</style>
在模板中,你需要使用 $style
对象来引用模块化的类名。$style
对象是由 Vue 自动创建的,它包含了所有在 <style module>
中定义的类名。如果你想要给模块化的类名添加前缀,以避免潜在的冲突,你可以在 module
属性中指定一个前缀:
<style module="myPrefix">
.red {
color: red;
}
</style>
然后在模板中使用时,你需要引用带有前缀的样式对象:
2.1.3.3. 区别
2.2. 模板语法
Vue 3的模板语法是为了将渲染的DOM与Vue实例的数据属性保持同步。它包括了一系列的指令和特殊的语法,用于模板的渲染和处理。使用这些语法和指令,可以创建动态的、响应式的用户界面。
2.2.1. 插值
{{ }}
:双大括号用于文本插值,可以将数据绑定到模板的文本内容中。例如:<span>{{ message }}</span>
,其中message
是Vue实例中的一个属性。注意,只能写表达式,不能使用语句。- 除了组件中的变量,vue还为我们提供了一系列全局变量可以访问,如:Data、Math、RegExp等。
- 我们也可以在main.js中声明全局变量,如
app.config.globalProperties.变量="值"
,这样就可以在任一组件中访问
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello, Vue 3!'
};
}
};
</script>
2.2.2. v-text
v-text
用于更新元素的textContent
,类似于{{ }}
插值,但是v-text
会替换整个元素的内容。
<template>
<p v-text="textContent"></p>
</template>
<script>
export default {
data() {
return {
textContent: 'This is some text.'
};
}
};
</script>
v-html*
v-html
用于更新元素的innerHTML
,它可以将HTML字符串插入到DOM中,但是需要注意避免XSS攻击,尽量别用。
<template>
<div v-html="htmlContent"></div>
</template>
<script>
export default {
data() {
return {
htmlContent: '<strong>This is bold text.</strong>'
};
}
};
</script>
2.2.3. v-bind
v-bind
用于绑定属性值,可以简写为:
。例如:<a v-bind:href="url">Link</a>
或<a :href="url">Link</a>
,其中url
是Vue实例中的一个属性。/
代表静态资源根目录public- 可以直接通过v-bind为一个标签绑定多个属性
<script setup>
const url = 'https://vuejs.org/'
const imgSrc = '/images/kun.png' //使用/public/images/kun.png无法访问,因为images前的/就是根目录public
const attrs = ref({
src: '/images/kun.png',
alt: 'kun plus',
style: 'width: 200px;'
})
</script>
<template>
<h2>超链接:<a v-bind:href="url">Visit Vue.js</a></h2>
<img :src="imgSrc" alt="kun kun" style="width: 200px;" />
<!-- 直接绑定多个属性 -->
<img :=attrs />
</template>
练习:使用v-bind切换图片
<script setup>
import {
ref
} from 'vue'
// 方式1
const imgSrc = ref('/images/kun.png')
const changeImg = () => {
imgSrc.value = '/images/kunpng'
}
// 方式2
const attrs = ref({
src: '/images/kun.png',
alt: 'kun plus',
style: 'width: 200px;'
})
const changeImg2 = () => {
attrs.value.src = '/images/kunpng'
}
</script>
<template>
<button type="button" @click="changeImg">切换图片1</button>
<img :src="imgSrc" alt="kun kun" style="width: 200px;" />
<br />
<button type="button" @click="changeImg2">切换图片2</button>
<img :=attrs />
</template>
2.2.4. v-for
v-for
用于基于源数据多次渲染一个元素或模板块。例如:<li v-for="item in items">{{ item.text }}</li>
,其中items
是一个数组,每个数组项都会渲染一个<li>
元素。
<template>
<ul>
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' }
]
};
}
};
</script>
2.2.5. v-if,v-else-if,v-else
- 这些指令用于条件渲染。
v-if
用于条件性地渲染一块内容,如果条件为false
,这块内容则不会渲染(直接移除)。v-else-if
和v-else
用于提供额外的条件渲染选项。 v-if
可以配合<template>
使用
<template>
<div v-if="type === 'A'">Type A</div>
<div v-else-if="type === 'B'">Type B</div>
<div v-else>Unknown Type</div>
</template>
<script>
export default {
data() {
return {
type: 'A'
};
}
};
</script>
2.2.6. v-show
v-show
也是用于条件渲染,与v-if
直接移除元素不同的是,v-show
的元素始终会被渲染并保留在DOM中,它只是切换元素的显示和隐藏(display)。- 切换性能比
v-if
高;但是需要对所有使用组件进行初始化,即使暂时不显示。
<template>
<div v-show="isVisible">Visible</div>
</template>
<script>
export default {
data() {
return {
isVisible: true
};
}
};
</script>
其他:
- v-on用于事件绑定,简写为
@
。例如:@click
,就是绑定了单击事件 - v-model用于双向数据绑定,后面会详细讲解
2.3. 事件处理
- 在Vue 3中,事件处理是组件与用户交互的重要部分,它允许开发者创建与用户交互的动态应用程序。
- 事件通常与用户交互相关联,比如点击按钮、输入文本等,这些事件是浏览器原生支持的。
- Vue提供了多种方式来绑定和处理事件,可以使用
v-on
绑定DOM事件,使用内联事件处理器或方法事件处理器处理事件。
2.3.1.1. 绑定事件
v-on用于绑定DOM事件,可以简写为
@。例如:
v-on:事件名或
@事件名
2.3.1.2. 事件处理器
2.3.1.2.1. 内联事件处理器
事件触发时,立即执行。例如:
<script setup>
import {
ref
} from "vue";
const count = ref(0);
</script>
<template>
<h2>count = {{count}}</h2>
<button @click="count++">+1</button>
</template>
2.3.1.2.2. 方法事件处理器
事件触发时,vue自动调用事件绑定的函数。例如:
<script setup>
function handleClick() {
alert("按钮被点击...");
}
</script>
<template>
<button @click="handleClick">方法事件</button>
<button @click="handleClick()">内联事件</button>
</template>
2.3.1.2.3. 区别
- 内联事件处理器的参数由我们自己传递,你想传递什么参数都行。
$event
就是事件对象,我们可以自己传递。 - 方法事件处理器的参数由vue自动传递,传递的参数为DOM事件对象,该对象包含事件触发时的相关信息。
<script setup>
function handleClick(...args) {
console.log(args);
}
</script>
<template>
<button @click="handleClick">方法事件</button>
<button @click="handleClick(1,2,'Hello', $event)">内联事件</button>
</template>
2.3.1.3. 事件修饰符
Vue还提供了一些事件修饰符,用于在v-on
指令中绑定事件监听器时控制事件的行为。这些修饰符包括:
.stop
:阻止事件冒泡,但是不停止事件的捕获。
<script setup>
function boxHandle(box) {
console.log(box);
}
</script>
<template>
<!-- 修改为@click.stop可以解决事件冒泡 -->
<div class="box1" @click="boxHandle('box1')">
box1
<div class="box2" @click="boxHandle('box2')">
box2
<div class="box3" @click="boxHandle('box3')">
box3
</div>
</div>
</div>
</template>
<style scoped>
.box1 {
width: 400px;
height: 400px;
background-color: red;
}
.box2 {
width: 300px;
height: 300px;
background-color: yellow;
}
.box3 {
width: 200px;
height: 200px;
background-color: greenyellow;
}
</style>
.prevent
:阻止事件默认行为。例如:
<!-- `handleClick`方法会被调用,但是按钮的默认点击行为会被阻止 -->
<button v-on:click.prevent="handleClick">Click me</button>
<!-- 简写 -->
<button @click="handleClick">Click me</button>
其他事件修饰符:
.capture
:事件侦听器会绑定到事件冒泡的起点。.self
:只当事件是从侦听器绑定的元素自身触发时才触发回调。.once
:事件侦听器在触发一次后会被移除。.passive
:告诉浏览器这个事件侦听器不会阻止事件的默认行为。
2.3.1.4. 自定义事件
- vue3支持自定义事件,这与原生 DOM 事件(如点击、鼠标移动等)类似,但它是 Vue 实例上的自定义事件,可以由开发者根据需要定义和触发。
自定义事件的常见用途包括:
- 父组件监听子组件发出的事件。
- 子组件向父组件传递数据或通知状态变化。
- 兄弟组件之间的通信,通过一个共同的父组件作为中介。
- 跨组件层级的事件传递,例如祖孙组件之间的通信。
2.4. 数据绑定
2.4.1. 单向数据绑定
假设a数据单向绑定了b数据,那么a数据的改变会影响b数据,但是b数据的改变不会影响a数据
<script setup>
import {
ref
} from 'vue';
const text = ref("");
function submitHandler() {
console.log(text);
}
</script>
<template>
<form @submit.prevent="submitHandler">
<div><input type="text" @input="(event) => (text = event.target.value)" /></div>
<div><input type="text" @input="(event) => (text = event.target.value)" /></div>
<div><button>提交</button></div>
</form>
</template>
2.4.2. 双向数据绑定
假设a数据与b数据双向绑定,那么改变任意一个数据,另外一个数据也会随之改变
<script setup>
import {
ref
} from 'vue';
const text = ref("");
function submitHandler() {
console.log(text);//双向绑定
}
</script>
<template>
<form @submit.prevent="submitHandler">
<div><input type="text" @input="(event) => (text = event.target.value)" :value="text" /></div>
<div><input type="text" @input="(event) => (text = event.target.value)" :value="text" /></div>
<div><button>提交</button></div>
</form>
</template>
2.4.3. v-model
前面的双向数据是我们手动进行的,比较麻烦。vue为我们提供了v-model
进行双向数据绑定。
<script setup>
import {
ref
} from 'vue';
const text = ref("");
function submitHandler() {
console.log(text); //双向绑定
}
</script>
<template>
<form @submit.prevent="submitHandler">
<!-- 手动双向绑定 -->
<div><input type="text" @input="(event) => (text = event.target.value)" :value="text" /></div>
<!-- v-model双向绑定 -->
<div><input type="text" v-model="text" /></div>
<div><button>提交</button></div>
</form>
</template>
常用表单的双向绑定:
<script setup>
import {
ref
} from "vue"
const bool = ref("否");
const hobbise = ref([])
const gender = ref('男')
const countries = ref("")
</script>
<template>
<form>
<!-- 多选 -->
<div>是否?<input type="checkbox" v-model="bool" true-value="是" false-value="否" /></div>
<hr>
<div>
<h3>爱好</h3>
<input type="checkbox" v-model="hobbise" name="bobby" value="唱歌" />唱歌
<input type="checkbox" v-model="hobbise" name="bobby" value="跳舞" />跳舞
<input type="checkbox" v-model="hobbise" name="bobby" value="rap" />rap
<input type="checkbox" v-model="hobbise" name="bobby" value="篮球" />篮球
</div>
<hr>
<!-- 单选 -->
<div>
性别
<input v-model="gender" type="radio" name="gender" value="男" />男
<input v-model="gender" type="radio" name="gender" value="女" />女
</div>
<hr>
<!-- 下拉列表 -->
<div>
<h3>国家</h3>
<select v-model="countries">
<option disabled value="">请选择你的阵营...</option>
<option value="中国">中国</option>
<option value="米国">米国</option>
<option value="小日本">小日本</option>
</select>
</div>
<hr />
</form>
</template>
2.4.4. v-model的修饰符
.lazy
使用change来处理数据,只有当绑定的数据失去焦点时才改变.trim
去除前后的空格.number
将数据转换为数值,如果不是数值就不会转换。
<script setup>
import {
ref
} from 'vue';
const text = ref("");
</script>
<template>
<form>
<div>输入信息1:<input type="text" v-model="text" /><button>提交</button></div>
<div>输入信息2:<input type="text" v-model.lazy="text" /><button>提交</button></div>
<div>输入信息3:<input type="text" v-model.trim="text" /><button>提交</button></div>
<div>输入信息4:<input type="text" v-model.number="text" /><button>提交</button></div>
<div>输入信息5:<input type="text" v-model.lazy.trim="text" /><button>提交</button></div>
</form>
</template>
3. Vue组件
- 在 Vue.js 中,组件是构建用户界面的基本单元。可以将一个页面分解为多个小的组件,每个组件负责一个具体的功能。而每个组件都是一个单独的、可重用的、可组合的模块。
- 每个组件都有其自己的模板
template
、样式style
和逻辑script
,这种模块化的方法使得代码更加组织化、可维护和可重用。
3.1. Vue 组件的基本概念
- Vue 组件是一个包含模板、逻辑和样式的结构,你可以理解为一个自定义的 HTML 元素。
- 学习Vue,必须逐步树立组件化开发的思想,将页面分解为小的、可复用的组件。
3.2. Vue 组件的创建
在 Vue 3 中,创建一个组件通常涉及以下几个步骤:
- 定义组件: 使用单文件组件(.vue 文件)来定义组件。
- 注册组件: 在父组件中注册子组件,这样就可以在父组件的模板中使用子组件。
- 使用组件: 在父组件的模板中,通过自定义元素的方式使用子组件。
3.3. 组件注册
在 Vue 中,组件在使用之前必须注册。注册组件有两种方式:全局注册和局部注册。
3.3.1. 全局注册*
- 我们可以在入口文件main.js进行全局注册。全局注册的组件可以在整个应用程序中的任何组件模板内使用,不需要在每个父组件中单独导入和注册。
- 这使得全局注册的组件特别适合那些在整个应用程序中频繁使用的通用组件,如按钮、输入框等。
- 然而,全局注册的组件也意味着即使它们在某个特定页面或组件中未被使用,它们也会被包含在最终构建的代码中,这可能会导致应用程序的大小增加。因此,对于不经常使用的组件,建议使用局部注册。
import { createApp } from 'vue';
import ExampleComponent from './ExampleComponent.vue';//注意路径
const app = createApp({});
app.component('example-component', ExampleComponent);
app.mount('#app');
3.3.2. 局部注册
我们可以在组件的<script>
中进行局部注册,局部注册的组件只能在注册它们的组件内部使用。
组合式API写法:
<script setup>
import ExampleComponent from './ExampleComponent.vue';//注意路径
//其他逻辑
</script>
选项式API写法:
<script>
import ExampleComponent from './ExampleComponent.vue';//注意路径
export default {
components: {
ExampleComponent
},
//其他逻辑
}
</script>
3.4. 组件通信★
组件之间的通信是构建复杂应用程序的关键。
3.4.1. 父子组件通信
3.4.1.1. Props
-
Props: 父组件通过 props 向子组件传递数据。
-
- 在子组件中,使用
defineProps()
声明需要父组件传递的值; - 在父组件中,直接在创建组件时使用v-bind向子组件传递;
- 父组件给子组件传递的值是只读的,不可修改。这个特性被称为
单向数据流
,这样设计是为了确保数据的安全性,方便快速找到数据错误的源头。 - 如果父组件给子组件传递的值是一个对象,那么可以在子组件中修改这个对象的属性。但是尽量不要通过props在父组件修改子组件的数据,要使用自定义事件修改。
- 推荐给props命名用
-
连接,例如max-length
- 在子组件中,使用
<!-- 子组件 Hello.vue -->
<script setup>
const props = defineProps(["count", "stu"]);
</script>
<template>
<h2>count = {{props.count}}</h2>
<button @click="props.count++">在子组件修改值</button><!-- 无法修改 -->
<hr />
<h2>全民制作人大家好,我是{{props.stu.name}},今年{{props.stu.age}}岁
</h2>
<button @click="props.stu.age++">在子组件修改值的属性</button><!-- 可以修改,但是不推荐 -->
</template>
<!-- App.vue -->
<script setup>
import Hello from './components/Hello.vue'
import {
reactive,
ref
} from 'vue';
const count = ref(0);
const stu = reactive({
name: "cxk",
age: 18,
gender: "女",
hobby: {
hobby1: "唱",
hobby2: "跳",
hobby3: "rap",
hobby4: "篮球",
}
})
</script>
<template>
<Hello :count="count" :stu="stu"></Hello>
</template>
3.4.1.2. 自定义事件
-
自定义事件: 子组件通过自定义事件向父组件发送消息。
-
- 在子组件中,使用
defineEmits()
声明事件。 - 在子组件和引用它的父组件中,使用
v-on
或:
绑定事件。 - 在子组件的模板中使用
$emit()
发送事件请求,在<script>
中使用emits()
发送请求 - 自定义事件除了要调用的方法,还可以使用内联事件处理器传递其他参数
- 自定义事件可以调用本组件和引用它的父组件中的函数。
- 在子组件中,使用
示例:
<!-- 子组件 Child.vue -->
<script setup>
const emits = defineEmits(["response"])
const sendMessage = () => {
emits('response', 'Hello from child')
}
</script>
<template>
<div>
<button @click="sendMessage">Send Message</button>
</div>
</template>
<!-- 父组件 App.vue -->
<script setup>
import Child from "./components/Child.vue"
const handleResponse = (response) => {
console.log('Received response from child:', response);
}
</script>
<template>
<div>
<Child @response="handleResponse('App.vue的参数')"></Child>
</div>
</template>
3.4.1.3. 父子相互通信示例
<!-- 父组件 App.vue -->
<script setup>
import Child from "./components/Child.vue"
const parentMessage = 'Hello from parent'
const handleResponse = (response) => {
console.log('Received response from child:', response);
}
</script>
<template>
<div>
<Child :message="parentMessage" @response="handleResponse"></Child>
</div>
</template>
<!-- 子组件 -->
<script setup>
// import {} from 'vue'
const props = defineProps(["message"])
const emits = defineEmits(["response"])
const sendMessage = () => {
emits('response', 'Hello from child')
}
</script>
<template>
<div>
<p>{{ props.message }}</p>
<button @click="sendMessage">Send Message</button>
</div>
</template>
3.4.2. 兄弟组件通信
vue.js没有提供方式让兄弟组件之间直接通信,但是可以通过共同的父组件作为中介进行间接通信,即状态提升。
在上图中,要实现AAA.vue和AABA.vue共用一个count数据,要如何实现?
我们可以将count数据定义在AA.vue中,AA.vue与AAA.vue间使用props传递数据,AA.vue与AABA.vue间通过依赖注入传递数据。
由于状态提升过于麻烦,我们后面会使用状态管理进行兄弟间或其他更复杂关系的通信。
3.4.3. 跨层级组件通信
3.4.3.1. 依赖注入
Provide / Inject: 允许一个祖先组件向其所有的后代组件传递数据,无论组件层次有多深。
步骤:
- 设置依赖 provide(name,value),为子元素提供依赖
- 注入数据 const value = inject(name, default), 只能注入祖先元素的依赖
注意:
- 只能实现子孙元素读取祖先元素的值(如下图,黑色箭头表示可以提供依赖,红色不可)
- 如果有多个祖先向某个子孙提供了同一个依赖,那么注入的是最近的祖先元素的值
<!-- App.vue -->
<script setup>
import AA from "./components/AA.vue"
import AB from "./components/AB.vue"
import {
provide,
inject
} from "vue";
provide("APPMsg", "这是来自APP.vue的msg");
const msg = inject("AAAMag", "没有收到msg");
</script>
<template>
<h1>依赖注入</h1>
<h2>{{msg}}</h2>
<hr />
<AA></AA>
<hr />
<AB></AB>
</template>
<!-- AA.vue -->
<script setup>
import AAA from "./AAA.vue"
import {
provide,
inject
} from "vue";
provide("AAMsg", "这是来自AA.vue的msg");
const msg = inject("AAAMag", "没有收到msg");
</script>
<template>
<div class="type1">
<h2>组件AA的div1</h2>
<h3>{{msg}}</h3>
<AAA></AAA>
</div>
</template>
<style scoped>
.type1 {
background-color: #aaff00;
width: 300px;
height: 300px;
}
</style>
<!-- AAA.vue -->
<script setup>
import {
provide,
inject
} from "vue";
const msg = inject("APPMsg");
// const msg = inject("AAMsg");
provide("AAAMag", "这是来自AAA.vue的msg");
</script>
<template>
<div class="type3">
<h3>组件AAA的div1</h3>
<h4>{{msg}}</h4>
</div>
</template>
<style scoped>
.type3 {
background-color: #ffff00;
height: 200px;
width: 200px;
}
</style>
3.4.3.2. 状态管理
3.4.4. 动态组件
component
是一个动态组件,其最终以什么标签呈现由is属性决定。- is属性除了可以是html的标签,还可以是引入的组件。
组件可以理解为一个自定义的html标签,你可以将一个页面拆分为多个组件,每个组件可以在不同页面使用
<!-- App.vue -->
<template>
<component is="h2">111</component>
<component is="h1">111</component>
<component is="div">111</component>
<component :is="A"></component>
</template>
<script setup>
import A from './components/A.vue'
import B from './components/B.vue'
</script>
3.4.5. 插槽(slots)
插槽让我们可以在组件的模板中预留一个位置,使得使用该组件的地方可以决定这些位置的具体内容。即插槽可以实现在父组件中指定子组件的内容(innerHTML)。
3.4.5.1. 默认插槽
在子组件中,我们可以使用 <slot>
元素来定义一个插槽。
<!-- 子组件 A.vue-->
<template>
<div>
<slot>默认内容</slot> <!-- 默认插槽 -->
<!-- 默认插槽里面可以有默认内容,如果父组件定义了该组件没有传递东西,就会显示slot中的默认内容 -->
</div>
</template>
在父组件中,我们可以将内容传递给这个插槽。
<!-- App.vue -->
<template>
<A></A>
<A>父组件的内容</A>
<A><span>父组件的内容</span></A>
<!-- 注意,这里B组件不是A的子组件,A、B组件都是App.vue的子组件,而A、B组件互为兄弟组件 -->
<A><B></B></A>
</template>
<script setup>
import A from './components/A.vue'
import B from './components/B.vue'
</script>
3.4.5.2. 具名插槽
如果组件中有多个插槽,我们可以通过给 <slot>
元素添加 name
属性来定义具名插槽。
<!-- 子组件 A.vue-->
<template>
<div>
<slot name="header"></slot>
<slot ></slot> <!-- 这是默认插槽的内容 -->
<slot name="footer"></slot>
</div>
</template>
在父组件中,我们可以通过 v-slot
指令(在 Vue 3 中,可以使用新的 #
符号作为 v-slot
的简写)来指定内容对应的具名插槽。
<!-- App.vue -->
<template>
<!-- 如果直接往子组件的插槽传值,只会修改子组件的默认插槽 -->
<A>
这是默认插槽的内容
</A>
<A>
<template v-slot:header>
<h1>这是头部内容</h1>
</template>
<template v-slot:default>
<p>这是默认插槽的内容</p>
</template>
<template #footer>
<h2>这是底部内容</h2>
</template>
</A>
</template>
<script setup>
import A from './components/A.vue'
</script>
3.4.5.3. 作用域插槽
作用域插槽允许子组件向父组件传递数据,使得父组件可以定义一个模板来接收子组件传递的数据。
<!-- 子组件 Child.vue-->
<template>
<div>
<slot :user="user"></slot>
</div>
</template>
<script>
export default {
data() {
return {
user: {
name: '张三',
age: 30
}
}
}
}
</script>
在父组件中,我们可以通过 v-slot
指令的值来定义一个插槽 prop,然后使用这个 prop。
<!-- 父组件 App.vue-->
<script setup>
import A from './components/Child.vue'
</script>
<template>
<Child>
<template v-slot="slotProps">
<p>姓名:{{ slotProps.user.name }}</p>
<p>年龄:{{ slotProps.user.age }}</p>
</template>
</Child>
</template>
3.4.5.4. 解构插槽 Prop
Vue 3 允许我们直接在 v-slot
指令中使用解构赋值,使得代码更加简洁。
<!-- 父组件 -->
<template>
<ChildComponent>
<template v-slot="{ user }">
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
</template>
</ChildComponent>
</template>
3.4.6. Teleprot
当需要显示模态框或弹出层时,我们可能会在Vue组件的模板中创建模态对话框,并将其作为DOM树的一部分。这意味着模态对话框将作为其父组件的一部分渲染。这样,如果页面中其他元素具有较高的z-index,可能会覆盖模态框。此外,可能会受到其他组件样式的干扰,需要额外的CSS规则来确保模态框的正确显示。
vue中,为我们提供了Teleprot
,方便设置模态框或弹出层。
Vue 3中的<Teleport>
是一个内置的组件,它的主要功能是允许开发者将子组件渲染到DOM树的其他位置,而不是组件实际所在的位置。这通常被称为“传送”或“瞬移”。具体来说,<Teleport>
接受一个to属性,该属性的值是一个CSS选择器,表示目标位置,Vue会确保<Teleport>
的内容渲染到该目标位置。
下面是使用<Teleport>
的一个基本示例,实现当点击登录按钮后,会弹出登录的模态框:
<!-- App.vue -->
<script setup>
import {
ref
} from 'vue';
const showModal = ref(false);
const username = ref('');
const password = ref('');
function submitForm() {
// 处理表单提交逻辑
console.log('用户名:', username.value);
console.log('密码:', password.value);
showModal.value = false; // 提交后关闭模态框
}
</script>
<template>
<div id="app">
<button @click="showModal = true">登录</button>
<Teleport to="body">
<div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
<div class="modal-content">
<h2>登录</h2>
<form @submit.prevent="submitForm">
<div>
<label for="username">用户名:</label>
<input type="text" id="username" v-model="username" required>
</div>
<div>
<label for="password">密码:</label>
<input type="password" id="password" v-model="password" required>
</div>
<button type="submit">提交</button>
</form>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</div>
</template>
<style scoped>
/* modal-overlay用于创建一个覆盖整个屏幕的背景,并且当点击背景时,会关闭模态框。 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
/* modal-content内部包含了登录表单和一个关闭按钮 */
.modal-content {
background: white;
padding: 20px;
border-radius: 5px;
width: 300px;
}
.modal-content h2 {
margin-top: 0;
}
.modal-content div {
margin-bottom: 10px;
}
</style>
3.5. 生命周期钩子
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,例如设置数据、渲染模板、处理数据变化等。在这个过程中,Vue 组件实例会调用一系列的生命周期钩子函数,这使得我们有机会在不同的阶段进行一些自定义操作。
使用方法:
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello, Vue!'
};
},
beforeCreate() {
console.log('beforeCreate: 实例初始化');
},
created() {
console.log('created: 数据观测和事件配置完成');
},
beforeMount() {
console.log('beforeMount: 模板编译完成,尚未挂载到 DOM');
},
mounted() {
console.log('mounted: 实例已挂载到 DOM');
},
beforeUpdate() {
console.log('beforeUpdate: 数据更新前,虚拟 DOM 重新渲染前');
},
updated() {
console.log('updated: 数据更新后,虚拟 DOM 重新渲染后');
},
beforeUnmount() {
console.log('beforeUnmount: 卸载组件实例前');
},
unmounted() {
console.log('unmounted: 组件实例已卸载');
}
};
</script>
3.6. 计算属性和侦听器
3.6.1. 计算属性
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。计算属性提供了一种方式来自定义模板表达式,它可以缓存计算结果,只有当依赖的数据发生变化时才会重新计算。
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
例如,下面的计算属性永远不会更新,因为 Date.now() 并不是一个响应式依赖
const now = computed(() => Date.now())
3.6.2. 侦听器
侦听器可以监听数据的变化,并在数据变化时执行一些特定操作。
<template>
<button @click="sum++">增加数值</button>
</template>
<script setup >
import { watch, ref } from 'vue'
const sum=ref(1);
// New: 新值 | Old: 老值
watch(sum,(New, Old)=>{
console.log(`新值:${New} ——— 老值:${Old}`)
})
</script>
监听reactive对象:
<template>
<button @click="sum.value++">增加数值</button>
</template>
<script setup lang="ts">
import { watch,reactive} from 'vue'
// 响应式变量
const sum = reactive({
value: 1
})
// watch是一个函数
// 第一个参数: 要监听的响应式变量(数据源)
// 第二个参数:执行的回调函数
// 第三个参数: 配置(例如deep深度监听/immediate是否立即监听等)
// 注意:当监听一个对象时,第一个参数数据源必须是函数且返回一个值即 "() => 对象.要监听的属性"
watch(() => sum.value, (New, Old) => {
console.log(`新值:${New} ——— 老值:${Old}`)
})
</script>
如果你需要监听一个对象或数组中的嵌套属性,可以使用深度侦听。设置 deep: true 来启用深度侦听。
<template>
<div>
<p>User Name: {{ user.name }}</p>
<p>User Age: {{ user.age }}</p>
<button @click="updateUser">更新用户信息</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const user = ref({ name: 'John Doe', age: 25 })
// 深度侦听 user.age
watch(user, {
handler(newUser, oldUser) {
console.log('User updated:', newUser, oldUser)
},
deep: true
})
function updateUser() {
user.value.age = 26
}
</script>
3.7. 第三方组件库
3.7.1. 介绍
Vue 3 的第三方组件库是指由社区成员或组织开发的、用于 Vue 3 的可重用组件集合。这些组件库提供了各种预制的 UI 组件,如按钮、输入框、模态框、表格、导航等,以帮助开发者快速构建用户界面。
以下是一些适合初学者学习且组件比较全面的 Vue 3 第三方组件库:
- Element Plus特点:来自 Element UI 的 Vue 3 版本,适用于企业级桌面应用,组件齐全。
- Naive UI特点:轻量级,支持 TypeScript,适合想要学习 TypeScript 和现代 UI 设计的初学者。
- Ant Design of Vue特点:企业级 UI 设计语言,组件丰富,适合想要学习企业级应用开发的初学者。
3.7.2. 使用(Element Plus为例)
要在Vue3项目中使用Element Plus,你可以按照以下步骤操作:
3.7.2.1. 安装Element Plus
使用npm:
npm install element-plus
3.7.2.2. 引入Element Plus
在你的main.js文件中,需要引入Element Plus的样式文件,并且使用Vue的createApp()
函数创建一个应用实例,然后使用use()
方法来使用Element Plus。
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/lib/theme-chalk/index.css' // 引入主题样式
const app = createApp({})
app.use(ElementPlus)
app.mount('#app')
3.7.2.3. 使用Element Plus组件
现在,你可以在你的Vue组件中使用Element Plus的组件了。例如,如果你想在某个组件中使用<el-button>
组件,你可以这样做:
import { ElButton } from 'element-plus'
export default {
components: {
ElButton
}
}
然后在模板中就可以使用<el-button>
标签了:
提交
具体配置教程,可以参考官网。
3.7.2.4. 修改样式
在Vue 3中使用Element Plus时,如果你想要覆盖组件的样式,但又遇到了使用了变量和!important
的情况,你可以通过以下几种方法来修改这些样式:
- 使用同样的
**!important**
标志:在你的自定义样式中,你可以使用相同的!important
标志来覆盖原有的样式。例如:
.el-menu--horizontal>.el-menu-item.is-active {
border-bottom: 2px solid blue; /* 替换为你想要的颜色 */
color: blue!important; /* 替换为你想要的颜色,并且使用!important */
}
- 增加特异性:通过增加选择器的特异性来覆盖原有的样式。例如,你可以添加一个父选择器或者使用属性选择器来增加特异性。
/* 增加父选择器 */
.some-parent-class .el-menu--horizontal>.el-menu-item.is-active {
border-bottom: 2px solid blue; /* 替换为你想要的颜色 */
color: blue!important; /* 替换为你想要的颜色,并且使用!important */
}
/* 使用属性选择器 */
.el-menu--horizontal>.el-menu-item.is-active[data-some-attribute] {
border-bottom: 2px solid blue; /* 替换为你想要的颜色 */
color: blue!important; /* 替换为你想要的颜色,并且使用!important */
}
- 修改变量值:如果样式使用了CSS变量,你可以通过修改这些变量的值来全局改变样式。在你的应用的主样式文件中,或者在
<style>
标签中,设置相同的CSS变量为新的值。
:root {
--el-menu-active-color: blue; /* 替换为你想要的颜色 */
}
- 使用深度选择器:在Vue单文件组件的
<style>
标签中使用/deep/
或::v-deep
伪元素,增加选择器的特异性,以覆盖Element Plus组件的样式。
<style scoped>
::v-deep .el-menu--horizontal>.el-menu-item.is-active {
border-bottom: 2px solid blue; /* 替换为你想要的颜色 */
color: blue!important; /* 替换为你想要的颜色,并且使用!important */
}
</style>
过度使用!important
可能会导致样式难以维护,尽量通过合理的CSS架构和特异性来避免使用!important
。如果你必须使用它,请确保你了解它的影响,并且只在绝对必要时使用。
4. 状态管理
4.1. 介绍
-
状态(state)
-
- 应用当中的数据就是状态
-
视图(view)
-
- 视图就是用来呈现数据的,用户通过视图访问数据
-
交互(actions)
-
- 用户的操作
- 状态会根据用户在视图中的操作发生变化
对于更复杂的状态管理需求,状态管理是非常必要的,主要有以下几个原因:
- 代码组织:随着应用的复杂度增加,状态可能会分散在各个组件中,这样会导致代码难以组织和理解。状态管理可以帮助我们将状态集中管理,使得代码更加清晰和易于维护。
- 组件间的通信:在大型应用中,组件间的通信可能会变得相当复杂。状态管理提供了一种中心化的方式来处理这种通信,使得组件间的通信变得更加简单和直观。
- 状态的可追踪性和可预测性:状态管理允许我们更好地追踪和管理状态的变化,这对于调试和测试非常有帮助。此外,良好的状态管理还可以帮助我们预测状态的变化,从而提前做出相应的反应。
4.2. 手动状态管理
在上图中,要实现AAA.vue和AABA.vue共用一个count数据,要如何实现?
状态管理的解决思路:创建一个store/count.js来存放count数据,当其他组件需要使用count,直接导入count.js
示例代码如下:
// store/count.js
import {
reactive
} from "vue"
export const countStore = reactive({
count: 0,
increment() {
this.count++;
}
})
<!-- AAA.vue -->
<script setup>
import {
countStore
} from "@/store/count" //@代表根目录
</script>
<template>
<div class="div">
<h3>AAA.vue -- count ={{countStore.count}} </h3>
<button @click="countStore.increment">按钮</button>
</div>
</template>
<style scoped>
.div {
width: 250px;
height: 100px;
background-color: #00ff00;
}
</style>
<!-- AABA.vue -->
<script setup>
import {
countStore
} from "@/store/count" //@代表根目录
</script>
<template>
<div class="div">
<h3>AABA.vue -- count = {{countStore.count}}</h3>
<button @click="countStore.increment">按钮</button>
</div>
</template>
<style scoped>
.div {
width: 250px;
height: 100px;
background-color: #00ffff;
}
</style>
4.3. pinia★
4.3.1. 介绍
- 更强的团队协作约定
- 与 Vue DevTools 集成,包括时间轴、组件内部审查和时间旅行调试
- 模块热更新 (HMR)
- 服务端渲染支持
Pinia 就是一个实现了上述需求的状态管理库(vue插件),对 Vue 2 和 Vue 3 都可用。
- Vuex是一个集中式状态管理库,它能够为大型应用提供一个可预测的状态管理方案。而Pinia 提供了一个更简单、更现代的 API,同时保持了 Vuex 的核心功能,并且在类型支持(ts)、模块热替换和打包大小方面有所改进。如果你正在构建一个新项目,特别是使用 Vue 3 时,Pinia 可能是一个更好的选择。
- 本文只介绍功能更强大且更易学的Pinia,如果对Vuex感兴趣可以自行了解。
4.3.2. 安装与配置
安装:
yarn add pinia
# 或者使用 npm
npm install pinia
在main.js进行以下配置:
- 引入createPinia()
- 通过
createPinia()
创建实例 - 将pinia配置为vue插件
示例(main.js):
import {
createApp
} from 'vue'
import {
createPinia
} from 'pinia' //1.引入pinia
import App from './App.vue'
const pinia = createPinia() //2.创建pinia实例
const app = createApp(App)
app.use(pinia) //3.配置vue使用pinia插件
// 挂载应用
app.mount('#app')
4.3.3. 选项式
创建仓库
import {
defineStore
} from "pinia"
export const useCountStore = defineStore("count", {
//数据
state: () => ({
count: 100
}),
//计算属性
getters: {
//箭头函数,不能使用this
double: (state) => state.count * 2,
//普通函数
//double(){return this.count * 2},
},
//方法
actions: {
//需要使用this访问state,不推荐使用箭头函数
increment() {
this.count++;
}
}
})
引入并使用:
<!-- App.vue -->
<script setup>
import {
useCountStore
} from "./store/counter"
const countStore = useCountStore()
</script>
<template>
<div class="div">
<h3>AAA.vue -- count = {{countStore.count}} -- double = {{countStore.double}} </h3>
<button @click="countStore.$patch({ count: countStore.count - 1 })">-</button>
<button @click="countStore.increment">+</button>
</div>
</template>
<style scoped>
.div {
width: 500px;
height: 100px;
background-color: #00ff00;
}
</style>
实际上pinia创建的状态可以在任何组件使用
4.3.4. 组合式
当定义一个 setup store 时,你几乎可以使用任何组合式函数,因为每一个属性都会被辨别为 state 、action 或者 getter
export const useCountStore = defineStore("count", () => {
const count = ref(50)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
return {
count,
double,
increment
}
})
4.3.5. 对象解构
- pinia示例如果需要解构,需要使用
storeToRefs
- 选项式的计算属性不能解构,组合式的方法不能解构
//count.js
export const useCountStore = defineStore("count", () => {
//定义数据
const count = ref(50)
//定义计算属性
const double = computed(() => count.value * 2)
//定义方法
function increment() {
count.value++
}
//暴露状态
return {
count,
double,
increment
}
})
//AAA.vue脚本部分
import {
useCountStore
} from "@/store/count"//引入仓库
import {
storeToRefs
} from "pinia";
const countStore = useCountStore()
const {
count,
double
} = storeToRefs(countStore)
4.3.6. store的修改
4.3.6.1. 直接修改
pinia解构后可以直接修改,但是不推荐使用。
//AAA.vue 脚本部分添加下面这句
count.value = 200;
4.3.6.2. 间接修改
通过方法修改
<!-- AAA.vue的模板部分 -->
<template>
<div class="div">
<h3>AAA.vue -- count = {{count}} -- double = {{double}} </h3>
<button @click="countStore.increment">按钮</button><!-- 点击调用方法间接修改 -->
</div>
</template>
$patch
补丁修改
- pinia提供了
$patch
进行补丁修改,可以同时修改多个状态 $patch
会直接替换之前的同名状态
const clickHandler = () => {
countStore.$patch({
count: 200
})
//或
countStore.$patch((state) => {
state.count = 300
})
}
$reset
状态重置
将修改后的状态重置回初始状态
模板写法(只适用于选项式api)
<button @click=“countStore.$reset()”>重置
如果使用组合式api,需要我们在main.js重写$reset
import {
createApp
} from 'vue'
import {
createPinia
} from 'pinia' //1.引入pinia
import App from './App.vue'
const pinia = createPinia() //2.创建pinia实例
const app = createApp(App)
pinia.use(({
store
}) => {
const initialState = JSON.parse(JSON.stringify(store.$state));
store.$reset = () => {//重写$rest
store.$patch(initialState);
}
})
app.use(pinia) //3.配置vue使用pinia插件
// 挂载应用
app.mount('#app')
4.3.7. store的监听(订阅)
- 使用
$subsribe
监听store是否变化,如果变化就会调用$subsribe
中的方法。$subsribe
只会监听定义它的组件是否修改了store,不会监听其他组件有无修改。 mutation
表示修改的信息。如果使用$patch
,mutation.payload
会记录新的状态(数据)。detached: true
表示即使组件被移除也会继续监听- 使用
$subsribe
时不要在其回调函数中修改state
//在任一使用了仓库的组件中(AAA.vue)
countStore.$subscribe(
(mutation, state) => {
//console.log(mutation.events)
console.log(mutation.payload)
console.log("state发生了变化,变化后的state:", state)
}, {
detached: true
}
)
类似的还有$onAction
,用来监听选项式api的actions或组合式api方法是否调用
countStore.$onAction(() => {
console.log("actions执行了")
})
可以传递以下参数:
- name 调用的action的名称
- store store的实例
- args action接收到的参数
- after() 设置一个回调函数,该函数会在action成功调用后触发
- onError() 设置一个回调函数,该函数会在action失败调用后触发
countStore.$onAction((name,store,agrs,after,onError) => {
console.log("actions name:",name)
console.log("actions store:",store)
console.log("actions agrs:",agrs)
after(() =>{
console.log("actions 执行成功")
})
})
4.3.8. 永久性存储
在 Pinia 中,"永久存储"通常指的是将状态(store)持久化到本地存储(如 localStorage 或 sessionStorage)中,以便在页面刷新或关闭后重新打开时能够恢复状态。
具体步骤如下:
- 添加依赖
# 该插件可以自动将 Pinia 的状态持久化到本地存储
npm install pinia-plugin-persistedstate
- 在main.js使用插件
import { createApp } from 'vue'
//...
import {
createPinia
} from 'pinia' //1.引入pinia
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia() //2.创建pinia实例
pinia.use(piniaPluginPersistedstate) //3.使用插件
const app = createApp(App)
app.use(pinia)
app.mount('#app');
- 设置store
// 集中管理用户数据
import {
defineStore
} from "pinia";
import {
ref
} from "vue";
export const useUserStore = defineStore("user", ()=>{
const cartStore = useCartStore()
// 创建用户数据
const userInfo = ref({});
//...
// 以对象形式返回数据和方法
return {
userInfo,
//...
}
}, {
persist: true//设置store永久存储
})
5. 路由管理Vue Router
5.1. 路由的基本概念
- Vue Router是Vue.js 的官方的客户端路由解决方案。使用Vue Router,你可以轻松地构建具有多个页面和复杂导航逻辑的Vue应用程序。
- Vue Router 基于 Vue 的组件系统构建,你可以通过配置路由来告诉 Vue Router 为每个 URL 路径显示哪些组件。
5.2. 路由的安装和设置
在 Vue 3 中,使用 Vue Router 来处理客户端路由。Vue Router 是 Vue 官方推荐的路由库,它允许你为不同的路径设置对应的组件,从而实现单页面应用(SPA)的页面切换和视图更新。
一般来说,我们使用vite创建项目时可以选择是否使用vue router。如果选择了就不需要再安装依赖,如果没有安装(在package.json中没有)就需要进行下面步骤:
5.2.1. 安装 Vue Router
首先,你需要安装 Vue Router。如果你使用 npm,可以运行以下命令:
npm install vue-router@4
注意,这里我们指定了版本 4,因为 Vue 3 对应的是 Vue Router 的第 4 版本。
5.2.2. 设置 Vue Router
5.2.2.1. 创建路由实例
创建一个 router
文件夹,并在其中创建一个 index.js
文件来设置路由。
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
];
const router = createRouter({
history: createWebHistory(),
routes,
//滚动行为:控制滚动条位置
scrollBehavior(){
left:0,
top:0
}
});
export default router;
这里我们使用了 createWebHistory
创建一个 history 模式的路由。如果你需要 hash 模式,可以使用 createWebHashHistory
。
5.2.2.2. 在主文件中引入路由
在你的主 Vue 文件(通常是 main.js
或 main.ts
)中,你需要导入并使用这个路由实例。
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(router);
app.mount('#app');
通过调用 app.use(router)
,你将路由插件安装到了你的 Vue 应用中。
5.3. 路由导航
在 Vue 3 中,路由导航是指在不同页面(路由)之间导航的过程。这是通过 Vue Router 这个官方的路由管理器来实现的。Vue Router 允许你为你的 Vue 应用定义路由,并且可以在不同的视图(组件)之间导航。
路由导航通常分为两种类型:组件导航和编程式导航。它们之间的主要区别在于如何触发路由变化,以及它们在模板和代码中的使用方式。
在实际开发中,组件导航和编程式导航通常是互补的。组件导航用于简单的导航需求,而编程式导航用于更复杂的路由逻辑和需要动态控制路由的行为。
5.3.1. 组件导航
现在你可以在你的 Vue 组件中使用 router-view
和 router-link
组件来渲染路由和创建导航链接。
<!-- App.vue -->
<template>
<div id=>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-view></router-view>
</div>
</template>
<!-- views/Home.vue -->
<template>
<div >
这是Home
</div>
</template>
<!-- views/About.vue -->
<template>
<div >
这是About
</div>
</template>
<router-view>
是一个容器组件,它会根据当前路由动态渲染匹配的组件。<router-link>
是一个组件,用于创建导航链接,它接受 to
属性来指定链接的目标路径。
不同于常规的 <a>
标签,我们使用组件 RouterLink
来创建链接。这使得 Vue Router 能够在不重新加载页面的情况下改变 URL,处理 URL 的生成、编码和其他功能。点击<router-link>
这些链接时,Vue Router 会自动处理跳转,用户无需手动调用任何 JavaScript 代码。
5.3.2. 编程式导航
编程式导航是 Vue Router 提供的编程方式,它允许你通过 JavaScript 代码来控制路由的变化。编程式导航通常在需要更复杂的路由逻辑时使用,比如处理异步数据加载、导航守卫、导航确认等。
在 Vue 组件中,推荐使用组合式 API useRouter 和 useRoute 访问路由实例。
可以通过 router.push 和 router.replace 方法来控制历史记录的行为。
下面是编程式导航的一个基本示例,模拟了登录成功跳转到首页:
<template>
<div>
<form @submit.prevent="handleSubmit">
<div>
<label for="username">Username:</label>
<input type="text" id="username" v-model="username" />
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" v-model="password" />
</div>
<button type="submit">登录</button>
</form>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
const username = ref('');
const password = ref('');
const handleSubmit = () => {
if (username.value === 'admin' && password.value === '123456') {
router.push({ path: '/home' });
} else {
alert('账号或密码错误!');
}
};
</script>
5.4. 路由守卫
Vue Router 提供了全局守卫、路由独享守卫和组件内守卫,允许你控制路由的访问权限和导航行为。路由守卫是一种机制,用于在路由跳转之前、之后或失败时执行代码。
每个路由守卫接受一个回调函数作为参数,该回调函数有三个参数:to
、from
和next
。
-
to
: 是一个即将进入的目标路由对象。 -
from
: 是一个当前导航正要离开的路由对象。 -
next
: 是一个函数,必须调用它来解析这个钩子。它可以接受以下参数: -
next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是确认的。next(false)
: 中断当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址会重置到from
路由对应的地址。next('/')
或next({ path: '/' })
: 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。next(error)
: 如果传入next
的参数是一个Error实例,则导航会被终止且该错误会被传递给router.onError()
注册过的回调。
5.4.1. 全局守卫
全局路由守卫通常直接在路由声明文件(router/index.js)中直接声明。
全局前置守卫
全局前置守卫是Vue Router中的一种导航守卫,它可以在每个路由跳转之前执行一些代码。
它可以用来检查用户的登录状态、权限验证、加载进度条等。全局前置守卫会在路由跳转前被调用,它允许你定义一些全局的逻辑,这些逻辑需要在每次导航发生之前执行。
下面是一个全局前置守卫的例子,用于检查用户是否已经登录:
//router/index.js
router.beforeEach((to, from, next) => {
if (to.path === '/admin') {
if (isAuthenticated()) {//isAuthenticated是用户定义的用来检验用户是否登录的函数
next();
} else {
next('/login');
}
} else {
next();
}
});
全局后置守卫
在路由匹配之后执行。可以用来记录日志、执行清理操作等。
router.afterEach((to, from) => {
console.log('Navigated from ' + from.path + ' to ' + to.path);
});
5.4.2. 路由独享守卫
路由守卫(又称路由独享守卫)是直接在路由定义中使用的守卫,它们可以应用于特定的路由。
路由独享守卫通常在路由声明文件(router/index.js)的路由定义中直接声明。
钩子:beforeEnter、 beforeLeave
前置路由守卫:
const routes = [
{
path: '/admin',
component: AdminComponent,
beforeEnter: (to, from, next) => {
if (isAuthenticated()) {
next();
} else {
next('/login');
}
},
},
];
后置路由守卫:
const routes = [
{
path: '/admin',
component: AdminComponent,
afterEnter: (to, from) => {
console.log('Admin route entered');
},
},
];
5.4.3. 组件内的守卫
组件内的守卫是在组件内部定义的守卫,它们可以应用于组件的内部路由。
钩子:beforeRouteEnter、 beforeRouteUpdate、 beforeRouteLeave
<-- Home.vue -->
<script setup>
beforeRouteEnter(to, from, next) {
if (isAuthenticated()) {
next();
} else {
next(false);
}
},
</script>
5.5. 懒加载路由组件
为了提高应用的性能,你可以使用动态导入来懒加载路由组件。
const routes = [
{
path: '/',
component: () => import('../views/Home.vue')
},
{
path: '/about',
component: () => import('../views/About.vue')
},
];
5.6. 嵌套路由和路由参数
在Vue 3中,嵌套路由和路由参数是强大且常用的功能,可以帮助你构建具有多层级页面结构和动态路径参数的应用程序。下面详细介绍一下Vue 3中的嵌套路由和路由参数的概念和用法:
5.6.1. 嵌套路由
嵌套路由允许你在一个路由中嵌套其他路由,从而构建多层级的页面结构。这对于构建具有层级关系的应用程序非常有用。
在Vue 3中,你可以在路由配置中的children
属性中定义嵌套路由。例如:
const routes = [
{
path: '/parent',
component: ParentComponent,
children: [
{
path: 'child',
component: ChildComponent
}
]
}
];
在上面的例子中,ParentComponent
是父级路由的组件,ChildComponent
是嵌套在父级路由中的子级路由的组件。当访问/parent/child
路径时,Vue Router会渲染ParentComponent
和ChildComponent
。
5.6.2. 路由参数(动态路由)
动态路由指的是路由路径中的参数,这些参数可以在 URL 中动态变化,允许你为不同的 URL 路径提供不同的组件。动态路由使用 : 符号来定义路径中的参数,这些参数被称为路由参数。
路由参数用于动态地传递路径参数,例如用户ID、商品ID等。
路由传参分为两种类型:params(参数)和 query(查询字符串)。它们在 URL 中以不同的方式呈现,并且与 HTTP 请求中的 POST 和 GET 请求有些相似之处。
name是什么呢?name 是配置路由时给 path 取的别名,方便使用。但要注意的是 地址栏显示的路径始终是 path 的值
//router/index.js
routes : [
{
path: '/login',
component: Login
},
{
path: '/home',
name: 'home',
component: Home
},
{
path: '/user/:id',
name: 'user',
component: User
}
]
在路由参数中还可以定义多个参数,例如:
routes : [
{
path: '/user/:id/post/:postId',
component: UserPostComponent
}
];
5.6.2.1. 路由 params 传参
-
定义:当使用动态路由时,例如 /user/:id,:id 是一个参数占位符,用于捕获 URL 中的值。
-
示例:当用户访问 /user/123,路由 params 会将 123 作为参数传递给组件。
-
位置:params 参数显示在 URL 的路径中,类似于网络请求中的 POST 请求的 body 部分。
-
特点:
-
- 不会显示在地址栏中(URL 中的 params 部分不会被浏览器的历史记录记录)。
- 不能刷新,刷新后 params 参数会丢失。
- 只能配合 name 使用,如果提供了 path,params 会失效。
组件导航:
编程式导航:
// 使用 params 传参
router.push({ name: 'user', params: { id: '123' } });
5.6.2.2. 路由 query 传参
-
定义:当使用查询字符串时,例如 /user?name=John,?name=John 是查询字符串。
-
示例:当用户访问 /user?name=John,路由 query 会将 name=John 作为查询字符串传递给组件。
-
位置:query 参数显示在 URL 的查询字符串中,类似于网络请求中的 GET 请求的 query 部分。
-
特点:
-
- 会显示在地址栏中,并且可以刷新。
- 较为灵活,既可以配合 path 使用,也能配合 name 使用。
组件导航:
编程式导航:
// 使用 query 传参
router.push({ path: '/user', query: { name: 'John' } });
5.7. 示例-极简音乐
- 创建工程并安装Vue Router
- 删除HelloWorld.vue和其在App.vue中的相关代码,清除其他用不到的代码
- 创建几个简单的组件(/components):Discover.vue,MyMusic.vue,Follow.vue
添加以下基本代码,以Discover.vue为例,其他类似
<template>
<div>
<h1 class="title">发现音乐</h1>
</div>
</template>
<script>
</script>
<style scoped>
.title{
color: red;
}
</style>
根组件(App.vue)声明路由导航
<template>
<div id="app">
<nav class="nav-link">
<router-link to="/discover">发现音乐</router-link>
<router-link to="/myMusic">我的音乐</router-link>
<router-link to="/follow">关注</router-link>
</nav>
<!-- 声明路由占位符 -->
<router-view></router-view>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.nav-link a{
/ * 不建议直接使用标签,推荐使用指定类名设置样式。 */
/* 直接使用.nav-link router-link无法指定router-link的样式,router-link 组件在渲染时会转换为一个 <a> 标签 */
display: inline-block;
width: 100px;
padding: 0 5px;
}
</style>
创建src/router/index.js,定义对应关系
import { createRouter, createWebHistory } from 'vue-router'
import Discover from '@/components/Discover.vue'
import Follow from '@/components/Follow.vue'
import MyMusic from '/src/components/MyMusic.vue'//也可以不使用别名
// 创建路由实例
const router = createRouter({
// 使用 createWebHistory() 创建历史记录
history: createWebHistory(),
routes: [
{path: '/', redirect:"/discover"},//路由重定向
{ path: '/discover', component: Discover},
{ path: '/follow', component: Follow},
{ path: '/myMusic', component: MyMusic},
]
})
// 导出路由实例
export default router
注意:这里使用了 @ 符号作为 src 目录的别名,你需要在你的项目根目录下创建或编辑 vite.config.js (使用vite)文件,添加以下内容来配置@
别名:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve:{
alias: {
//指定别名
'@': '/src',
}
}
})
在main.js挂载路由
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 可以通过 app.config 设置一些全局配置
app.config.productionTip = false
// 使用路由
app.use(router)
// 挂载应用程序
app.mount('#app')
路由嵌套:修改Discover.vue
,修改后的代码如下
<template>
<div>
<h1 class="title">发现音乐</h1>
<!-- 子路由链接 -->
<nav class="nav-link">
<router-link to="/discover/toplist">推荐</router-link>
<router-link to="/discover/playlist">歌单</router-link>
</nav>
<!-- 声明路由占位符 -->
<router-view></router-view>
</div>
</template>
<script>
</script>
<style scoped>
.title{
color: red;
}
.nav-link{
background-color: #bfa;
}
</style>
创建组件TopList.vue
,PlayList.vue
<template>
<h3>推荐</h3>
</template>
<!-- PlayList.vue类似 -->
修改src/router/index.js
,添加子路由对应关系
import {
createRouter,
createWebHistory
} from 'vue-router'
import Discover from '@/components/Discover.vue'
import Follow from '@/components/Follow.vue'
import MyMusic from '@/components/MyMusic.vue'
import PlayList from '@/components/PlayList.vue'
import TopList from '@/components/TopList.vue'
// 创建路由实例
const router = createRouter({
// 使用 createWebHistory() 创建历史记录
history: createWebHistory(),
routes: [{
path: '/',
redirect: "/discover"
}, //路由重定向
{
path: '/discover',
component: Discover,
children: [ // 使用数组定义嵌套路由
{
path: "toplist",
component: TopList
},
{
path: "playlist",
component: PlayList
},
]
},
{
path: '/follow',
component: Follow
},
{
path: '/myMusic',
component: MyMusic
},
]
})
// 导出路由实例
export default router
- 老实说,配置 @ 别名为/src还是保守了,我们可以取一个别名为
@cpns
,其路径为/src/components
//vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve:{
alias: {
'@': '/src',
'@cpns': '/src/components',
}
}
})
修改MyMusic.vue,添加Music.vue
<template>
<div>
<h1 class="title">我的音乐</h1>
<ul class="music-list">
<li><router-link to="/music/1">Music1</router-link></li>
<li><router-link to="/music/2">Music2</router-link></li>
<li><router-link to="/music/3">Music3</router-link></li>
</ul>
<router-view></router-view>
</div>
</template>
<script>
</script>
<style scoped>
.title {
color: orange;
}
.music-list li {
color: blue;
list-style: none;
margin-top: 10px;
}
</style>
<!-- Music.vue -->
<template>
<h3>Music</h3>
</template>
<script>
</script>
<style>
</style>
修改src/router/index.js
,添加子路由对应关系(只修改myMusic路由)
{
path: '/myMusic',
component: MyMusic,
children: [{
path: ':id',
component: Music,
props: true//方便传递props自定义属性
}]
},
- 为了看出区别,我们修改
Music.vue
<template>
<h3>Music{{id}}</h3>
</template>
<script>
export default {
props: ["id"],
}
</script>
<style>
</style>
http://localhost:3000/myMusic/1
效果:
6. HTTP通信
6.1. Http协议
HTTP(超文本传输协议)是用于从Web服务器传输超文本到本地浏览器的传送协议。它是一个应用层协议,设计目的是保证客户端和服务器之间的通信,并且能够处理各种数据类型,如文本、图像、视频等。
你可以打开浏览器的开发者工具(F12),通过网络(Network)查看网络请求与响应。
6.1.1. 主要特点
- 无状态:HTTP 是无状态协议,这意味着每个请求都是独立的,服务器不会保留任何关于先前请求的信息。但为了实现需要会话状态的应用,通常会使用 cookies 或者其他机制来保存状态信息。
- 基于请求-响应模型:HTTP 采用客户端-服务器架构,其中客户端发送一个请求给服务器,然后服务器根据请求返回一个响应。
- 灵活:HTTP 允许传输任意类型的数据对象,通过 MIME 类型来标识不同的数据格式。
- 可扩展:HTTP 协议支持通过设置额外的头部字段来进行功能扩展,例如添加认证、缓存控制等功能。
6.1.2. HTTP 请求
一个典型的 HTTP 请求包含以下几个部分:
- 请求行:包括方法(GET, POST 等)、URL 和 HTTP 版本。
- 请求头:包含了关于请求的各种信息,如客户端可以接受的内容类型、编码、语言等。
- 空行:分隔请求头部和请求体。
- 请求体:对于某些请求方法(如POST),这部分可能包含要发送给服务器的数据。
6.1.3. HTTP 响应
一个典型的 HTTP 响应由几个部分组成:
- 状态行:包括 HTTP 版本、状态码(如 200 表示成功,404 表示未找到资源等)和状态消息。
- 响应头部:提供了关于响应的各种信息,如内容类型、长度、最后修改时间等。
- 空行:分隔响应头部和响应体。
- 响应体:实际返回给客户端的数据,比如 HTML 页面、图片等。
6.1.4. HTTP 方法
HTTP 定义了几种常用的方法来指示特定的请求动作:
- GET:请求获取由请求 URL 标识的资源。
- POST:向指定资源提交数据进行处理请求(如提交表单或上传文件)。
- PUT:向指定资源位置上传其最新内容。
- DELETE:请求服务器删除 Request-URL 所标识的资源。
- HEAD:类似于 GET,但只返回响应头,不返回响应体。
- OPTIONS:用于描述目标资源的通信选项。
- PATCH:对资源进行部分修改。
在RESTful API中:
- GET:用来获取资源。
- POST:用来创建新的资源
- PUT 或 PATCH:用来更新现有资源。PUT 通常用来替换整个资源,而 PATCH 用来做局部更新。
- DELETE:用来删除资源。
6.2. axios
- 在Vue 3中,你可以使用
axios
库进行http数据交互。 axios
是一个基于promise网络请求库,可以在浏览器和Node.js环境中使用。你可以在浏览器使用axios发起请求,然后在Node.js中使用axios返回响应,从而完成一个具有相对完整功能的应用。- 本文只介绍axios库在浏览器中的使用
- 特性:
axios
在浏览器或nodejs环境使用XMLHttpRequests发送网络请求,并能自动完成JSON数据的转换- 可以拦截请求和响应,并进行处理
- 超时处理
- 官方文档
6.2.1. 安装和引入
你可以通过 npm 引入 Axios:
npm install axios
或者使用 CDN:
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
6.2.2. 发起请求
6.2.2.1. 发送 GET 请求
import axios from 'axios';
axios.get('https://api.example.com/data')
.then(response => {
// 处理成功的响应结果
console.log(response.data);
})
.catch(error => {
// 处理错误的响应结果
console.error(error);
});
6.2.2.2. 发送 POST 请求
import axios from 'axios';
const data = {
name: '张三',
age: 30
};
axios.post('https://api.example.com/data', data)
.then(response => {
// 处理成功的响应结果
console.log(response.data);
})
.catch(error => {
// 处理错误的响应结果
console.error(error);
});
6.2.2.3. 使用 async/await
你也可以使用 async/await
语法来处理 Axios 请求。
作用:
- 可以使用标准的 try…catch 语句来捕获和处理异步操作中的错误,而不需要使用 .catch() 方法
- 可以在多个异步操作之间轻松地传递和处理中间结果。
import axios from 'axios';
async function fetchData() {
try {
const response = await axios.get('https://api.example.com/data');
console.log(response.data);
} catch (error) {
console.error(error);
}
}
fetchData();
6.2.2.4. request
axios还支持直接在reuqest自定义请求
import axios from 'axios';
export function doLoginAPI({ username, password }) {
return axios.request({
url: '/login',
method: 'post',//请求方法
headers:{
//请求头
}
data: {//请求体
username,
password
}
});
}
6.2.3. 请求和响应拦截器
Axios 提供了拦截器功能,允许你在请求发送之前或响应到达之前对其进行处理。
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
6.2.4. 错误处理
Axios 请求失败时,会返回一个错误对象。这个对象包含一个 response
属性,该属性包含了服务器返回的信息。
axios.get('https://api.example.com/data')
.catch(error => {
if (error.response) {
// 请求已发出,服务器响应的状态码不在 2xx 范围内
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// 请求已发出,但没有收到响应
console.log(error.request);
} else {
// 发生了设置请求时的某些问题
console.log('Error', error.message);
}
console.log(error.config);
});
6.2.5. Axios 实例
你还可以创建一个 Axios 实例,以便在多个请求之间共享配置。
import axios from 'axios';
const instance = axios.create({
baseURL: 'http://localhost:8080', //基地址
timeout: 1000, //请求响应最长时间
});
instance.get('/data')
.then(response => {
// 处理成功情况
console.log(response.data);
})
.catch(error => {
// 处理错误情况
console.error(error);
});
6.2.6. axios封装
- 创建一个API服务:可以在Vue项目(或其他)中创建一个单独的文件,例如
utils/http.js
,用于封装 axios。
//utils/http.js
import axios from 'axios'
// 创建axios实例
const httpInstance = axios.create({
baseURL: 'http://localhost:8080/',
timeout: 2000
})
// axios请求拦截器
httpInstance.interceptors.request.use(config => {
//这里可以对请求处理
return config
}, e => Promise.reject(e))
// axios响应式拦截器
httpInstance.interceptors.response.use(res => {
//这里可以对响应处理
return res.data
}, e => {
return Promise.reject(e)
})
export default httpInstance
在apis目录下进一步封装请求
例如封装登录请求:
import httpInstance from '@/utils/http';
export async function doLoginAPI({ username, password }){
return httpInstance.request({
url: '/login',
method: 'post',
data: {
username,
password
}
});
}
在Vue组件中使用API服务:
const loginHandler = () => {
const { username, password } = form.value;
const res = await doLoginAPI({ username, password });
console.log(res);
};
6.3. MockJS
6.3.1. Mock.js介绍
什么是Mock.js?
在前面,我们已经使用axios发送了请求,但是我们没有一个工具处理请求并返回响应。
你当然可以使用node.js或spring boot来处理请求,但是学习成本较高。
这时,mock.js的作用就体现出来了。
Mock.js 是一款前端开发中拦截Ajax请求再生成随机数据响应的工具,可以用 来模拟服务器响应
Mock.js的特性
- 使用mockjs模拟后端接口,可随机生成所需数据,模拟对数据的增删改查
- 数据类型丰富,支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等。
- 拦截Ajax请求不需要修改既有代码就可以拦截,返回模拟的响应数据。
官网:http://mockjs.com
6.3.2. 安装
mockjs --save-dev # 安装mockjs
在使用ts的过程中,如果报错 尝试 npm install @xxxxx (如果存在),或者添加一个包含 declare module ‘xxxxx‘;的新声明(.d.ts)文件
,需要新建一个ts文件(src/types/index.d.ts)进行声明
declare module 'mockjs';
6.3.3. 使用Mock.js
在src目录下创建mock目录,定义mock主文件index.js,并在该文件中存放我们的mock数据
在main.js引入mock文件,方便在其他文件中使用(无需其他操作)
import "@/mock/index";
编写mock/index.js,配置要拦截的请求与返回的数据
import Mock from 'mockjs';
// 拦截Ajax请求,/api/users是拦截的请求地址
Mock.mock('/api/users', 'get', {
// 返回随机数据
'data|1-10': [{//生成1~10条数据 数组
'id|+1': 1,//生成id,自增1
'name': '@cname', // 随机生成中文名字
'email': '@email', // 随机生成邮箱
'date': '@date("yyyy-MM-dd")' // 随机生成日期
}]
});
// 如果需要,还可以继续添加其他拦截规则
// Mock.mock('/api/orders', 'get', {...});
在相关组件接收响应回来的数据(这里以App.vue为例)
<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
const users = ref([]);
const fetchUsers = async () => {
try {
const response = await axios.get('/api/users');
users.value = response.data;
} catch (error) {
console.error('There was an error!', error);
}
};
onMounted(() => {
fetchUsers();
});
</script>
<template>
<div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users.data" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.date }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<style scoped>
/* 添加一些样式来美化表格 */
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ccc;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
</style>
6.3.4. Mock.Random
- Mock.Random 是一个工具类,用于生成各种随机数据。
- Mock.Random 的方法在数据模板中称为『占位符』,书写格式为 @占位符(参数) 。
const Mock = require("mockjs")
const Random = Mock.Random
export const userData = Mock.mock("/data/list", "post", {
// 属性 list 的值是一个数组,随机生成 1 到 10 个元素
"list|1-10": [
{
// 生成随机字符串 长度为 5
"string": Random.string(5), // "jPXEu"
"string2": '@string(5)', // "jPXEu"
// 生成随机邮箱地址 可以指定域名,例如 163.com
"email": Random.email('163.com'), // "l.fvilfpz@163.com"
"email2": '@email()', // "l.fvilfpz@163.com"
// 返回一个随机的布尔值。
"boolean": Random.boolean(), // true
"boolean2": '@boolean()', // true
// 生成 60-100 随机整数
"point": Random.integer(60, 100), // 69
"point2": '@integer(60, 100)', // 98
// // 生成一个浮点数,整数部分大于等于 1、小于等于 100,小数部分保留 3 到 5 位。
"floatNumber": Random.float(1, 100, 3, 5), // 60.695
"floatNumber2": '@float(1, 100, 3, 5)', // 19.29368
// 随机日期
"date": Random.datetime('yyyy-MM-dd'), // "2017-05-01"
"date2": "@datetime()", // "1973-06-12 13:05:18"
// 随机时间
"time": Random.time(), // "21:33:01"
"time2": "@time()", // "21:33:01"
// 当前日期
"now": Random.now('year'), // "2023-01-01 00:00:00"
"now2": "@now('year')", // "2023-01-01 00:00:00"
// 随机生成图片 Random.image( size, background, foreground, format, text )
"img": Random.image('200x100', '#16d46b', '#fff', 'png', 'Hello'), // "http://dummyimage.com/200x100/16d46b/fff.png&text=Hello"
// 随机生成颜色,格式为 '#RRGGBB'。
"color": Random.color(), // "#94f279"
"color2": '@color()', // "#94f279"
// 随机生成颜色,格式为 'rgb(r, g, b, a)'。
"rgbaColor": Random.rgba(), // "rgba(242, 121, 183, 0.22)"
// 随机生成一段文本 文本中句子的个数为 2 到 5。默认值为 3 到 7
"paragraph": Random.paragraph(2, 5), // "Ymkp nvyryy vieq hlqdb pplbbikbd mtqiq uue jdufhkxy wpybjqi djico jxqkwvw kbmsscpfw owtgsqwn."
"paragraph2": '@paragraph(2, 5)', // "Ymkp nvyryy vieq hlqdb pplbbikbd mtqiq uue jdufhkxy wpybjqi djico jxqkwvw kbmsscpfw owtgsqwn."
// 随机生成一段中文文本 参数同 Random.paragraph( min?, max? )
"cparagraph": Random.cparagraph(), // "重工边政应信江半实金改北反调程五八。张资圆向规成新家天交对传许。军较军七养多认维市般况验式华行证。"
"cparagraph2": '@cparagraph(2, 5)', // "重工边政应信江半实金改北反调程五八。张资圆向规成新家天交对传许。军较军七养多认维市般况验式华行证。"
// 随机生成一个句子,第一个单词的首字母大写。 句子中单词的个数为 2 到 5 。默认值为 12 到 18
"sentence": Random.sentence(2, 5), // "Yyfvs genrdeiyf."
"sentence2": '@sentence(2, 5)', // "Yyfvs genrdeiyf."
// 随机生成一段中文文本,参数同 Random.sentence( min?, max? )
"csentence": Random.csentence(2, 5), // "积现。"
"csentence2": '@csentence(2, 5)', // "积现。"
// 随机生成一个单词,单词中字符的个数为 2 到 5 个。默认值为 3 到 10
"word": Random.word(2, 5), // "nlgcl"
"word2": '@word(2, 5)', // "nlgcl"
// 随机生成一个汉字,汉字中字符串的长度为 2 到 5 个。默认值为 1
"cword": Random.cword(2, 5), // "系即感"
"cword2": '@cword(2, 5)', // "系即感"
// 随机生成一句标题,其中每个单词的首字母大写。单词中字符的个数为 2 到 5。默认值为 3 到 7
"title": Random.title(2, 5), // "Vmpx Rizds Smguoqki"
"title2": '@title(2, 5)', // "Vmpx Rizds Smguoqki"
// 随机生成一句中文标题,参数同 Random.title( min?, max? )
"ctitle": Random.ctitle(2, 5), // "其感期"
"ctitle2": '@ctitle(2, 5)', // "其感期"
// 随机生成一个常见的英文名
"firstName": Random.first(), // "Michelle"
"firstName2": '@first()', // "Jose"
// 随机生成一个常见的英文姓。
"lastName": Random.last(), // "Taylor"
"lastName2": '@last()', // "Clark"
// 随机生成一个常见的英文姓名。括号里的布尔值,指示是否生成中间名(可选)。
"name": Random.name(true), // "Donald Eric Jackson"
"name2": '@name(true)', // "Donald Eric Jackson"
// 随机生成一个常见的中文姓
"cfirstName": Random.cfirst(), // "任"
"cfirstName2": '@cfirst()', // "郭"
// 随机生成一个常见的中文名。
"clastName": Random.clast(), // "芳"
"clastName2": '@clast()', // "芳"
// 随机生成一个常见的中文姓名。
"cname": Random.cname(), // "程强"
"cname2": '@cname()', // "程强"
// 随机生成一个URL。可以指定url协议,域名和端口号。例如'http' nuysoft.com。
'url': Random.url('http', 'nuysoft.com'), // "http://nuysoft.com/ysq"
'url2': '@url()', // "http://nuysoft.com/ysq"
// 随机生成一个 IP 地址
'IP': Random.ip(), // "112.127.151.37"
'IP2': '@ip()', // "233.144.17.219"
// 随机生成一个(中国)大区。
"region": Random.region(), // "华北"
"region2": '@region()', // "华北"
// 随机生成一个(中国)省(或直辖市、自治区、特别行政区)。
"province": Random.province(), // "澳门特别行政区"
"province2": '@province()', // "澳门特别行政区"
// 随机生成一个(中国)市。括号里的布尔值,指是否生成所属的省(可选)
"city": Random.city(true), // "广东省 肇庆市"
"city2": '@city()', // "广东省 肇庆市"
// 随机生成一个(中国)县。括号里的布尔值,指是否生成所属的省、市(可选)
"county": Random.county(true), // "江苏省 常州市 其它区"
"county2": '@county()', // "江苏省 常州市 其它区"
// 随机生成一个邮政编码(六位数字)。
"zip": Random.zip(), // "806124"
"zip2": '@zip()', // "806124"
// 把字符串的第一个字母转换为大写。
"capitalize": Random.capitalize('hello'), // "Hello"
"capitalize2": '@capitalize("hello")', // "Hello"
// 把字符串转换为大写。
"upper": Random.upper('hello'), // "HELLO"
"upper2": '@upper("hello")', // "HELLO"
// 把字符串转换为小写。
"lower": Random.lower('HELLO'), // "hello"
"lower2": '@lower("HELLO")', // "hello"
// 从数组中随机选取一个元素并返回。
"pick": Random.pick(['a', 'e', 'i', 'o', 'u']), // "e"
"pick2": '@pick(["a", "e", "i", "o", "u"])', // "e"
// 打乱数组中元素的顺序,并返回。
"shuffle": Random.shuffle(['a', 'e', 'i', 'o', 'u']), // ['o', 'a', 'i', 'e', 'u']
"shuffle2": '@shuffle(["a", "e", "i", "o", "u"])', // ['o', 'a', 'i', 'e', 'u']
// 随机生成一个 18 位身份证。
"id": Random.id(), // 112.127.151.37
"id2": '@id()' // 97.46.129.222
},
],
code: 200,
message: 'ok',
});
更多细节请参考官网。