feat: add PWA support with manifest and service worker
This commit is contained in:
@@ -52,6 +52,11 @@ traveling-around-the-world/
|
||||
- Modal 集中展示語言切換、目前分類切換以及開發相關連結(OpenStreetMap、React Leaflet)。
|
||||
- 便於未來擴充更多設定選項,而不把 App Bar 擠得太滿。
|
||||
|
||||
### PWA 支援
|
||||
- 新增 Web App Manifest(`client/public/manifest.webmanifest`)與 service worker 註冊,可在行動裝置安裝為獨立 App。
|
||||
- 透過 `vite-plugin-pwa` 自動產生快取策略,支援離線瀏覽與版號自動更新。
|
||||
- 內建 `icons/icon-192.svg`、`icons/icon-512.svg`,可於日後替換為專屬圖示。
|
||||
|
||||
### 多語系支援
|
||||
- 透過 `react-i18next` 提供繁體中文與英文,語系檔位於 `client/src/locales/`。
|
||||
- 右上角的語言選單 (`LanguageSwitcher`) 可即時切換語言,並記錄在 LocalStorage 方便下次載入。
|
||||
@@ -66,6 +71,7 @@ traveling-around-the-world/
|
||||
### 專案建置
|
||||
- 推薦將 `client/package.json` 的 `build` 指令改為 `"build": "tsc --noEmit && vite build"`,避免 `tsconfig.node.json` 的 `noEmit` 限制造成錯誤。
|
||||
- 調整後可執行 `npm run build`,產出靜態檔案於 `client/dist`。
|
||||
- 首次啟用 PWA 時記得在 `client` 目錄重新執行 `npm install`,以安裝 `vite-plugin-pwa` 相依套件。
|
||||
|
||||
### 部署到 Vercel
|
||||
1. 先在本地確認 `npm run build` 可成功(參考上節的建置指令變更)。
|
||||
|
@@ -3,7 +3,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
<title>Traveling Around The World</title>
|
||||
</head>
|
||||
<body class="bg-slate-100 text-slate-900">
|
||||
|
2551
client/package-lock.json
generated
2551
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,7 @@
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.2",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-pwa": "^0.17.5",
|
||||
"autoprefixer": "^10.4.20"
|
||||
}
|
||||
}
|
||||
|
5
client/public/icons/icon-192.svg
Normal file
5
client/public/icons/icon-192.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<circle cx="32" cy="32" r="30" fill="#0b7285"/>
|
||||
<path d="M20 26c4-8 16-10 24 0s-8 20-8 20l-4-8-8 8" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="32" cy="24" r="4" fill="#fff"/>
|
||||
</svg>
|
After Width: | Height: | Size: 310 B |
5
client/public/icons/icon-512.svg
Normal file
5
client/public/icons/icon-512.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<circle cx="32" cy="32" r="30" fill="#0b7285"/>
|
||||
<path d="M20 26c4-8 16-10 24 0s-8 20-8 20l-4-8-8 8" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="32" cy="24" r="4" fill="#fff"/>
|
||||
</svg>
|
After Width: | Height: | Size: 310 B |
25
client/public/manifest.webmanifest
Normal file
25
client/public/manifest.webmanifest
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "Traveling Around The World",
|
||||
"short_name": "Travel Map",
|
||||
"description": "記錄旅程、管理旅遊分類並在地圖上標記足跡的 PWA。",
|
||||
"lang": "zh-Hant",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#0f172a",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512.svg",
|
||||
"sizes": "512x512",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
@@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client';
|
||||
import { App } from './app/App';
|
||||
import './i18n';
|
||||
import './styles/global.css';
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
|
||||
@@ -17,3 +18,7 @@ root.render(
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
|
||||
registerSW({ immediate: true });
|
||||
}
|
||||
|
1
client/src/vite-env.d.ts
vendored
1
client/src/vite-env.d.ts
vendored
@@ -1,4 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-pwa/client" />
|
||||
|
||||
declare module '*.png?url' {
|
||||
const url: string;
|
||||
|
@@ -1,9 +1,47 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
const pwaManifest = {
|
||||
name: 'Traveling Around The World',
|
||||
short_name: 'Travel Map',
|
||||
description: '記錄旅程、管理旅遊分類並在地圖上標記足跡的 PWA。',
|
||||
lang: 'zh-Hant',
|
||||
start_url: '/',
|
||||
scope: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#ffffff',
|
||||
theme_color: '#0f172a',
|
||||
icons: [
|
||||
{
|
||||
src: '/icons/icon-192.svg',
|
||||
sizes: '192x192',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: '/icons/icon-512.svg',
|
||||
sizes: '512x512',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any maskable'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['favicon.svg', 'icons/icon-192.svg', 'icons/icon-512.svg'],
|
||||
manifest: pwaManifest,
|
||||
workbox: {
|
||||
navigateFallback: '/index.html',
|
||||
globPatterns: ['**/*.{js,css,html,svg,png,ico,json}']
|
||||
}
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@shared': fileURLToPath(new URL('../shared', import.meta.url))
|
||||
|
Reference in New Issue
Block a user