Руководство по обновлению
Обновление Nuxt
Последний релиз Nuxt 3
Чтобы обновить Nuxt до последнего релиза v3, используйте команду nuxt upgrade с флагом --channel=v3.
npx nuxt upgrade --dedupe --channel=v3
yarn nuxt upgrade --dedupe --channel=v3
pnpm nuxt upgrade --dedupe --channel=v3
bun x nuxt upgrade --dedupe --channel=v3
@nuxt/cli, в которой реализован флаг --channel. Если нет — для первого обновления используйте nuxi@latest, например: npx nuxi@latest upgrade --dedupe --channel=v3.Последний релиз
Чтобы обновить Nuxt до последней версии, используйте команду nuxt upgrade.
npx nuxt upgrade
yarn nuxt upgrade
pnpm nuxt upgrade
bun x nuxt upgrade
deno x nuxt upgrade
Канал ночных сборок (nightly)
Чтобы использовать последнюю сборку Nuxt и тестировать функции до их выхода, см. руководство по каналу ночных релизов.
latest ночного канала сейчас следует за веткой Nuxt v4 — возможны частые ломающие изменения; будьте осторожны. Для ночных сборок ветки 3.x укажите "nuxt": "npm:nuxt-nightly@3x".Тестируем Nuxt 4
Релиз Nuxt 4 запланирован на второй квартал 2025 года. В него войдут все возможности, которые сейчас доступны через compatibilityVersion: 4.
До выхода релиза многие критические изменения Nuxt 4 уже можно пробовать, начиная с Nuxt 3.12+.
Внедрение Nuxt 4
Первым шагом необходимо обновить Nuxt до последней версии.
Затем вы можете установить compatibilityVersion для соответствия поведению Nuxt 4:
export default defineNuxtConfig({
future: {
compatibilityVersion: 4,
},
// To re-enable _all_ Nuxt v3 behavior, set the following options:
// srcDir: '.',
// dir: {
// app: 'app'
// },
// experimental: {
// scanPageMeta: 'after-resolve',
// sharedPrerenderData: false,
// compileTemplate: true,
// resetAsyncDataToUndefined: true,
// templateUtils: true,
// relativeWatchPaths: true,
// normalizeComponentNames: false,
// spaLoadingTemplateLocation: 'within',
// parseErrorData: false,
// pendingWhenIdle: true,
// alwaysRunFetchOnKeyChange: true,
// defaults: {
// useAsyncData: {
// deep: true
// }
// }
// },
// features: {
// inlineStyles: true
// },
// unhead: {
// renderSSRHeadOptions: {
// omitLineBreaks: false
// }
// }
})
При compatibilityVersion: 4 дефолты в конфигурации соответствуют Nuxt v4; отдельные опции можно вернуть к поведению v3 по закомментированным подсказкам выше. Сообщайте о проблемах — так проще поправить Nuxt и экосистему.
Ломающие или значительные изменения, а также шаги по миграции и доступные опции конфигурации, описаны ниже.
Миграция с использованием Codemods
Чтобы упростить обновление, мы сотрудничали с командой Codemod над автоматизацией шагов миграции открытыми codemod.
npx codemod feedback 🙏Полный список codemod для Nuxt 4, описание каждого, исходники и способы запуска — в Codemod Registry.
Все codemod из этого руководства можно запустить одним рецептом codemod:
# Using pinned version due to https://github.com/codemod/codemod/issues/1710
npx codemod@0.18.7 nuxt/4/migration-recipe
# Using pinned version due to https://github.com/codemod/codemod/issues/1710
yarn dlx codemod@0.18.7 nuxt/4/migration-recipe
# Using pinned version due to https://github.com/codemod/codemod/issues/1710
pnpm dlx codemod@0.18.7 nuxt/4/migration-recipe
# Using pinned version due to https://github.com/codemod/codemod/issues/1710
bun x codemod@0.18.7 nuxt/4/migration-recipe
# Using pinned version due to https://github.com/codemod/codemod/issues/1710
deno x codemod@0.18.7 nuxt/4/migration-recipe
Команда последовательно выполнит все codemod; при необходимости можно отключить отдельные шаги. Ниже каждый codemod перечислен с описанием изменения и может быть запущен отдельно.
Новая структура директорий
🚦 Уровень влияния: Значительный
Теперь Nuxt по умолчанию использует новую структуру каталогов с обратной совместимостью: если обнаружена прежняя схема (например, каталог pages/ в корне проекта), новая не навязывается. Общая схема — в документации по структуре каталогов.
Что изменилось
- в новом Nuxt по умолчанию
srcDirтеперьapp/, и большинство вещей решается оттуда. serverDirтеперь по умолчанию имеет значение<rootDir>/server, а не<srcDir>/server.layers/,modules/иpublic/по умолчанию разрешаются относительно<rootDir>.- при использовании Nuxt Content v2.13+,
content/разрешается относительно<rootDir> - добавлена директория
dir.appдляrouter.options.tsиspa-loading-template.html— по умолчанию это<srcDir>/ - появилась директория
shared/для кода, общего между Vue-приложением и сервером Nitro, с автоимпортом изshared/utils/иshared/types/
Пример структуры папок v4.
.output/
.nuxt/
app/
assets/
components/
composables/
layouts/
middleware/
pages/
plugins/
utils/
app.config.ts
app.vue
router.options.ts
content/
layers/
modules/
node_modules/
public/
shared/
types/
utils/
server/
api/
middleware/
plugins/
routes/
utils/
nuxt.config.ts
~ по умолчанию указывает на каталог app/ (ваш srcDir). То есть ~/components ведёт в components/, ~/pages — в pages/ и т.д.👉 Более подробную информацию можно найти в PR, реализующем это изменение.
Причины таких изменений
- Производительность — если весь код лежит в корне репозитория, наблюдатели ФС чаще затрагивают
.git/иnode_modules/, что заметно замедляет старт на ОС, отличных от macOS. - Типы в IDE —
server/и клиентский код живут в разных контекстах с разными глобальными импортами; вынестиserver/из папки приложения — первый шаг к предсказуемому автодополнению.
Этапы миграции
- Создайте новую директорию с именем
app/. - Переместите в неё каталоги
assets/,components/,composables/,layouts/,middleware/,pages/,plugins/иutils/, а такжеapp.vue,error.vue,app.config.ts. Если у вас уже лежат файлыapp/router-options.tsилиapp/spa-loading-template.html, пути к ним сохраняются. - Убедитесь, что папки
nuxt.config.ts,content/,layers/,modules/,public/,shared/иserver/находятся вне папкиapp/, в корне вашего проекта. - Обновите конфигурации сторонних инструментов под новую структуру (например Tailwind или ESLint). Для
@nuxtjs/tailwindcssнастройка Tailwind обычно подхватывается автоматически.
npx codemod@latest nuxt/4/file-structureОднако миграция не обязательна: при текущей раскладке каталогов Nuxt подстроится сам (если нет — сообщите об issue). Исключение — если у вас уже задан свой srcDir: тогда modules/, public/, shared/ и каталог server/ резолвятся от rootDir, а не от вашего пользовательского srcDir. При необходимости переопределите dir.modules, dir.public и serverDir.
Вы также можете принудительно использовать структуру папок v3 с помощью следующей конфигурации:
export default defineNuxtConfig({
// This reverts the new srcDir default from `app` back to your root directory
srcDir: '.',
// This specifies the directory prefix for `app/router.options.ts` and `app/spa-loading-template.html`
dir: {
app: 'app',
},
})
Единый слой загрузки данных (один общий кэш на приложение)
🚦 Уровень влияния: Умеренный
Что изменилось
Система загрузки данных (useAsyncData и useFetch) перестроена ради производительности и предсказуемого поведения:
- Общие ref для одного ключа: все вызовы
useAsyncData/useFetchс одним ключом делят одни и те жеdata,errorиstatus. Поэтому при явном ключе нельзя смешивать конфликтующие опцииdeep,transform,pick,getCachedDataилиdefault. - Тоньше управление
getCachedData:getCachedDataвызывается при каждой загрузке данных, в том числе из-за watcher илиrefreshNuxtData(раньше в этих случаях функция не вызывалась). В аргументах теперь есть контекст с причиной запроса. - Реактивные ключи: ключом могут быть вычислимое свойство (
computed), обычныйrefили геттер — данные перезапрашиваются автоматически и хранятся раздельно. - Очистка данных: когда последний компонент, использовавший данные из
useAsyncData, размонтирован, Nuxt удаляет их, чтобы не копить память.
Зачем это сделано
Меньше потребление памяти и единообразие состояний загрузки между вызовами useAsyncData.
Шаги миграции
- Проверьте несовместимые опции: компоненты с одним ключом, но разными опциями или разными функциями запроса.
// This will now trigger a warning const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false }) const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })
Имеет смысл вынести вызовы с общим ключом и своими опциями в отдельный композабл:composables/useUserData.tsexport function useUserData (userId: string) { return useAsyncData( `user-${userId}`, () => fetchUser(userId), { deep: true, transform: user => ({ ...user, lastAccessed: new Date() }), }, ) } - Обновите реализации
getCachedData:useAsyncData('key', fetchFunction, { - getCachedData: (key, nuxtApp) => { - return cachedData[key] - } + getCachedData: (key, nuxtApp, ctx) => { + // ctx.cause - can be 'initial' | 'refresh:hook' | 'refresh:manual' | 'watch' + + // Example: Don't use cache on manual refresh + if (ctx.cause === 'refresh:manual') return undefined + + return cachedData[key] + } })
Временно это поведение можно отключить так:
export default defineNuxtConfig({
experimental: {
granularCachedData: false,
purgeCachedData: false,
},
})
Исправленный порядок загрузки модулей в слоях
🚦 Уровень влияния: Минимальный
Что изменилось
Порядок загрузки модулей при слоях Nuxt приведён к ожидаемому: раньше модули из корня проекта шли раньше модулей из расширяемых слоёв — вопреки ожидаемому.
Теперь порядок такой:
- Сначала модули слоёв (в порядке
extends— сначала «глубже» лежащие слои) - В конце модули проекта (наивысший приоритет)
На это влияет:
- массив
modulesвnuxt.config.ts - автоподхват модулей из каталога
modules/
Зачем это сделано
Так достигается следующее:
- расширяемые слои имеют ниже приоритет, чем проект, который их подключает
- порядок выполнения модулей совпадает с интуитивным наследованием слоёв
- конфигурация и хуки модулей ведут себя предсказуемо при нескольких слоях
Шаги миграции
Большинству проектов ничего менять не нужно — исправлен только порядок под стандартное ожидание.
Если же вы опирались на прежний ошибочный порядок, может понадобиться:
- Проверить зависимости модулей: нет ли привязки к конкретному порядку загрузки
- Скорректировать конфигурацию модулей, если она обходила старый баг
- Протестировать работу после исправления
Пример правильного порядка:
// Layer: my-layer/nuxt.config.ts
export default defineNuxtConfig({
modules: ['layer-module-1', 'layer-module-2'],
})
// Project: nuxt.config.ts
export default defineNuxtConfig({
extends: ['./my-layer'],
modules: ['project-module-1', 'project-module-2'],
})
// Loading order (corrected):
// 1. layer-module-1
// 2. layer-module-2
// 3. project-module-1 (can override layer modules)
// 4. project-module-2 (can override layer modules)
Если порядок модулей мешает зарегистрировать хук вовремя, используйте хук modules:done — он выполняется после загрузки всех модулей.
👉 Подробности: PR #31507 и issue #25719.
Дедупликация метаданных маршрута
🚦 Уровень влияния: Минимальный
Что изменилось
Часть полей маршрута задаётся через definePageMeta (name, path и т.д.). Раньше они дублировались и на объекте маршрута, и в meta (например route.name и route.meta.name).
Теперь они доступны только на объекте маршрута.
Зачем это сделано
Включён по умолчанию experimental.scanPageMeta — это оптимизация производительности.
Шаги миграции
Обычно достаточно заменить обращение к meta:
const route = useRoute()
- console.log(route.meta.name)
+ console.log(route.name)
Нормализованные имена компонентов
🚦 Уровень влияния: Умеренный
Vue теперь генерирует имена компонентов в том же стиле, что и схема имён Nuxt.
Что изменилось
Если имя не задано вручную, Vue присваивает компоненту имя, совпадающее с именем файла.
├─ components/
├─── SomeFolder/
├───── MyComponent.vue
Для Vue имя компонента было бы MyComponent — его нужно было бы указывать в <KeepAlive> и в Vue DevTools.
Для автоимпорта Nuxt использовалось имя SomeFolderMyComponent.
Теперь оба значения совпадают: имя в Vue совпадает с паттерном имён Nuxt.
Шаги миграции
Обновите имена в тестах с findComponent из @vue/test-utils и в <KeepAlive>, если они завязаны на старое имя компонента.
Временно это поведение можно отключить так:
export default defineNuxtConfig({
experimental: {
normalizeComponentNames: false,
},
})
Unhead v2
🚦 Уровень влияния: Минимальный
Что изменилось
Unhead (генерация тегов <head>) обновлён до версии 2. В целом совместим, но есть ломающие изменения низкоуровневого API:
- удалены пропсы:
vmid,hid,children,body - на вход больше нельзя передавать Promise
- теги по умолчанию сортируются через Capo.js
Шаги миграции
Для большинства приложений влияние небольшое.
При проблемах проверьте:
- что не используются удалённые пропсы
useHead({
meta: [{
name: 'description',
// meta tags don't need a vmid, or a key
- vmid: 'description'
- hid: 'description'
}]
})
- при использовании Template Params или Alias Tag Sorting эти возможности нужно явно подключить плагинами
import { AliasSortingPlugin, TemplateParamsPlugin } from '@unhead/vue/plugins'
export default defineNuxtPlugin({
setup () {
const unhead = injectHead()
unhead.use(TemplateParamsPlugin)
unhead.use(AliasSortingPlugin)
},
})
Импорты из @unhead/vue лучше заменить на #imports или nuxt/app (не обязательно, но рекомендуется).
-import { useHead } from '@unhead/vue'
+import { useHead } from '#imports'
При необходимости можно вернуться к поведению v1, включив head.legacy:
export default defineNuxtConfig({
unhead: {
legacy: true,
},
})
Новое расположение SPA-экрана загрузки в DOM
🚦 Уровень влияния: Минимальный
Что изменилось
Для клиентских страниц (ssr: false) экран загрузки из app/spa-loading-template.html раньше мог рендериться внутри корня приложения Nuxt:
<div id="__nuxt">
<!-- spa loading template -->
</div>
Теперь по умолчанию шаблон выводится рядом с корнем:
<div id="__nuxt"></div>
<!-- spa loading template -->
Зачем это сделано
Шаблон остаётся в DOM до завершения suspense у приложения Vue — меньше «белой вспышки».
Шаги миграции
Если вы стилизовали экран загрузки через CSS или document.querySelector, обновите селекторы. Для этого добавлены опции app.spaLoaderTag и app.spaLoaderAttrs.
Либо можно вернуть прежнее поведение так:
export default defineNuxtConfig({
experimental: {
spaLoadingTemplateLocation: 'within',
},
})
Разбор error.data
🚦 Уровень влияния: Минимальный
Раньше можно было выбросить ошибку со свойством data, но оно не разбиралось. Теперь оно парсится и доступно в объекте error. Хотя это исправление, технически это ломающее изменение, если вы полагались на прежнее поведение и разбирали data вручную.
Шаги миграции
В кастомном error.vue уберите дополнительный разбор error.data:
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps({
error: Object as () => NuxtError
})
- const data = JSON.parse(error.data)
+ const data = error.data
</script>
Либо можно отключить это изменение:
export default defineNuxtConfig({
experimental: {
parseErrorData: false,
},
})
Более точечная инлайн-стилизация
🚦 Уровень влияния: Умеренный
Теперь Nuxt инлайнит стили только для компонентов Vue, а не глобальный CSS.
Что изменилось
Раньше Nuxt инлайнил весь CSS, включая глобальные стили, и убирал теги <link> на отдельные CSS-файлы. Теперь так делается только для компонентов Vue (которые раньше давали отдельные чанки CSS). Так достигается лучший баланс: по-прежнему меньше отдельных сетевых запросов (как и раньше, при первой загрузке не будет отдельных запросов на каждый .css на страницу или компонент), плюс можно кэшировать один глобальный CSS-файл и уменьшить размер HTML первого ответа.
Шаги миграции
Поведение полностью настраивается: чтобы вернуться к прежнему варианту, задайте inlineStyles: true — тогда будут инлайниться и глобальный CSS, и стили по компонентам.
export default defineNuxtConfig({
features: {
inlineStyles: true,
},
})
Сканирование метаданных страниц после разрешения
🚦 Уровень влияния: Минимальный
Что изменилось
Метаданные страниц (из definePageMeta) теперь сканируются после вызова хука pages:extend, а не до него.
Зачем это сделано
Так можно учитывать метаданные страниц, которые добавляются в pages:extend. При этом по-прежнему можно менять или переопределять метаданные в новом хуке pages:resolved.
Шаги миграции
Чтобы переопределить метаданные страниц, делайте это в pages:resolved, а не в pages:extend.
export default defineNuxtConfig({
hooks: {
- 'pages:extend'(pages) {
+ 'pages:resolved'(pages) {
const myPage = pages.find(page => page.path === '/')
myPage.meta ||= {}
myPage.meta.layout = 'overridden-layout'
}
}
})
Либо можно вернуть прежнее поведение так:
export default defineNuxtConfig({
experimental: {
scanPageMeta: true,
},
})
Общие данные пререндера
🚦 Уровень влияния: Умеренный
Что изменилось
Мы включили ранее экспериментальную возможность обмена данными из вызовов useAsyncData и useFetch на разных страницах. См. оригинальный PR.
Причины таких изменений
Эта функция автоматически разделяет данные между страницами, которые подвергаются пререндеру. Это может привести к значительному повышению производительности при предрендеринге сайтов, использующих useAsyncData или useFetch и получающих одни и те же данные на разных страницах.
Например, если ваш сайт требует вызова useFetch для каждой страницы (например, для получения навигационных данных для меню или настроек сайта из CMS), эти данные будут получены только один раз при предварительном рендеринге первой страницы, которая их использует, а затем кэшированы для использования при предварительном рендеринге других страниц.
Этапы миграции
Убедитесь, что любой уникальный ключ ваших данных всегда можно разрешить в те же самые данные. Например, если вы используете useAsyncData для получения данных, относящихся к определенной странице, вы должны предоставить ключ, который однозначно соответствует этим данным. (Функция useFetch должна сделать это автоматически).
// This would be unsafe in a dynamic page (e.g. `[slug].vue`) because the route slug makes a difference
// to the data fetched, but Nuxt can't know that because it's not reflected in the key.
const route = useRoute()
const { data } = await useAsyncData(async () => {
return await $fetch(`/api/my-page/${route.params.slug}`)
})
// Instead, you should use a key that uniquely identifies the data fetched.
const { data } = await useAsyncData(route.params.slug, async () => {
return await $fetch(`/api/my-page/${route.params.slug}`)
})
Но вы все равно можете отключить эту функцию с помощью:
export default defineNuxtConfig({
experimental: {
sharedPrerenderData: false,
},
})
Значения по умолчанию data и error в useAsyncData и useFetch
🚦 Уровень влияния: Минимальный
Что изменилось
Объекты data и error, возвращаемые из useAsyncData, теперь будут по умолчанию иметь значение undefined.
Причины таких изменений
Ранее data инициализировалась в null, но сбрасывалась в clearNuxtData в undefined. error инициализировался в null. Это изменение призвано обеспечить большую согласованность.
Этапы миграции
Если вы проверяли, являются ли data.value или error.value null, вы можете обновить эти проверки, чтобы проверять на undefined вместо этого.
npx codemod@latest nuxt/4/default-data-error-valueЕсли у вас возникнут какие-либо проблемы, вы можете вернуться к прежнему поведению с помощью:
export default defineNuxtConfig({
experimental: {
defaults: {
useAsyncData: {
value: 'null',
errorValue: 'null',
},
},
},
})
Пожалуйста, сообщите о проблеме, если вы делаете это, так как мы не планируем сохранять эту настройку.
Удаление устаревших значений boolean для опции dedupe при вызове refresh в useAsyncData и useFetch
🚦 Уровень влияния: Минимальный
Что изменилось
Ранее в refresh можно было передавать dedupe: boolean. Это были псевдонимы cancel (true) и defer (false).
// @errors: 2322
const { refresh } = await useAsyncData(() => Promise.resolve({ message: 'Hello, Nuxt!' }))
async function refreshData () {
await refresh({ dedupe: true })
}
Причины таких изменений
Эти псевдонимы были удалены для большей ясности.
Проблема возникла при добавлении dedupe в качестве опции к useAsyncData, и мы удалили булевы значения, поскольку они оказались противоположными.
refresh({ dedupe: false }) означало не отменять существующие запросы в пользу нового. Но передача dedupe: true в опциях useAsyncData означает не делать никаких новых запросов, если есть существующий отложенный запрос. (См. PR.)
Этапы миграции
Миграция должна быть простой:
const { refresh } = await useAsyncData(async () => ({ message: 'Hello, Nuxt 3!' }))
async function refreshData () {
- await refresh({ dedupe: true })
+ await refresh({ dedupe: 'cancel' })
- await refresh({ dedupe: false })
+ await refresh({ dedupe: 'defer' })
}
npx codemod@latest nuxt/4/deprecated-dedupe-valueСоблюдайте значения по умолчанию при очистке data в useAsyncData и useFetch
🚦 Уровень влияния: Минимальный
Что изменилось
Если вы указали собственное значение default для useAsyncData, то теперь оно будет использоваться при вызове clear или clearNuxtData и будет сбрасываться до значения по умолчанию, а не просто сниматься.
Причины таких изменений
Часто пользователи задают соответствующее пустое значение, например, пустой массив, чтобы избежать необходимости проверки на null/undefined при итерации по нему. Это должно соблюдаться при сбросе/очистке данных.
Этапы миграции
Если у вас возникнут какие-либо проблемы, вы можете пока что вернуться к прежнему поведению, используя:
export default defineNuxtConfig({
experimental: {
resetAsyncDataToUndefined: true,
},
})
Пожалуйста, сообщите о проблеме, если вы делаете это, так как мы не планируем сохранять эту настройку.
Согласование значения pending в useAsyncData и useFetch
🚦 Уровень влияния: Средний
Объект pending, возвращаемый из useAsyncData, useFetch, useLazyAsyncData и useLazyFetch, теперь — вычисляемое свойство: он true только когда status тоже в состоянии pending.
Что изменилось
При передаче immediate: false значение pending будет false до первого запроса. Раньше pending оставался true до первого запроса.
Зачем это сделано
Смысл pending приведён в соответствие со свойством status: и то и другое отражает «запрос выполняется».
Шаги миграции
Если вы опираетесь на свойство pending, убедитесь, что логика учитывает новое поведение: pending будет true только когда status тоже в состоянии pending.
<template>
- <div v-if="!pending">
+ <div v-if="status === 'success'">
<p>Data: {{ data }}</p>
</div>
<div v-else>
<p>Loading...</p>
</div>
</template>
<script setup lang="ts">
const { data, pending, execute, status } = await useAsyncData(() => fetch('/api/data'), {
immediate: false
})
onMounted(() => execute())
</script>
Либо временно можно вернуть прежнее поведение так:
export default defineNuxtConfig({
experimental: {
pendingWhenIdle: true,
},
})
Поведение ключа в useAsyncData и useFetch
🚦 Уровень влияния: Средний
Что изменилось
При реактивных ключах в useAsyncData или useFetch Nuxt автоматически перезапрашивает данные при смене ключа. Если задано immediate: false, useAsyncData при смене ключа запросит данные только если они уже хотя бы раз были получены.
Раньше у useFetch было иное поведение: при смене ключа запрос выполнялся всегда.
Теперь useFetch и useAsyncData ведут себя одинаково: при смене ключа данные запрашиваются снова только если они уже были получены хотя бы раз.
Зачем это сделано
Так выравнивается поведение useAsyncData и useFetch и убираются неожиданные запросы. При immediate: false нужно вызвать refresh или execute, иначе в useFetch и useAsyncData данные так и не загрузятся.
Шаги миграции
В большинстве случаев поведение станет ожидаемее; если же вы рассчитывали, что смена ключа или опций у несрочного (immediate: false) useFetch сама запустит первый запрос, теперь первый раз это нужно вызвать вручную.
const id = ref('123')
const { data, execute } = await useFetch('/api/test', {
query: { id },
immediate: false
)
+ watch(id, () => execute(), { once: true })
Чтобы отключить это поведение:
// Or globally in your Nuxt config
export default defineNuxtConfig({
experimental: {
alwaysRunFetchOnKeyChange: true,
},
})
Поверхностная реактивность данных в useAsyncData и useFetch
🚦 Уровень влияния: Минимальный
Объект data, возвращаемый при использовании useAsyncData, useFetch, useLazyAsyncData и useLazyFetch, теперь представляет собой shallowRef, а не ref.
Что изменилось
При получении новых данных все, что зависит от data, все равно будет реактивным, потому что весь объект будет заменен. Но если ваш код изменит свойство внутри этой структуры данных, это не вызовет никакой реактивности в вашем приложении.
Причины таких изменений
Это даёт значительное повышение производительности для глубоко вложенных объектов и массивов, поскольку Vue не нужно следить за изменением каждого отдельного свойства/массива. В большинстве случаев data также должна быть неизменяемой.
Этапы миграции
В большинстве случаев миграция не требуется, но если вы полагаетесь на реактивность объекта данных, то у вас есть два варианта:
- Вы можете точечно выбрать глубокую реактивность на основе каждого компонента:
- const { data } = useFetch('/api/test') + const { data } = useFetch('/api/test', { deep: true }) - Вы можете изменить поведение по умолчанию в масштабах всего проекта (не рекомендуется):
nuxt.config.ts
export default defineNuxtConfig({ experimental: { defaults: { useAsyncData: { deep: true, }, }, }, })
npx codemod@latest nuxt/4/shallow-function-reactivityАбсолютные пути наблюдения в builder:watch
🚦 Уровень влияния: Минимальный
Что изменилось
Хук builder:watch теперь передаёт абсолютный путь, а не относительный к srcDir.
Причины таких изменений
Это позволяет нам поддерживать просмотр путей, которые находятся за пределами вашего srcDir, а также обеспечивает лучшую поддержку слоев и других более сложных паттернов.
Этапы миграции
Мы уже проактивно мигрировали публичные модули Nuxt, которые, как нам известно, используют этот хук. Смотрите issue #25339.
Однако, если вы являетесь автором модуля, использующего хук builder:watch, и хотите сохранить обратную/будущую совместимость, вы можете использовать следующий код, чтобы убедиться, что ваш код работает одинаково как в Nuxt v3, так и в Nuxt v4:
+ import { relative, resolve } from 'node:fs'
// ...
nuxt.hook('builder:watch', async (event, path) => {
+ path = relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, path))
// ...
})
npx codemod@latest nuxt/4/absolute-watch-pathУдаление объекта window.__NUXT__
Что изменилось
Мы удаляем глобальный объект window.__NUXT__ после того, как приложение завершает гидратацию.
Причины таких изменений
Это открывает путь к сценариям с несколькими приложениями (#21635) и оставляет один каноничный доступ к данным — useNuxtApp().
Этапы миграции
Данные по-прежнему доступны, но доступ к ним можно получить с помощью useNuxtApp().payload:
- console.log(window.__NUXT__)
+ console.log(useNuxtApp().payload)
Сканирование индекса директории
🚦 Уровень влияния: Средний
Что изменилось
Дочерние папки в каталоге middleware/ (посредники маршрута) также проверяются на наличие файлов index, и теперь они тоже регистрируются как посредники маршрута в проекте.
Причины таких изменений
Nuxt автоматически сканирует ряд папок, в том числе middleware/ (посредники маршрута) и plugins/.
Дочерние папки в каталоге plugins/ сканируются на наличие файлов index; для остальных сканируемых директорий мы унифицировали это поведение.
Этапы миграции
Скорее всего, миграция не нужна; если нужно вернуть прежнее поведение, добавьте хук, отфильтровывающий такие посредники маршрута:
export default defineNuxtConfig({
hooks: {
'app:resolve' (app) {
app.middleware = app.middleware.filter(mw => !/\/index\.[^/]+$/.test(mw.path))
},
},
})
Изменения в компиляции шаблонов
🚦 Уровень влияния: Минимальный
Что изменилось
Ранее Nuxt использовал lodash/template для компиляции шаблонов, расположенных в файловой системе, используя формат/синтаксис файла .ejs.
Кроме того, мы предоставили несколько утилит для шаблонов (serialize, importName, importSources), которые можно было использовать для генерации кода внутри этих шаблонов, а теперь они удалены.
Причины таких изменений
В Nuxt v3 мы перешли на 'виртуальный' синтаксис с функцией getContents(), которая является гораздо более гибкой и производительной.
Кроме того, у lodash/template была целая череда проблем с безопасностью. Они не совсем относятся к проектам Nuxt, поскольку используются во время сборки, а не во время выполнения, и в доверенном коде. Тем не менее они всё ещё всплывают в аудитах. Плюс lodash — тяжёлая зависимость, которая большинству проектов не нужна.
Наконец, предоставление функций сериализации кода непосредственно в Nuxt не является идеальным решением. Вместо этого мы поддерживаем такие проекты, как unjs/knitwork, которые могут быть зависимы от вашего проекта, и где о проблемах безопасности можно сообщать/решать напрямую, не требуя обновления самого Nuxt.
Этапы миграции
Мы подняли PR для обновления модулей с использованием синтаксиса EJS, но если вам нужно сделать это самостоятельно, есть три варианта, совместимые и со старым, и с новым поведением:
- Перенос логики интерполяции строк непосредственно в
getContents(). - Использование пользовательской функции для обработки замены, как, например, в https://github.com/nuxt-modules/color-mode/pull/240.
- Используйте
es-toolkit/compat(прямая замена шаблонов lodash) как зависимость вашего проекта, а не Nuxt:
+ import { readFileSync } from 'node:fs'
+ import { template } from 'es-toolkit/compat'
// ...
addTemplate({
fileName: 'appinsights-vue.js'
options: { /* some options */ },
- src: resolver.resolve('./runtime/plugin.ejs'),
+ getContents({ options }) {
+ const contents = readFileSync(resolver.resolve('./runtime/plugin.ejs'), 'utf-8')
+ return template(contents)({ options })
+ },
})
Наконец, если вы используете шаблонные утилиты (serialize, importName, importSources), вы можете заменить их на утилиты из knitwork следующим образом:
import { genDynamicImport, genImport, genSafeVariableName } from 'knitwork'
const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"\{(.+)\}"(?=,?$)/gm, r => JSON.parse(r).replace(/^\{(.*)\}$/, '$1'))
const importSources = (sources: string | string[], { lazy = false } = {}) => {
return toArray(sources).map((src) => {
if (lazy) {
return `const ${genSafeVariableName(src)} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}`
}
return genImport(src, genSafeVariableName(src))
}).join('\n')
}
const importName = genSafeVariableName
npx codemod@latest nuxt/4/template-compilation-changesИзменения конфигурации TypeScript по умолчанию
🚦 Уровень влияния: Минимальный
Что изменилось
compilerOptions.noUncheckedIndexedAccess теперь по умолчанию true, а не false.
Зачем это сделано
Продолжение обновления конфига в 3.12: дефолты ближе к рекомендациям Total TypeScript.
Шаги миграции
Два варианта:
- Запустить проверку типов и исправить новые ошибки (рекомендуется).
- Переопределить значение в
nuxt.config.ts:export default defineNuxtConfig({ typescript: { tsConfig: { compilerOptions: { noUncheckedIndexedAccess: false, }, }, }, })
Разделение конфигурации TypeScript
🚦 Уровень влияния: Минимальный
Что изменилось
Nuxt генерирует отдельные tsconfig для разных контекстов, чтобы улучшить проверку типов:
- Новые файлы конфигурации:
.nuxt/tsconfig.app.json— код приложения (компоненты Vue, композаблы и т.д.).nuxt/tsconfig.server.json— сервер (Nitro, каталогserver/).nuxt/tsconfig.node.json— код времени сборки (модули,nuxt.config.tsи т.д.).nuxt/tsconfig.shared.json— общий код между клиентом и сервером (типы, утилиты без привязки к среде выполнения).nuxt/tsconfig.json— прежний вариант для обратной совместимости
- Обратная совместимость: проекты, расширяющие
.nuxt/tsconfig.json, продолжают работать как раньше. - По желанию — ссылки между проектами в TypeScript (project references): новые проекты или те, кому нужна более строгая типизация, могут включить этот режим.
- Проверка по контексту: у каждого контекста свои
compilerOptions,include/exclude. - Опция
typescript.nodeTsConfig: настройка TypeScript для Node-сборки (модули, конфиг).
Зачем это сделано
- Точнее типы для кода приложения, сервера и этапа сборки с разными глобалами и API.
- Удобнее в IDE: подсказки и ошибки соответствуют контексту фрагмента кода.
- Меньше путаницы: сервер не подсказывает клиентские API и наоборот.
- Производительность: TypeScript проверяет суженные области.
Например, автоимпорты недоступны в nuxt.config.ts, но раньше TypeScript это не подсвечивал. В server/ IDE уже понимала отдельный контекст, а единая проверка типов — нет (нужен был отдельный шаг).
Шаги миграции
Менять ничего не обязательно — старые проекты работают как прежде.
Чтобы использовать улучшенную проверку, можно перейти на ссылки между проектами (project references):
- Корневой
tsconfig.jsonсо ссылками:{ // Remove "extends": "./.nuxt/tsconfig.json" if present "files": [], "references": [ { "path": "./.nuxt/tsconfig.app.json" }, { "path": "./.nuxt/tsconfig.server.json" }, { "path": "./.nuxt/tsconfig.shared.json" }, { "path": "./.nuxt/tsconfig.node.json" } ] } - Удалите ручные
server/tsconfig.json, если они только расширяли.nuxt/tsconfig.server.json. - Скрипт проверки типов — с флагом сборки для project references:
- "typecheck": "nuxt prepare && vue-tsc --noEmit" + "typecheck": "nuxt prepare && vue-tsc -b --noEmit" - При необходимости настройте TypeScript для Node:
export default defineNuxtConfig({ typescript: { // customize tsconfig.app.json tsConfig: { // ... }, // customize tsconfig.shared.json sharedTsConfig: { // ... }, // customize tsconfig.node.json nodeTsConfig: { // ... }, }, nitro: { typescript: { // customize tsconfig.server.json tsConfig: { // ... }, }, }, }) - Обновите CI и скрипты, если они запускают проверку типов, чтобы использовать project references.
Новая схема даёт лучшую типобезопасность и подсказки в IDE тем, кто её подключит, при полной обратной совместимости для остальных.
Удаление экспериментальных функций
🚦 Уровень влияния: Минимальный
Что изменилось
Четыре экспериментальные функции больше не настраиваются в Nuxt 4:
experimental.treeshakeClientOnlyбудетtrue(по умолчанию с v3.0)experimental.configSchemaбудетtrue(по умолчанию с v3.3)experimental.polyfillVueUseHeadбудетfalse(по умолчанию с v3.4)experimental.respectNoSSRHeaderбудетfalse(по умолчанию с v3.4)vite.devBundlerбольше не настраивается — по умолчанию используетсяvite-node.
Причины таких изменений
Эти параметры уже давно имеют текущие значения, и у нас нет причин полагать, что они должны оставаться настраиваемыми.
Этапы миграции
polyfillVueUseHeadреализуется на территории пользователя с помощью этого плагинаrespectNoSSRHeaderреализуется на стороне проекта с помощью серверного промежуточного слоя Nitro
Удаление корневой опции generate
🚦 Уровень влияния: Минимальный
Что изменилось
В Nuxt 4 нет верхнеуровневой секции generate и всех её полей, в том числе:
generate.exclude— исключение маршрутов из пререндераgenerate.routes— явный список маршрутов для пререндера
Зачем это сделано
generate в корне — наследие Nuxt 2. Для пререндера давно рекомендуется nitro.prerender в Nuxt 3+.
Шаги миграции
Замените generate на соответствующие опции nitro.prerender:
export default defineNuxtConfig({
- generate: {
- exclude: ['/admin', '/private'],
- routes: ['/sitemap.xml', '/robots.txt']
- }
+ nitro: {
+ prerender: {
+ ignore: ['/admin', '/private'],
+ routes: ['/sitemap.xml', '/robots.txt']
+ }
+ }
})
Nuxt 2 против Nuxt 3+
Ниже — краткое сравнение трёх линеек:
| Возможность / версия | Nuxt 2 | Nuxt Bridge | Nuxt 3+ |
|---|---|---|---|
| Vue | 2 | 2 | 3 |
| Стабильность | 😊 Стабильный | 😊 Стабильный | 😊 Стабильный |
| Производительность | 🏎 Быстрый | ✈️ Быстрее | 🚀 Самый быстрый |
| Движок Nitro | ❌ | ✅ | ✅ |
| Поддержка ESM | 🌙 Частично | 👍 Лучше | ✅ |
| TypeScript | ☑️ По желанию | 🚧 Частично | ✅ |
| Composition API | ❌ | 🚧 Частично | ✅ |
| Options API | ✅ | ✅ | ✅ |
| Автоимпорт компонент | ✅ | ✅ | ✅ |
Синтаксис <script setup> | ❌ | 🚧 Частично | ✅ |
| Автоимпорты | ❌ | ✅ | ✅ |
| webpack | 4 | 4 | 5 |
| Vite | ⚠️ Частично | 🚧 Частично | ✅ |
| Nuxi CLI | ❌ Устарел | ✅ nuxt | ✅ nuxt |
| Статические сайты | ✅ | ✅ | ✅ |
С Nuxt 2 на Nuxt 3+
В документации по миграции — пошаговое сопоставление возможностей Nuxt 2 и Nuxt 3+ и рекомендации по переносу приложения.
С Nuxt 2 на Nuxt Bridge
Если вы предпочитаете постепенно переводить приложения Nuxt 2 на Nuxt 3, используйте Nuxt Bridge — слой совместимости, который подключает возможности Nuxt 3+ в Nuxt 2 по мере необходимости. Подробнее — документация Nuxt Bridge.