Современные JavaScript-приложения часто пишутся так, будто они выполняются в единой среде. На практике существует как минимум два крупных контекста выполнения: браузер и Node.js. Эти среды предоставляют разные наборы глобальных объектов и API, что приводит к несовместимости кода при переносе между ними.
В Node.js доступны такие сущности, как process,
Buffer, __dirname, __filename. В
браузере они отсутствуют. В браузере, в свою очередь, доступны
window, document, fetch (в
зависимости от окружения), но их нет в Node.js.
При сборке через Rollup возникает дополнительный слой сложности: модули могут неявно опираться на глобальные переменные, которые в целевой среде отсутствуют. Это приводит к ошибкам на этапе выполнения, даже если сборка проходит успешно.
Shim (подмена или полифил) в контексте сборки — это механизм, который имитирует отсутствие глобальных переменных или API в целевой среде.
В Rollup shim используется для следующих задач:
Shim не изменяет исходный код библиотек напрямую. Вместо этого он внедряется на этапе бандлинга через плагины и виртуальные модули.
Наиболее проблемные глобальные объекты:
process — один из самых часто используемых глобальных
объектов Node.js. Он предоставляет доступ к:
process.env)process.version,
process.platform)В браузере process отсутствует полностью. Однако многие
библиотеки используют конструкции вида:
if (process.env.NODE_ENV === 'production') {
// оптимизированный код
}
Без shim такой код вызовет ReferenceError.
Buffer используется для работы с бинарными данными:
В браузере аналог существует частично (например,
Uint8Array), но прямого Buffer нет.
Типичный пример зависимости:
const buf = Buffer.from('hello');
Без подмены это также приводит к ошибке выполнения.
Эти переменные предоставляют информацию о текущем пути файла в Node.js. В браузере концепция файловой системы отсутствует, поэтому такие переменные не имеют смысла.
Rollup не предоставляет встроенной полной эмуляции Node.js. Вместо этого используется комбинация плагинов и ручной конфигурации.
Основные подходы:
@rollup/plugin-replace@rollup/plugin-injectrollup-plugin-node-globalsrollup-plugin-node-polyfillsКаждый подход решает свою часть задачи.
Один из самых простых способов — статическая замена выражений.
Пример конфигурации:
import replace from '@rollup/plugin-replace';
export default {
plugins: [
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify('production')
})
]
};
Здесь process.env.NODE_ENV заменяется строковым
литералом на этапе сборки.
Преимущества:
Недостатки:
processДля более сложных случаев используется
@rollup/plugin-inject.
import inject from '@rollup/plugin-inject';
export default {
plugins: [
inject({
process: 'process/browser'
})
]
};
Это приводит к автоматической подстановке импорта:
import process from 'process/browser';
Таким образом, process становится модулем, а не
глобальной переменной.
Реализация process/browser обычно берётся из пакета
process, который частично эмулирует Node.js API.
Для Buffer используется отдельный полифилл:
import inject from '@rollup/plugin-inject';
export default {
plugins: [
inject({
Buffer: ['buffer', 'Buffer']
})
]
};
Это трансформируется в:
import { Buffer } from 'buffer';
Модуль buffer предоставляет реализацию поверх
Uint8Array.
Плагин rollup-plugin-node-globals выполняет более
агрессивную подмену:
processglobalBufferОн создаёт глобальные shim-объекты, имитирующие Node.js окружение.
Пример:
import globals from 'rollup-plugin-node-globals';
export default {
plugins: [
globals()
]
};
Механизм работы:
Недостаток заключается в увеличении размера бандла и потенциальной избыточности кода.
Плагин rollup-plugin-node-polyfills решает проблему
комплексно, добавляя набор стандартных Node.js модулей:
pathcryptostreambufferutileventsОн полезен при портировании библиотек из Node.js в браузер.
Пример:
import polyfills from 'rollup-plugin-node-polyfills';
export default {
plugins: [
polyfills()
]
};
Однако этот подход приводит к значительному увеличению размера сборки, так как многие Node.js API эмулируются поверх браузерных примитивов.
В реальных проектах редко используется один подход. Чаще применяется комбинация:
replace для статических значенийinject для глобальных объектовПример конфигурации:
import replace from '@rollup/plugin-replace';
import inject from '@rollup/plugin-inject';
export default {
plugins: [
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify('production')
}),
inject({
process: 'process/browser',
Buffer: ['buffer', 'Buffer']
})
]
};
Такой подход минимизирует размер бандла при сохранении совместимости.
Shim-слой почти всегда увеличивает итоговый размер сборки.
Основные причины:
Особенно критично это для:
streamcryptobufferЭти модули могут добавлять десятки килобайт к бандлу.
Типичные проблемы:
Причина: отсутствие inject или replace для process.
Причина: отсутствие polyfill для buffer.
Причина: частичная эмуляция Node.js API, когда библиотека ожидает полную совместимость.
Диагностика обычно включает:
npm lsShim не является полноценной виртуализацией среды. Его ограничения:
Shim решает только задачу синтаксической и частично семантической совместимости.
Вместо массового использования shim иногда применяется иной подход:
Пример conditional exports:
{
"exports": {
"browser": "./dist/browser.js",
"node": "./dist/node.js"
}
}
Это уменьшает необходимость в shim и делает сборку более предсказуемой.
Rollup работает на уровне ES-модулей и не навязывает runtime. Это означает, что:
Shim становится внешним слоем, который компенсирует различия между средами, но не является частью ядра сборщика.