Команда npm run build запускает Vite/Rollup и создаёт оптимизированную статическую сборку в папке dist/:
npm run builddist/
├── index.html (точка входа)
└── assets/
├── index-Abc123.js (основной JS с хэшем для кэша)
├── vendor-Xyz789.js (зависимости: vue, vue-router...)
└── index-Def456.cssВсе файлы минифицированы, tree-shaking удалил неиспользуемый код, CSS объединён. Имена файлов содержат хэш содержимого — браузер кэширует их навсегда.
Vercel — самый простой способ задеплоить Vue SPA:
npm install -g vercel
vercelИли через GitHub: подключаете репозиторий в дашборде Vercel, и каждый push в main автоматически деплоит новую версию.
Для Vue Router (history mode) нужен vercel.json:
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}Аналогично Vercel. Файл netlify.toml для настройки:
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200Правило redirect здесь критично: без него при переходе по прямой ссылке /about Netlify вернёт 404 вместо index.html.
При самостоятельном хостинге используют nginx. Ключевая настройка — try_files:
server {
listen 80;
server_name myapp.com;
root /var/www/dist;
index index.html;
# Сжатие gzip
gzip on;
gzip_types text/plain text/css application/javascript application/json;
# Кэширование статики (файлы с хэшем — кэш навсегда)
location ~* \.(js|css|png|jpg|ico|woff2)$ {
expires max;
add_header Cache-Control "public, immutable";
}
# SPA fallback — всё остальное отдаёт index.html
location / {
try_files $uri $uri/ /index.html;
}
}Без try_files ... /index.html при обновлении страницы на /about nginx вернёт 404.
# .env.production
VITE_API_URL=https://api.myapp.com
VITE_SENTRY_DSN=https://xxx@sentry.io/123# При сборке Vite подставляет значения напрямую в JS-код:
npm run buildНа Vercel/Netlify переменные добавляются в настройках проекта (не в файлах), чтобы не попасть в git-репозиторий.
# Этап 1: сборка
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Этап 2: продакшен-образ
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]docker build -t my-vue-app .
docker run -p 8080:80 my-vue-appMulti-stage build: финальный образ содержит только nginx + статические файлы, без Node.js и исходного кода. Размер образа ~25MB вместо ~500MB.
npm run build # убедиться что сборка проходит без ошибок
npm run test # прогнать все тесты
npm run lint # проверить код на ошибкиСимуляция деплой-пайплайна: сборка, проверка артефактов, стратегии кэширования и маршрутизация SPA
// ============================================
// Симуляция деплой-пайплайна Vue SPA
// ============================================
// 1. Симуляция артефактов сборки (npm run build)
function simulateBuild(config) {
console.log('=== npm run build ===')
console.log(` Mode: ${config.mode}`)
console.log(` Outdir: ${config.outDir}`)
// Rollup генерирует хэши для долгосрочного кэширования
function hash(str) {
let h = 0
for (const c of str) h = ((h << 5) - h + c.charCodeAt(0)) | 0
return Math.abs(h).toString(36).slice(0, 8)
}
const chunks = [
{ name: 'vendor', content: 'vue+vue-router+pinia', size: 142 },
{ name: 'index', content: 'App+HomePage+AboutPage', size: 38 },
{ name: 'admin', content: 'AdminDashboard+UserTable', size: 22 },
]
const files = [
{ path: 'index.html', size: 1, type: 'html' },
...chunks.map(c => ({
path: `assets/${c.name}-${hash(c.content)}.js`,
size: c.size,
type: 'js',
name: c.name,
})),
{ path: `assets/index-${hash('styles')}.css`, size: 18, type: 'css' },
]
const totalSize = files.reduce((s, f) => s + f.size, 0)
console.log('\n Артефакты:')
for (const f of files) {
const bar = '█'.repeat(Math.ceil(f.size / 5))
console.log(` ${f.path.padEnd(40)} ${String(f.size + 'KB').padStart(6)} ${bar}`)
}
console.log(`\n Итого: ${totalSize}KB`)
return { files, success: true }
}
// 2. Симуляция nginx try_files — SPA routing
function nginxRouter(requestPath, distFiles) {
console.log(`\n GET ${requestPath}`)
// Nginx пробует: 1) точный путь, 2) путь/ (директория), 3) /index.html
const exactMatch = distFiles.find(f => '/' + f.path === requestPath)
if (exactMatch) {
console.log(` -> 200 OK: ${exactMatch.path} (${exactMatch.type})`)
return { status: 200, file: exactMatch }
}
// SPA fallback — всегда отдаём index.html
const indexHtml = distFiles.find(f => f.path === 'index.html')
console.log(` -> 200 OK: index.html (SPA fallback)`)
return { status: 200, file: indexHtml }
}
// 3. Стратегия кэширования (Cache-Control заголовки)
function getCacheHeader(filePath) {
if (filePath === 'index.html') {
// index.html НЕ кэшируем — он ссылается на актуальные хэшированные файлы
return 'no-cache'
}
if (/\/assets\/.*\.(js|css)$/.test(filePath)) {
// JS/CSS с хэшем в имени — кэш навсегда (содержимое никогда не меняется)
return 'public, max-age=31536000, immutable'
}
if (/\.(png|jpg|ico|woff2|svg)$/.test(filePath)) {
return 'public, max-age=86400' // изображения — 1 день
}
return 'no-cache'
}
// 4. Замена переменных окружения (как делает Vite при сборке)
function injectEnvVars(code, env) {
let result = code
for (const [key, value] of Object.entries(env)) {
// Vite заменяет import.meta.env.VITE_XXX на строковый литерал
result = result.replaceAll(
`import.meta.env.${key}`,
JSON.stringify(value)
)
}
return result
}
// ============================================
// Демонстрация
// ============================================
const buildResult = simulateBuild({
mode: 'production',
outDir: 'dist',
})
console.log('\n=== Стратегии кэширования ===')
for (const file of buildResult.files) {
const header = getCacheHeader(file.path)
console.log(` ${file.path.split('/').pop().padEnd(30)} Cache-Control: ${header}`)
}
console.log('\n=== SPA Routing (nginx try_files) ===')
const requests = ['/', '/about', '/users/42', '/assets/index-abc123.js', '/favicon.ico']
for (const req of requests) {
nginxRouter(req, buildResult.files)
}
console.log('\n=== Замена env переменных при сборке ===')
const sourceCode = `
const api = import.meta.env.VITE_API_URL
const title = import.meta.env.VITE_APP_TITLE
const debug = import.meta.env.DEV
`.trim()
const compiled = injectEnvVars(sourceCode, {
VITE_API_URL: 'https://api.myapp.com',
VITE_APP_TITLE: 'Моё приложение',
DEV: false,
})
console.log(' До:')
sourceCode.split('\n').forEach(l => console.log(' ' + l))
console.log(' После:')
compiled.split('\n').forEach(l => console.log(' ' + l))Команда npm run build запускает Vite/Rollup и создаёт оптимизированную статическую сборку в папке dist/:
npm run builddist/
├── index.html (точка входа)
└── assets/
├── index-Abc123.js (основной JS с хэшем для кэша)
├── vendor-Xyz789.js (зависимости: vue, vue-router...)
└── index-Def456.cssВсе файлы минифицированы, tree-shaking удалил неиспользуемый код, CSS объединён. Имена файлов содержат хэш содержимого — браузер кэширует их навсегда.
Vercel — самый простой способ задеплоить Vue SPA:
npm install -g vercel
vercelИли через GitHub: подключаете репозиторий в дашборде Vercel, и каждый push в main автоматически деплоит новую версию.
Для Vue Router (history mode) нужен vercel.json:
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}Аналогично Vercel. Файл netlify.toml для настройки:
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200Правило redirect здесь критично: без него при переходе по прямой ссылке /about Netlify вернёт 404 вместо index.html.
При самостоятельном хостинге используют nginx. Ключевая настройка — try_files:
server {
listen 80;
server_name myapp.com;
root /var/www/dist;
index index.html;
# Сжатие gzip
gzip on;
gzip_types text/plain text/css application/javascript application/json;
# Кэширование статики (файлы с хэшем — кэш навсегда)
location ~* \.(js|css|png|jpg|ico|woff2)$ {
expires max;
add_header Cache-Control "public, immutable";
}
# SPA fallback — всё остальное отдаёт index.html
location / {
try_files $uri $uri/ /index.html;
}
}Без try_files ... /index.html при обновлении страницы на /about nginx вернёт 404.
# .env.production
VITE_API_URL=https://api.myapp.com
VITE_SENTRY_DSN=https://xxx@sentry.io/123# При сборке Vite подставляет значения напрямую в JS-код:
npm run buildНа Vercel/Netlify переменные добавляются в настройках проекта (не в файлах), чтобы не попасть в git-репозиторий.
# Этап 1: сборка
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Этап 2: продакшен-образ
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]docker build -t my-vue-app .
docker run -p 8080:80 my-vue-appMulti-stage build: финальный образ содержит только nginx + статические файлы, без Node.js и исходного кода. Размер образа ~25MB вместо ~500MB.
npm run build # убедиться что сборка проходит без ошибок
npm run test # прогнать все тесты
npm run lint # проверить код на ошибкиСимуляция деплой-пайплайна: сборка, проверка артефактов, стратегии кэширования и маршрутизация SPA
// ============================================
// Симуляция деплой-пайплайна Vue SPA
// ============================================
// 1. Симуляция артефактов сборки (npm run build)
function simulateBuild(config) {
console.log('=== npm run build ===')
console.log(` Mode: ${config.mode}`)
console.log(` Outdir: ${config.outDir}`)
// Rollup генерирует хэши для долгосрочного кэширования
function hash(str) {
let h = 0
for (const c of str) h = ((h << 5) - h + c.charCodeAt(0)) | 0
return Math.abs(h).toString(36).slice(0, 8)
}
const chunks = [
{ name: 'vendor', content: 'vue+vue-router+pinia', size: 142 },
{ name: 'index', content: 'App+HomePage+AboutPage', size: 38 },
{ name: 'admin', content: 'AdminDashboard+UserTable', size: 22 },
]
const files = [
{ path: 'index.html', size: 1, type: 'html' },
...chunks.map(c => ({
path: `assets/${c.name}-${hash(c.content)}.js`,
size: c.size,
type: 'js',
name: c.name,
})),
{ path: `assets/index-${hash('styles')}.css`, size: 18, type: 'css' },
]
const totalSize = files.reduce((s, f) => s + f.size, 0)
console.log('\n Артефакты:')
for (const f of files) {
const bar = '█'.repeat(Math.ceil(f.size / 5))
console.log(` ${f.path.padEnd(40)} ${String(f.size + 'KB').padStart(6)} ${bar}`)
}
console.log(`\n Итого: ${totalSize}KB`)
return { files, success: true }
}
// 2. Симуляция nginx try_files — SPA routing
function nginxRouter(requestPath, distFiles) {
console.log(`\n GET ${requestPath}`)
// Nginx пробует: 1) точный путь, 2) путь/ (директория), 3) /index.html
const exactMatch = distFiles.find(f => '/' + f.path === requestPath)
if (exactMatch) {
console.log(` -> 200 OK: ${exactMatch.path} (${exactMatch.type})`)
return { status: 200, file: exactMatch }
}
// SPA fallback — всегда отдаём index.html
const indexHtml = distFiles.find(f => f.path === 'index.html')
console.log(` -> 200 OK: index.html (SPA fallback)`)
return { status: 200, file: indexHtml }
}
// 3. Стратегия кэширования (Cache-Control заголовки)
function getCacheHeader(filePath) {
if (filePath === 'index.html') {
// index.html НЕ кэшируем — он ссылается на актуальные хэшированные файлы
return 'no-cache'
}
if (/\/assets\/.*\.(js|css)$/.test(filePath)) {
// JS/CSS с хэшем в имени — кэш навсегда (содержимое никогда не меняется)
return 'public, max-age=31536000, immutable'
}
if (/\.(png|jpg|ico|woff2|svg)$/.test(filePath)) {
return 'public, max-age=86400' // изображения — 1 день
}
return 'no-cache'
}
// 4. Замена переменных окружения (как делает Vite при сборке)
function injectEnvVars(code, env) {
let result = code
for (const [key, value] of Object.entries(env)) {
// Vite заменяет import.meta.env.VITE_XXX на строковый литерал
result = result.replaceAll(
`import.meta.env.${key}`,
JSON.stringify(value)
)
}
return result
}
// ============================================
// Демонстрация
// ============================================
const buildResult = simulateBuild({
mode: 'production',
outDir: 'dist',
})
console.log('\n=== Стратегии кэширования ===')
for (const file of buildResult.files) {
const header = getCacheHeader(file.path)
console.log(` ${file.path.split('/').pop().padEnd(30)} Cache-Control: ${header}`)
}
console.log('\n=== SPA Routing (nginx try_files) ===')
const requests = ['/', '/about', '/users/42', '/assets/index-abc123.js', '/favicon.ico']
for (const req of requests) {
nginxRouter(req, buildResult.files)
}
console.log('\n=== Замена env переменных при сборке ===')
const sourceCode = `
const api = import.meta.env.VITE_API_URL
const title = import.meta.env.VITE_APP_TITLE
const debug = import.meta.env.DEV
`.trim()
const compiled = injectEnvVars(sourceCode, {
VITE_API_URL: 'https://api.myapp.com',
VITE_APP_TITLE: 'Моё приложение',
DEV: false,
})
console.log(' До:')
sourceCode.split('\n').forEach(l => console.log(' ' + l))
console.log(' После:')
compiled.split('\n').forEach(l => console.log(' ' + l))Реализуй функцию `buildManifest(files)`, которая принимает массив объектов `{ name: string, content: string, type: "js"|"css"|"html" }` и возвращает массив с добавленным полем `outputPath`. Правила: HTML файлы получают имя без изменений (`index.html`); JS и CSS файлы получают имя с хэшем: `name-XXXXXX.ext`, где хэш — первые 8 символов результата простой хэш-функции от content.
Для разбивки имени: const parts = file.name.split("."); const ext = parts.pop(); const basename = parts.join("."). Для outputPath: type === "html" ? file.name : "assets/" + basename + "-" + simpleHash(file.content) + "." + ext.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке