Разделение кода в проектах с использованием библиотеки Tom Sel ect необходимо для уменьшения размера начального бандла, ускорения загрузки интерфейса и изоляции функциональности. Особенно актуально это при использовании большого количества плагинов, асинхронных загрузчиков, пользовательских рендеров и интеграции с фреймворками.
Основная проблема монолитной сборки заключается в том, что даже неиспользуемые компоненты Tom Sel ect могут попадать в итоговый bundle. Это увеличивает время загрузки страницы и расход памяти браузера.
Разделение кода позволяет:
Самая простая форма code splitting — динамический импорт.
async function initSelect() {
const TomSelectModule = await import('tom-select');
const TomSelect = TomSelectModule.default;
new TomSelect('#tags', {
create: true
});
}
document
.querySelector('#open-form')
.addEventListener('click', initSelect);
В этом случае библиотека не попадает в основной bundle и загружается
только после вызова initSelect.
Tom Select включает как JavaScript, так и стили. Часто требуется загружать их независимо.
async function loadTomSelect() {
await import('tom-select/dist/css/tom-select.css');
const { default: TomSelect } = await import('tom-select');
return TomSelect;
}
Такой подход особенно полезен при:
Отложенная инициализация предотвращает загрузку библиотеки до момента взаимодействия пользователя с интерфейсом.
const element = document.querySelector('#countries');
let initialized = false;
element.addEventListener('focus', async () => {
if (initialized) return;
initialized = true;
const { default: TomSelect } = await import('tom-select');
new TomSelect(element, {
maxItems: 5
});
});
Подобная стратегия уменьшает нагрузку при первом рендеринге страницы.
Во фреймворках с роутингом Tom Select обычно требуется только на отдельных страницах.
const routes = {
'/users': async () => {
const page = await import('./pages/users.js');
return page.render();
},
'/settings': async () => {
const page = await import('./pages/settings.js');
return page.render();
}
};
Внутри страницы:
export async function render() {
const { default: TomSelect } = await import('tom-select');
new TomSelect('#roles', {
plugins: ['remove_button']
});
}
Теперь Tom Select будет загружаться только на странице
/settings.
Плагины могут существенно увеличивать bundle. Особенно это касается пользовательских расширений.
async function createSelect() {
const [
TomSelectModule,
pluginModule
] = await Promise.all([
import('tom-select'),
import('./plugins/custom-plugin.js')
]);
const TomSelect = TomSelectModule.default;
TomSelect.define(
'custom_plugin',
pluginModule.default
);
new TomSelect('#items', {
plugins: ['custom_plugin']
});
}
При большом количестве расширений используется отдельный загрузчик.
const pluginLoaders = {
tags: () => import('./plugins/tags.js'),
users: () => import('./plugins/users.js'),
analytics: () => import('./plugins/analytics.js')
};
async function loadPlugin(name, TomSelect) {
const module = await pluginLoaders[name]();
TomSelect.define(name, module.default);
}
Использование:
const { default: TomSelect } = await import('tom-select');
await loadPlugin('analytics', TomSelect);
new TomSelect('#events', {
plugins: ['analytics']
});
Render-функции нередко содержат сложную HTML-логику, шаблонизаторы или форматтеры.
// renderers/userRenderer.js
export function renderUser(data, escape) {
return `
<div class="user">
<img src="${escape(data.avatar)}">
<span>${escape(data.name)}</span>
</div>
`;
}
Подключение:
const [
TomSelectModule,
rendererModule
] = await Promise.all([
import('tom-select'),
import('./renderers/userRenderer.js')
]);
const TomSelect = TomSelectModule.default;
new TomSelect('#users', {
render: {
option: rendererModule.renderUser,
item: rendererModule.renderUser
}
});
Асинхронная загрузка данных не должна находиться внутри конфигурации Tom Select.
Плохая архитектура:
new TomSelect('#users', {
load(query, callback) {
fetch(`/api/users?q=${query}`)
.then(r => r.json())
.then(callback);
}
});
Лучший вариант:
// services/userService.js
export async function searchUsers(query) {
const response = await fetch(
`/api/users?q=${encodeURIComponent(query)}`
);
return response.json();
}
const [
TomSelectModule,
userService
] = await Promise.all([
import('tom-select'),
import('./services/userService.js')
]);
const TomSelect = TomSelectModule.default;
new TomSelect('#users', {
async load(query, callback) {
try {
const data = await userService.searchUsers(query);
callback(data);
} catch {
callback();
}
}
});
Иногда необходимо заранее загрузить Tom Select перед открытием интерфейса.
let tomSelectPromise = null;
function preloadTomSelect() {
if (!tomSelectPromise) {
tomSelectPromise = import('tom-select');
}
return tomSelectPromise;
}
Использование:
document
.querySelector('#open-modal')
.addEventListener('mouseenter', preloadTomSelect);
Браузер начинает загрузку заранее, что уменьшает задержку при открытии модального окна.
Крупные конфигурации удобнее хранить отдельно.
// config/userSelectConfig.js
export default {
valueField: 'id',
labelField: 'name',
searchField: ['name', 'email'],
maxOptions: 100
};
Подключение:
const [
TomSelectModule,
configModule
] = await Promise.all([
import('tom-select'),
import('./config/userSelectConfig.js')
]);
const TomSelect = TomSelectModule.default;
new TomSelect('#users', configModule.default);
Один из лучших подходов — отделение логики формы от логики Tom Select.
new TomSelect('#products', {
onChange(value) {
updateCart(value);
validateOrder();
saveState();
sendAnalytics(value);
}
});
function handleProductChange(value) {
updateCart(value);
validateOrder();
saveState();
sendAnalytics(value);
}
new TomSelect('#products', {
onChange: handleProductChange
});
Tom Select зависит от DOM, поэтому прямой импорт на сервере вызывает ошибки.
if (typeof window !== 'undefined') {
const { default: TomSelect } = await import('tom-select');
new TomSelect('#categories');
}
В React обычно используется lazy или динамический импорт
внутри useEffect.
import { useEffect, useRef } fr om 'react';
export function UserSelect() {
const ref = useRef(null);
useEffect(() => {
let instance = null;
async function init() {
const { default: TomSelect } = await import('tom-select');
instance = new TomSelect(ref.current, {
create: true
});
}
init();
return () => {
instance?.destroy();
};
}, []);
return <sel ect ref={ref}></select>;
}
import { onMounted, ref } fr om 'vue';
export default {
setup() {
const sel ect = ref(null);
onMounted(async () => {
const { default: TomSelect } =
await import('tom-select');
new TomSelect(select.value);
});
return {
select
};
}
};
import { onMount } fr om 'svelte';
let element;
onMount(async () => {
const { default: TomSelect } =
await import('tom-select');
const instance = new TomSelect(element);
return () => {
instance.destroy();
};
});
При множестве селектов удобно создавать фабрики.
// factories/createTagSelect.js
export async function createTagSelect(selector) {
const { default: TomSelect } =
await import('tom-select');
return new TomSelect(selector, {
create: true,
persist: false
});
}
Использование:
import { createTagSelect }
fr om './factories/createTagSelect.js';
await createTagSelect('#tags');
Bundler может автоматически выделять общий chunk.
optimization: {
splitChunks: {
cacheGroups: {
tomselect: {
test: /[\\/]node_modules[\\/]tom-select/,
name: 'tom-select',
chunks: 'all'
}
}
}
}
Tom Select поддерживает несколько CSS-тем. Нет необходимости загружать все сразу.
async function loadTheme(name) {
await import(
`tom-select/dist/css/${name}.css`
);
}
Часто форма появляется только после открытия модального окна.
async function openUserModal() {
const [
modalModule,
TomSelectModule
] = await Promise.all([
import('./modal.js'),
import('tom-select')
]);
modalModule.open();
const TomSelect = TomSelectModule.default;
new TomSelect('#user-select');
}
Разделение кода помогает уменьшать потребление памяти при уничтожении интерфейсов.
const instance = new TomSelect('#tags');
instance.destroy();
При SPA-навигации это особенно важно, поскольку динамически загруженные компоненты могут оставаться в памяти.
Некоторые страницы могут использовать собственные наборы расширений.
src/
├── admin/
│ ├── adminSelect.js
│ └── plugins/
├── public/
│ ├── searchSelect.js
│ └── plugins/
Такой подход предотвращает попадание административных плагинов в публичную сборку.
В крупных системах Tom Select может существовать как изолированный модуль.
export async function mount(element) {
const { default: TomSelect } =
await import('tom-select');
return new TomSelect(element, {
create: true
});
}
При работе с разными API загрузчики удобно изолировать.
const loaders = {
users: () => import('./loaders/users.js'),
tags: () => import('./loaders/tags.js'),
products: () => import('./loaders/products.js')
};
Иногда библиотека вообще не требуется на мобильных устройствах.
if (window.innerWidth > 768) {
const { default: TomSelect } =
await import('tom-select');
new TomSelect('#desktop-select');
}
После разделения кода необходимо анализировать результат сборки.
Типичные проблемы:
Для анализа обычно применяются:
src/
├── components/
│ ├── selects/
│ │ ├── userSelect.js
│ │ ├── tagSelect.js
│ │ └── productSelect.js
│ │
│ ├── plugins/
│ │ ├── analytics.js
│ │ └── virtualScroll.js
│ │
│ ├── renderers/
│ │ ├── userRenderer.js
│ │ └── productRenderer.js
│ │
│ ├── services/
│ │ ├── users.js
│ │ └── products.js
│ │
│ └── config/
│ ├── users.js
│ └── products.js
│
├── pages/
├── routes/
└── app.js
Грамотное разделение кода в проектах с Tom Select обеспечивает: