Vue - Vue3的特性介绍以及项目的简单创建
前言
近期,我需要支援兄弟组帮忙写页面,而对应使用的前端框架是Vue3
,不再是我自己熟悉的React
。趁此机会,复习一下Vue
(会,但是不熟)以及学习下Vue3
相关知识。这里我主要看的是大圣老师的《玩转 Vue 3 全家桶》
一. 前端框架的发展史
这一块看下来,受益匪浅。因此我整理了一下大圣的文章。我们先来看下前端的发展史:
石器时代:
- 前端网页基本上都是静态页面。前端工作则一般都是写一下
HTML+CSS
样式。 - 但是随着后端的复杂度不断的增加,出现分层(
MVC
),需要在前端模板显示需要的后端数据。因此就有了JSP
,就有了动态网页。但是它有着一定的缺点:任何数据更新都需要刷新页面。耗时。 - 推出了
Ajax
技术,即允许我们异步获取数据并且刷新页面。因此前端不再受限于后端。Web2.0
的时代到来。
铁器时代:
- 铁器时代到来的标志就是
JQuery
的出现。它的使用步骤很简单:靠$
来找到某个元素,再对该元素进行DOM
操作。JQuery
成为了前端初步的主流技术。
工业时代:
- 随着前端规模的逐渐升级,前端需要进行规模化的时候。2009年就出现了
NodeJs
以及AngualarJs
(想了想我当时实习的时候,用的就是它,那不知不觉中,我三大框架都算接触过了) - 三大框架的产生:
Angular、Vue、React
。
1.1 三大前端框架
三大框架,他们的主要区别就是针对同一个问题有着不同的解决方案。即:当数据发生变化后,我们如何取通知页面并进行更新渲染?
① Angular
先说下Angular
,也是我自己最先接触的框架:
Angular
采取的是脏检查:每次用户交互时都检查一次数据是否变化,有变化就去更新DOM
。
② Vue
其次是Vue
:
Vue1
采取响应式:初始化的时候,每个数据都有自己专属的Watcher
监听器。这样数据发生改变的时候,就能精确地知道哪个数据发生改变,即可以针对性的修改对应的DOM
。如图:
③ React
React
对于数据监听的实现:
- 初始化的时候,在浏览器
DOM
之上,创建一个虚拟DOM
:用一个JSON
对象来描述整个DOM
树。通过虚拟DOM
的计算来识别出变化的数据,再进行精确的修改。
例如这么一段页面代码:
<div id = "app">
<p class = "item">Item1</p>
<div class = "item">Item2</div>
</div>
对应的虚拟DOM
对象为:
{
tag: "div",
attrs: {
id: "app"
},
children: [
{
tag: "p",
attrs: { className: "item" },
children: ["Item1"]
},
{
tag: "div",
attrs: { className: "item" },
children: ["Item2"]
}
]
}
1.2 Vue 和 React 对比
首先,在数据发生变化之后,通知页面进行更新的方式不同:
Vue
:数据改变,框架主动告知你哪些数据被修改。(每个数据都对应着一个监听器)React
:只能通过新老数据的计算来得知数据的变化。(虚拟DOM
的Diff
算法,这里可以看下我的这篇文章:ReactDOM的Diffing算法)
但是从他们的实现角度来说,就有各自的优劣势了。我们来说下性能瓶颈问题:
Vue
:每个响应式数据和一个Watcher
进行绑定监听。比较损耗性能。在项目比较大的情况下,影响性能。React
:采取虚拟DOM
的Diff
计算,如果虚拟DOM
树太大了,计算时间超过了16.6ms
。(1秒 / 60帧),那么就容易造成卡顿。
那么为了解决这种性能瓶颈问题,两大框架又引入了不同的解决方案:
-
Vue
:为了解决响应式数据过多导致的内存占用问题,推出Vue2
,使用虚拟DOM
:组件之间的变化,通过响应式来更新通知,而组件内部的数据变化则通过虚拟DOM
来更新页面。将Watcher
控制在组件级。 如图:
-
React
:参考操作系统的时间分片概念,引入Fiber
架构。即将虚拟DOM
树进行微观化,变成链表。在浏览器的空闲时间来计算Diff
。一旦有级别更高的任务,可以暂停计算,将主进程的控制权交还给浏览器。等待下次空闲时间。 如图:
在书写模板上,两者也有区别:
Vue
:采用template
,例如:v-for,v-if
等语法。语法固定。React
:采用JSX
,拥有JS
的全部特性。
备注:
- 目前浏览器大多是 60Hz(60帧/s)。
React
和Vue
都是单向的数据流,和我们通常说的双向绑定不是一个东西,指的是Vue
中的v-model
这样的语法糖。
1.3 Vue2 简单使用
安装命令:
npm install vue-cli -g
然后快速创建:
>vue init webpack "vue2-test"
如图:
我们来看下Vue
的基础用法。首先我们要npm install
下,再npm run dev
启动。下文内容只需要修改App.vue
文件即可。
1.3.1 数据展示
需求内容:
- 在输入框输入内容时候,另外一个区域能够实时的更新显示文本框内容。
编码需要注意的点:
- 首先需要在
data
模块中声明数据。在展示的标签中,例如Input
框,通过v-model
进行数据的双向绑定。 - 在
HTML
模板里面,使用{{}}
来标记数据即可显示。
案例如下:
<template>
<div id="app">
<h1>{{title}}</h1>
<input type="text" v-model="title" />
</div>
</template>
<script>
export default {
name: 'App',
// 数据声明的地方
data: function () {
return {
title: '',
}
}
}
</script>
效果如图:
1.3.2 集合渲染和用户交互
需求:
- 我们能够展示一个清单列表,表示我们即将的事情(集合)。
- 在文本框中输入的内容,进行回车后,能够将其添加到清单列表中。同时页面能够更新列表。
- 每个清单前面有一个复选框,勾上代表这个清单已经做过。同时计算所有清单中未完成和完成的个数统计。
- 有一个按钮可以清除所有的清单,如果清除了,页面显示“暂无要做清单”。
编码需要注意的点:
- 数组在
vue
中的循环采用v-for
来进行遍历。 Input
文本框需要绑定交互事件。vue
中使用@
来标记用户的交互操作。@click
是点击操作。@keydown
是键盘敲击。- 在
vue
中,冒号:
开头的属性是用来传递数据的。 事件相关的函数一般在method模块中编写。 - 还有一个属性为
computed
,一般用于放计算属性,内置缓存功能。如果依赖数据没变化,多次使用计算属性会直接返回缓存结果。 vue
可以使用v-if,v-else
结合的方式,来判断页面展示的内容。
案例如下:
<template>
<div id="app">
<h1>{{ title }}</h1>
<input type="text" v-model="title" @keydown.enter="addTodo" /> <button @click="clear">清除清单</button>
<!-- 如果清单数>0,展示清单内容 -->
<div v-if="todoList.length > 0">
<ul>
<!-- 还要绑定下唯一标识key -->
<li v-for="(todo, index) in todoList" v-bind:key="index">
<input type="checkbox" v-model="todo.done" />
<span :class="{ done: todo.done }"> {{ todo.title }}</span>
</li>
</ul>
<div style="color: red">未完成: {{ activeList }}</div>
<div style="color: blue">已完成: {{ doneList }}</div>
</div>
<!-- 否则,清单数=0 -->
<div v-else>
<h1>暂无要做清单</h1>
</div>
</div>
</template>
<script>
export default {
name: 'App',
// 响应数据声明,记得要把所有变量都返回出去
data: function () {
return {
title: '',
todoList: [
// done 代表是否处理过这个清单
{ title: '吃饭', done: false },
{ title: '购物', done: false },
{ title: '学习', done: false },
{ title: '睡觉', done: true }
]
}
},
// 函数声明
methods: {
// 添加一个清单,清单名称title和Input框进行了v-model绑定,刚加入的清单属于未处理。
addTodo () {
this.todoList.push({ title: this.title, done: false })
// 添加完成之后,清空文本框内容
this.title = ''
},
clear () {
this.todoList = []
}
},
computed: {
activeList () {
return this.todoList.filter((v) => !v.done).length
},
doneList () {
return this.todoList.filter((v) => v.done).length
}
}
}
</script>
<style>
#app {
margin-left: 45%;
}
/* done的样式,就是文字变暗,然后加一条横线 */
.done {
color: gray;
text-decoration: line-through;
}
</style>
效果如下:
其实这些案例看下来,我们可以发现,在使用vue
上,我们只需要关注数据本身的状态,无需关注数据如何进行状态更新。不像React
。需要使用useState、useEffect
等操作来完成响应式和监听。
二. Vue3简介和Vite脚手架的使用
再说Vue3
之前,我们先来说下Vue2
的一些问题。先从上面的案例出发,我们来看几个问题。
- 当代码量增多,尤其是响应式变量增多,而且还和函数进行关联绑定的时候。当代码行很多的时候,在代码编写的时候,无法避免的会在代码中上下滚条滚动,即反复横跳。编写不方便。 即所有的
data、methods、computed
都在一个对象里配置,不易维护。 Vue2
采用Flow.js
进行校验(做静态类型检查的),而Flow.js
已经停止维护,目前主流的则使用TS来维护。Vue2
响应式是基于Object.defineProperty()
实现的。它是对某个属性进行拦截来完成的。无法监听删除数据的操作,需要外部API
辅助监听。
2.1 Vue3 新特性
Vu3
在继承了Vue2
具有的响应式、虚拟DOM
、组件化等优点的情况下,在很多方面还有了改进。我们来探讨下。我们只说几个比较重要的地方。
2.1.1 响应式的实现方式
在vue2
中,如上文所说,是根据 Object.defineProperty()
实现的。我们来看一下实际的用法。案例如下:
let number = 18
let person = {
name: '码农',
sex: '男',
age: 18
}
Object.defineProperty(person, 'age', {
// 当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
get() {
console.log('有人读取age属性了,值是:', number)
return number
},
// 当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
set(value) {
console.log('有人修改了age属性,值是:', value)
number = value
}
})
// 读取,调用到get函数
console.log(person.age)
// 赋值修改,调用到set函数
person.age = 20
脚本运行之后,结果如下:
vue2
就是通过这样的拦截方式结合发布订阅来实现响应式功能的。vue3
则使用了ES6
的ProxyAPI
进行数据代理。通过 reactive()
函数给每⼀个对象都包⼀层 Proxy
,通过 Proxy
监听属性的变化。案例如下:
let number = 18
let person = {
name: '码农',
sex: '男',
age: 18
}
const p = new Proxy(person, {
//当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
get() {
console.log('有人读取age属性了,值是:', number)
return number
},
//当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
set(value) {
console.log('有人修改了age属性,值是:', value)
number = value
}
})
console.log(p.age)
p.age = 20
执行结果如下:
我们可以发现,使用Proxy
的好处:
Object.defineProperty()
只能监听一个对象的某个属性,而Proxy
可以全局监听。Proxy
还可以监听数组。
2.1.2 Vue3 支持碎片(Fragments)、
vue2
中只能允许有一个根节点。
<template>
<div id="app">1</div>
<div>2</div>
</template>
否则就会报错:
而vue3
中则可以有多个根节点。
2.1.3 使用 TypeScript 重构
我们知道,JS
是弱类型的语言,一般编码的时候无需指定类型。而TS是强类型系统语言。带来了更方便的提示,并且让我们的代码能够更健壮。
2.1.4 Composition API 组合语法
vue2
使用的是选项类型API(Options API)
,而vue3
使⽤的是组合式API(Composition API)
首先看下一些属性的定义。vue2
中的写法:data、methods、computed
分开来写。
<template>
<div id="app">
<h1 @click="add">{{count}} * 2 = {{double}}</h1>
</div>
</template>
<script>
export default {
name: 'App',
// 响应数据声明,记得要把所有变量都返回出去
data: function () {
return {
count: 1
}
},
// 函数声明
methods: {
add () {
this.count++
}
},
computed: {
double () {
return this.count * 2
}
}
}
</script>
而vue3
中则可以这么写在一起(可以先看2.2节,把项目创建出来即可):
方式一
<template>
<div>
<h1 @click="add">{{ state.count }} * 2 = {{ double }}</h1>
</div>
</template>
<script lang="ts">
import { reactive, computed, defineComponent } from "vue";
export default defineComponent({
setup() {
const state = reactive({ count: 1 });
function add() {
state.count++;
}
const double = computed(() => state.count * 2);
return { state, add, double };
},
});
</script>
方式二,使用<script setup>,无需将属性和函数return出去
<template>
<div>
<h1 @click="add">{{ state.count }} * 2 = {{ double }}</h1>
</div>
</template>
<script setup lang="ts">
import { reactive, computed } from "vue";
const state = reactive({ count: 1 });
function add() {
state.count++;
}
const double = computed(() => state.count * 2);
</script>
效果如下:
稍微总结下:
- 所有
API
通过import
来引入。 - 可以把同一个功能模块的
methods、data
放到同一个函数中书写(setup
函数),维护起来更轻松。如图:
setup
函数可以使vue3
中属性和方法的入口,它可以接受两个参数setup(props,context)
props
:接收来自父组件传来的参数。context
:上下文,主要包含3个使用参数:attrs,emits,slots
。
一般需要将对象或者渲染函数return
出去,入上文的案例。当然也有一种替代方式,就是使用 <script setup>
特性来清除这种return
写法。同时我们还注意到,代码中已经没有this
这样的语句了。
同时,生命周期钩子函数也有一定的不同:
2.1.5 vite 支持
虽然vite
并不在vue3
的包内,也并非和其强绑定。但是现在使用vue3
的时候,基本上都会和vite
一并使用。Vite
和Webpack
是同一层级的框架。他们的作用相同。
Webpack
原理,就是根据你的 import
依赖逻辑,形成一个依赖图,然后调用对应的处理工具,把整个项目打包后,放在内存里再启动调试。因此需要预打包,在项目复杂程度不断增加的时候,启动项目的时间就会越来越长。而 Vite
就是为了解决这个时间资源的消耗问题出现的。打包原理如图:
在大部分浏览器已经默认支持ES6
的import
语法的背景下,Vite
只把首页依赖的文件,依次通过网络请求去按需加载获取。
这里是我启动vue2
项目的时候,控制台的输出:我这里大概8秒钟。
这是我启动vue3
项目的时候, 控制台的输出:大概1秒钟。
2.2 Vue3 简单使用(Vite 构建)
使用命令:
npm create vite@latest
会依次让你选择:
- 项目名称。
- 使用哪个框架。
Vue、React
等。 - 使用的语言。
结果如图:
2.2.1 全局组件的注册
我们可以随便创建一个自定义的组件:
注册完后,在vue3
中,可以再入口处main.ts
文件和中进行全局组件的注册:
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import MyComponent from './components/MyComponent.vue'
createApp(App)
// 注册
.component('MyComponent', MyComponent)
.mount('#app')
这样在其他的任何一个组件当中,都可以直接显式地引用名称为MyComponent
的组件了,如图:
效果如下:
2.2.2 响应式数据声明
先给出案例:
<template>
<div>
<h1 @click="add">{{ state.count }} * 2 = {{ double }}</h1>
</div>
<div>
数字:{{num}}<button @click="add2" style="color:red;background:yellow">数字加1</button>
</div>
<MyComponent />
</template>
<script setup lang="ts">
import { reactive, computed, ref } from "vue";
let num = ref<number>(1);
const state = reactive({ count: 1 });
const add = () => {
state.count++;
};
const add2 = () => {
num.value++;
};
const double = computed(() => state.count * 2);
</script>
效果如下:
从上面的案例我们还注意到,我们使用了两种方式来声明不同的数据。
reactive
:用于声明引用数据类型,返回的响应式数据本质是Proxy
对象。ref
:用于声明基础数据类型或者引用数据类型。值保存在返回的响应式数据的value
属性上。
其中衍生的还有这么几个函数,他们不是用来创建响应式的,而是用来延续响应式,即把响应式对象的数据进行拆分和扩散。
toRef
:接收两个参数,响应式对象以及属性名称。toRefs
:接收一个参数,响应式对象,可以直接获得对应的Ref属性。
<template>
<div>数字:{{ state.foo }}</div>
<div>
<button @click="add1">add1</button>
</div>
<div>
<button @click="add2">add2</button>
</div>
<div>
<button @click="add3">add3</button>
</div>
</template>
<script setup lang="ts">
import { reactive, toRef, toRefs } from "vue";
const state = reactive({
foo: 1,
bar: 2,
});
const foorRef = toRef(state, "foo");
const { foo, bar } = toRefs(state);
const add1 = () => {
foorRef.value++;
};
const add2 = () => {
state.foo++;
};
const add3 = () => {
foo.value++;
};
</script>
效果如下:都能对对应属性做出响应式更新。
2.2.3 项目架构和功能分类
Vue
:负责核心页面的搭建。Vuex
:负责管理数据。Vue-router
:负责管理路由。
因此我们在上面的项目基础上,还需要安装Vuex
以及Vue-router
:
npm install vue-router@next vuex@next
一般我们的项目,有这么几个文件目录组成:
api
:数据请求。assets
:静态资源。components
: 组件。pages
:页面。router
:路由配置。store
:vuex
数据。utils
:工具类。
1.我们现在pages
目录下创建两个页面:
2.创建路由,我们在router
文件下下面创建一个index.ts
文件:
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../pages/Home.vue'
import About from '../pages/About.vue'
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/about', name: 'About', component: About }
]
export default createRouter({
history: createWebHashHistory(),
routes
})
createRouter
:用来新建路由实例。createWebHashHistory
:用来配置我们内部使用hash
模式的路由,即url
上会通过#
来区分。
3.在入口处main.ts
文件中添加路由配置:
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import MyComponent from './components/MyComponent.vue'
import router from './router/index'
createApp(App)
.component('MyComponent', MyComponent)
// 使用我们自定义的路由
.use(router)
.mount('#app')
4.最后修改首页:
<template>
<div>
<router-link to="/">首页</router-link> |
<router-link to="/about">关于</router-link>
</div>
<router-view></router-view>
</template>
来说下这几个标签:他们都是由 vue-router
注册的全局组件。我们可以直接拿来用。
router-link
:负责跳转不同的页面。页面地址由to
属性指定。router-view
:负责渲染路由匹配到的对应组件内容。
效果如下:
同时我们的地址就变成了:http://localhost:5173/#/about
以及http://localhost:5173/#/