跳至内容

全局 API 树摇
重大变更

2.x 语法

如果您曾经需要手动操作 DOM,您可能遇到过这种模式

js
import Vue from 'vue'

Vue.nextTick(() => {
  // something DOM-related
})

或者,如果您一直在对涉及异步组件的应用程序进行单元测试,您很有可能写过类似这样的代码

js
import { shallowMount } from '@vue/test-utils'
import { MyComponent } from './MyComponent.vue'

test('an async feature', async () => {
  const wrapper = shallowMount(MyComponent)

  // execute some DOM-related tasks

  await wrapper.vm.$nextTick()

  // run your assertions
})

Vue.nextTick() 是直接在单个 Vue 对象上公开的全局 API - 事实上,实例方法 $nextTick() 只是 Vue.nextTick() 的一个方便的包装器,回调的 this 上下文会自动绑定到当前实例以方便使用。

但是,如果您从未处理过手动 DOM 操作,也没有在您的应用程序中使用或测试异步组件呢?或者,出于任何原因,您更喜欢使用传统的 window.setTimeout() 呢?在这种情况下,nextTick() 的代码将成为死代码 - 也就是说,已编写但从未使用的代码。死代码并不是一件好事,尤其是在我们的客户端环境中,每个字节都很重要。

像 webpack 和 Rollup(Vite 基于它)这样的模块捆绑器支持 树摇,这是一个“死代码消除”的专业术语。不幸的是,由于以前 Vue 版本中代码的编写方式,Vue.nextTick() 这样的全局 API 不可进行树摇,无论它们实际使用与否,都会包含在最终的捆绑包中。

3.x 语法

在 Vue 3 中,全局和内部 API 已针对树摇支持进行了重构。因此,现在只能通过 ES 模块构建的命名导出访问全局 API。例如,我们之前的代码片段现在应该如下所示

js
import { nextTick } from 'vue'

nextTick(() => {
  // something DOM-related
})

js
import { shallowMount } from '@vue/test-utils'
import { MyComponent } from './MyComponent.vue'
import { nextTick } from 'vue'

test('an async feature', async () => {
  const wrapper = shallowMount(MyComponent)

  // execute some DOM-related tasks

  await nextTick()

  // run your assertions
})

直接调用 Vue.nextTick() 现在会导致臭名昭著的 undefined is not a function 错误。

通过此更改,只要模块捆绑器支持树摇,Vue 应用程序中未使用的全局 API 将从最终的捆绑包中删除,从而实现最佳的文件大小。

受影响的 API

这些 Vue 2.x 中的全局 API 受此更改的影响

  • Vue.nextTick
  • Vue.observable(已替换为 Vue.reactive
  • Vue.version
  • Vue.compile(仅在完整构建中)
  • Vue.set(仅在兼容构建中)
  • Vue.delete(仅在兼容构建中)

内部帮助程序

除了公共 API 之外,许多内部组件/帮助程序现在也作为命名导出导出。这允许编译器输出仅在使用时才导入功能的代码。例如,以下模板

html
<transition>
  <div v-show="ok">hello</div>
</transition>

编译成类似于以下内容

js
import { h, Transition, withDirectives, vShow } from 'vue'

export function render() {
  return h(Transition, [withDirectives(h('div', 'hello'), [[vShow, this.ok]])])
}

这实际上意味着 Transition 组件仅在应用程序实际使用它时才会被导入。换句话说,如果应用程序没有 <transition> 组件,则支持此功能的代码将不会出现在最终的捆绑包中。

通过全局树摇,用户只为他们实际使用的功能“付费”。更棒的是,知道可选功能不会增加不使用它们的应用程序的捆绑包大小,框架大小在未来对于额外的核心功能来说已经不再是一个问题,如果有的话。

重要

以上内容仅适用于 ES 模块构建,用于与支持树摇的捆绑器一起使用 - UMD 构建仍然包含所有功能,并在 Vue 全局变量上公开所有内容(编译器将生成适当的输出以使用全局变量上的 API 而不是导入)。

插件中的使用

如果您的插件依赖于受影响的 Vue 2.x 全局 API,例如

js
const plugin = {
  install: Vue => {
    Vue.nextTick(() => {
      // ...
    })
  }
}

在 Vue 3 中,您需要显式导入它

js
import { nextTick } from 'vue'

const plugin = {
  install: app => {
    nextTick(() => {
      // ...
    })
  }
}

如果您使用像 webpack 这样的模块捆绑器,这可能会导致 Vue 的源代码被捆绑到插件中,而这通常不是您想要的。防止这种情况发生的常见做法是配置模块捆绑器以从最终捆绑包中排除 Vue。在 webpack 的情况下,您可以使用 externals 配置选项

js
// webpack.config.js
module.exports = {
  /*...*/
  externals: {
    vue: 'Vue'
  }
}

这将告诉 webpack 将 Vue 模块视为外部库,而不是将其捆绑在一起。

如果您的模块捆绑器恰好是 Rollup,您基本上可以免费获得相同的效果,因为默认情况下 Rollup 会将绝对模块 ID(我们情况下的 'vue')视为外部依赖项,并且不会将其包含在最终的捆绑包中。但是,在捆绑过程中,它可能会发出 “将 vue 视为外部依赖项” 警告,可以使用 external 选项来抑制该警告

js
// rollup.config.js
export default {
  /*...*/
  external: ['vue']
}