Vue3 新特性及生态介绍

2,587 阅读5分钟

Vue3 新特性

Composition API

  • Composition API does wonders for code organization by keeping code for individual features together. At the end of the day this means a cleaner code, easier to read and share.
  • Vue3 为什么要使用 Composition API

优点

【高内聚】聚合业务逻辑,提高代码可读性

  • Vue2 中逻辑关注点是碎片化的,使得理解和维护复杂组件变得困难。属性选项(options,如 data、computed、methods 等)的分离增加了潜在的逻辑风险。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的属性选项块(option blocks)。
  • Vue3 的组合式 API 使我们能将与同一个逻辑关注点相关的代码配置在一起,使代码更加明晰、易读。

1.png

【低耦合】便于代码拆分,提高复用性(Hooks)

  • Vue2 中的常用代码复用方式 —— 混入 Mixin 和 范围插槽 Scoped Slots,都存在限制。
    • 混入最大的缺点是:依赖关系不清晰,不能直观知道它给组件增加了什么属性,而且还可能导致与现有属性和函数的重名冲突。
    • 使用范围插槽,可以通过 v-slot 属性确切地知道可以访问哪些属性;但缺点是,只能在模板中访问它,它只能在组件范围内使用。
  • 得益于全局 Composition API, Vue3 可以轻松做到代码拆分,可以对复用的逻辑进行随意的组合和配置,并放入setup函数内执行。Vue3 的 setup 聚合了所有的逻辑,容易产生面条代码,合理使用自定义 hooks,可以有效的减少面条代码,提升代码可维护性、可复用性。并且 Vue3 的 hooks 比 react 更加简单高效,不会多次执行,不受调用顺序影响,不存在闭包陷阱等等,几乎可以没有任何心智负担的使用。通过对可复用代码的抽离,我们的代码组织可能变成这样:

2.png

应用

Vue2

// src/components/UserRepositories.vue

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: { 
      type: String,
      required: true
    }
  },
  data () {
    return {
      repositories: [], // 1
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // 使用 `this.user` 获取用户仓库
    }, // 1
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}

Vue3

  1. 将具体的业务代码提取到一个独立的组合式函数中,成为单独的功能模块(Hook)
  2. 在组件中使用它们
// src/composables/useUserRepositories.js

import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'

export default function useUserRepositories(user) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)
  watch(user, getUserRepositories)

  return {
    repositories,
    getUserRepositories
  }
}
// src/components/UserRepositories.vue
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const { user } = toRefs(props)

    const { repositories, getUserRepositories } = useUserRepositories(user)

    const {
      searchQuery,
      repositoriesMatchingSearchQuery
    } = useRepositoryNameSearch(repositories)

    const {
      filters,
      updateFilters,
      filteredRepositories
    } = useRepositoryFilters(repositoriesMatchingSearchQuery)

    return {
      // 因为我们并不关心未经过滤的仓库
      // 我们可以在 `repositories` 名称下暴露过滤后的结果
      repositories: filteredRepositories,
      getUserRepositories,
      searchQuery,
      filters,
      updateFilters
    }
  }
}

可能的劣势

Vue3 composition-api 有哪些劣势? - 知乎 reactive 和 ref 的响应式开发的心智负担?见仁见智。

Fragments

In Vue 3, components now have official support for multi-root node components, i.e., fragments!

Vue2

<!-- Layout.vue -->
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>
  • 在 2.x 中,由于不支持多根节点组件,当开发者意外创建多个时会发出警告。
  • 需要把各组件都包裹在一个 <div> 中。

Vue3

<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>
  • 在 3.x 中,组件可以包含多个根节点。
  • 非 prop attribute:指传向一个组件,但是该组件并没有相应 propsemits 定义的 attribute。常见的示例包括 classstyleid 属性。
    • 当组件返回单个根节点时,非 prop attribute 将自动添加到根节点的 attribute 中。而具有多个根节点的组件不具有该自动 attribute 回退行为。如果未显式绑定 $attrs,将发出运行时警告。
    • 因此,要求开发者显式定义 attribute 应该分布在哪里。如上所述例子,指明所有传给 Layout 组件的非 prop attribute 应用于 main 元素($attrs 被传递到<main>元素)。
  • Vue3 中,组件的 template 被一层不可见的 Fragment 包裹,支持多个根节点。解决了组件需要被一个唯一根节点包裹的问题,减少了 dom 层级、提升了渲染性能

Teleport

The Teleport feature allows you to render code from one component to others, in different parts of the DOM tree. Previously known as portals, Teleport is now built into Vue 3.

  • Teleport,即传送门。<teleport> 组件能在不改变组件内部元素父子关系的情况下,将子元素“传送”到其他指定节点下渲染,允许我们控制在 DOM 中哪个父节点下渲染 HTML,而不必求助于全局状态或将其拆分为两个组件。
  • 适用于某些逻辑上属于某一组件、视图位置上相对于其他组件的部分。将移动实际的 DOM 节点,而不是被销毁和重新创建,并且它还将使任何组件实例保持活动状态。所有有状态的 HTML 元素(如正在播放的视频)都将保持它们的状态。
  • 如下:dialog 直接挂载在 container 下,超出部分不可见。
<template>
  <div class="container" style="width: 100px; height: 100px; overflow: hidden">
    <div class="dialog" style="width: 500px; height: 400px;">
      ...
    </div>
  </div>
</template>

Vue3 + <teleport> 组件

<template>
  <div class="container" style="width: 100px; height: 100px; overflow: hidden">
    <teleport to="body">
      <div class="dialog" style="width: 500px; height: 400px;">
        ...
      </div>
    </teleport>
  </div>
</template>
  • 外面加一层 Teleport,dialog 被渲染为 body 标签的子级,展示不会被遮挡。且 dialog 依然是 container 的子元素,逻辑关系不变。
  • to 属性指定<teleport>内容将被移动到的目标元素,值为 string,必须是有效的查询选择器,如to="#some-id"to=".some-class"等。
  • disabled(可选的 prop)可用于禁用<teleport>的功能,值为 boolean,设为 true 时插槽内容不会移动到任何地方,而是呈现在<teleport>在周围父组件中指定的位置。
<teleport to="#popup" :disabled="displayVideoInline">
  <video src="./my-movie.mp4">
</teleport>

Suspense【实验性】

Suspense is an experimental new feature and the API could change at any time. It should NOT be used in production applications.【实验性,生产环境不可用】

  • 很多情况下组件在正确呈现之前需要执行某种异步请求。对于中间的等待过程,通常我们在每个单独的组件中进行处理。<suspense> 组件提供了一种替代方法,允许在组件树上进一步处理等待。
  • <suspense> 组件有两个插槽,各都只允许一个直接子节点。如果在default 插槽中的节点可用(异步组件可处于该节点组件树的任何深度,并且不需要与<suspense>自身出现在同一个模板中。只有该节点所有后代都加载完成时,才认为该节点可用),则展示default 插槽内容;否则展示fallback 插槽内容。
  • 例:
<template>
  <div class="container">
    <div v-if="init">
      <todo-list />
    </div>
    <div v-else>
      Loading...
    </div>
  </div>
</template>

Vue3 + <suspense> 组件

<template>
  <suspense>
    <template #default>
      <todo-list />
    </template>
    <template #fallback>
      <div>
        Loading...
      </div>
    </template>
  </suspense>
</template>

<script>
export default {
  components: {
    TodoList: defineAsyncComponent(() => import('./TodoList.vue'))
  }
}
</script>
  • 其中,defineAsyncComponent方法用于定义异步组件

编译时优化(略)

3.png

单文件组件 SFC 语法

<script setup>标签

  • A new script type in Single File Components: <script setup>, which exposes all its top level bindings to the template.【2021.8.5 发布的 Vue.js 3.2 版本中已更新为稳定性】
  • Vue3 高效开发 script-setup
  • 主要目标是通过将 <script setup> 的上下文直接暴露给模板<template>来减少在单文件组件 (SFC) 中使用 Composition API 的冗长性。
  • 当使用<script setup>的时候,任何在<script setup>声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的组件、函数等) 都能在模板中直接使用

Usage Example

<script setup>
  // imported components are also  directly usable in template
  import Foo from './Foo.vue'
  import { ref } from 'vue'

  // write Composition API code just like in a normal setup()
  // but no need to manually return everything
  const count = ref(0)
  const inc = () => {
    count.value++
  }
</script>
<template>
  <Foo :count="count" @click="inc" />
</template>
  • 只需要在script上配置setup即可。
  • 省去了组件的注册步骤,也没有显式的导出变量的动作。

Compiled Output

import Foo from './Foo.vue'
import { ref } from 'vue'
export default {
  setup() {
    const count = ref(0)
    const inc = () => {
      count.value++
    }

    return function render() {
      return h(Foo, {
        count,
        onClick: inc
      })
    }
  }
}
  • SFC compiler 会从 <script setup> 中提取绑定元数据并在模板编译时使用;因此即使 Foo 组件是从 setup() 返回的而非通过 components 选项注册的,template 仍可使用 Foo 作为组件

基本用法

Components
<script setup>
  import Foo from './Foo.vue'
  import MyComponent from './MyComponent.vue'
</script>
<template>
  <Foo />
  <!-- kebab-case also works -->
  <my-component />
</template>
  • <script setup> 范围内的值也可以直接用作自定义组件标签名,类似于在 JSX 中的工作方式
  • 强烈建议使用 PascalCase 格式作为组件的标签名称,以便于更好的一致性,同时也有助于区分原生的自定义元素。
    • camelCase 🆚 PascalCase:第一个单词的首字母小 / 大写
Dynamic Components
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>

<template>
  <component :is="Foo" />
  <component :is="someCondition ? Foo : Bar" />
</template>
  • 由于组件被引用为变量而不是作为字符串键来注册的,在<script setup>中要使用动态组件的时候,就应该使用动态的 :is 来绑定
Function
<script setup>
import { capitalize } from './helpers'
</script>

<template>
  <div>{{ capitalize('hello') }}</div>
</template>
  • 可以在模板表达式中直接使用导入的函数,不需要手动 return 或通过 methods 选项来暴露它
Reactivity
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>
  • 响应式状态需要明确使用响应式 APIs 来创建,在模板中使用时会自动解包
propsemits 选项
<script setup>
const props = defineProps({
  foo: String,
  bar: {
      type: Number,
      required: false
  }
})

const emit = defineEmits(['change', 'update'])
emit('change', params)
// setup code
</script>
  • definePropsdefineEmits API 在 <script setup> 中是自动可用的,不需要导入,且会在处理 <script setup>时被编译处理掉。用于声明 propsemits 选项且具备完整的类型推断。
  • defineProps 接收与 props 属性相同的值,defineEmits 也接收 emits 属性相同的值。
  • 支持使用 TypeScript 的类型注解:
    • 注:两个写法不可以一起进行使用,即一个 defineProps 不能既使用 TS 类型声明,也使用基础用法。
const props = defineProps<{
  foo: string
  bar?: number
}>()

const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
顶级 await 的支持
<script setup>
const post = await fetch(`/api/post/1`).then(r => r.json())
</script>
  • 在 script-setup 模式下,不必再配合 async 就可以直接使用 await 了,这种情况下,组件的 setup 会自动变成async setup()
  • async setup() 必须与 Suspense 组合使用,Suspense 目前还是处于实验阶段的特性。vue 打算在将来的某个发布版本中开发完成并提供文档,如果感兴趣,可以参照 tests 看它是如何工作的。

Q & A

状态驱动的 CSS 变量( 中的 v-bind)

SFC <style> tags support linking CSS values to dynamic component state using the v-bind CSS function.

  • 单文件组件的 <style> 标签可以通过 v-bind 这一 CSS 函数将 CSS 的值关联到动态的组件状态上;实际的值会被编译成 hash 的 CSS 自定义 property,CSS 本身仍然是静态的。自定义 property 会通过内联样式的方式应用到组件的根元素上,并且在源值变更的时候响应式更新。
  • 这个语法同样也适用于 <script setup>,且支持 JavaScript 表达式 (需要用引号包裹起来)
<script setup>
const theme = {
  color: 'red'
}
const size = ref('24px');
</script>

<template>
  <p>hello</p>
</template>

<style scoped>
p {
  color: v-bind('theme.color');
  font-size: v-bind(size);
}
</style>

新的生命周期钩子

  • 除了 setup 外,Vue3 的其他生命周期钩子都添加了 on 前缀,更加规范统一。
  • 新的钩子需要在 setup 中使用。
  • destroyed 生命周期选项被重命名为 unmountedbeforeDestroy 生命周期选项被重命名为 beforeUnmount
Vue2 生命周期Vue3 生命周期执行时间说明
setup在 Vue2 的beforeCreatecreated之前执行;可以完全代替 2.x 的 beforeCreatecreated这 2 个钩子函数。执行 setup 时,组件实例尚未被创建;只能访问以下 property:propsattrsslotsemit无法访问以下组件选项:datacomputedmethods
beforeCreate组件创建前执行
created组件创建后执行
beforeMountonBeforeMount组件挂载到节点上之前执行
mountedonMounted组件挂载完成后执行
beforeUpdateonBeforeUpdate组件更新之前执行
updatedonUpdated组件更新完成之后执行
beforeDestroyonBeforeUnmount组件卸载之前执行
destroyedonUnmounted组件卸载完成后执行
errorCapturedonErrorCaptured当捕获一个来自子孙组件的异常时激活钩子函数
  • 当生命周期混用时,主版本的回调钩子会相对优先执行ref)。即,如果在 vue3 中同时使用了 vue2 的生命周期写法,vue3 的写法会优先执行。参考例子
  • 另外,被包含在 <keep-alive> 中的组件,会多出两个生命周期钩子函数: | Vue2 生命周期 | Vue3 生命周期 | 执行时间说明 | | --- | --- | --- | |activated|onActivated|被激活时执行 |deactivated|onDeactivated|切换组件后,原组件消失前执行

Example

  • 每个生命周期函数都要先导入才可以使用,并且所有生命周期函数统一放在 setup 里运行。
import { defineComponent, onBeforeMount, onMounted } from 'vue'

export default defineComponent({
  setup () {

    console.log(1);
    
    onBeforeMount( () => {
      console.log(2);
    });
    
    onMounted( () => {
      console.log(3);
    });

    console.log(4);

    return {}
  }
})
  • Output
// 1
// 4
// 2
// 3

Tree-Shaking

Allow you to only import the parts of libraries that you need. Whatever you don't need will be removed via tree-shaking.

  • Vue3 一共开放了 113 个 API,可以通过如下方式引用:
import { ref, reactive, h, onMounted } from "vue";
  • 通过 ES6 modules 的引入方式,能够被 AST 静态语法分析感知,从而可以只提取用到的代码片段,达到 Tree-Shaking 的效果,使得 Vue3 最终打包出来的包更小,加载更快。据尤大去年 4 月在 B 站的直播:基本的 hello world 项目大小为 13.5kb,Composition API 仅有 11.75kb,包含所有的运行态仅 22.5kb。

重写 VDOM

优化前优化后
在一个默认的 Virtual Dom 的 diff 中,需要遍历所有节点,而且每一个节点都要比较旧的 props 和新的 props 有没有变化,虽然 js 做这些工作很快,但是在一个大型应用中,不可避免的会影响性能。diff 算法看到一个 Block 就只需要看里面有没有动态变化的节点(带PatchFlag标记)并追踪就可以了。

4.png

Vue3 生态

【释义】生态:构成开发环境的库、工具以及周边

开发体验改进

构建工具:vite /vit/

  • 一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:
    1. 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)
    2. 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
  • Vite 意在提供开箱即用的配置,同时它的 插件 APIJavaScript API 带来了高度的可扩展性,并有完整的类型支持。

5.png

6.png

浏览器插件:Vue devtools beta channel 6.0

  • The Vue Devtools is an invaluable browser extension to Chrome and Firefox that will speed up your development and bug hunting.
  • Vue Devtools 6 supports Vue 3 and as of right now it's in beta and available for Chrome and Firefox.

7.png

更好的 IDE / TS 支持

  • 推荐使用 VSCode 和官方拓展 Vetur,它为 Vue 3 提供了全面的 IDE 支持。
  • Volar:在 VSCode 里可以提供模板中的代码提示功能,提供 TSX 级别的开发体验。

8.png

9.png

Vue Router 4

用法

Vue Router3
  • 创建方式
import VueRouter from "vue-router"
const router = new VueRouter({
    // options
    ......
})
  • 路由模式
    • hash模式利用 URL hash 来模拟完整的 URL,这样当 URL 发生变化时,页面不会被重新加载。
    • history模式利用 HTML5 History API 来实现 URL 导航,而不需要重新加载页面。
const router = new VueRouter({
    mode: 'hash' / 'history'
 })
  • 重定向
{
    path: '*',
    redirect: Home
}
  • 挂载方式
    • 以属性的方式进行挂载
import router from './router.js'
new Vue({
   router
})
  • 组件中的使用
export default({
   methods:{
     linkToHome(){
         this.$router.push({
               path:'/'
          })
      }
   }
})
Vue Router4
  • 创建方式
    • 用 createRouter 创建 router 对象
import { createRouter } from "vue-router"
const router = createRouter({
    // options
    .....
})
  • 路由模式
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
const router = createRouter({ 
    history:createWebHistory() / createWebHashHistory() 
})
  • 重定向
{
    path: '/:pathMatch(.*)*', // 需要使用正则去匹配
    redirect: Home,
}
  • 挂载方式
    • 因为 vue3 的 composition api,vue-router 的挂载方式以插件来挂载
import { createApp } from 'vue'
import router from './router.js'
import App from './App.vue' 
createApp(App).use(router).mount('#app');
  • 组件中的使用
    • 因为 setup 中不能访 this,所以提供 useRouter()useRoute() 两个 api 来获取 routerroute
import { useRouter,useRoute } from "vue-router"

export default({
   setup(){
     const router = useRouter();
     const route = useRoute();
     const linkToHome = () => {
           router.push({
                path:'/'
           })
      }
      return{
          linkToHome
      }
  }
})
  • 导航守卫
    • 由于 vue3 composition api,beforeRouteUpdate 和 beforeRouteLeave 被替换为 onBeforeRouteUpdate 和 onBeforeRouteLeave

Vuex 4

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

  • Vuex 4.0 提供了 Vue 3 支持,其 API 与 3.x 基本相同。唯一的突破性变化是插件的安装方式
  • 新特性:Vuex 4 引入了一个新的 API 用于在组合式 API 中与 store 进行交互。可以在组件的 setup 钩子函数中使用 useStore 组合式函数来检索 store。
import { useStore } from 'vuex'

export default {
  setup () {
    const store = useStore()
  }
}

UI 组件库

现阶段应该使用什么 Vue 的 UI 框架? - 知乎

Element Plus

一套为开发者、设计师和产品经理准备的基于 Vue 的桌面端组件库。

Vuetify

Vuetify 是一款精致的 UI 框架,提供了很多常用的组件,依靠 Material Design 的设计优势,让开发者无需编写一行 css 代码就可以得到非常美观的界面功能。响应式较强,移动 PC 多端支持,配置灵活,组件种类功能较为全面。

Ant Design Vue

开发和服务于企业级后台产品。

Vant(Vue 3 版本)

有赞前端团队开源的移动端组件库,于 2017 年开源,已持续维护 4 年时间,是业界主流的移动端 Vue 组件库之一。目前 Vant 官方提供了 Vue 2 版本Vue 3 版本微信小程序版本,并由社区团队维护 React 版本

开发框架

Quasar

Effortlessly build high-performance & high-quality Vue.js 3 user interfaces in record time. 编写一次代码,可同时将其部署为网站、移动应用程序和/或电子应用程序(Electron App)。

Nuxt 3

  • A framework making web development simple and powerful.
  • Nuxt.js 是一个基于Vue.js 的轻量级应用框架,可用来创建服务端渲染(SSR) 应用,也可充当静态站点引擎生成静态站点应用,具有优雅的代码结构分层和热加载等特性。

VuePress

VuePress 专注在以内容为中心的静态网站上,同时提供了一些为技术文档定制的开箱即用的特性。由两部分组成:一个极简静态网站生成器(包含由 Vue 驱动的主题系统插件 API)+ 为书写技术文档而优化的默认主题;初衷是为了支持 Vue 及其子项目的文档需求。

VitePress

VitePress is VuePress' little brother, built on top of Vite.

10.png

兼容性

  • 不支持 IE11
  • 2021 年第二季度底,Vue3 成为默认版本
  • 默认现代模式:vite + vue3

vue2 TO vue3 工具:gogocode

由于 Vue3 对于 Vue2 在 Api 层面存在诸多兼容问题,并不能做到平滑升级。利用 gogocode 这个代码转换利器,根据 v3迁移指南,利用它操作AST,开发了一套 Vue2 升级工具。利用这套工具能够快速地把 Vue2 代码升级到 Vue3。

Reference