初识Vue3--令人焕然一新的使用逻辑和代码组织方式!
本文最后更新于 2024-04-03,文章内容可能已经过时。
一、vue2 遗留的历史问题:
vue2 使用的是 Flow.js 进行类型校验,但是现在 Flow.js 已经停止维护了,整个社区都在使用 TypeScript 做类型校验。
Vue2 的响应式并不是真正意义上的代理,而是基于 Object.defineProperty() 实现的。
**选项 API 在组织代码较多的组件的时候不易维护。**对于 Option API 来说,所有的 methods、computed 都在一个对象里配置,这对小应用来说还好。但代码超过 300 行的时候,新增或者修改一个功能,就需要不停地在 data,methods 里跳转写代码,我称之为上下反复横跳。
二、vue3 的新特性
1、 组合式 api
2、增加了三个新组件。
3、新一代工程化工具 Vite,现代浏览器已经默认支持了 ES6 的 import 语法,Vite 就是基于这个原理来实现的。具体来说,在调试环境下,我们不需要全部预打包,只是把你首页依赖的文件,依次通过网络请求去获取,整个开发体验得到巨大提升,做到了复杂项目的秒级调试和热更新。
4、全部模块使用 TypeScript 重构,类型系统带来了更方便的提示,并且让我们的代码能够更健壮。
5、响应式系统使用了 Proxy 进行了代理。
三、使用 vite 构建 vue3 项目
mac 上第一次使用vite 创建 vue 项目[[使用vue3重构一款博客#1、vue 3 项目搭建]]
npm init @vitejs/app
输入项目名称
框架选择 vue
variant 选择vue,因为在项目里,我们没有选择 TS,所以这里我们依然选择 vue 即可。
工程结构展示:
.
├── README.md
├── index.html 入口文件
├── package.json
├── public 资源文件
│ └── favicon.ico
├── src 源码
│ ├── App.vue 单文件组件
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── HelloWorld.vue
│ └── main.js 入口
└── vite.config.js vite工程化配置文件
安装依赖:
npm install
安装 vuex 和 vue-router
npm install vue-router@next vuex@next
启动项目:
npm run dev
项目架构展示:
src目录结构组织:
├── src
│ ├── api 数据请求
│ ├── assets 静态资源
│ ├── components 组件
│ ├── pages 页面
│ ├── router 路由配置
│ ├── store vuex数据
│ └── utils 工具函数
在实际项目开发中还会有各种工具的集成,比如写 CSS 代码时,我们需要预处理工具 stylus 或者 sass;组件库开发中,我们需要 Element3 作为组件库;网络请求后端数据的时候,我们需要 Axios。
对于团队维护的项目,工具集成完毕后,还要有严格的代码规范。我们需要 Eslint 和 Prettier 来规范代码的格式,Eslint 和 Prettier 可以规范项目中 JavaScript 代码的可读性和一致性。
代码的管理还需要使用 Git,我们默认使用 GitHub 来托管我们的代码。此外,我们还会使用 commitizen 来规范 Git 的日志信息。
对于我们项目的基础组件,我们还会提供单元测试来确保代码质量和可维护性,最后我们还会配置 GitHub Action 来实现自动化的部署。
最完整的项目架构如下图:
四:新的代码组织方式:
Composition API + (< script setup > )到底好在哪里?
我们可以将一个相关功能的所有方法封装称一个函数,返回所需要的数据或者方法。此外,我们可以将一些全局通用的方法封装在 utils
文件夹下,然后引入使用,这样大大提高了代码的服用程度,并且可维护性高。
示例:
<template>
<div class="todo_list">
<h2 class="list_title">清单应用</h2>
<input
type="text"
v-model="title"
@keydown.enter="addTodo"
class="list_input"
/>
<button class="clear_btn" @click="clear">清理</button>
<ul v-if="todos.length">
<li v-for="(todo, index) in todos" :key="index">
<input type="checkbox" v-model="todo.done" id="checxbox" />
<span :class="{ done: todo.done }">{{ todo.title }}</span>
</li>
</ul>
<div v-else>暂无数据</div>
<div class="allCheck">
全选<input type="checkbox" v-model="allDone" id="checxbox" />
<span class="msg">{{ active }}/{{ all }}</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
function useTodo() {
let title = ref("");
let todos = ref([{ title: "学习vue3", done: false }]);
const addTodo = () => {
todos.value.push({
title: title.value,
done: false,
});
title.value = "";
};
// 清理已经完成的任务
function clear() {
todos.value = todos.value.filter((item) => !item.done);
}
// 返回没有完成的任务清单
let active = computed(() => {
return todos.value.filter((item) => !item.done).length;
});
let all = computed(() => todos.value.length);
let allDone = computed({
get() {
// 判断没有完成的任务,如果为零 返回true
return active.value === 0;
},
set(value) {
todos.value.forEach((item, index) => {
item.done = value;
});
},
});
return { title, todos, addTodo, clear, active, all, allDone };
}
let { title, todos, addTodo, clear, active, all, allDone } = useTodo();
</script>
<style>
li {
list-style: none;
}
#checxbox {
width: 25px;
height: 25px;
border: 1px solid #eee;
border-radius: 2px;
}
.todo_list {
width: 50%;
margin: 10px auto;
}
.todo_list .allCheck {
width: 350px;
height: 30px;
margin: 0 auto;
display: flex;
justify-content: flex-start;
}
.todo_list .allCheck #checxbox {
margin: 1px 0 0 10px;
}
.todo_list .allCheck .msg {
margin-top: 5px;
margin-left: 5px;
}
.todo_list .list_title {
height: 30px;
line-height: 30px;
text-align: center;
margin: 0 auto;
}
input,
button {
background: none;
outline: none;
border: none;
}
.todo_list .list_input {
margin-top: 10px;
height: 28px;
line-height: 28px;
font-size: 20px;
color: black;
border: 2px solid rgb(168, 163, 163);
border-radius: 5px;
}
.todo_list .clear_btn {
margin-left: 5px;
width: 72px;
height: 32px;
background: rgb(96, 157, 191);
color: black;
font-size: 18px;
border-radius: 3px;
border: 1px solid #eee;
}
</style>
五、深入理解响应式:
(一)、响应式原理
1、vue 中曾经采用过的三种响应式解决方案,分别是 defineProperty(vue2)
、Proxy(reactive)
、value setter
。
2、vue2 中的响应式缺陷:
vue2 中的响应式是基于 defineProperty
来实现的,所以在删除值得时候是不会发生变化的,所以 vue2 的原型中要使用 $delete()
来进行删除数据。
function getDouble(value) {
return value * 2
}
let obj = {
count: 1
}
let double
let proxy = new Proxy(obj, {
get: function (target, prop) {
return target[prop]
},
set: function (target, prop, value) {
target[prop] = value;
if (prop === 'count') {
double = getDouble(value)
}
},
deleteProperty(target, prop) {
delete target[prop]
if (prop === 'count') {
double = NaN
}
}
})
console.log(obj.count, double)
proxy.count = 2
console.log(obj.count, double)
delete proxy.count
// 删除属性后,我们打印log时,输出的结果就会是 undefined NaN
console.log(obj.count, double)
使用 get 和 set 实现的响应式:
let getDouble = n => n * 2;
let _value = 1;
let double = getDouble(_value)
let count = {
get value() {
return _value
},
set value(val) {
_value = val
}
}
console.log(count);
console.log(count.value);
count.value = 10
console.log(count.value);
// 打印处的count 值信息,所以在使用 ref 包裹响应式数据的时候,需要 .value ,他只是对某一属性进行了拦截
{}
value: (...)
get value: ƒ value()
set value: ƒ value(val)
[[Prototype]]: Object
3、三种响应式原理实现的对比:
(二)、响应式的应用:
实际开发中你用到的任何数据或者浏览器属性,都封装成响应式数据,这样就可以极大地提高我们的开发效率。
本地存储的数据、网络请求的数据、其他属性我们都可以使用响应式将数据包裹,这样做可以让我们应用的数据更加灵敏。
使用useStorage 讲数据同步到本地仓库中的例子:
// 在 utils 里边定义一个单独的钩子函数,来处理本地数据的缓存
import { ref, watchEffect } from 'vue'
export default function useStorage(name, value) {
let lcoalValue = JSON.parse(localStorage.getItem(name) || value)
let data = ref(lcoalValue)
watchEffect(() => {
let value = data.value;
localStorage.setItem(name, JSON.stringify(value))
})
console.log(data.value, "hooks");
return data;
}
//在相应的组件里边导入使用
import useStorage from "../utils/useStorage";
function useTodo() {
let title = ref("");
let todos = useStorage("todos");
(三)、VueUse
VueUser 是一个工具包,是一个工具类集合,他把一些平常开发中经常使用的属性都封装成为响应式函数。
安装:
npm install @vueuse/core
useFullscreen 的封装逻辑和 useStorage 类似,都是屏蔽了浏览器的操作,把所有我们需要用到的状态和数据都用响应式的方式统一管理,VueUse 中包含了很多我们常用的工具函数,我们可以把网络状态、异步请求的数据、动画和事件等功能,都看成是响应式的数据去管理。
六、组件化:如何像搭积木一样开发网页
一、基本介绍:
一般在项目的开发中,组件被分为两种,第一种是通用类组件,另一种是业务型组件。
通用组件就是各大组件库的组件风格,包括按钮,表单,弹窗等通用功能。业务型组件包含业务的交互逻辑,比如购物车,登录注册等功能。
二、示例:评分组件
该组件中实现功能使用到的 vue3 api
- 为了实现父子组件传值,使用v-bind绑定一个可以修改的值
value
,同事绑定了一个确定值theme
。vue2中我们使用的是 props 定义传递值得格式,在vue3中,我们使用 setup 写法时,需要用definePros
来规定传递值得格式。 - 绑定组件事件,在 vue2 中,我们使用的是 this.$emit(“绑定的父组件的事件名”,“传递的值”),在 vue3中我们使用一个新的 api
defineEmits
来让子组件触发父组件事件。- 注意,使用该函数的时候,必须要注意其传递的参数是数组字符串类型的参数。
========== Rate.vue组件 ==========
<template>
<div :style="fontstyle">
<div class="rate" @mouseout="mouseOut">
<span @mouseover="mouseOver(num)" v-for="num in 5" :key="num">☆</span>
<span class="hollow" :style="fontwidth">
<span
@click="onRate(num)"
@mouseover="mouseOver(num)"
v-for="num in 5"
:key="num"
>★</span
>
</span>
</div>
</div>
</template>
<script setup>
import { ref, defineProps, computed, defineEmits } from "vue";
let props = defineProps({
value: Number,
theme: { type: String, default: "orange" },
});
let width = ref(props.value);
let rate = computed(() =>
"★★★★★☆☆☆☆☆".slice(5 - props.value, 10 - props.value)
);
// 鼠标悬浮事件
function mouseOver(i) {
width.value = i;
}
// 鼠标移出事件
function mouseOut() {
width.value = props.value;
}
// 为悬浮在评分上的元素渲染样式
const fontwidth = computed(() => `width:${width.value}em;`);
// 绑定评分事件
let emits = defineEmits(["update-rate"]);
function onRate(num) {
emits("update-rate", num);
}
const themeObj = {
black: "#00",
white: "#fff",
red: "#f5222d",
orange: "#fa541c",
yellow: "#fadb14",
green: "#73d13d",
blue: "#40a9ff",
};
const fontstyle = computed(() => {
return `color:${themeObj[props.theme]};`;
});
</script>
<style >
.rate {
position: relative;
display: inline-block;
}
.rate > span.hollow {
position: absolute;
display: inline-block;
top: 0;
left: 0;
width: 0;
overflow: hidden;
}
</style>
父组件:
<template>
<div>
<Rate :value="score" @update-rate="rate"></Rate>
</div>
</template>
<script setup>
import Rate from "../components/Rate.vue";
import { ref } from "vue";
let score = ref(3.5);
function rate(num) {
score.value = num;
}
</script>
<style>
</style>
七、vue中的动画效果实现:
一、前端过渡和动效
使用css动画 transition
来实现对一个元素属性值的控制,缓慢变成另外一个值,
此外,还可以使用 css 动画中的 keyframes
和 animation
的组合来定义一些动画。
二、vue3中的动画设置方式
1、基本动画的渲染
使用内置组件transition
将需要动画过渡的元素进行包裹。他会在包裹的元素中设置一个类名,通过对类名中的修改实现动画效果。
<transition name="fade">
<h1 v-if="showTitle">你好 Vue 3</h1>
</transition>
name 值就是v-enter-from
中的v,这里的name
值是 fade
,所以我们的动画过渡名称也相应地变为 fade-enter-from
对与 class 类名,vue中做的解释很详细可参见下图:
在 vue 文件中的style 属性将两个动画过渡属性的类名进行详细的描述,可以有很多种写法,搭配不同动画过程:
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s linear;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
2、列表动画的渲染
使用transition-group
去包裹一个元素,使用 tag 属性去指定渲染一个元素。这个动画渲染出现了一个新的功能,就是 v-move,他是一个过渡属性。
3、页面切换动画
这个动画切换场景的核心原理和弹窗动画是一样的,都是通过transition 标签控制页面进入和离开的class。
如果要在路由组件上使用专场,并对导航进行动画处理,就需要使用 v-slot API 。在下面的代码中,router-view 通过 v-slot 获取渲染的组件并且赋值给 Component,然后使用 transition 包裹需要渲染的组件,并且通过内置组件 component 的 is 属性动态渲染组件。这里 vue-router 的动画切换效果算是抛砖引玉,关于 vue-router 进阶的适用内容,全家桶实战篇后面的几讲还会继续深入剖析。
<router-view v-slot="{ Component }">
<transition name="route" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
4、Javascript 动画
有些开发场景中需要 JavaScript 来实现一些特殊的动画,比如购物车,地图等场景。在下面的代码中,我们为清单应用添加一个删除事项的功能,当点击删除图标的死后,可以删除一行。
在transition
组件中,可以通过 before-enter,enter,after-enter
三个函数来更精确地控制动画。
在实际开发中如果想实现更复杂的动画,比如常见电商中商品飞入购物车的效果,管理系统中丰富的动画效果等,只借助 transition 组件是很难实现的。你需要借助 JavaScript 和第三方库的支持,在 beforeEnter、enter、afterEnter 等函数中实现动画。
5、完整代码
<template>
<div class="todo_list">
<transition>
<div class="info-wrapper" v-if="showModal">
<div class="info">哥,你啥也没输入!</div>
</div>
</transition>
<h2 class="list_title">清单应用</h2>
<input
type="text"
v-model="title"
@keydown="downKey"
@keydown.enter="addTodo"
class="list_input"
/>
<button class="clear_btn" @click="clear">清理</button>
<span class="dustbin"> 🗑 </span>
<div class="animate-wrap">
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
>
<div class="animate" v-show="animate.show">📋</div>
</transition>
</div>
<ul v-if="todos.length" class="list_wrap">
<transition-group name="flip-list" tag="ul">
<li v-for="(todo, index) in todos" :key="index">
<input type="checkbox" v-model="todo.done" id="checxbox" />
<span :class="{ done: todo.done }">{{ todo.title }}</span>
<span class="remove-btn" @click="removeList($event, index)">❌</span>
</li>
</transition-group>
</ul>
<div v-else>暂无数据</div>
<div class="allCheck">
全选<input type="checkbox" v-model="allDone" id="checxbox" />
<span class="msg">{{ active }}/{{ all }}</span>
</div>
</div>
</template>
<script setup>
import { ref, computed, reactive } from "vue";
import useStorage from "../utils/useStorage";
function useTodo() {
let title = ref("");
let showModal = ref(false);
let todos = useStorage("todos");
// 在组件 transtion 上定义组件动画
let animate = reactive({ show: false, el: null });
function beforeEnter(el) {
let dom = animate.el;
let rect = dom.getBoundingClientRect();
let x = window.innerWidth - rect.left - 60;
let y = rect.top - 10;
el.style.transform = `translate(-${x}px, ${y}px)`;
}
function enter(el, done) {
document.body.offsetHeight;
el.style.transform = `translate(0,0)`;
el.addEventListener("transitionend", done);
}
function afterEnter(el) {
animate.show = false;
el.style.display = "none";
}
if (!title.value) {
showModal.value = true;
}
function downKey() {
showModal.value = false;
}
const addTodo = () => {
todos.value.push({
title: title.value,
done: false,
});
title.value = "";
showModal.value = true;
};
// 清理已经完成的任务
function clear() {
todos.value = todos.value.filter((item) => !item.done);
}
// 在userStorage 中我们已经监听了data。values,数据已经成为响应式
function removeList(e, i) {
animate.el = e.target;
animate.show = true;
todos.value.splice(i, 1);
}
// 返回没有完成的任务清单
let active = computed(() => {
return todos.value.filter((item) => !item.done).length;
});
let all = computed(() => todos.value.length);
let allDone = computed({
get() {
// 判断没有完成的任务,如果为零 返回true
return active.value === 0;
},
set(value) {
todos.value.forEach((item, index) => {
item.done = value;
});
},
});
return {
title,
todos,
addTodo,
clear,
active,
all,
allDone,
showModal,
downKey,
removeList,
beforeEnter,
enter,
afterEnter,
animate,
};
}
let {
title,
todos,
addTodo,
clear,
active,
all,
allDone,
showModal,
downKey,
removeList,
beforeEnter,
enter,
afterEnter,
animate,
} = useTodo();
</script>
<style scoped>
li {
list-style: none;
}
#checxbox {
width: 25px;
height: 25px;
border: 1px solid #eee;
border-radius: 2px;
}
.todo_list {
width: 50%;
margin: 10px auto;
}
.todo_list .allCheck {
width: 350px;
height: 30px;
margin: 0 auto;
margin-left: 220px;
display: flex;
justify-content: flex-start;
font-size: 18px;
}
.todo_list .allCheck #checxbox {
margin: 1px 0 0 10px;
}
.todo_list .allCheck .msg {
margin-top: 5px;
margin-left: 5px;
}
.todo_list .list_title {
height: 30px;
line-height: 30px;
text-align: center;
margin: 0 auto;
}
input,
button {
background: none;
outline: none;
border: none;
}
.todo_list .list_input {
margin-top: 10px;
margin-left: 50px;
height: 28px;
line-height: 28px;
font-size: 20px;
color: black;
border: 2px solid rgb(168, 163, 163);
border-radius: 5px;
}
.todo_list .clear_btn {
margin-left: 5px;
width: 72px;
height: 32px;
background: rgb(96, 157, 191);
color: black;
font-size: 18px;
border-radius: 3px;
border: 1px solid #eee;
}
.todo_list .info-wrapper {
margin-bottom: 15px;
}
.todo_list .info-wrapper .info {
padding: 6px;
color: white;
background: #d88986;
margin: 0 auto;
}
.v-enter-from {
opacity: 0;
transform: translateY(-60px);
}
.v-enter-active {
transition: all 0.3s linear;
}
.v-leave-to {
opacity: 0;
transform: translateY(-60px);
}
.v-leave-active {
transition: all 0.3s linear;
}
.todo_list .list_wrap {
width: 330px;
height: auto;
margin: 10px auto;
margin-bottom: 10px;
}
.todo_list .list_wrap li {
width: 330px;
height: 30px;
margin: 0 auto;
padding: 5px;
text-align: left;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
}
.todo_list .list_wrap li span {
display: inline-block;
height: 30px;
line-height: 30px;
}
.done {
text-decoration: line-through;
}
/* 列表动画过渡属性 */
.flip-list-move {
transition: transform 0.8s ease;
}
.flip-list-enter-active,
.flip-list-leave-active {
transition: all 1s ease;
}
.flip-list-enter-from,
.flip-list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 垃圾桶动画 */
.animate-wrap .animate {
position: fixed;
right: 550px;
top: 200px;
z-index: 100;
transition: all 1s linear;
}
</style>
- 感谢你赐予我前进的力量