简介 | Vue.js (vuejs.org)

基础介绍

环境准备

vue安装

方式一 vue CLI脚手架

npm安装:npm install -g @vue/cli,通过vue create 项目名创建

方式二 vite

npm安装:npm init vue@latest

注意: cli(webpack编写)和vite(es6编写)是完全不同的,两者在配置上有区别。

项目配置

Vue创建项目的步骤_npm init vue@latest-CSDN博客

框架介绍

单文件组件

单文件组件( .vue 文件,缩写为 SFC)。顾名思义,Vue 的单文件组件会将一个组件的逻辑 (JavaScript),模板 (HTML) 和样式 (CSS) 封装在同一个文件里。

声明式渲染

Vue 的核心功能是声明式渲染:通过扩展于标准 HTML 的模板语法,我们可以根据 JavaScript 的状态来描述 HTML 应该是什么样子的。当状态改变时,HTML 会自动更新。

API风格

选项式API:

可以用包含多个选项的对象来描述组件的逻辑,例如 datamethodsmounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script>
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 `this` 上
data() {
return {
count: 0
}
},

// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件处理器绑定
methods: {
increment() {
this.count++
}
},

// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
mounted() {
console.log(`The initial count is ${this.count}.`)
}
}
</script>

<template>
<button @click="increment">Count is: {{ count }}</button>
</template>

组合式API:

可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与<script setup>搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
count.value++
}

// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
<button @click="increment">Count is: {{ count }}</button>
</template>


模板语法

data 文本插值

使用**data组件选项声明响应式状态,此选项是一个返回对象的函数**。

1
2
3
4
5
6
7
8
export default {
data() {
return {
message: 'Hello World!',
htmlMessage: '<h1>Hello World!</h1>'
}
}
}

返回的属性对象可以在模板中使用,使用双花括号法渲染动态文本:

1
2
3
<h1>{{ message }}</h1>
// 花括号中的内容可以使用任何能被求值的js表达式(能够写在`return`后面)
<h1>{{ message.split('').reverse().join('') }}</h1>

v-html

双大括号的内容为纯文本,而不是html。插入html语句时需要使用v-html

1
<p v-html='htmlMessage'></p>

v-bind 属性绑定

属性绑定静态值与html相同:

1
<h1 class="title">Make me red</h1>

但这样不能绑定动态值,双大括号也不能绑定html属性。使用v-bind可以给动态绑定属性。

和文本插值类似,指令的值是可以访问组件状态的 JavaScript 表达式。例如:

1
<div v-bind:id="dynamicId"></div>

v-bind:id可以简写为:id

冒号后面的部分 (:id) 是指令的“参数”。此处,元素的 id 将与组件状态里的 dynamicId 属性保持同步。

如果绑定的值是nullundefined,该属性则会被移除

v-bind的多种使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script>
export default {
data() {
return {
titleClass: 'title',
errorClass: 'text-danger',
isActive: true,
hasError: false,
object:{
isActive: true,
hasError: false
}
}
}
}
</script>
<template>
<!-- 1. 此处添加一个动态 class 绑定data中的属性,通过此属性内容再绑定js表达式中的内容 -->
<h1 :class="titleClass">Make me red</h1>
<!-- 2. 表示color和text-danger是否存在取决于isActive和hasError的真假值 -->
<div :class={ color: isActive ,'text-danger': hasError }></div>
<!-- 3. 可以直接绑定一个对象,与2渲染结果相同 -->
<div :class="object"></div>
<!-- 4. 可以绑定一个数组来绑定多个class -->
<div :class="[titleClass, errorClass]"></div>
</template>
<style>
.title {
color: red;
}
</style>

v-on 事件监听

使用v-on监听DOM事件,v-on可简写成@:

1
2
<button v-on:click="increment">{{ count }}</button>
<button @click="increment">{{ count }}</button>

函数声明放在methods选项中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {this.count++},
say(message) {alert(message)},
warn(message, event) {
// 这里可以访问 DOM 原生事件
if (event) {event.preventDefault()}
alert(message)
}
}
}
</script>
<template>
<!-- 内联事件处理器 -->
<button @click="count++">Add 1</button>
<!-- 方法事件处理器 -->
<button @click="increment">Count is: {{ count }}</button>
<!-- 事件处理器中传参 -->
<button @click="say('hello')">Say hello</button>

<!-- 事件处理器中传入事件参数 -->
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">Submit</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.',event)">Submit</button>
</template>

事件修饰符:事件处理 | Vue.js (vuejs.org)


v-model 表单绑定

通过v-on和v-bind的使用可以使元素与属性内容双向绑定,但手动使用事件监听器修改值是一件麻烦的事,使用v-model可以简化此步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
export default {
data() {
return {
text: ''
}
}
}
</script>

<template>
<input v-model="text" placeholder="Type here">
<p>{{ text }}</p>
</template>

v-model 会将被绑定的值与 <input> 的值自动同步,这样就不必再使用事件处理函数了。

更多操作:表单输入绑定 | Vue.js (vuejs.org)


v-if 条件渲染

使用v-if对元素是否渲染进行条件判断,当条件为真时渲染,否则不渲染。

也可以使用 v-elsev-else-if 来表示其他的条件分支:

1
2
3
4
5
<template>
<button @click="!isToggle">Toggle</button>
<h1 v-if="isToggle">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
</template>

v-show

v-show也可以设置按条件显示元素:

1
<h1 v-show="ok">Hello!</h1>

不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。

v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。

v-ifv-show

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。

v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。

相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。


v-for 列表渲染

使用v-for 渲染一个基于源数组的列表,指令的值需要使用 item in items 或者item of items形式的特殊语法,其中 items 是源数据数组,而 item 是迭代项的别名:

1
2
3
4
5
6
7
8
9
10
data() {
return {
parentMessage: 'Parent',
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
<li v-for="item in items">{{ item.message }}</li>
<!-- 添加顺序 -->
<li v-for="(item, index) in items">{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

渲染为:

image-20240518175058078

通过key管理状态:Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。

为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute:

1
<div v-for="item in items" :key="item.id">

**v-for可以遍历范围:**但起点为1,不是0。

1
<span v-for="n in 10">{{ n }}</span>

v-for更新数组列表有两种方式。

在源数组上添加,如:this.todos.push(newTodo)

使用新的数组替代原数组,如:this.todos = this.todos.filter(/* ... */)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<script>
// 给每个 todo 对象一个唯一的 id
let id = 0

export default {
data() {
return {
newTodo: '',
todos: [
{ id: id++, text: 'Learn HTML' },
{ id: id++, text: 'Learn JavaScript' },
{ id: id++, text: 'Learn Vue' }
]
}
},
methods: {
addTodo() {
this.todos.push({ id: id++, text: this.newTodo })
this.newTodo = ''
},
removeTodo(todo) {
this.todos = this.todos.filter((t) => t !== todo)
}
}
}
</script>

<template>
<form @submit.prevent="addTodo">
<input v-model="newTodo" required placeholder="new todo">
<button>Add Todo</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="removeTodo(todo)">X</button>
</li>
</ul>
</template>

computed 计算属性

模板中可以进行表达式计算,但重复且复杂的计算会使模板显得臃肿。使用计算属性在**computed**中描述依赖响应式状态的复杂逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default {
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
]
}
}
},
computed: {
// 一个计算属性的 getter
publishedBooksMessage() {
// `this` 指向当前组件实例
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
}

<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>

计算属性computed与方法methods的差别:

1
2
3
4
5
methods: {
calculateBooksMessage() {
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}

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

相比之下,方法调用总是会在重渲染发生时再次执行函数。


ref 模板引用

通过ref模板引用可以手动操作DOM。

通过给元素添加ref属性,可将此元素挂载到ref属性名上:

1
<input ref="input">

挂载结束后引用都会被暴露在 this.$refs 之上。需要使用此元素时,通过this.$refs.属性名进行引用(注意是refs)。

1
2
3
4
5
6
7
8
9
10
11
12
<script>
export default {
mounted() {
<!-- 可以访问原生js的属性 -->
console.log(this.$refs.input.innerHTML)
}
}
</script>

<template>
<div ref="input" />hello</div>
</template>

除非特殊需求,一般不建议直接操作DOM。



组件

component 注册组件

vue组件在使用前需要先进行注册,这样在渲染时才能找到对应实现。

全局注册

在main.js中使用App.components()全局注册组件:

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './App.vue'
// 1. 引入组件
import Register_g from './components/register_g.vue'
const app = createApp(App)
// 2. 全局注册组件
app.component('register_g',Register_g)

app.mount('#app')

在任一vue中均可直接应用:

1
2
3
4
<template>
<!-- 3. 显示组件 -->
<register_g />
</template>

局部注册

当单一vue需要引用组件时,直接在此vue上引用并注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<!-- 3. 显示组件 -->
<register/>
</template>

<script>
// 1. import 引入组件
import register from "./components/register.vue"
export default{
// 2. components 注入组件
components:{
register
}
}
</script>

局部注册的组件在后代组件中不可用。

props 传递数据

使用props可以实现父级向子级传递数据。例如以下情况:

父级app.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<register :turnOn="login"/>
<div @click="login=!login">登录/注册</div>
</template>
<script>
export default{
components:{
register,
},
data(){
return{
login:false
}
}
</script>

子级register.vue:

1
2
3
4
5
6
7
8
<template >
<div class="window" v-if="turnOn"></div>
</template>
<script>
export default{
props:['turnOn']
}
</script>

可以实现控制register页面显示的效果。

在子级注册props属性,添加需要接收属性的字符串,即可在父级使用组件时传递数据。

props在子级是只读,不能修改的。

**props类型效验:**可以指定传入的数据类型

1
2
3
4
5
6
7
8
9
10
 props:{
turnOn:{
<!--可以规定类型,使用数组[]可以规定多种类型-->
type:Boolean,
<!--设置默认值-->
default:false,
<!--设置传参是否必要-->
required:true
}
}

props可以实现子传父:使用props让父级给子级传递一个函数,子级通过此函数传递数据给父级。但用$emit更易理解且操作方便。

$emit 组件事件

通过设置$emit组件事件,可以实现子级对父级的事件调用。

子级设置this.$emit("自定义事件名")调用自定义事件:

1
2
3
4
5
<script>    
methods:{
closeEvent(){this.$emit("closeMenu")}
}
</script>

父级通过自定义事件实现事件调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<register :turnOn="login" @closeMenu="openLoginMenu"/>
</template>
<script>
export default{
components:{register,},
data(){return{login:false}},
methods:{
openLoginMenu(){
this.login=!this.login;
}
}
}
</script>

子级可以通过调用closeEvent()向父级调用自定义事件closeMenu,进而调用父级函数,修改参数。

attributes 透传

调用组件时,可以添加class、style、id等属性,当组件以单个元素为根作渲染时,透传的属性会添加到根元素上。

1
2
3
<template>
<myComponent class="abc"/>
</template>

子组件可以禁用attributes继承:

1
2
3
4
5
<script>
export default{
inheritAttrs:false
}
</script>

透传在实际使用较少,了解即可。

slots 插槽

组件设置slot,父级可以添加元素到slot对应的位置上。

1
2
3
4
5
6
7
8
9
10
<!-- 父级 -->
<FancyButton>
Click me <!-- slot content -->
</FancyButton>
<!-- 子组件FancyButton 设置slot -->
<template>
<button class="fancy-btn">
<slot/> <!-- slot -->
</button>
</template>

父级可以直接使用双括号添加动态元素。

组件的slot可以添加默认内容,当父级没有传入时会自动显示。

1
<slot>HELLO<!-- 默认内容 --></slot>

v-slot 具名插槽

父级传入多个插槽时,通过给插槽命名,可以指定插槽位置。

具名插槽需要使用一个含 v-slot(简写为#) 指令的 <template> 元素,并将目标插槽的名字传给该指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 父级 -->
<BaseLayout>
<template v-slot:header><!-- header 插槽的内容放这里 --></template>

<!-- 隐式默认插槽 -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>

<template #[footer]><!-- 动态名字footer 插槽的内容放这里 --></template>
</BaseLayout>


<!-- 子级 -->
<div class="container">
<header><slot name="header"></slot></header>
<footer><slot name="footer"></slot></footer>
</div>

传入多个插槽时,未使用template包裹的部分会视作默认插槽。

动态插槽名使用[]包裹即可。

条件插槽

需要根据插槽是否存在来渲染内容时,可以通过$slots实现。

1
2
3
4
5
6
<template>
<div class="card">
<div v-if="$slots.header" class="card-header"><slot name="header" /></div>
<div v-if="$slots.default" class="card-content"><slot /></div>
</div>
</template>

当header, default存在时,会得到额外的class渲染。

后端接口

axios

AJAX笔记 | Luvmand

跨域

以vite举例:vite.config.js文件添加proxy代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default defineConfig({
server: {
// proxy 配置代理会规则
proxy: {
// 需要代理的接口路径,以'/api'为开头的访问路径会被代理至target目标地址
'/api': {
// 需要访问的服务器地址
target: 'http://192.168.1.103:80',
// 是否允许跨域
changeOrigin: true,
// 重写请求路径,将路径中的'/api'重写为''
pathRewrite: {
'^/api': ''
}
}
}
}
)}

注意,cli 要将server换成devServer。vite和cli不一样。踩了很久的坑。