# Поддержка tree-shaking для глобального API
кардинальное изменение

# Синтаксис в 2.x

Если приходилось когда-нибудь вручную манипулировать DOM во Vue, то наверняка сталкивались с таким шаблоном:

import Vue from 'vue'

Vue.nextTick(() => {
  // что-то связанное с DOM
})
1
2
3
4
5

Или если использовали модульное тестирование в приложении с использованием асинхронных компонентов, то возможно писали что-то подобное:

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

test('какая-то асинхронная возможность приложения', async () => {
  const wrapper = shallowMount(MyComponent)

  // выполнение некоторых задач, связанных с DOM

  await wrapper.vm.$nextTick()

  // выполнение проверок
})
1
2
3
4
5
6
7
8
9
10
11
12

Глобальный API Vue.nextTick(), предоставляемый непосредственно из объекта Vue, на самом деле метод экземпляра $nextTick(), который является удобной обёрткой над Vue.nextTick() и для которого this автоматически привязан к текущему экземпляру.

Но что если нужно манипулировать DOM вручную, а также использовать или тестировать асинхронные компоненты в приложении? Или если по какой-то причине предпочитаете использовать старый добрый window.setTimeout()? В таком случае, код реализующий nextTick() становится мёртвым кодом — кодом, который написан, но никогда не будет выполняться. Мёртвый код — не лучшая вещь, особенно в контексте разработки сборки для клиентской стороны, где каждый килобайт имеет значение.

Системы сборок такие как webpack (opens new window), поддерживают технологию tree-shaking (opens new window), что подразумевает «тотальное уничтожение неиспользуемого кода». К сожалению, из-за того как был написан код предыдущих версий Vue, глобальные API, такие как Vue.nextTick(), создавались без учёта tree-shaking и всегда попадут в код финальной сборки, независимо от того использовались ли они фактически или нет.

# Синтаксис в 3.x

Во Vue 3, глобальные и внутренние API были реорганизованы с учётом поддержки tree-shaking. В итоге, доступ к глобальным API теперь возможен через именованные экспорты сборок ES-модулей. Например, предыдущие фрагменты кода теперь будут выглядеть так:

import { nextTick } from 'vue'

nextTick(() => {
  // что-то связанное с DOM
})
1
2
3
4
5

и соответственно

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

test('какая-то асинхронная возможность приложения', async () => {
  const wrapper = shallowMount(MyComponent)

  // выполнение некоторых задач, связанных с DOM

  await nextTick()

  // выполнение проверок
})
1
2
3
4
5
6
7
8
9
10
11
12
13

Теперь вызов напрямую Vue.nextTick() будет приводить к широко известной ошибке undefined is not a function.

Но благодаря этим изменениям, система сборки с поддержкой tree-shaking сможет удалять из итоговой сборки все глобальные API, которые не используются в приложении Vue, уменьшая итоговый размер сборки.

# Затрагиваемые API

Следующие глобальные API во Vue 2.x затрагиваются этим изменением:

  • Vue.nextTick
  • Vue.observable (заменено на Vue.reactive)
  • Vue.version
  • Vue.compile (только в полных сборках)
  • Vue.set (только в сборках для совместимости)
  • Vue.delete (только в сборках для совместимости)

# Внутренние вспомогательные методы

Кроме публичного API, многие из внутренних компонентов/вспомогательных методов также теперь доступны через именованные экспорты. Это позволяет компилятору генерировать код, импортирующий только используемые функции. Например, для следующего шаблона:

<transition>
  <div v-show="ok">hello</div>
</transition>
1
2
3

будет скомпилирован такой код:

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

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

По сути, это означает что компонент Transition будет импортироваться лишь тогда, когда приложение действительно использует его. Другими словами, если не используется ни одного компонента <transition> в приложении, то весь код для поддержки этой возможности не будет присутствовать в финальной сборке.

При глобальной поддержке tree-shaking пользователь «платит» только за функции которые используются. Более того, если дополнительные возможности не увеличивают размер сборки для приложений, которые их не используют, то и размер фреймворка менее подвержен появлению в будущем новых возможностей в ядре.

Важно

Вышесказанное относится к сборкам в виде ES-модулей при использовании вместе с системой сборки с поддержкой tree-shaking — в UMD-сборках всё также включены все возможности и экспортируется доступ ко всем API через глобальную переменную Vue (и компилятор будет генерировать соответствующий код для использования API из неё, вместо импортирования).

# Использование в плагинах

Если плагин полагается на затрагиваемый глобальный API Vue 2.x, например:

const plugin = {
  install: Vue => {
    Vue.nextTick(() => {
      // ...
    })
  }
}
1
2
3
4
5
6
7

Во Vue 3 теперь потребуется явно его импортировать:

import { nextTick } from 'vue'

const plugin = {
  install: app => {
    nextTick(() => {
      // ...
    })
  }
}
1
2
3
4
5
6
7
8
9

При использовании системы сборки, такой как webpack, это может привести к тому, что исходный код Vue добавится в плагин, и чаще всего это не тот результат, который нужен. Обычная практика для предотвращения такого заключается в исключении Vue из итоговой сборки настройками сборщика. В случае с webpack для этого есть опция externals (opens new window):

// webpack.config.js
module.exports = {
  /*...*/
  externals: {
    vue: 'Vue'
  }
}
1
2
3
4
5
6
7

Это подскажет webpack обрабатывать модуль Vue как внешнюю библиотеку, а не добавлять его в сборку.

При использовании системы сборки Rollup (opens new window) этот эффект получаете бесплатно, так как по умолчанию Rollup рассматривает все ID модулей (в нашем случае 'vue') как внешние зависимости и не включает их в финальную сборку. Но во время сборки может вывести предупреждение «Treating vue as external dependency» (opens new window), которое можно отключить через опцию external:

// rollup.config.js
export default {
  /*...*/
  external: ['vue']
}
1
2
3
4
5