---
title: "Веб-приложение без своего сервера: ESM и CDN"
description: "Как ES-модули, npm и CDN позволяют развернуть небольшое браузерное приложение без собственной серверной инфраструктуры."
date: 2024-07-04
---

Небольшому веб-приложению больше не всегда нужна сложная инфраструктура.
ES-модули и CDN позволяют запустить код прямо в браузере пользователя:
статическая страница размещается отдельно, а библиотеки загружаются по
сети. Разберём это на примере.

Демонстрационное
[приложение](https://flancer64.github.io/demo-wa-esm-openai/#/) — чат,
который работает на стороне клиента, берёт библиотеки из
[jsDelivr](https://www.jsdelivr.com/) и обращается к внешнему API. Его
`index.html` размещён на GitHub Pages.

<zoom-img src="/medium/img/a68e6632999f/image-01.png" alt="Пример диалога в приложении" width="100%"></zoom-img>

<zoom-img src="/medium/img/a68e6632999f/image-02.png" alt="Принципиальная схема приложения" width="100%"></zoom-img>

После публикации пакета в [npm](https://www.npmjs.com/) его можно
доставлять браузеру через CDN, например jsDelivr или
[unpkg](https://unpkg.com/).

## Из чего состоит пример

Интерфейс собран с Vue, Vue Router и Quasar. Навигация минимальна: чат,
конфигурация и справка.

<zoom-img src="/medium/img/a68e6632999f/image-03.png" alt="Навигация приложения" width="100%"></zoom-img>

В конфигурации пользователь выбирает модель и системную инструкцию.
Любой секретный ключ, введённый в браузер, следует считать доступным
этому устройству и его расширениям: для реальных продуктов лучше
использовать свой серверный посредник с ограниченными учётными данными,
а не передавать постоянный API-ключ в клиент.

<zoom-img src="/medium/img/a68e6632999f/image-04.png" alt="Конфигурация приложения" width="100%"></zoom-img>

<zoom-img src="/medium/img/a68e6632999f/image-05.png" alt="Чат приложения" width="100%"></zoom-img>

## `index.html`

Точка входа получается очень компактной. В ней подключаются стили,
шрифты, `importmap` и главный ES-модуль приложения:

``` html
<script type="importmap">
{ "imports": {
  "vue": "https://cdn.jsdelivr.net/npm/vue@3.4.31/dist/vue.esm-browser.prod.js",
  "openai/core": "https://cdn.jsdelivr.net/npm/openai@4.52.2/core.mjs"
} }
</script>
<script type="module">
import app from 'https://cdn.jsdelivr.net/npm/@flancer64/demo-wa-esm-openai@0.1.0/src/App.mjs';
app.mount('#app');
</script>
```

`importmap` даёт браузеру node-подобные имена импортов. Сам документ
также подгружает ресурсы и монтирует приложение в элемент страницы.

## Пакет приложения

Код
[@flancer64/demo-wa-esm-openai](https://www.npmjs.com/package/@flancer64/demo-wa-esm-openai)
состоит из ES-модулей. В `Mod/` расположены модели действий, в `Ui/` —
Vue-компоненты.

<zoom-img src="/medium/img/a68e6632999f/image-06.png" alt="Исходные файлы пакета приложения" width="100%"></zoom-img>

В `App.mjs` ESM-версии Vue и Vue Router импортируются статически. Quasar
в показанной версии был UMD-библиотекой, поэтому перед динамическим
импортом он получает Vue через глобальный объект:

``` js
import * as Vue from 'https://cdn.jsdelivr.net/npm/vue@3.4.31/dist/vue.esm-browser.prod.js';
window.Vue = Vue;
await import('https://cdn.jsdelivr.net/npm/quasar@2.16.4/dist/quasar.umd.prod.js');
const Quasar = window.Quasar;
```

Маршруты используют ленивую загрузку. Компонент придёт по сети лишь при
переходе на соответствующий экран:

``` js
const routes = [
  { path: '/', component: () => import('./Ui/Route/Chat.mjs') },
  { path: '/config', component: () => import('./Ui/Route/Config.mjs') },
];
```

## Что видит браузер

На вкладке Sources видно, что статический хостинг отдаёт только
`index.html` и стили, а пакет и сторонние зависимости приходят с CDN.
Непосещённые маршруты не загрузят свои компоненты. Шрифты и картинки
тоже могут оставаться на общедоступных источниках, а пользовательские
настройки — в локальном хранилище.

<zoom-img src="/medium/img/a68e6632999f/image-07.png" alt="Источники ресурсов в браузере" width="100%"></zoom-img>

<zoom-img src="/medium/img/a68e6632999f/image-08.png" alt="Ресурсы GitHub Pages" width="100%"></zoom-img>

<zoom-img src="/medium/img/a68e6632999f/image-09.png" alt="Собственные ESM-исходники в CDN" width="100%"></zoom-img>

<zoom-img src="/medium/img/a68e6632999f/image-10.png" alt="Сторонние ESM-ресурсы" width="100%"></zoom-img>

<zoom-img src="/medium/img/a68e6632999f/image-11.png" alt="Сторонние UMD-ресурсы" width="100%"></zoom-img>

<zoom-img src="/medium/img/a68e6632999f/image-12.png" alt="Шрифты и изображения" width="100%"></zoom-img>

<zoom-img src="/medium/img/a68e6632999f/image-13.png" alt="Локальное хранилище браузера" width="100%"></zoom-img>

## Вывод

Современный стек позволяет создавать и распространять небольшие
приложения без собственных виртуальных серверов: код и зависимости можно
публиковать в открытых сервисах. CDN дают хорошую скорость и кэширование
при повторном запуске.

<zoom-img src="/medium/img/a68e6632999f/image-14.png" alt="Статистика загрузки сети" width="100%"></zoom-img>

<zoom-img src="/medium/img/a68e6632999f/image-15.png" alt="Использование кэша браузера" width="100%"></zoom-img>

Модули можно грузить с точностью до пользовательского действия, не
собирая отдельные бандлы и не выполняя tree-shaking заранее. Но открытая
цепочка поставки повышает риск вредоносного или неожиданно изменившегося
кода. Фиксируйте версии, оценивайте доверие к пакетам и CDN, используйте
CSP и не размещайте важные секреты в клиенте. Для небольшого открытого
приложения этот подход практичен; для чувствительных данных те же
ESM-принципы можно применить на собственном хостинге.
