Параметр build.rollupOptions.external используется для
исключения зависимостей из итогового бандла во время сборки проекта
через Vite. Настройка передаётся напрямую в конфигурацию Rollup, который
используется внутри production-сборки Vite.
Механизм особенно важен при:
По умолчанию Vite старается включить все импортируемые зависимости в
финальный бандл. external изменяет это поведение и сообщает
Rollup, что определённые модули не должны попадать в сборку.
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
external: ['vue']
}
}
})
В этом примере пакет vue не будет встроен в итоговый
файл.
Если в исходном коде присутствует импорт:
import { ref } from 'vue'
то после сборки Rollup сохранит импорт как внешний:
import { ref } from 'vue'
Вместо внедрения всего runtime Vue внутрь бандла.
Без external библиотека может случайно включить огромные
зависимости:
Это приводит к:
Классическая проблема — две копии React или Vue в приложении.
Например:
В результате появляются:
Правильная стратегия — помечать фреймворк как external.
Чаще всего external используется совместно с
peerDependencies.
Пример package.json:
{
"peerDependencies": {
"vue": "^3.4.0"
}
}
Конфигурация Vite:
export default defineConfig({
build: {
rollupOptions: {
external: ['vue']
}
}
})
Такой подход означает:
export default defineConfig({
build: {
rollupOptions: {
external: [
'vue',
'axios',
'lodash'
]
}
}
})
Теперь Rollup оставит все эти импорты внешними.
Иногда импортируются внутренние пути:
import debounce from 'lodash/debounce'
Простого указания 'lodash' недостаточно.
Необходимо:
external: [
'lodash',
'lodash/debounce'
]
Или использовать функцию.
external как функцияНаиболее гибкий вариант.
export default defineConfig({
build: {
rollupOptions: {
external(id) {
return id.includes('lodash')
}
}
}
})
Теперь все импорты lodash автоматически становятся внешними:
lodash
lodash/map
lodash/debounce
lodash/throttle
externalRollup передаёт несколько параметров:
external(id, parentId, isResolved)
idИдентификатор импортируемого модуля.
external(id) {
console.log(id)
}
Пример значений:
vue
react
lodash/debounce
./utils.js
parentIdМодуль, из которого выполняется импорт.
external(id, parentId) {
console.log(parentId)
}
Полезно для сложной логики.
isResolvedПоказывает, был ли путь уже разрешён Rollup.
Иногда требуется оставить внешними абсолютно все зависимости.
external(id) {
return !id.startsWith('.') && !path.isAbsolute(id)
}
Такой подход особенно популярен при сборке Node.js-библиотек.
Полный пример:
import path from 'path'
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
external(id) {
return !id.startsWith('.') && !path.isAbsolute(id)
}
}
}
})
При разработке backend-библиотек часто исключают встроенные модули Node.js.
external: [
'fs',
'path',
'os',
'crypto'
]
Или автоматически:
import { builtinModules } from 'module'
export default defineConfig({
build: {
rollupOptions: {
external: builtinModules
}
}
})
Rollup поддерживает регулярные выражения.
external: [
/^lodash/
]
Подойдут:
lodash
lodash/map
lodash/debounce
Популярная практика — динамически читать
package.json.
import { defineConfig } from 'vite'
import packageJson from './package.json'
export default defineConfig({
build: {
rollupOptions: {
external: Object.keys(
packageJson.peerDependencies || {}
)
}
}
})
Это избавляет от дублирования списка.
Наиболее частый сценарий.
export default defineConfig({
build: {
lib: {
entry: 'src/index.js',
name: 'MyLibrary',
fileName: 'my-library'
},
rollupOptions: {
external: ['vue']
}
}
})
external и
output.globalsЕсли сборка генерирует формат:
umd
iife
то необходимо указать глобальные переменные.
Пример:
export default defineConfig({
build: {
lib: {
entry: 'src/index.js',
name: 'MyLib',
formats: ['umd']
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
}
}
}
}
})
UMD и IIFE работают в браузере через глобальные объекты.
Если Vue исключён из сборки:
external: ['vue']
то Rollup должен понимать:
window.Vue
является реализацией пакета vue.
Исходный код:
import { ref } from 'vue'
export function useCounter() {
const count = ref(0)
return {
count
}
}
Результат:
(function(global, factory) {
factory(global.Vue)
})(this, function(Vue) {
})
globalsЕсли забыть output.globals, Rollup может выдать:
No name was provided for external module
или браузер получит:
Vue is not defined
external часто используется совместно с
CDN-подключением.
Например:
<script src="https://unpkg.com/vue@3"></script>
<script src="my-lib.js"></script>
Конфигурация:
external: ['vue']
external от
aliasresolve.alias изменяет путь импорта:
resolve: {
alias: {
'@': '/src'
}
}
external полностью исключает модуль из бандла.
Это разные механизмы.
external от optimizeDeps.excludeМногие путают эти настройки.
optimizeDeps.excludeРаботает только в dev-режиме.
Влияет на pre-bundling через esbuild.
build.rollupOptions.externalРаботает только во время production-сборки.
Управляет содержимым итогового бандла.
ssr.externalVite также поддерживает:
ssr: {
external: []
}
Это отдельная система для SSR-сборок.
Она не связана напрямую с
build.rollupOptions.external.
Внешние зависимости не участвуют в tree-shaking внутри текущего бандла.
Например:
external: ['lodash']
Rollup больше не анализирует содержимое lodash.
Если забыть external:
React duplicated
Vue duplicated
Библиотека может весить:
2 KB → 700 KB
из-за встроенного фреймворка.
Особенно критично для:
Например:
Library → Vue 3.3
Application → Vue 3.5
Встроенная копия может привести к нестабильной работе.
После build полезно анализировать итоговые файлы.
Если external настроен правильно:
Популярный инструмент:
npm install rollup-plugin-visualizer -D
Подключение:
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer()
]
})
Позволяет увидеть:
В monorepo часто исключают внутренние пакеты.
external: [
'@company/ui',
'@company/core'
]
Это предотвращает:
При использовании:
output: {
preserveModules: true
}
external помогает сохранять структуру модулей без встраивания зависимостей.
Типичный production-вариант:
export default defineConfig({
build: {
lib: {
entry: 'src/index.jsx',
formats: ['es', 'umd']
},
rollupOptions: {
external: [
'react',
'react-dom'
],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM'
}
}
}
}
})
export default defineConfig({
build: {
lib: {
entry: 'src/index.js',
formats: ['es']
},
rollupOptions: {
external: ['vue']
}
}
})
import { builtinModules } from 'module'
export default defineConfig({
build: {
target: 'node18',
rollupOptions: {
external: [
...builtinModules,
'axios'
]
}
}
})
Не стоит исключать зависимости, если:
Наиболее распространённая схема:
peerDependencies{
"peerDependencies": {
"react": "^18.0.0"
}
}
externalexternal: ['react']
globalsglobals: {
react: 'React'
}
import packageJson from './package.json'
function getExternalPackages() {
return [
...Object.keys(
packageJson.dependencies || {}
),
...Object.keys(
packageJson.peerDependencies || {}
)
]
}
export default defineConfig({
build: {
rollupOptions: {
external: getExternalPackages()
}
}
})
Для формата:
es
внешние импорты сохраняются как ES imports:
import React from 'react'
Это оптимальный вариант для современных bundlers.
При генерации CommonJS:
cjs
внешние зависимости преобразуются:
const React = require('react')
Если dependency external, то браузер обязан получить её отдельно:
Иначе приложение не сможет найти модуль во время выполнения.
Неверно:
external: ['./utils.js']
Обычно internal-модули не должны быть external.
lodash/debounce
rxjs/operators
date-fns/format
Очень частая проблема при публикации библиотек.
Если пакет находится в peerDependencies, но отсутствует в external, он всё равно попадёт в бандл.
Для библиотек:
Для обычных SPA-приложений external используется
значительно реже, поскольку приложения обычно собираются в
self-contained bundle.