# Поддержка tree-shaking для глобального API
кардинальное изменение
# Синтаксис в 2.x
Если приходилось когда-нибудь вручную манипулировать DOM во Vue, то наверняка сталкивались с таким шаблоном:
import Vue from 'vue'
Vue.nextTick(() => {
// что-то связанное с DOM
})
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()
// выполнение проверок
})
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
})
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()
// выполнение проверок
})
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>
2
3
будет скомпилирован такой код:
import { h, Transition, withDirectives, vShow } from 'vue'
export function render() {
return h(Transition, [withDirectives(h('div', 'hello'), [[vShow, this.ok]])])
}
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(() => {
// ...
})
}
}
2
3
4
5
6
7
Во Vue 3 теперь потребуется явно его импортировать:
import { nextTick } from 'vue'
const plugin = {
install: app => {
nextTick(() => {
// ...
})
}
}
2
3
4
5
6
7
8
9
При использовании системы сборки, такой как webpack, это может привести к тому, что исходный код Vue добавится в плагин, и чаще всего это не тот результат, который нужен. Обычная практика для предотвращения такого заключается в исключении Vue из итоговой сборки настройками сборщика. В случае с webpack для этого есть опция externals
(opens new window):
// webpack.config.js
module.exports = {
/*...*/
externals: {
vue: 'Vue'
}
}
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']
}
2
3
4
5