Vue基础与相关使用

Vue

简单介绍

Vue是一款用于构建用户界面的JavaScript框架内,基于标准的HTML、CSS和JavaScript构建,同时提供了一套声明式的、组件化的编程模型。相比于传统的前端三件套,Vue框架提供了更为优秀的开发体验,能够帮助开发人员高效地开发用户界面。

项目创建

接下来可以创建一个Vue项目。项目创建可以通过npm来完成,在此之前我们需要确保Node.js相关环境已经安装成功。之后运行如下命令:

1
npm init vue@latest

这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。它会使用vite来进行项目构建。vite是一个轻量级、速度极快的构建工具。执行指令之后,我们将会看到诸如 TypeScript 和测试支持之类的可选功能提示,之后按照提示进行操作即可。这里需要注意的是,vite在引入组件的时候默认不支持省略.vue,为了配合它,需要在pycharm中设置引入时保留后缀.vue。相关设置可以查看参考文章。下面是一些目录说明:

  • node_modules:第三方库
  • public:首页html(index.html)等,一些静态资源可能要放到这个目录下面才可以被访问到
  • src:源码存储
    • assets:项目中的静态资源,css样式表、图片资源等
    • components:可复用的组件
    • main.ts:项目的入口文件,整个项目的运行需要先执行main.ts
    • App.vue:项目的根组件

在项目的实际开发过程中,我们是离不开安装外部依赖的,通常我们使用npm install进行。在使用npm install安装模块或插件时,有两种参数会把它们写入到 package.json 文件中去,在 package.json里面体现出的区别就是,使用 --save安装的插件,会被写入到 dependencies(需要发布到生产环境)对象里面去,使用--save -dev安装的插件,会被写入到 devDependencies (这里的插件只用于开发环境)对象里面去。

一个比较方便的工具,可以在本地模拟服务器。

1
npm install -g live-server

首先使用npm安装live-server,然后将Vue项目进行打包。项目打包之后会生成一个dist文件夹,在dist文件夹中直接输入live-server,就可以得到作为一个前端的简易服务器。

vue组件

简介

一个Vue项目的起始组件是src目录下的App.vue组件,这也是整个项目的根组件,这个根组件需要在main.ts中进行注册。而一个真实的项目大多都是由一棵嵌套的,可重用的组件树构成的。组件可以理解为是一个自定义标签,其中可以放入对应的内容,同时我们可以在其他地方通过标签引用这个组件,显示相应的内容。Vue项目中的组件统一放在src/components目录下。一个Vue组件通常遵循下面的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
test data {{count}}
</div>
</template>

<script>
export default {
name: "TestComponent",
data() {
return {
count: 0
}
},
methods: {}
}
</script>

<style scoped>

</style>

template标签中,定义了该组件的基本内容模板,在script标签中,定义了组件可以使用的一些相关属性和方法,其中基础的有name,data和methods,分别代表组件的名称,组件数据以及组件方法。在data中定义的数据可以在template中使用插值表达式{{}}进行获取,同时这里的数据,如果在data中发生变化,在页面上也会同步发生变化。而在style标签中,则定义了该组件使用的css风格。默认情况下是scoped,代表css仅在这个组件中局部生效。

组件注册

定义完成组件之后,如果需要在其他组件中使用,则需要经过一下步骤。首先使用import语法导入需要的组件,之后使用components注册组件,之后就可以以标签形式使用注册的组件了。注意,通过components注册的是私有子组件。如果在声明组件的时候,没有为组件指定name名称,则组件的名称默认是注册时候的名称。

1
2
3
4
5
export default {
components: {
ComponentA: xxxx
}
}

不过在<script setup>的单文件组件中,导入的组件可以直接在模板中使用,无需注册。

如果想要全局注册,则需要在vue项目的main.ts入口文件中,通过Vue.component()方法来完成,这个方法接收两个参数(名称字符串,对应组件),其中名称字符串就是给组件注册的名字,也就是后面使用的标签名称。

组件数据传递

父向子传递

组件之间的相互使用形成了父子组件的关系,在一些场景中就会涉及到组件之间传值的需求。其中完成数据从父组件传递到子组件的是prop

在子组件中指定prop,其中中可以指定字符串数组,也可以指定对象形式(其中包括各自的名称和类型),表示在组件中可以接收到外面的数据。在组件内部的模板中,需要使用插值表达式来将属性值展开{{}}

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
outer data: {{data}}
</div>
</template>

<script>
export default {
name: "TestComponent",
props:['data']
}
</script>

之后父组件可以在调用的时候通过标签进行数据传递:

1
<test-component data="app-data"/>

这里的关键就在于propsprops是组件的自定义属性,在封装通用组件的时候,合理地使用props可以极大提高组件的复用性。props属性的值可以直接在插值表达式中使用,同时props是只读的。props可以使用列表,也可以使用一个对象,用于指定默认值:

1
2
3
4
5
6
7
8
props:{
属性A:{配置选项}
init:{
default:0,
type:Number,
require:ture,//必填项约束
}
}

关于props中相关命名,有如下的说明:HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名)。也就是说在向子组件传递值的时候,需要使用短横线风格。

从父级prop到子组件中的数据流动是单向的。父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态。这意味着你应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

子向父传递

父组件向子组件传值,使用props。子组件向父组件传值,则可以使用自定义命令。

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
//父组件中调用子组件
<template>
<son @numchange='getNewcount'></son>
</template>

<script>
data(){
return {count_from_son:0}
},
method:{
getNewcount(value){
this.count_from_son = value
}
}
</script>

//子组件
<template>...</template>

<script>
data(){
return {count:0}
},
method:{
add(){
this.count += 1;
//通过$emit()触发自定义事件,参数为自定义事件名和事件传递参数
this.$emit('numchange',this.count)
}
}
</script>

兄弟传递

兄弟组件之间的数据共享方案:EventBus

首先定义一个中间方,创建eventBus.js模块,并向外共享一个Vue的实例对象

1
2
3
4
5
//中间方:EventBus 文件名:eventbus.js
import Vue from ‘vue’

//向外共享Vue的实例对象
export default new Vue()

在数据发送方,调用bus.$emit('事件名称',发送数据)方法触发自定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//兄弟组件A:数据发送方
import bus from './eventbus.js'

export defalut{
data(){
return {
msg:'hello,mybrother',
}
},
methods:{
sendMsg(){
bus.$emit('share', this.msg),
}
}
}

在数据接收方,调用bus.$on('事件名称',事件处理函数)方法注册一个自定义事件(需要在created生命周期中完成。自定义事件不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。也就是说需要完全匹配。这里建议不使用驼峰命名法,而是使用小写+短横线命名法( kebab-case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import bus from './eventbus.js'

export default{
data(){
return {
msgFromBro:''
}
},
created(){
bus.$on('share', value=>{
this.msgFromBro = value
})
}
}

动态组件

动态组件指的是组件之间的动态切换,动态的显示和隐藏。vue提供了一个内置的<component>组件,专门用来实现动态组件的渲染

1
2
<button @click="conName = 'Left'">切换left</button>
<component :is="conName"></component>

这里的<component>是一个占位符,给组件占用位置。其中is指定组件的名称,动态修改is的值,就可以完成组件的动态切换。通常是绑定事件来修改is对应的值。

默认情况下,组件切换会重新渲染,在每次进行组件切换的时候,隐藏组件会被销毁,显示组件会重新生成。我们可以使用keep-alive防止这种情况。使用keep-alive标签将上面的component包起来,就可以把内部组件缓存而不是销毁。

keep-alive包含相关的的生命周期函数,组件被缓存(deactivated)以及组件被激活(activated)。keep-alive作为一个标签,也有一些属性:

  • include:指定缓存哪些属性,多个属性使用逗号分隔。默认不指定表示缓存所有属性
  • exclude:指定哪些属性不被缓存,用法与include一致,但是不能和上面的include同时使用

vue指令

Vue指令是一种以v-开头的特殊语法,下面介绍一些常用的vue指令。

v-text:写在标签的属性当中,设置标签的内容,默认写法会替换全部内容(原有的标签的内容被覆盖),可以使用插值表达式{{}}来替换部分内容

v-html:设置标签的内嵌innerHTML,如果是普通字符,则和上面的v-text类似;如果是内嵌的html标签格式,那么可以渲染成html对应格式

v-on:为元素绑定事件,绑定触发条件以及调用函数,下面是一个示例,其中使用了一个语法糖,将v-on:替换为@,完整的写法为v-on:click='add'

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
<template>
<div>
count data: {{ count }}
<button @click="add">add</button>
<button @click="print('haha')">print</button>
</div>
</template>

<script>
export default {
name: "TestComponent",
data() {
return {
count: 0
}
},
methods: {
add: function () {
this.count += 1;
},
print: function (message) {
alert(message)
}
}
}
</script>

v-show:根据表达式的真假,切换元素的显示和隐藏。这里v-show接收的是一个字符串,里面可以的东西最终都是转化成布尔值,例如可以写'false' 'true' 'data' 'data<1',也可以写表达式。原理为修改标签的display来操纵样式。并且数据改变,元素同步更新

v-if:根据表达式的真假,切换元素的显示和隐藏,效果与v-show类似,但是这个的原理是直接操作DOM树,在审查元素中直接看不到该元素了。建议频繁的切换使用v-show,反之使用v-if。前者的切换消耗更小

v-bind:设置元素的属性,多用于动态设置,来绑定元素的attribute。完整使用为在标签中添加v-bind:src="data中的一个key",语法糖可以将v-bind:替换为:。如果需要动态绑定多个值,则可以使用不带参数的v-bind

v-for:根据数据生成列表结构,用于迭代遍历生成多个标签。这个指令通常与数组结合使用。

1
2
3
<ul>
<li v-for='item in arr'>{{con[item-1]}}</li>
</ul>

v-model:获取和设置表单元素的值,注意是双向数据绑定,一般与表单元素一起使用。

1
2
3
<input type="text" @keyup.enter='add' v-model='item_add'>
# v-model将数据绑定,获取用户输入
# @keyup.enter='add'表示将此时的数据加入列表当中

axios

axios是一个开源的可以用在浏览器端和NodeJs的异步通信框架,主要作用是实现AJAX异步通信。当然也可以用来存取本地存在的json文件。axios专注于网络请求,以较为简单的方式进行网络请求。下面是axios的基础语法。当然在使用之前,还需要安装axios模块,同时在需要使用的地方进行引入。

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
40
41
42
43
44
45
// 模块安装
npm install axios --save

// 引入
import axios from "axios";

// 语法:
axios({
method:'请求类型',
url:'请求的url地址',
params:{} //url中的查询参数(query)
data:{} //请求体参数(post)
}).then((result) => {
//.then用来指定请求成功后的回调函数
//形参中的result是请求成功之后的结果,是一个Promise对象
})

// eg:
axios.get("http://localhost:8080").then(response => {
console.log(response.data)
}).catch(reason => {
console.log(reason)
})

// 请求本地json
data:{
return {
nodes:[]
}
}
methods:{
async get_data(){
// 本地json放在public目录下
const {data: res} = await axios.get('/json/node.json');

for(var index in res){
var url = res[index]['image'];
res[index]['image'] = require('../assets'+url);
}
this.nodes = res;
}
},
created(){ // 生命周期
this.get_data()
}

调用axios方法得到的返回值是一个Promise对象,其中返回的不只是接口的数据,axios在请求得到数据之后,在这个数据之上进行了一层包装,因此其中还会有一系列其他参数数据。实际我们需要的业务数据放在data属性中。在获取到数据之后,我们可以在回调中进行处理。如果在获取过程中出现了错误,也可以通过catch进行获取并处理。

在axios的使用过程中,经常会结合async/await使用。async/await是一种基于Promise的解决异步的方案。async是一个加在函数前的修饰符,被async定义的函数会默认返回一个Promise对象resolve的值。因此对async函数可以直接then,返回值就是then方法传入的函数。

await 也是一个修饰符,只能放在async定义的函数内。可以理解为等待。await 修饰的如果是Promise对象:可以获取Promise中返回的内容(resolve或reject的参数),且取到值后语句才会往下执行;如果不是Promise对象:把这个非promise的东西当做await表达式的结果。

路由router

基本使用

路由router指的是地址与页面之间的对应关系。Vue官方推荐使用vue-router完成路由的使用。官方文档地址为:Vue Router (vuejs.org)。在使用之前需要安装对应的包,不过通常这一步在创建项目的配置过程中已经选中了。

要使用vue-router,首先需要配置路由模块。创建src/router/index.ts路由模块,并书写如下的初始化代码:

1
2
3
4
5
6
7
8
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: []
})

export default router

在这个路由模块中,最重要的是routes列表,这里面定义了所有的映射关系。例如我们可以如下定义。这里的component组件在使用之前需要引入。

1
2
3
4
routes: [
{path: '/test_1', component: TestPage1},
{path: '/test_2', component: TestPage2}
]

定义好路由之后,就可以在代码中使用,分别使用vue-router提供的路由组件进行链接声明和占位符定义:

1
2
3
<router-link to="/test_1">test_1</router-link>
<router-link to="/test_2">test_2</router-link>
<router-view></router-view>

除了router-link标签之外,也可以通过$router来访问路由实例。因此我们可以调用this.$router.push()来进行路由跳转。这点在实现按钮点击跳转的时候非常常用。这个方法可以接收是一个字符串路径,或者一个描述地址的对象,下面是官方文档中的示例:

1
2
3
4
5
6
7
8
9
const username = 'eduardo'
// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`) // -> /user/eduardo
// 同样
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` 不能与 `path` 一起使用
router.push({ path: '/user', params: { username } }) // -> /user

动态路由

动态路由指的是可以在路由中传入参数,例如/test_3/:id,任何形如/test_3/xxx的路由都会进行匹配,同时我们可以获取这个id参数。我们可以在路由模块中定义动态路由:

1
{path: '/test_3/:id', component: TestPage3}

这个参数可以通过$route.params.id获取。同时我们可以通过v-bind来指定router-link中的跳转路径,使其能够动态变化:

1
2
<router-link :to="/test_3/+count">test_3</router-link>
<router-view></router-view>

views文件夹

通过vite创建出的默认项目中会存在一个views目录。这个目录中通常会存放我们已经写好的各种页面,例如login、main等。src/componentssrc/views中均包含了Vue组件,区别在于views中存放的组件通常是一些已经写好的具体页面,是页面级组件,会被至少一个router使用,而在src/components中存放的是各种公共组件,例如header,sider等。在开发的时候通常建议将可重用的组件放在src/components中,而将能够通过路由访问到的视图组件存放在src/views中。

插槽Slot

Slot,意为插槽。这是vue为组件的封装者提供的能力,允许开发者在封装组件的时候,把不确定的,希望由用户指定的部分定义为插槽。我们可以把插槽认为是组件封装期间,为用户预留的内容的占位符。下面是插槽的基本使用:

1
2
3
4
5
6
7
8
9
<template>
<p>
这是第一个p标签
</p>
<slot>默认内容(自定义内容)</slot>
</template>

// 在使用的时候,自定义内容就会填充到插槽所在的位置
<my-com>自定义内容</my-com>

需要注意的是,没有预留插槽的自定义内容会被丢弃。同时在封装组件的时候,可以为预留的slot插槽提供默认内容,如果组件的使用者没有为插槽提供任何默认内容,则后备内容生效。

如果在封装组件的时候需要预留多个插槽节点,则需要为每一个slot插槽指定更具体的name名称,这种带有具体名称的插槽叫做具名插槽

1
2
3
4
5
6
7
8
9
10
11
<div>
<slot name='header'></slot>
<slot name='footer'></slot>
</div>

使用:
<my-com>
<template v-slot:header>
添加到指定的插槽
</template>
</my-com>

在具名插槽中,有一些规则,没有指定name名称的插槽,会有隐含的名称叫做“defalut”。如果没有指定填充到哪一个插槽,则填充到默认插槽。同时我们可以将v-slot:header简写为#header

在封装组件的过程中,可以为预留的slot插槽绑定props数据,这种带有props数据的slot插槽就叫做作用域插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
<div>
<slot :info="infomation" :msg="message"></slot>
</div>

// 使用
// 这里的scope可以接收到上面slot中的内容,包括info和msg。其中scope的名称可以自定义
<my-com>
<template v-slot="scope">{{scope.info}}</template>
</my-com>
// 使用对象的解构赋值
<my-com>
<template v-slot="{info,msg}">{{info}}{{msg}}</template>
</my-com>

状态管理 [deprecated]

注意,目前在Vue中推荐使用Pinia进行状态管理,vue-x的使用已经过时。Pinia的使用可以参考Pinia状态管理- EverNorif

在Vue中可以实现组件之间的数据共享,包括父子组件之间传值,兄弟组件之间传值。但是这种数据的传输只适合在小范围内进行,如果范围扩大的话,就会变得非常的繁琐。我们希望能有一种管理更大范围数据状态,同时复杂度较低的机制。Vuex就是这样一种实现组件全局状态(数据)管理的机制,可以方便的实现组件之间的数据共享。Vuex有如下优点:

  • 能够在vuex中集中管理共享的数据,易于开发和后期维护
  • 能够高效地实现组件之间的数据共享,提高开发效率
  • 存储在vuex中的数据都是响应式的,能够实时保持数据与页面的同步

一般来说,只有组件之间共享的数据才有必要存储到vuex中。

Vuex的基本安装只需要使用npm install即可。

1
npm install vuex --save

安装完成之后,需要导入Vuex包:

1
2
import Vues from 'vuex'
Vue.use(Vuex)

创建store对象:

1
2
3
const store = new Vuex.Store({
state:{count:0} //state中存放的就是全局共享的数据
})

将store对象挂载到vue实例中:

1
2
3
4
5
6
new Vue({
el:'#app',
render: h => h(app),
router,
store
})

下面将介绍Vuex中的核心概念

State:state提供唯一的公共数据源,所有共享的数据都要统一放到Store的State中进行存储。在组件中访问State数据有如下的方式:

1
2
3
4
5
6
7
8
9
10
//第一种方式
this.$store.state.全局数据名称

//第二种方式
import { mapState } from 'vuex' //按需导入mapState函数

//将全局数据映射为当前组件的计算属性
computed: {
...mapState(['count'])
}

Mutations:vuex不允许组件直接修改store中的公有数据,而是利用Mutations来变更store中的数据。虽然通过这种方式虽然操作起来稍微繁琐,但是可以统一监控所有的数据的变化,相当于提供一种统一管理的方法来修改store中的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const store = new Vuex.Store({
state:{count:0}, //state中存放的就是全局共享的数据
mutations: {
add(state){
state.count++
},
addN(state, step){
state.count += step
}
}
})
//在组件中触发mutation
this.$store.commit('add') //第一种方式
this.$store.commit('add', 3) //带参触发

//第二种方式
import { mapMutations } from 'vuex' //从vuex中按需导入mapMutations函数

//将需要的mutations函数,映射为当前组件的methods方法
methods:{
...mapMutations(['add','addN'])
}

Actions:mutations中不能使用异步函数,触发异步任务需要actions。如果通过异步操作变更数据,必须通过Action,而不能使用Mutation,但是在Action中还是要通过触发Mutation的方法间接变更数据

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
const store = new Vuex.Store({
state:{count:0}, //state中存放的就是全局共享的数据
actions: {
addAsync(context){
setTimeout((=>{
context.commit('add')
}))
},
addNAsync(context, step){
setTimeout((=>{
context.commit('addN', step)
}))
}
}
})
//在组件中触发actions
this.$store.dispatch('addAsync') //第一种方式
this.$store.dispatch('addNAsync', 3) //带参触发

//第二种方式
import { mapActions } from 'vuex' //从vuex中按需导入mapActions函数

//将需要的Actions函数,映射为当前组件的methods方法
methods:{
...mapActions(['addAsync','addNAsync'])
}

Getters:getter用于对Store中的数据进行加工处理形成新的数据。getter可以对store中已有的数据进行加工处理之后形成新的数据,类似Vue的计算属性。Store中数据发生改变,getter的数据也会跟着变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const store = new Vuex.Store({
state:{count:0}, //state中存放的就是全局共享的数据
getters:{
showNum(state){
return '当前最新的数量是' + state.count
}
}
})
//在组件中使用getters
this.$store.getters.名称 //第一种方式

//第二种方式
import { mapGetters} from 'vuex' //从vuex中按需导入mapGetters函数

//将全局数据映射为当前组件的计算属性
computed: {
...mapGetters(['count'])
}

监听器

在Vue中数据的改变会实时反应在页面上,数据是双向绑定的。同时在Vue中也可以使用watch监听器来监听某个对象的变化,当对象发生变化时,就会触发对应的行动。例如在下面的例子中,我们为count数据绑定了一个监听器方法,同时可以分别接收这个数据的新值和旧值。

1
2
3
4
5
6
7
8
9
10
11
12
export default {
data() {
return {
count: 0
}
},
watch: {
count(newCount, oldCount) {
alert("oldCount: " + oldCount + ",newCount: " + newCount)
}
}
}

如果想要监听一个对象中的值,则可以使用相应的路径,注意这里的引号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default {
data() {
return {
object: {
count: 0
}
}
},
watch: {
'object.count'() {
alert("count change")
}
}
}

不过watch监听器默认是浅层的,只有当在被监听的属性赋新值的时候才会触发,而嵌套属性的变化并不会被监听到。例如在上面的例子中,如果我们只监听object的话,该对象中count的变化并不会触发监听器。如果想要做到嵌套属性的监听,则需要深层监听器。不过仍然需要注意的是,如果只是内部嵌套属性变化,而object本身没有变化的话,那么这里的newObject和oldObject是相同的。

1
2
3
4
5
6
7
8
watch: {
object: {
handler(newObject, oldObject) {
alert("object change")
},
deep: true
}
}

watch相关还有一些知识,例如即使回调,回调触发时机,监听器停止等,更多细节可以参考官方文档。

参考文章

  1. Vue.js - 渐进式 JavaScript 框架 | Vue.js (vuejs.org)
  2. vite忽略.vue扩展名
  3. webstorm引入时不要忽略文件扩展名

Vue基础与相关使用
http://example.com/2023/03/29/Vue基础与相关使用/
作者
EverNorif
发布于
2023年3月29日
许可协议