本文最后更新于 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 动画中的 keyframesanimation 的组合来定义一些动画。

二、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>