Добавлены изменения для PWA
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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/],
|
||||
};
|
||||
Reference in New Issue
Block a user