Добавлены изменения для PWA

This commit is contained in:
mi
2026-07-21 18:49:21 +03:00
parent 627371b221
commit cc0163eb94
22 changed files with 592 additions and 208 deletions
@@ -13,7 +13,7 @@ ENV EXPO_PUBLIC_API_BASE_URL=$EXPO_PUBLIC_API_BASE_URL \
EXPO_PUBLIC_KEYCLOAK_REALM=$EXPO_PUBLIC_KEYCLOAK_REALM \
EXPO_PUBLIC_KEYCLOAK_CLIENT_ID=$EXPO_PUBLIC_KEYCLOAK_CLIENT_ID \
EXPO_PUBLIC_APP_ENV=$EXPO_PUBLIC_APP_ENV
RUN npm run build
RUN npm run build:pwa
# One-shot Compose init container copies the immutable export to nginx's volume.
FROM alpine:3.22 AS static
+22 -2
View File
@@ -10,11 +10,31 @@ npm install
npm run web
```
Проверки: `npm test`, `npm run typecheck`, `npm run build`, `npm run test:e2e`.
Проверки: `npm test`, `npm run typecheck`, `npm run build:pwa`, `npm run test:e2e`.
## PWA
Иконки лежат в `public/`:
| Файл | Размер |
|---|---|
| `favicon.png` | 64×64 |
| `icon-192.png` | 360×360 |
| `icon-512.png` | 1024×1024 |
| `apple-touch-icon.png` | 360×360 |
Manifest: `public/manifest.json`. Корневой HTML: `app/+html.tsx`.
```bash
npm run build:pwa # export + service-worker.js (Workbox)
npm run serve # локальная проверка dist/
```
Chrome DevTools → Application → Manifest / Service Workers. API и `/auth/` не кэшируются.
## Production
`npm run build` создаёт `dist/`. Каталог монтируется в корневой nginx системы; отдельный frontend nginx не используется. Для SPA nginx должен применять `try_files $uri /index.html`, не кэшировать `index.html` и бессрочно кэшировать hashed assets.
`npm run build:pwa` создаёт `dist/` с manifest, иконками и `service-worker.js`. Каталог монтируется в корневой nginx; отдельный frontend nginx не используется. Для SPA nginx должен применять `try_files $uri /index.html`, не кэшировать `index.html`, `manifest.json`, `service-worker.js` и бессрочно кэшировать hashed assets.
Dockerfile собирает статический OCI-артефакт `/dist` без runtime-сервера:
@@ -9,7 +9,18 @@ const config: ExpoConfig = {
userInterfaceStyle: "light",
experiments: { typedRoutes: true },
plugins: ["expo-router", "expo-secure-store"],
web: { bundler: "metro", output: "static" },
web: {
bundler: "metro",
output: "static",
shortName: "HAN Chat",
name: "HAN Chat",
description: "Помощник по документам и жизни в России",
display: "standalone",
orientation: "portrait",
backgroundColor: "#ffffff",
themeColor: "#030213",
favicon: "./public/favicon.png",
},
};
export default config;
@@ -0,0 +1,30 @@
import { ScrollViewStyleReset } from "expo-router/html";
import type { PropsWithChildren } from "react";
const serviceWorkerBootstrap = `
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').catch(() => {});
});
}
`;
export default function Root({ children }: PropsWithChildren) {
return (
<html lang="ru">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#030213" />
<meta name="description" content="Помощник по документам и жизни в России" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/png" sizes="64x64" href="/favicon.png" />
<link rel="apple-touch-icon" sizes="360x360" href="/apple-touch-icon.png" />
<script dangerouslySetInnerHTML={{ __html: serviceWorkerBootstrap }} />
<ScrollViewStyleReset />
</head>
<body>{children}</body>
</html>
);
}
@@ -11,6 +11,8 @@
"test:watch": "vitest",
"test:e2e": "playwright test",
"build": "expo export --platform web",
"build:pwa": "expo export --platform web && workbox generateSW workbox-config.js",
"icons:pwa": "powershell -ExecutionPolicy Bypass -File ./scripts/generate-pwa-icons.ps1",
"serve": "npx serve dist"
},
"dependencies": {
@@ -44,6 +46,7 @@
"@vitejs/plugin-react": "6.0.3",
"typescript": "7.0.2",
"vite": "8.1.4",
"vitest": "4.1.10"
"vitest": "4.1.10",
"workbox-cli": "7.4.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,31 @@
{
"short_name": "HAN Chat",
"name": "HAN Chat",
"description": "Помощник по документам и жизни в России",
"icons": [
{
"src": "/favicon.png",
"sizes": "64x64",
"type": "image/png"
},
{
"src": "/icon-192.png",
"type": "image/png",
"sizes": "360x360",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"type": "image/png",
"sizes": "1024x1024",
"purpose": "any maskable"
}
],
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#030213",
"background_color": "#ffffff",
"lang": "ru"
}
@@ -0,0 +1,28 @@
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Drawing
function New-HanIcon([int]$size, [string]$path) {
$bmp = New-Object System.Drawing.Bitmap $size, $size
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
$g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAliasGridFit
$g.Clear([System.Drawing.Color]::FromArgb(255, 3, 2, 19))
$fontSize = [math]::Max(8, [math]::Floor($size * 0.22))
$font = New-Object System.Drawing.Font("Segoe UI", $fontSize, [System.Drawing.FontStyle]::Bold)
$brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
$format = New-Object System.Drawing.StringFormat
$format.Alignment = "Center"
$format.LineAlignment = "Center"
$rect = New-Object System.Drawing.RectangleF 0, 0, $size, $size
$g.DrawString("HAN", $font, $brush, $rect, $format)
$g.Dispose()
$bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
}
$publicDir = Join-Path $PSScriptRoot ".." "public"
New-Item -ItemType Directory -Force -Path $publicDir | Out-Null
New-HanIcon 512 (Join-Path $publicDir "icon-512.png")
New-HanIcon 192 (Join-Path $publicDir "icon-192.png")
New-HanIcon 180 (Join-Path $publicDir "apple-touch-icon.png")
New-HanIcon 32 (Join-Path $publicDir "favicon.png")
@@ -0,0 +1,9 @@
/** @type {import('workbox-build').GenerateSWOptions} */
module.exports = {
globDirectory: "dist",
globPatterns: ["**/*.{js,css,html,ico,png,json,woff2,svg,webp}"],
globIgnores: ["**/node_modules/**"],
swDest: "dist/service-worker.js",
navigateFallback: "/index.html",
navigateFallbackDenylist: [/^\/api/, /^\/auth/],
};
@@ -9,6 +9,11 @@ location = /index.html {
add_header Cache-Control "no-cache";
include /etc/nginx/generated/security-headers.conf;
}
location = /manifest.json {
root /usr/share/nginx/html;
add_header Cache-Control "no-cache";
include /etc/nginx/generated/security-headers.conf;
}
location = /service-worker.js {
root /usr/share/nginx/html;
add_header Cache-Control "no-cache";