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)。
|
- Modal 集中展示語言切換、目前分類切換以及開發相關連結(OpenStreetMap、React Leaflet)。
|
||||||
- 便於未來擴充更多設定選項,而不把 App Bar 擠得太滿。
|
- 便於未來擴充更多設定選項,而不把 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/`。
|
- 透過 `react-i18next` 提供繁體中文與英文,語系檔位於 `client/src/locales/`。
|
||||||
- 右上角的語言選單 (`LanguageSwitcher`) 可即時切換語言,並記錄在 LocalStorage 方便下次載入。
|
- 右上角的語言選單 (`LanguageSwitcher`) 可即時切換語言,並記錄在 LocalStorage 方便下次載入。
|
||||||
@@ -66,6 +71,7 @@ traveling-around-the-world/
|
|||||||
### 專案建置
|
### 專案建置
|
||||||
- 推薦將 `client/package.json` 的 `build` 指令改為 `"build": "tsc --noEmit && vite build"`,避免 `tsconfig.node.json` 的 `noEmit` 限制造成錯誤。
|
- 推薦將 `client/package.json` 的 `build` 指令改為 `"build": "tsc --noEmit && vite build"`,避免 `tsconfig.node.json` 的 `noEmit` 限制造成錯誤。
|
||||||
- 調整後可執行 `npm run build`,產出靜態檔案於 `client/dist`。
|
- 調整後可執行 `npm run build`,產出靜態檔案於 `client/dist`。
|
||||||
|
- 首次啟用 PWA 時記得在 `client` 目錄重新執行 `npm install`,以安裝 `vite-plugin-pwa` 相依套件。
|
||||||
|
|
||||||
### 部署到 Vercel
|
### 部署到 Vercel
|
||||||
1. 先在本地確認 `npm run build` 可成功(參考上節的建置指令變更)。
|
1. 先在本地確認 `npm run build` 可成功(參考上節的建置指令變更)。
|
||||||
|
@@ -3,7 +3,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<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="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#0f172a" />
|
||||||
<title>Traveling Around The World</title>
|
<title>Traveling Around The World</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-slate-100 text-slate-900">
|
<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",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
"vite": "^5.1.4",
|
"vite": "^5.1.4",
|
||||||
|
"vite-plugin-pwa": "^0.17.5",
|
||||||
"autoprefixer": "^10.4.20"
|
"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 { App } from './app/App';
|
||||||
import './i18n';
|
import './i18n';
|
||||||
import './styles/global.css';
|
import './styles/global.css';
|
||||||
|
import { registerSW } from 'virtual:pwa-register';
|
||||||
|
|
||||||
const container = document.getElementById('root');
|
const container = document.getElementById('root');
|
||||||
|
|
||||||
@@ -17,3 +18,7 @@ root.render(
|
|||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</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/client" />
|
||||||
|
/// <reference types="vite-plugin-pwa/client" />
|
||||||
|
|
||||||
declare module '*.png?url' {
|
declare module '*.png?url' {
|
||||||
const url: string;
|
const url: string;
|
||||||
|
@@ -1,9 +1,47 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react-swc';
|
import react from '@vitejs/plugin-react-swc';
|
||||||
import { fileURLToPath, URL } from 'node:url';
|
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({
|
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: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@shared': fileURLToPath(new URL('../shared', import.meta.url))
|
'@shared': fileURLToPath(new URL('../shared', import.meta.url))
|
||||||
|
Reference in New Issue
Block a user