Skip to main content

从项目搭建到学习 Vue3 新特性

Vue2.x 已经发展了很久,周边的生态也都非常的完善了,使用 Vue.js 几乎满足开发我们的日常所有需求,在我们看来 Vue.js 框架已经足够优秀,而尤大看来还不够完美,仍然还有许多痛点值得让他去优化, 所以后续有了 Vue3.0 的升级,这篇文章主要对 Vue3 新特性的一些知识点使用分享。

创建 Vue3 项目#

下面我们工程化方式创建 Vue3 项目,可以使用 Vite 工具创建项目或者使用官方最新版 Vue-cli 脚手架搭建。

Vite 搭建项目如下:

运行命令:

// npm 安装npm init vite@latest my-vue3-app --template vue-ts
// ts 安装npm init vite@latest my-vue3-app --template vue-ts
// yarn 安装yarn create vite my-vue3-app --template vue

这里我选择 yarn 安装,安装速度更快一些。

image-20210803213912292.png

安装完毕,依次运行提示命令即可,就创建成功了:

|-- my-vue3-app    |-- public                  -- 公共文件夹    |   |-- favicon.ico         -- 网站的显示图标    |-- src                     -- 源文件目录,编写的代码基本都在这个目录下        |-- App.vue             -- 根组件        |-- main.ts             -- 入口页面        |-- shims-vue.d.ts      -- 定义文件,因为.vue结尾的文件在ts中不认可,所以要有定义文件        |-- vite-env.d.ts       -- 客户端类型等说明文件(可看官网https://cn.vitejs.dev/guide/features.html#hot-module-replacement)        |-- assets              -- 静态资源文件,比如图片        |   |-- logo.png        |-- components          -- Vue的组件文件,自定的组件可以写在这            |-- HelloWorld.vue    |-- index.html              -- 入口的html文件    |-- package-lock.json    |-- package.json            -- 命令配置和包管理文件    |-- README.md               -- 项目的说明文件    |-- tsconfig.json           -- TypeScript    |-- vite.config.ts          -- vite 相关配置

这里项目目录结构生成 我们使用 mddir 生成。

启动成功:

image-20210803221515421.png

也可以使用 vue-cli 官方最新版,升级之后创建项目之前会让你选择 Vue.js 的框架版本,选择 Vue3 版本即可,如图:

使用 Vue-cli 创建:

yarn global add @vue/cli

创建项目:

vue create my-vue3-app

选择 Vue3 版本即可:

image-20210803214840860.png

安装完毕后就可以使用 Vue3 模式 编写了。

setup() 入口函数#

setup 是什么

setup 它将接受两个参数:props、context。可以返回一个对象,对象的属性如果被响应式定义,将在模板上进行响应式渲染。它会在 Vue2.x 中的 beforeCreate 之后 created 之前执行,也就是在 beforeMount 之前执行。

  • props

props 是响应式的,当传入新的 prop 时,它将被更新。

官网:因为 props 是响应式的,我们不能使用 ES6 解构,因为它会消除 prop 的响应性。

如果需要解构,可以使用 toRefs 完成安全操作。

// example.vue import { toRefs } from 'vue' setup(props) { const { number } =toRefs(props) console.log(number.value) }
  • context

    context 会暴露三个组件的 property

    • attrs:组件属性
    • slots:插槽
    • emit:向组件外暴露属性,同 vue2.x 的 this.$emit
setup(props, context) { const {attrs, slots, emit} = context }

setup 中没有 this,会输出为 undefined

createApp#

调用 createApp 会返回一个实例

import { createApp } from "vue";import App from "./App.vue";const app = createApp(App);

这个 app 实例上挂载全局相关方法

  • 注册全局组件

    // ...省略 createApp 引入
    import Header from "@/components/Header/Index.vue";// 注册一个名为 Header 的组件app.component("Header", Header);
  • 注册自定义指令

    app.directive("focus", {  mounted(el) {    el.focus();  },});
  • 全局 mixin

    app.mixin({  created() {    console.log("全局mixin");  },});
  • 挂载全局属性和方法

    app.config.globalProperties.$filters = {  decrypt(value) {    return sm4Decrypt(value);  },};

    模板使用:

    <template>  <div>    {{ $filters(value) }}  </div></template>
    • setup 入口函数中使用 $filters
     setup() {    const { appContext: { config: { globalProperties: { $filters } } } } = getCurrentInstance()    console.log($filters,'filters') }

image-20210810164347748.png

生命周期#

Vue3.x 中去掉了 beforeCreatecreated 改为setup代替。

Vue2Vue3
beforeCreatesetup代替
createdsetup代替
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted
activatedonActivated
deactivatedonDeactivated
errorCapturedonErrorCaptured
onRenderTracked
onRenderTriggered

总的来说 Vue3 的声明周期没有太大变化,名称相比较 Vue2.x 的名称开头多了 on,以及新增的调试函数。

Methods#

Vue2 事件方法 通常定义在 methods 中,Vue3 通过定义方法在 setup 入口函数内,并返回它即可。

<template>  <button @click="handleClick">+1</button></template>// .. setup(props, ctx) { const handleClick = (e) => { console.log('+1') }return { handleClick, }; },

从子组件触发自定义事件:

// 子组件<template>  <button @click="handleClick">+1</button></template>// .. setup(props, ctx) { const handleClick = (e) => { ctx.emit("handleClick") }return { handleClick, }; }, // 父组件<Parent @handleClick="count" />

Vue3.x 中自定义事件,只需要从 setup 的第二个参数中 context 中拿到 emit,不再 this.$emit()

ref ,reactive,toRef,toRefs#

ref

ref 函数接受一个内部值(可以是基本类型中的 string、boolean、array、object、null、undefined 等)并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value

<template>  <div>{{ number }}</div></template><script lang="ts">import { defineComponent, ref } from "vue";export default defineComponent({  name: "example",  setup(props, ctx) {    const number = ref(0); // 响应式number    // 赋值    number.value = 1;    return {      number,    };  },});</script>

reactive

与 reactive 同 ref 一样返回一个响应式,但是不会存在 .value 属性,与 Vue2 中的 Vue.observable() 一个概念。只能接收引用数据类型!

如:

<template>  <div>{{ observable.number }}</div></template>setup(props, ctx) { const observable = reactive({ number: 0, }); // number 赋值observable.number = 1; return { observable, }; },

toRefs

通俗来说就是帮助我们解构响应式对象属性。

上面说到,props 是响应式的,所以无法使用 ES6 结构,因为会消除响应性。

<template>  <button @click="handleClick">+1</button>  <div>{{ number }}</div></template>// ... props: { number: { type: Number, default: 0, }, }, setup(props, ctx) {let { number } = props; const handleClick = () => { console.log('number':number) }; return { handleClick, }; },

点击页面内容我们输出的是:

image-20210803235151962.png

此时输出变成了一个普通值。

注意:props 不需要需通过 setup 函数 return,也可以在 template 进行绑定对应的值

使用 toRefs

<template>  <button @click="handleClick">+1</button>  <div>{{ number }}</div></template>// ... 导入 props: { number: { type: Number, default: 0, }, }, setup(props, ctx){ let { number } = toRefs(props); const handleClick = () => {console.log('number': number) }; return { handleClick, }; },

再次输出:

image-20210803235614177.png

这样可以不用破坏原本的 props

重要用途来了,之前我们定义一个 reactive 响应式对象,模板取值中需要 observable.number , 如果有多个每一个都需要 observable.x 获取,不能像定义 ref 一样直接拿到,这时候我们可以使用 toRefs 在不丢失响应性的情况下对返回的对象进行分解/扩散:

<template>  <div>{{ number }}</div></template>// ... setup(props, ctx) { const observable = reactive({ number: 0, }); return {...toRefs(observable), }; },

toRef

toRef 的用法,就是多了一个参数,允许针对某个 key 进行包装

// ...const number = toRef(props, "number");console.log("number:", number.value);

image-20210803235614177.png

也可以用来为源响应式对象上的 property 性创建一个 ref

setup(props, ctx) {    const observable = reactive({      number: 0,    });
    const number = toRef(observable, 'number');
    console.log(number.value) // 0    return {      number,    };},

watchEffect & watch 监听器#

watchEffect

他接收一个回调函数,不需要指定监听谁,只要在回调函数中使用了定义的响应式属性,它会自动收集依赖,并在其依赖变更时重新运行该函数。

setup() {    const number = ref(0);    watchEffect(() => console.log(number.value)); // 0
    setTimeout(() => {      number.value = 2; // 一秒后number改变再次执行 watchEffect ,输出为 2    }, 1000);},

image-20210805114339085.png

直白的意思就是,再 watchEffect 接收的函数内使用到了响应式值,发生改变就会重新运行该函数。

停止监听

watchEffect 会返回一个停止函数,调用它会停止监听。

setup(props) {    const number = ref(0);    const stop = watchEffect(() => console.log(number.value));    setInterval(() => {      number.value++;    }, 1000);    setTimeout(stop, 2000);    return {      number,    };},

在两秒后会听见监听 number 值的变化。

清除副作用

传入的函数可以接收一个 onInvalidate 函数作入参, 用来注册清理失效时的回调。

注意:onInvalidate() 执行时机只有在依赖了响应式属性注册的时候才会执行回调。

执行失效回调时机:

  • 副作用即将重新执行时,也就是依赖发生改变时,默认注册时就会执行。
  • 组件卸载时
watchEffect((onInvalidate) => {  // do something...  onInvalidate(() => {    // 注册/组件卸载时  });});

通常我们用 watchEffect 解决子组件 props 属性改变,子组件不会重新渲染问题:

// example.vue<template>  <div>{{ number }}</div></template>
<script lang="ts">import { defineComponent, reactive, toRefs, watchEffect } from "vue";export default defineComponent({  name: "example",  props: {    number: { type: Number, default: 0 },  },  setup(props) {    const state = reactive({      number: 0,    });    watchEffect(() => {      state.number = props.number;    });    return {      ...toRefs(state),    };  },});</script>

watch

Vue3 的 watch 与 Vue2 的概念差不多,监听针对某一个值的变化。而且注册时并不会立即执行,只有来指定依赖发生改变是才会执行回调函数,看看它的基础用法:

// 监听单个const number = ref(0);watch(number, (newVal, oldVal) => {  console.log(newVal, oldVal);});
// 对象深度监听const obj = ref({ name: "watch" });watch(  obj,  (newVal, oldVal) => {    console.log(newVal, oldVal);  },  {    deep: true,  });
// 监听多个watch([a, b], ([newValA, newValB], [oldValA, oldValB]) => {  console.log(newValA, newValB, "——————————", oldValA, oldValB);});

使用 reactive 定义响应式数据,并想侦听单个的时候:

// 侦听一个 getterconst state = reactive({ count: 0 });// 直接 state.count 会报错watch(  () => state.count,  (count, prevCount) => {    /* ... */  });

computed 计算属性#

与 Vue2 用法相同差不多,Vue3 中删除了 filters 过滤器,我们可以使用 computed 代替它。

<template>  <div>{{ fullName }}</div>  <div>{{ accept("Foo") }}</div></template>
<script lang="ts">import { computed, defineComponent, ref } from "vue";
export default defineComponent({  setup(props) {    const firstName = ref("Foo");    const lastName = ref("Bar");    // 基础用法    const fullName = computed(() => {      return firstName.value + lastName.value;    });
    // 传递参数    const accept = computed(() => {      return (fullName: string) => {        return fullName === "Foo" ? firstName.value : lastName.value;      };    });    return {      fullName,      accept,    };  },});</script>

fragments(片段)#

片段的意思是 Vue2.x 组件中,并不支持多个根节点,在 Vue3.x 组件中可以包含多个根节点。

<template>  <div class="fragments">...</div>  <div>...</div></template>

从父组件传递 class 给子组件 ,当子组件的如果只有一个根组件的时候,默认会将 class 放入根的节点上,如果有多个,默认不会添加,需要 v-bind="$attrs"指定添加到父节点上

// 子组件<template>  <div class="children">children</div>  <div v-bind="$attrs">2222</div></template>
// 父组件<template>  <div class="fragments"></div>  <Childern class="parant" /></template>

效果:

image-20210806161151729.png

瞬间移动组件 Teleport#

teleport 翻译过来就是传送的意思,怎么理解这个瞬间移动组件呢?就是将组件内的节点放入指定 DOM 位置上。

Vue或者 React 项目中都会有一个根节点,页面内容也是挂载在这个根节点下 <div id='app'></div>,在写一些弹窗组件或者全局提示状态组件,我们希望独立组件外,放在与根组件同级兄弟节点下,这样更容易受控制,不会被根节点一些 css 属性影响。 Teleport的作用就是让我们可以将 Dom 移动到一个哪个干净的父节点下渲染 HTML,来看看用法

我们在 index.html 再添加一个 <div>id="app2"</div>节点:

image-20210809213925150.png

<template>  <div>    <div class="example">example</div>    <teleport to="#app2">      <div class="teleport">teleport</div>    </teleport>  </div></template>

效果:

image-20210809214417172.png

<div class="teleport">teleport</div>Dom 被移动到了 id="app2"节点下,来看看基础语法:

通过选择器移动到父节点下位置:

// id<teleport to="#app2">
// class<teleport to=".teleport-class">
// data selector<teleport to="[data-app]">// data selector 父节点<div data-app/>
// 动态更新<teleport :to="currentRef">

异步组件 Suspense#

在渲染组件之前会进行一些异步请求,在异步请求成功数据完全渲染之前比如做一些 Loading、骨架屏(Skeleton)等一些方案,<Suspense > 组件能够更好的帮助实现这样的需求。该组件有两个插槽,一个是完全请求成功之后等待组件树处理完毕显示,一个是未请求成功返回数据时候显示该内容。我们来简单写个列子,看看实际操作。

Async.vue

image-20210810155716528.png

Suspense.vue

请求返回一张签名图片:

image-20210810155815161.png

来看看效果:

为了让效果更明显 可以自定网络加载速度 NetworkNo throtting 选择 Add

GIF 2021-8-10 16-07-58.gif

gif 生成工具我使用的是 gitcam 很好用!~

但是官方说明:Suspense 是一个试验性的新特性并且其 API 可能随时更改。特此声明以便社区能够为当前的实现提供反馈。

它不应该被用在生产环境。

控制台也有提示

image-20210810163905069.png

关于模块化使用#

对于模块化的理解,通俗的说逻辑抽离,让代码能够复用,优化代码结构,好的代码可读性,更有利于后期的维护性。Vue3.x 的 CompositionAPI 让我们这样使用的理由也在此,也可以看看这篇文章 不要再用 vue2 的思维写 vue3 了。写一个简单的例子。

就是加 1,减 1

通常我们都会这样写:

<template>  <div class="module">    <div>{{ number }}</div>    <van-button @click="increase">加加</van-button>    <van-button @click="reduce">减减</van-button>  </div></template>
<script>import { defineComponent, ref } from "vue";
export default defineComponent({  setup() {    const number = ref(0)    const increase = () => {      number.value++    }    const reduce = () => {      number.value--    }    return {      number,      increase,      reduce    }  }})

没有毛病,这只是其中一个例子,如果有更多更多的逻辑功能,我们肯定也会继续往setup入口函数内继续添加逻辑,这样就变成拉面条,以前通常的思维就是能复用的就抽离,没有复用的就一起写,发现慢慢一个组件功能多起来,变得难以查找很混乱。

我们修改一下利用 CompositionAPI 给我们的特性使用模块化的方式重新优化一下,也是我们常常提到的 hooks 怎么写。

useCount.ts

import { ref } from "vue";export default () => {  const number = ref(0);  const increase = () => {    number.value++;  };  const reduce = () => {    number.value--;  };  return {    number,    increase,    reduce,  };};
<template>  <div class="module">    <div>{{ number }}</div>    <van-button @click="increase">加加</van-button>    <van-button @click="reduce">减减</van-button>  </div></template>
<script>import { defineComponent } from "vue";import useCount from "./useCount";export default defineComponent({  setup() {    // 计算逻辑    const { number, increase, reduce } = useCount();    // 其他逻辑    return {      // ...useCount(),      number,      increase,      reduce,    };  },});</script>

可以看到我们只需要引入拿到相关的值 return 即可,遇到其他页面有相同逻辑的,我们同样引入返回即可使用。这样写可以让代码可读性也更高,更利于代码后期维护。也可以看看这篇文章业务 hooks 使用

总结#

Vue3 的版本升级改进,在性能优化,源码优化等等有了很大的提升,CompositionAPI 代码编写也更加的函数式,使得逻辑抽离更加灵活,不用被 OptionsAPI 限制思维。

项目用起来,感受 Vue3 的快乐~