Эта статья — перевод оригинальной статьи Andy Li из Vue Mastery "Vue 3 Migration Changes: Replace, Rename, and Remove (Pt. 2)".
Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.
Вступление
Команда Vue недавно выпустила долгожданный билд миграции для Vue 3. Если вы думали об обновлении своего приложения с Vue 2 до Vue 3, это то, что вам нужно.
Процесс обновления приложения до последней версии фреймворка может оказаться непростой задачей. Эта серия статей создана, чтобы упростить этот процесс.
Серия миграции на Vue 3 состоит из двух частей:
Изменения для миграции на Vue (эта статья)
Если вы не знакомы с билдом для миграции, ознакомьтесь со статьей Билд для миграции на Vue 3, это предварительная подготовка для этой статьи. Если у вас нет приложения для переноса, вы все равно можете использовать эту статью, чтобы узнать, что изменилось в Vue 3. Но имейте в виду, что мы обсуждаем здесь только изменения, мы не будем углубляться в новые функции, такие как Composition API. (ознакомьтесь с курсом Vue Mastery's Composition API, если вам это интересно)
Наряду с этой статьей мы также создали наглядный альбом для наиболее распространенных изменений.
Процесс миграции
Это процесс использования билда миграции, который вы могли видеть в предыдущей статье:

В предыдущей статье мы исправили несколько простых устаревших функций, чтобы проиллюстрировать, как использовать билд миграции. Эта статья об устаревших функциях Vue 2.
Основываясь на описанном выше процессе миграции, функции, устаревшие в Vue 3, можно разделить на четыре категории: несовместимые, замененные, переименованные и удаленные.

Каждая категория будет содержать различные устаревшие фичи и соответствующие им стратегии миграции (то, что вам нужно сделать, чтобы ваш код снова работал на Vue 3).
Для удобства использования (и поиска в Google) я поместил названия флагов "устаревания" в соответствующие разделы статьи. Это флаги, которые появляются, когда вы запускаете сборку миграции с устаревшим кодом.

Опять же, если вы не понимаете что здесь происходит, вы можете освежить свою память с помощью статьи Билд для миграции на Vue 3.
Без лишних слов, давайте начнем наше путешествие!
Несовместимые
Устаревшие фичи в этой категории фактически приведут к тому, что ваше приложение вообще не будет работать, даже если запущен билд миграции. Поэтому нам нужно исправить их, прежде всего.

Named и Scoped слоты (заменены)
Слоты и слоты с ограниченной областью видимости - это сложные темы, которые мы не будем рассматривать в этой статье. Но это руководство может помочь вам реорганизовать код для Vue 3, если вы уже используете <slot> в своих компонентах.
В Vue 2 вы можете создать именованный слот с ограниченной областью видимости следующим образом:
<ItemList>
<template slot="heading" slot-scope="slotProps">
<h1>My Heading for {{ slotProps.items.length }} items</h1>
</template>
</ItemList>
(Это все еще работает в Vue 2.6, но считается устаревшим и больше не будет работать в Vue 3.)
В Vue 3 вам нужно будет изменить его на это:
<ItemList>
<template v-slot:heading="slotProps">
<h1>My Heading for {{ slotProps.items.length }} items</h1>
</template>
</ItemList>
Изменения:
Использование v-slot вместо того, чтобы объединить вместе slot и slot-scope, чтобы сделать то же самое.
Если вам не нужен slotProps, вы можете просто указать атрибут v-slot: heading без значения.
INSTANCE_SCOPED_SLOTS
Кстати, если ваш код использует свойство $scopedSlots, его необходимо переименовать в $slots в Vue 3.
Functional атрибут (удален)
COMPILER_SFC_FUNCTIONAL
В Vue 2 вы можете создать функциональный компонент в однофайловом компоненте (SFC) следующим образом:
<template functional>
<h1>{{ text }}</h1>
</template>
<script>
export default {
props: ['text']
}
</script>
В Vue 3 вам придется удалить атрибут functional:
<template>
<h1>{{ text }}</h1>
</template>
<script>
export default {
props: ['text']
}
</script>
Таким образом, технически вы больше не можете создавать функциональные компоненты в формате SFC. Но поскольку преимущество функциональных компонентов в производительности во Vue 3 намного меньше, это в любом случае небольшая потеря. (Мы все еще можем создавать функциональные компоненты в Vue 3, но не в <template> в файле .vue. Подробнее об этом в разделе «Функциональные компоненты» ниже)
Mounted Container
GLOBAL_MOUNT_CONTAINER
Vue 3 не заменяет элемент, к которому подключено ваше приложение, поэтому вы можете увидеть два div с id="app" в обработанном HTML.

Чтобы избежать дублирования стилей, вам придется удалить id="app" из одного из двух тегов <div>
v-if и v-for
COMPILER_V_IF_V_FOR_PRECEDENCE
Если вы используете v-if и v-for вместе в одном элементе, вам придется реорганизовать свой код.
Поскольку настройка Vue CLI ESLint по умолчанию фактически не позволит вам использовать v-if и v-for вместе в одном элементе даже в приложении Vue 2, маловероятно, что у вас действительно есть такой код.
Но в том случае, если он есть, то вот что изменилось.
В Vue 2 v-for имеет приоритет над v-if, а в Vue 3 v-if имеет приоритет над v-for.
Итак, код на Vue 2, который отображает числа меньше 10 выглядит так:
<ul>
<li v-for="num in nums" v-if="num < 10">{{ num }}</li>
</ul>
В Vue 3 вам нужно написать это так:
<ul>
<li v-for="num in numsLower10">{{ num }}</li>
</ul>
numsLower10 должно быть вычисляемым свойством.
v-if branch keys
COMPILER_V_IF_SAME_KEY
Если у вас есть один и тот же ключ для нескольких ветвей одного и того же условного v-if:
<ul>
<li v-for="num in nums">
<span v-if="num < 10" :key="myKey">{{ num }}</span>
<span v-else class="high" :key="myKey">{{ num }}</span>
</li>
</ul>
В Vue 3 вам придется удалить их (или назначить им разные ключи):
<ul>
<li v-for="num in nums">
<span v-if="num < 10">{{ num }}</span>
<span v-else class="high">{{ num }}</span>
</li>
</ul>
Vue автоматически назначит им уникальные ключи.
v-for key
COMPILER_V_FOR_TEMPLATE_KEY_PLACEMENT
Если вы используете v-for на <template> с :key на внутренних элементах:
<template v-for="num in nums">
<div :key="num.id">{{ num }}</div>
</template>
Тогда в Vue 3 вам нужно будет поместить :key в <template>
<template v-for="num in nums" :key="num.id">
<div>{{ num }}</div>
</template>
Transition classes (переименованы)
Если вы используете элемент <Transition> для анимации, вам придется переименовать имена классов для v-enter и v-leave:
v-enter => v-enter-from
v-leave => v-leave-from
(Это изменение немного особенное, потому что билд миграции не предупредит вас об этом.)
Теперь мы можем перейти к другим, менее неизбежным отказам от поддержки. Вы можете работать с ними в любом порядке.
Заменённые
Устаревшие фичи в этой категории удаляются, но заменяются новыми функциями в качестве решений.

Инициализация приложения (заменено)
GLOBAL_MOUNT / GLOBAL_EXTEND / GLOBAL_PROTOTYPE / GLOBAL_OBSERVABLE / GLOBAL_PRIVATE_UTIL
Ваш файл Vue 2 main.js может выглядеть примерно так:
import Vue from "vue" // import an object
import App from './App.vue'
import router from './router'
import store from './store'
Vue.use(store)
Vue.use(router)
Vue.component('my-heading', {
props: [ 'text' ],
template: '<h1>{{ text }}</h1>'
})
// create an instance using the new keyword
const app = new Vue(App)
app.$mount("#app");
В Vue 3 вам нужно будет изменить его на это:
import { createApp } from 'vue' // import a function
import App from './App.vue'
import router from './router'
import store from './store'
// create an instance using the function
const app = createApp(App)
app.use(store)
app.use(router)
app.component('my-heading', {
props: [ 'text' ],
template: '<h1>{{ text }}</h1>'
})
// no dollar sign
app.mount('#app')
Изменения:
Больше нет импорта Vue из пакета vue. Мы должны использовать новую функцию createApp для создания экземпляра приложения.
У метода mount нет знака доллара.
Вместо использования таких функций, как Vue.use и Vue.component, которые повлияли бы на поведение Vue глобально, теперь мы должны использовать эквивалентные методы экземпляра, такие как app.use и app.component.
Вот список изменений от старого Global API к новому Instance API:
Vue.component
⇒app.component
Vue.use
⇒app.use
Vue.config
⇒app.config
Vue.directive
⇒app.directive
Vue.mixin
⇒app.mixin
Vue.prototype
⇒Vue.config.globalProperties
Vue.extend
⇒(nothing)
Vue.util
⇒(nothing)
Vue.extend удален. Поскольку экземпляр приложения больше не создается с помощью ключевого слова new и конструктора Vue, нет необходимости создавать конструктор подкласса путем наследования базового конструктора Vue с помощью функции extend.
Хотя Vue.util все еще существует, но теперь он приватный, поэтому вы тоже не сможете его использовать.
Vue.config.ignoredElements заменяется на app.config.compilerOptions.isCustomElement, который должен быть функцией вместо массива. И Vue.config.productionTip удален.
Функциональный компонент (заменен)
COMPONENT_FUNCTIONAL
Без использования <template> функциональный компонент Vue 2 можно создать следующим образом:
export default {
functional: true,
props: ['text'],
render(h, { props }) {
return h(`h1`, {}, props.text)
}
}
В Vue 3 вам нужно будет изменить его на это:
import { h } from 'vue'
const Heading = (props) => {
return h('h1', {}, props.text)
}
Heading.props = ['text']
export default Heading
Изменения:
Функциональный компонент должен быть функцией, а не параметром.
Хотя это и не является особенной темой функционального компонента, функция h в Vue 3 должна быть импортирована из пакета vue, а не передаваться в качестве параметра функции рендеринга.
v-for References (заменено)
V_FOR_REF
Если вы используете ref на элементе v-for для взятия всех ссылок HTML-элементов, к которым вы сможете получить доступ позже через this.$Refs.myNodes:
<template>
<ul>
<li v-for="item in items" :key="item.id" ref="myNodes">
...
</li>
</ul>
</template>
// later
...
mounted () {
console.log(this.$refs.myNodes) // list of HTML element nodes
}
В Vue 3 вам нужно будет использовать :ref (с двоеточием) для привязки к коллбэк функции:
<template>
<ul>
<li v-for="item in items" :key="item.id" :ref="setNode">
...
</li>
</ul>
</template>
// later
...
data() {
return {
myNodes: [] // create an array to hold the nodes
}
},
beforeUpdate() {
this.myNodes = [] // reset empty before each update
},
methods: {
setNode(el) { // this will be called automatically
this.myNodes.push(el) // add the node
}
},
updated() {
console.log(this.myNodes) // finally, a list of HTML node references
},
Здесь мы используем коллбэк функцию для добавления каждого узла в массив во время рендеринга. В конце у вас будет this.myNodes в качестве замены для this.$Refs.myNodes.
Native event (заменён)
COMPILER_V_ON_NATIVE
Чтобы проиллюстрировать проблему, допустим, у нас есть такой компонент SpecialButton:
<template>
<div>
<button>Special Button</button>
</div>
</template>
Когда вы используете этот компонент (в родительском компоненте), давайте также предположим, что вы хотите добавить собственное событие click к элементу <div>, вы бы сделали это так (в Vue 2):
<SpecialButton v-on:click.native="foo" />
В Vue 3 модификатор native удален, поэтому приведенный выше код не будет работать.
Итак, как нам добавить нативное событие click к элементу <div>, расположенному в нашем компоненте SpecialButton?
Нужно будет удалить модификатор native, и он снова заработает:
<SpecialButton v-on:click="foo" />
Это устаревание было легко исправлено, но из-за этого возникла новая проблема.
В Vue 3 все события, прикрепленные к компоненту, будут обрабатываться как нативные события и будут добавляться к корневому элементу этого компонента. Это поведение по умолчанию, и поэтому нам больше не нужен модификатор native.
Но что, если событие, которое мы добавляем, не предназначено для использования в качестве нативного?
Например, мы хотим, чтобы SpecialButton генерировал кастомное событие:
<template>
<div>
<button v-on:click="$emit('special-click')">Special Button</button>
</div>
</template>
В родительском компоненте мы должны слушать событие следующим образом:
<SpecialButton v-on:click="foo" v-on:special-click="bar" />
Как и событие click, кастомное событие click по умолчанию будет прикреплено к корневому элементу (<div>) компонента SpecialButton, что не является тем, чего мы хотим добиться. Хотя кастомное событие click по-прежнему будет генерироваться, проблема в том, что это же событие также присоединяется к элементу как нативное событие.
Сейчас это не кажется большой проблемой, так как кастомное событие click никогда не будет запускаться на <div>. Но это может быть проблемой, если настраиваемое событие называется click или любым именем, которое также является нативным событием. В этом случае один щелчок мыши вызовет несколько событий click. (один на <button>, другой ошибочно на <div>)
Решением этого является новый способ оповещения в Vue 3.
С помощью параметра emits мы можем настроить так как нам необходимо, чтобы наше событие не было ошибочно принято за нативное событие и добавлено в корневой элемент.
Итак, если мы укажем все пользовательские события (в нашем случае только одно), которые будут генерироваться в SpecialButton:
export default {
name: 'SpecialButton',
emits: ['special-click'] // ADD
}
Vue будет знать, что это кастомное событие, и не будет прикреплять наше событие click к корневому элементу как нативное событие.
Поэтому, если вы используете модификатор .native, вам придется его удалить. А чтобы предотвратить добавление непреднамеренных событий к корневому элементу, вам придется использовать emits для документирования всех настраиваемых событий, которые может генерировать компонент.
Переименованные
Устаревания в этой категории самые тривиальные. Все, что вам нужно сделать, это изменить старые имена на новые.

v-model prop and event (переименованные)
COMPONENT_V_MODEL
Если вы используете v-model в компоненте, вам придется переименовать этот prop и событие:
value ⇒ modelValue
$emit("input") ⇒ $emit("update:modelValue")
(Не забудьте указать название события в параметре emits, как упоминалось ранее)
Lifecycle hooks (переименованные)
OPTIONS_BEFORE_DESTROY / OPTIONS_BEFORE_DESTROY
Если вы используете хуки жизненного цикла beforeDestroy и destroy, вам придется переименовать их:
beforeDestroy ⇒ beforeUnmount
destroyed ⇒ unmounted
(Никаких изменений в других хуках)
Lifecycle events (переименованные)
INSTANCE_EVENT_HOOKS
Если вы отслеживаете события жизненного цикла компонента в его родительском компоненте:
<template>
<MyComponent @hook:mounted="foo">
</template>
В Vue 3 вам придется переименовать префикс атрибута с @hook: на @vnode-:
<template>
<MyComponent @vnode-mounted="foo">
</template>
Как упоминалось в предыдущем разделе, beforeDestroy и destroy были переименованы. Поэтому, если вы используете @hook:beforeDestroy и @hook:destroy, вам придется вместо этого переименовать их в @vnode-beforeMount и @vode-unmounted.
Custom directive hooks (переименованные)
CUSTOM_DIR
Если вы создавали свои собственные директивы, вам придется переименовать следующие хуки в своих реализациях директив:
bind ⇒ beforeMount
inserted ⇒ mounted
componentUpdated ⇒ updated
unbind ⇒ unmounted
Если вы используете update хук, он был удалён в Vue 3, поэтому вам нужно переместить код оттуда в updated хук.
Удалённые
Эта категория посвящена удаленным фичам. Для некоторых из них нам просто нужно будет прекратить использовать их в нашем коде, а для других нужно будет найти обходные пути.

Reactive property setters (удалённые)
GLOBAL_SET / GLOBAL_DELETE / INSTANCE_SET / INSTANCE_SET
Vue 3 был переписан с новой системой реактивности, основанной на технологиях ES6, поэтому нет необходимости делать отдельные свойства реактивными. В результате Vue 3 больше не предлагает следующие API, поэтому вам придется их удалить:
Vue.set
Vue.delete
vm.$set
vm.$delete
(vm ссылается на экземпляр Vue)
vm.$children (удалённые)
INSTANCE_CHILDREN
Если вы используете this.$children в своем компоненте для доступа к дочернему компоненту:
<template>
<AnotherComponent>Hello World</AnotherComponent>
</template>
...
mounted() {
console.log(this.$children[0])
},
В Vue 3 вам придется использовать атрибут ref вместе со свойством this.$refs в качестве обходного пути.
Вкратце, если вы установите ref с именем дочернего компонента:
<template>
<AnotherComponent ref="hello">Hello World</AnotherComponent>
</template>
Вы сможете получить к нему доступ, используя свойство $refs в своем JavaScript коде:
mounted() {
console.log(this.$refs.hello)
}
vm.$listeners (удалённые)
INSTANCE_LISTENERS
Если вы используете this.$listeners для доступа к обработчикам событий, переданных из родительского компонента:
// Parent component
<MyComponent v-on:click="foo" v-on:mouseenter="bar" />
// Child component
mounted() {
console.log(this.$listeners)
}
В Vue 3 вам придется обращаться к ним отдельно через свойство $attrs:
mounted() {
console.log(this.$attrs.onClick)
console.log(this.$attrs.onMouseenter)
}
vm.$on, vm.$off, vm.$once (удалённые)
INSTANCE_EVENT_EMITTER
Это может повлиять на ваш проект, если вы использовали vm.$on, vm.$off или vm.$once как часть механизма для PubSub. Вам придется удалить эти методы экземпляра и использовать для этого другую библиотеку.
В качестве потенциального решения обратите внимание на сторонний инструмент под названием tiny-emitter.
Filters (удалённые)
FILTERS
Если вы используете фильтры (синтаксис вертикальной черты) в своем шаблоне:
<template>
<p>{{ num | roundDown }}</p>
</template>
...
filters: {
roundDown(value) {
return Math.floor(value)
}
},
В Vue 3 вам придется вместо этого использовать простое вычисляемое свойство:
</template>
...
computed: {
numRoundedDown() {
return Math.floor(this.num)
}
},
Обоснование удаления фильтров состоит в том, что используемый синтаксис не соответствует реальному поведению JavaScript (вертикальная черта должна быть побитовым оператором в JavaScript). Большинство вещей, которые мы помещаем в двойные фигурные скобки, являются настоящим JavaScript, поэтому было бы ошибкой, если бы этого не было.
Еще одно преимущество вычисляемого свойства над фильтром состоит в том, что код шаблона будет чище.
is attribute (удалённые)
COMPILER_IS_ON_ELEMENT
В Vue 2 вы можете применить атрибут is к нативному элементу, чтобы отобразить его как компонент:
<button is="SpecialButton"></button>
В Vue 3 вам нужно будет заменить нативный элемент на <component>, чтобы получить такое же поведение:
<component is="SpecialButton"></component>
Если вы не используете формат Single File Component, вы можете просто добавить префикс vue:
<button is="vue:SpecialButton"></button>
Keyboard codes (удалённые)
V_ON_KEYCODE_MODIFIER
В Vue 2 вы можете прослушивать события на определенных клавишах на клавиатуре с соответствующими кодами клавиш:
<input type="text" v-on:keyup.112="validateText" />
(112 представляет собой клавишу Enter)
В Vue 3 вам придется вместо этого использовать название:
<input type="text" v-on:keyup.enter="validateText" />
Всякое разное
В этой категории есть все устаревания, которые не соответствуют трем основным категориям (заменить, переименовать и удалить).

v-bind порядок
COMPILER_V_BIND_OBJECT_ORDER
v-bind теперь чувствителен к порядку.
Если вы используете v-bind="object" и ожидаете, что один или несколько других атрибутов переопределяют свойства объекта, вам необходимо переместить атрибут v-bind перед всеми другими указанными атрибутами.
Допустим, у вас было это:
<Foo a="1" b="2" v-bind:"{ a: 100, b: 200 }">
Свойства объекта будут переопределены двумя другими атрибутами, поскольку у них одинаковые имена a и b.
В Vue 3 вам нужно будет поставить v-bind перед другими атрибутами, чтобы добиться того же «перекрывающего» эффекта:
<Foo v-bind:"{ a: 100, b: 200 }" a="1" b="2">
v-bind модификатор sync
COMPILER_V_BIND_SYNC
Если вы используете v-bind с модификатором sync:
<MyComponent v-bind:title.sync="myString" />
В Vue 3 вам придется вместо этого использовать v-model:
<MyComponent v-model:title="myString" />
v-model был улучшен в Vue 3. Теперь вы можете передать аргумент для v-model, как в это с title выше, и вы даже можете иметь несколько v-model.
Но ваш линтер может выдать ошибку об этом новом способе использования v-model:

Чтобы исправить это, вам нужно добавить правило в свой package.json, чтобы отключить его:
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {
"vue/no-v-model-argument": "off" // ADD
}
(делайте это только в том случае, если ваш линтер выдает ошибку)
Async Component
COMPONENT_ASYNC
Асинхронный компонент в Vue 2 выглядит так:
const MyComponent = {
component: () => import('./MyComponent.vue'),
...
}
В Vue 3 вам нужно будет изменить его на это:
import { defineAsyncComponent } from 'vue'
const MyComponent = defineAsyncComponent({
loader: () => import('./MyComponent.vue'),
...
})
Изменения:
Использование новой функции defineAsyncComponent.
Название поля component изменено на loader.
Если ваш асинхронный компонент является просто функцией, он также должен быть заключен в defineAsyncComponent:
// Before
const MyComponent = () => import('./MyComponent.vue')
// After
const MyComponent = defineAsyncComponent(() => import('./MyComponent.vue'))
watch array deep
WATCH_ARRAY
Если вы используете watch для наблюдения за массивом, вы должны передать параметр deep:
watch: {
items: {
handler(val, oldVal) {
console.log(oldVal + ' --> ' + val)
},
deep: true // ADD
}
},
false в атрибутах
ATTR_FALSE_VALUE / ATTR_ENUMERATED_COERSION
Если вы используете false для удаления атрибута, который не является boolean:
// template
<img src="..." :alt="false" />
// rendered
<img src="..." />
В Vue 3 вместо этого нужно использовать null:
// template
<img src="..." :alt="null" />
// rendered
<img src="..." />
Сделав его false в Vue 3, он просто отобразит false в HTML.
Если вы используете так называемые «перечисляемые атрибуты», такие как draggable и проверка орфографии, они также подчиняются указанным выше правилам в Vue 3: false для установки false, null для удаления.
class и style
INSTANCE_ATTRS_CLASS_STYLE
В Vue 3 class и style включены в $attrs, поэтому вы можете столкнуться с некоторыми «сбоями», если ваш код ожидает, что class и style не будут частью $attrs.
В частности, если ваш код использует inheritAttrs: false в компоненте, в Vue 2 class и style по-прежнему будут передаваться корневому элементу этого компонента, поскольку они не являются частью $attrs, но в Vue 3 class и style больше не будет передаваться в корень, поскольку они являются частью $attrs.
Vuex и Vue Router
Если вы используете Vuex и Vue router, вам необходимо обновить их до Vuex 4 и Vue Router 4.
"dependencies": {
"vuex": "^4.0.0",
"vue-router": "^4.0.0",
...
}
Подобно Vue 3, Vuex 4 и Vue Router 4 также изменили свои глобальные API. Теперь вам нужно использовать createStore и createRouter так же, как и createApp:
import { createStore } from 'vuex'
import { createRouter } from 'vue-router'
const store = createStore({
state: {...},
mutations: {...},
actions: {...},
})
const router = createRouter({
routes: [...]
})
Ещё
Есть еще несколько устаревших штук, которые не рассматриваются здесь, потому что они либо слишком тривиальны, чтобы повлиять на что-либо, либо просто очень необычны. Но с приложением, запущенным в билде миграции, если вы когда-нибудь столкнетесь с каким-либо предупреждением, которое не было упомянуто здесь, вы можете поискать флаг предупреждения в Google и прочитать страницу документации об этом.
Как упоминалось ранее, мы также создали шпаргалку по некоторым устаревшим функциям, описанным в этой статье.