Vue 是一个渐进式现代 JavaScript 框架,用于构建用户界面( UI )。它的目标是通过尽可能简单的 Api 实现响应式数据绑定和组合的视图集合( 组件 )

Vue 的核心只关注视图层,如果需要其它高级功能可以逐步引入( 如 路由、状态管理 等 )

Vue 起源于开源项目,由前 Google 员工尤雨溪开发并维护,它在开源社区中极具人气,尤其在亚洲地区广泛流行

MVVM 架构

MVVM 是一种软件架构模式,它将应用程序分成三层:模型(Model)、视图(View)、视图模型(ViewModel)

Vue.js 参照了 MVVM 架构的思想设计

2023-10-26 151205

  • 模型(Model)

    • 代表应用程序的数据和业务逻辑
    • 它是应用的核心部分,包含了所有的数据处理、验证以及对数据的操作
  • 视图(View)

    • 代表应用程序的用户界面( UI )。它呈现模型中的数据给用户,并通过用户交互反馈数据的变化
    • 在 Web前端中,通常由 HTML、CSS 组成,用于表示界面的结构和样式
  • 视图模型(ViewModel)

    • 连接模型( Model )和视图( View )。它是一个中间层,负责处理视图和模型之间的双向数据绑定
    • 将模型中的数据转换成视图可识别的格式( HTML DOM ),并将视图的操作反馈给模型

创建 Vue 工程项目

确保你的机器上安装了 Node.js,之后我们使用 Vite 脚手架来创建项目,它会帮我们生成项目所需的依赖,以及构建工具相关的配置等…

1
2
3
4
5
6
npm create vite
# 或者
npm init vite
# 或者
npx create-vite
# 之后根据提示输入项目名称和选择项目模版( Vue + TypeScript 或 Vue + JavaScript )

npm create 命令:

npm create 命令是用来运行远程的项目生成器( 脚手架 )的命令

它会自动下载并执行远程的 create-<package-name> 命名的包来创建一个项目

Vue 项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
my-vue-app/
├── node_modules/ # npm 包依赖目录
├── public/ # 静态资源目录(例如 index.html 和静态文件)
├── src/ # 源代码目录
│ ├── assets/ # 资源文件(图片、样式等)
│ ├── components/ # Vue 组件目录
│ ├── App.vue # 主应用组件
│ ├── main.js # 应用入口文件
├── .gitignore # Git 忽略文件
├── package.json # 项目的依赖和脚本配置
├── package-lock.json # 锁定的依赖版本信息
├── vite.config.js # Vite 构建工具的项目配置文件
└── README.md # 项目说明文件

Vue 的基本概念

响应式

响应式是 Vue 中的一种机制,指的是当数据发生变化时,Vue 会自动检测到这个变化并更新相关的视图。这种自动的同步更新被称为“响应式系统”。Vue 的响应式系统主要依赖于“依赖追踪”和“数据劫持”技术

组件化

image

组件化指的是将应用程序拆分为多个独立的、可复用的模块(组件),每个模块负责特定的功能或界面部分。组件化开发能够提升代码的组织性、复用性和可维护性,尤其适合开发大型的前端应用

在 Vue 中组件可以用 .vue 文件( 单文件组件SFC )表示,每个组件都封装了自己独立的模板逻辑(如数据和方法)、以及样式。它包含以下三个部分:

  • template:定义了组件的 HTML 结构
  • script:定义了组件的逻辑,包括数据、方法、生命周期钩子等
  • style:定义了组件的样式

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- src/components/CountBtn.vue 计数器按钮组件 -->
<script>
export default {
data() {
return {
count: 0
}
}
}
</script>

<template>
<button @click="count++">{{ count }}</button>
</template>

<style scoped>
button {
padding: 10px 38px;
border-radius: 10px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- src/App.vue 根组件 -->
<template>
<CountBtn />
</template>

<script>
import CountBtn from './components/CountBtn.vue';
export default {
components: {
CountBtn
}
}
</script>
1
2
3
4
5
6
7
/* src/main.js 应用程序的入口文件 */
import { createApp } from 'vue'
// 导入根组件
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

组件命名:

  • kebab-case命名: school / my-school

  • CamelCase命名: School / MySchool

  • 组件名不能用 HTML 已有元素的名称

注册组件:

  • 局部注册

    局部注册的组件只在指定的 Vue 实例或组件中可用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- Header组件 -->
    <template>
    <div class="header">Header组件</div>
    </template>

    <!-- scoped 局部CSS样式 -->
    <style scoped>
    .header {
    width: 100%;
    height: 75px;
    box-sizing: border-box;
    border: 10px solid black;
    font-size: 23px;
    text-align: center;
    line-height: 55px;
    }
    </style>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <template>
    <!-- 使用Header组件 -->
    <Header />
    </template>

    <script>
    // 导入Header组件(注册前需先导入对应组件)
    import Header from './components/Header.vue';

    export default {
    components: {
    // 注册Header组件, 只能在当前组件中使用
    Header
    }
    }
    </script>
  • 全局注册

    全局注册的组件可以在任何地方使用,通常在 main.js( 程序入口文件 ) 中进行全局注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /* src/main.js */
    import { createApp } from 'vue'
    // 导入Header组件
    import Header from './components/Header.vue';

    const app = createApp(App)
    // 注册全局组件
    app.component('Header', Header)

    app.mount('#app')

在 Vue 中全局注册使用相对较少,主要原因如下:

  • 命名冲突:全局注册的组件在整个应用中可见,容易与其它组件名称冲突
  • 难以维护:如果大量组件都全局注册,可能会造成组件引用混乱,管理维护变得复杂
  • 性能开销:全局注册的组件在应用加载时就被解析( 不管是否使用 ),可能会增加应用的初始加载时间

Vue.createApp 方法

描述:用来创建 Vue 应用实例的方法,它接收一个对象作为参数,该对象是当前 Vue 应用实例的根组件( Root Component )

组件中包含很多配置选项,常见的包括:

  • template:用于定义组件的模板

  • data:用于定义组件的数据,可以在模板中使用

  • methods:用于定义组件的方法,可以在模板中调用

  • ……

在实际开发中我们会在单个 Vue 应用实例中使用组件来构建复杂的单页面应用

app.mount 方法

描述:将 Vue 应用实例挂载到指定的 DOM 元素上以使 Vue 应用实例在页面中生效。Vue 应用实例挂在后,它会响应式地去渲染页面

语法:app.mount(挂载点)

挂载点可以是一个 DOM元素 或是一个 CSS 选择器,它会返回根组件的实例

模板 template

Vue 模板是一种描述视图的语法结构。它允许你将 HTML 和 Vue 的特殊语法结合在一起,创建动态的、响应式的用户界面

Vue 模板主要增加了以下几种语法:

插值( Mustache ):

  • 语法:{{ expression }} | 只能在元素内容中使用,不能在元素属性中使用

  • 双大括号( {{ }} )中的内容是 Vue 表达式,它可以访问组件实例中的数据

  • 作用:用于在模板中插入组件实例中的数据,实现数据的动态渲染,即数据发生变化时,视图自动更新变化

指令( Directive ):

  • 指令是带有特殊功能的 Vue 模板属性,它通常以 v- 开头,用于在 DOM 元素中添加特定行为和功能

Vue 表达式:

  • Vue表达式可以使用 JavaScript 表达式和一些常用 JavaScript 内置对象( 如 Date, Object 等 ),最重要的是它能访问组件实例上的属性

例:

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
<!-- 通过 cdn 链接来使用 Vue -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app"></div>

<script>
const { createApp } = Vue
const app = createApp({
// 定义组件的数据
/*
data 函数返回的对象会被 Vue 代理(new Proxy) ->
当 data 中的数据发生变化时, Vue 会重新解析模板, 从而使页面与数据保持同步
*/
data() {
return {
text: 'Hello, Vue!'
}
},
// 定义组件的方法
// 方法中的 this 指向组件的实例(被 Vue 代理的对象, this === vm)
methods: {
alert() {
alert(this.text)
}
},
// 定义组件的模板
// template 属性中的内容会被 Vue 解析( to Virtual DOM )
// 如果不指定 template 属性,挂载元素的内容就会成为 Vue 模板
template: '<button v-on:click="alert">click me!</button>'
})
// 挂载 Vue 应用实例,返回根组件的实例(被 Vue 代理的对象)
const vm = app.mount('#app')
</script>

style scoped(局部样式)

局部样式是指组件内的 CSS样式只在该组件内有效,而不会影响到其他组件的样式

要使用局部样式只需要在 <style> 标签上添加 scoped 属性即可:

例:

1
2
3
4
5
6
<!-- 局部样式, 如果去除 scoped, 则样式会对所有组件有效(全局样式) -->
<style scoped>
.box {
color: red;
}
</style>

style scoped 的原理:

Vue 会在组件的每个元素上添加一个自动生成的属性(如 data-v-xxxx),并将 style scoped 样式中的选择器转换为只匹配带有该属性的元素。例如,上面的 .box 样式会被编译为类似以下的代码:

1
2
3
4
/* data-v-xxxx 相当于一个身份标识, 每个组件生成的都不相同, 这样确保了组件中的样式只在该组件中有效 */
.box[data-v-xxxx] {
color: red;
}

Composition API(组合式 API)

Vue3 引入了组合式 API(Composition API),以便更灵活地组织和复用组件逻辑

Vue2 中使用 datamethods 等选项来定义组件状态和方法。Vue3 的组合式 API 则不再强制划分这些选项,可以自由组合和组织逻辑,使代码结构更清晰、复用性更强

setup 函数

在组合式 API 中,setup 函数是组件的入口。它在组件实例创建前调用。setup 函数返回的内容会暴露给模板使用

例:

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
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">Increase</button>
</div>
</template>

<script>
import { ref } from 'vue'

export default {
/*
setup 是一个钩子函数, 会被 Vue 调用
Vue 调用时会将 this 指定为 undefined, 因此无法使用 this 来访问组件实例
*/
setup() {
// 定义响应式数据
const count = ref(0)

// 定义方法
const increment = () => {
count.value++
}

/*
返回模板中使用的变量和方法, 如果不返回对象, 组件不会暴露任何内容给模板
Vue 在模板编译时,会优先从组件的 setupState 对象中访问,也就是这里返回的对象中访问,如果访问不到,则会从组件实例中访问
*/
return { count, increment }
}
}
</script>

ref 函数

  • ref 函数用于创建响应式数据的工具
  • 它会将数据包裹在一个名为 RefImpl 类型的代理( Proxy )对象中返回。在 setup 函数中要访问响应式数据必须通过对象中的 value 属性来访问,但在模版中无需通过 value 来访问,Vue 会自动解包( 在模板编译时, Vue 会自动将 RefImpl 类型对象转换为 ref.value )访问
  • value 属性的操作会被 Vue 拦截(get & set),当修改 value 属性的值为基本数据类型时,Vue 会响应式的更新,如果将其值修改为复杂数据类型(如 数组、对象)时,Vue 会调用 reactive 函数来创建复杂数据类型的响应式数据

reactive 函数:

reactive 函数用于创建复杂数据类型(如 数组、对象)的响应式数据,它会通过创建代理(new Proxy)的方式来实现响应式,这种方式可以实现数组或对象的深层响应式,但不能实现基本数据类型的响应式

toRefs 函数

toRefs 是 Vue3 中提供的一个工具函数,用于将一个响应式对象中的每个属性转换为单独的 ref 对象( RefImpl )。它的主要用途是解决直接解构响应式对象丢失响应性的问题

例:

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
<template>
<div>
<p>{{ person.name }}</p>
<p>{{ person.age }}</p>
<p>{{ person.gender }}</p>
</div>
</template>

<script>
import { toRefs, reactive } from 'vue'

export default {
setup() {
const person = reactive({
name: '张三',
age: 22,
gender: '男'
})
// toRefs 函数会自动调用 ref 函数, 将对象中的属性全部转换成 RefImpl 类型对象, 并且保持其响应性。如果不用 toRefs 函数直接解构, 则会丢失响应性
let { name, age } = toRefs(person)
// 解构出来的数据通过 value 属性来访问, 当修改 value 属性时, 源对象中对应的属性也会同时被修改(这里源对象就是 person)
name.value = '李四'
age.value = 19
return { person }
}
}
</script>

script setup(组合式 API 语法糖)

script setup 是 Vue3 提供的一个开发编译时的语法糖,用来代替传统的 setup 函数。它直接把 setup 的逻辑写在 <script setup> 标签内,无需显式定义 setup 函数

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- setup 启用组合式 API 语法糖 -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
const increment = () => {
count.value++
}
// script setup 语法不支持 export 导出
</script>

<template>
<div>
<p>{{ count }}</p>
<button @click="increment">Increase</button>
</div>
</template>

特点:

  • 无需显示的将变量或函数通过 return 暴露给模板,Vue 会在开发编译时自动将顶层( 全局作用域 )的变量或函数暴露给模板( setupState )
  • 导入的组件无需注册,Vue 会自动注册

props 组件间通信

props 是 Vue 中的一种机制,用于在父组件中向子组件传递数据。父组件可以在模板中给子组件添加属性将数据传递给子组件,而子组件则通过 props 接收这些数据,从而实现父子组件之间的通信

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 父组件: src/components/Parent.vue -->
<template>
<!-- 这里 : 是 Vue 的指令, 用于动态绑定数据, 如果去除 : 则是直接传递一个静态的字符串 -->
<Child :message="parentMessage" :count="5" />
</template>

<script>
import Child from './Child.vue';

export default {
components: { Child },
data() {
return {
parentMessage: 'Hello from Parent'
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 子组件: src/components/Child.vue -->
<template>
<div>
<p>Message: {{ message }}</p>
<p>Count: {{ count }}</p>
</div>
</template>

<script>
export default {
// 定义 props, 被定义的 props 将会被 Vue 代理到组件实例上, 我们可以直接通过 this 访问
props: {
message: String, // 接收字符串类型的数据, 如果父组件传递数据类型不同, 则会提示警告
count: {
type: Number, // 接收数字类型的数据, 可以通过数组来指定多个类型, 如: [Object, Array, Number]
default: 0 // 设置默认值, 对于数组和对象类型需要通过函数返回默认值
}
}
}
/*
数组写法定义 props, 无默认值和类型限定
props: ['message', 'count']
*/
</script>
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 父组件: src/components/Parent.vue -->
<template>
<!-- 这里 : 是 Vue 的指令, 用于动态绑定数据, 如果去除 : 则是直接传递一个静态的字符串 -->
<Child :message="parentMessage" :count="5" />
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

let parentMessage = ref('Hello from Parent')
</script>
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
<!-- 子组件: src/components/Child.vue -->
<template>
<div>
<p>Message: {{ message }}</p>
<p>Count: {{ count }}</p>
</div>
</template>

<script setup>
/*
defineProps 是 Vue 的宏函数(开发编译时运行, 仅限顶层使用), 用来定义 props,它无需 import 可以直接在 script setup 中使用
返回定义的 props, 等同于组件实例上的 $props
*/
const props = defineProps({
message: String, // 接收字符串类型的数据, 如果父组件传递数据类型不同, 则会提示警告
count: {
type: Number, // 接收数字类型的数据, 可以通过数组来指定多个类型, 如: [Object, Array, Number]
default: 0 // 设置默认值, 对于数组和对象类型需要通过函数返回默认值
}
})
/*
数组写法定义 props, 无默认值和类型限定
defineProps(['message', 'count'])
*/
</script>

props 可以通过 :指令(v-bind) 动态的传递数据,这意味着当父组件传递的 props 数据变化时,子组件会立即响应式的更新

props 在子组件中是只读的,不能直接修改,否则会提示警告

单向数据流:

单向数据流是指数据在组件之间的传递方向:从父组件流向子组件,但子组件不能直接修改从父组件传递过来的数据( props 只读 )。单向数据流确保了数据流向的单一性和可预测性,避免了数据状态的混乱导致状态管理变得复杂。如果需要更新 props( 修改 props ) 时,可以使用事件通知父组件

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 父组件: src/components/Parent.vue -->
<template>
<Child :count="parentCount" @increment="increaseCount" />
</template>

<script>
import Child from './Child.vue';

export default {
components: { Child },
data() {
return {
parentCount: 0,
}
},
methods: {
increaseCount() {
this.parentCount += 1
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 子组件: src/components/Child.vue -->
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="$emit('increment')">Increment</button>
</div>
</template>

<script>
export default {
props: ['count'],
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 父组件: src/components/Parent.vue -->
<template>
<Child :count="parentCount" @increment="increaseCount" />
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue';

let parentCount = ref(0)
const increaseCount = () => {
parentCount.value += 1
}
</script>
1
2
3
4
5
6
7
8
9
10
11
<!-- 子组件: src/components/Child.vue -->
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="$emit('increment')">Increment</button>
</div>
</template>

<script setup>
defineProps(['count'])
</script>

events 组件间通信

events 即组件事件,它是指组件之间通过自定义事件实现的通信方式。组件事件允许子组件向父组件发送消息,使父组件能够响应子组件中的操作。这种机制在 Vue 中主要是通过 $emitv-on(缩写 @)来实现

通过自定义事件实现子组件向父组件传递数据:

在 Vue 中,子组件可以使用组件实例上的 $emit 方法触发一个自定义事件,并可将数据传递给父组件。父组件通过 v-on(或 @)监听该事件,并在事件发生时接收数据和执行回调函数

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- src/components/Child.vue -->
<template>
<!-- 点击 button 时执行 handleChangeParent 方法 -->
<button @click="handleChangeParent">Change Parent</button>
</template>

<script>
export default {
methods: {
handleChangeParent() {
// 触发 changeMsg 事件, 并传递数据给父组件
this.$emit('changeMsg', 'Hello from child')
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- src/components/Parent.vue -->
<template>
<div>
<p>{{ msg }}</p>
<!-- 当子组件触发 changeMsg 事件时, 父组件执行 handleChangeMsg 方法, 用来接收子组件传递的数据 -->
<Child @changeMsg="handleChangeMsg" />
</div>
</template>

<script>
export default {
data() {
return {
msg: 'Hello Perent.vue'
}
},
methods: {
// data 参数由子组件通过 $emit 函数传递
handleChangeMsg(data) {
this.msg = data
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- src/components/Child.vue -->
<template>
<!-- 点击 button 时执行 handleChangeParent 函数 -->
<button @click="handleChangeParent">Change Parent</button>
</template>

<script setup>
// defineEmits 宏函数, 用来定义事件, 它返回 emits 函数, 用来触发定义的事件, 等同于 $emit
const emits = defineEmits(['changeMsg'])

const handleChangeParent = () => {
// 触发 changeMsg 事件, 并传递数据给父组件
emits('changeMsg', 'Hello from child')
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- src/components/Parent.vue -->
<template>
<div>
<p>{{ msg }}</p>
<!-- 当子组件触发 changeMsg 事件时, 父组件执行 handleChangeMsg 方法, 用来接收子组件传递的数据 -->
<Child @changeMsg="changeMsg" />
</div>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

let msg = ref('Hello Perent.vue')
// data 参数由子组件通过 emits 函数传递
const changeMsg = data => {
msg.value = data
}
</script>

Vue 推荐事件及 props 的名称都使用 “kebab-case” 的命名方式,比如 child-clicksubmit-form。这样格式更统一,可读性也更强