Appearance
Web App Manifest - 完整PWA配置指南
1. Manifest基础
1.1 什么是Web App Manifest
Web App Manifest是一个JSON文件,为PWA提供应用元数据,控制应用的外观和行为。
json
{
"name": "我的PWA应用",
"short_name": "PWA",
"description": "这是一个渐进式Web应用",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2196F3",
"orientation": "portrait",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}1.2 引入Manifest
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 引入manifest -->
<link rel="manifest" href="/manifest.json">
<!-- iOS特殊处理 -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="PWA">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<!-- 主题颜色 -->
<meta name="theme-color" content="#2196F3">
<title>PWA应用</title>
</head>
<body>
<div id="root"></div>
</body>
</html>2. Manifest属性详解
2.1 基本信息
json
{
"name": "完整应用名称",
"short_name": "短名称",
"description": "应用描述文字",
"lang": "zh-CN",
"dir": "ltr",
"scope": "/app/",
"start_url": "/?source=pwa",
"display": "standalone",
"orientation": "any",
"theme_color": "#2196F3",
"background_color": "#ffffff"
}name和short_name
json
{
"name": "我的超级应用 - 最好的PWA体验",
"short_name": "超级应用",
"name_translations": {
"en": "My Super App - Best PWA Experience",
"ja": "マイスーパーアプリ"
}
}scope和start_url
json
{
"scope": "/app/",
"start_url": "/app/?utm_source=homescreen",
"start_url": "/?utm_source=pwa&utm_medium=homescreen&utm_campaign=install"
}2.2 显示模式
json
{
"display": "standalone",
"display_override": [
"window-controls-overlay",
"minimal-ui",
"standalone",
"browser"
]
}typescript
const displayModes = {
fullscreen: {
description: '全屏模式',
ui: '隐藏所有浏览器UI',
usage: '游戏、视频应用'
},
standalone: {
description: '独立应用模式',
ui: '隐藏浏览器UI,保留状态栏',
usage: '大多数PWA推荐'
},
'minimal-ui': {
description: '最小UI模式',
ui: '保留最小浏览器控制',
usage: '需要导航控制的应用'
},
browser: {
description: '浏览器模式',
ui: '完整浏览器界面',
usage: '降级选项'
}
};
// 检测显示模式
function getDisplayMode() {
if (window.matchMedia('(display-mode: fullscreen)').matches) {
return 'fullscreen';
}
if (window.matchMedia('(display-mode: standalone)').matches) {
return 'standalone';
}
if (window.matchMedia('(display-mode: minimal-ui)').matches) {
return 'minimal-ui';
}
return 'browser';
}2.3 方向设置
json
{
"orientation": "portrait",
"orientations": [
"any",
"natural",
"landscape",
"landscape-primary",
"landscape-secondary",
"portrait",
"portrait-primary",
"portrait-secondary"
]
}2.4 颜色主题
json
{
"theme_color": "#2196F3",
"background_color": "#ffffff",
"theme_color_media_queries": [
{
"media": "(prefers-color-scheme: dark)",
"color": "#1a1a1a"
},
{
"media": "(prefers-color-scheme: light)",
"color": "#ffffff"
}
]
}3. 图标配置
3.1 基础图标
json
{
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}3.2 可遮罩图标
json
{
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}typescript
const iconPurposes = {
any: {
description: '任何场景',
requirement: '普通图标'
},
maskable: {
description: '可遮罩图标',
requirement: '图标内容在安全区域内',
safeZone: '80%中心区域',
tools: ['maskable.app生成器']
},
monochrome: {
description: '单色图标',
usage: '通知、快捷方式'
}
};3.3 多格式图标
json
{
"icons": [
{
"src": "/icons/icon.svg",
"sizes": "any",
"type": "image/svg+xml"
},
{
"src": "/icons/icon.webp",
"sizes": "512x512",
"type": "image/webp"
},
{
"src": "/icons/icon.png",
"sizes": "512x512",
"type": "image/png"
}
]
}4. 启动画面
4.1 启动画面配置
json
{
"name": "我的应用",
"background_color": "#ffffff",
"theme_color": "#2196F3",
"icons": [
{
"src": "/icons/splash-640x1136.png",
"sizes": "640x1136",
"type": "image/png"
}
]
}4.2 生成启动画面
typescript
// 自动生成启动画面配置
function generateSplashScreens() {
const devices = [
{ name: 'iPhone SE', width: 640, height: 1136 },
{ name: 'iPhone 8', width: 750, height: 1334 },
{ name: 'iPhone 11', width: 828, height: 1792 },
{ name: 'iPhone 11 Pro Max', width: 1242, height: 2688 },
{ name: 'iPad', width: 1536, height: 2048 },
{ name: 'iPad Pro 12.9"', width: 2048, height: 2732 }
];
return devices.map(device => ({
src: `/splash/splash-${device.width}x${device.height}.png`,
sizes: `${device.width}x${device.height}`,
type: 'image/png',
device: device.name
}));
}
// iOS启动画面meta标签
const iosSplashScreens = `
<link rel="apple-touch-startup-image"
href="/splash/splash-640x1136.png"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)">
<link rel="apple-touch-startup-image"
href="/splash/splash-750x1334.png"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)">
`;5. 快捷方式
5.1 应用快捷方式
json
{
"shortcuts": [
{
"name": "新建文档",
"short_name": "新建",
"description": "创建新文档",
"url": "/new?source=shortcut",
"icons": [
{
"src": "/icons/new-doc.png",
"sizes": "96x96",
"type": "image/png"
}
]
},
{
"name": "最近文档",
"short_name": "最近",
"description": "查看最近编辑的文档",
"url": "/recent?source=shortcut",
"icons": [
{
"src": "/icons/recent.png",
"sizes": "96x96",
"type": "image/png"
}
]
},
{
"name": "搜索",
"short_name": "搜索",
"description": "搜索文档",
"url": "/search?source=shortcut",
"icons": [
{
"src": "/icons/search.png",
"sizes": "96x96",
"type": "image/png"
}
]
}
]
}5.2 动态快捷方式
typescript
// 更新快捷方式(需要后端支持)
async function updateShortcuts() {
const manifest = {
...existingManifest,
shortcuts: [
{
name: '继续编辑',
url: '/edit/' + lastDocumentId,
icons: [{ src: '/icons/edit.png', sizes: '96x96' }]
},
...otherShortcuts
]
};
// 生成新的manifest
const manifestBlob = new Blob([JSON.stringify(manifest)], {
type: 'application/json'
});
const manifestURL = URL.createObjectURL(manifestBlob);
// 更新manifest引用
document.querySelector('link[rel="manifest"]')?.setAttribute('href', manifestURL);
}6. 分享目标
6.1 接收分享
json
{
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [
{
"name": "media",
"accept": ["image/*", "video/*"]
}
]
}
}
}6.2 处理分享
typescript
// 处理分享数据
app.post('/share', async (req, res) => {
const { title, text, url } = req.body;
const files = req.files;
// 处理分享内容
console.log('接收到分享:', { title, text, url });
if (files) {
for (const file of files) {
await saveFile(file);
}
}
// 重定向到应用
res.redirect('/');
});
// React处理分享
export function ShareHandler() {
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const sharedTitle = urlParams.get('title');
const sharedText = urlParams.get('text');
const sharedUrl = urlParams.get('url');
if (sharedTitle || sharedText || sharedUrl) {
handleSharedContent({
title: sharedTitle,
text: sharedText,
url: sharedUrl
});
}
}, []);
return null;
}7. 协议处理
7.1 URL协议处理
json
{
"protocol_handlers": [
{
"protocol": "web+music",
"url": "/play?track=%s"
},
{
"protocol": "mailto",
"url": "/compose?to=%s"
}
]
}7.2 文件处理
json
{
"file_handlers": [
{
"action": "/open-file",
"accept": {
"image/*": [".png", ".jpg", ".jpeg", ".gif"],
"application/pdf": [".pdf"]
}
}
]
}8. 相关应用
8.1 关联原生应用
json
{
"related_applications": [
{
"platform": "play",
"id": "com.example.app",
"url": "https://play.google.com/store/apps/details?id=com.example.app"
},
{
"platform": "itunes",
"url": "https://apps.apple.com/app/id123456789"
}
],
"prefer_related_applications": false
}8.2 优先原生应用
json
{
"related_applications": [
{
"platform": "play",
"id": "com.example.app"
}
],
"prefer_related_applications": true
}9. 类别和IARC评级
9.1 应用类别
json
{
"categories": [
"productivity",
"business",
"utilities"
],
"available_categories": [
"books",
"business",
"education",
"entertainment",
"finance",
"fitness",
"food",
"games",
"government",
"health",
"kids",
"lifestyle",
"magazines",
"medical",
"music",
"navigation",
"news",
"personalization",
"photo",
"politics",
"productivity",
"security",
"shopping",
"social",
"sports",
"travel",
"utilities",
"weather"
]
}9.2 IARC评级
json
{
"iarc_rating_id": "e84b072d-71b3-4d3e-86ae-31a8ce4e53b7"
}10. React中的Manifest管理
10.1 动态生成Manifest
tsx
// manifest-generator.ts
export function generateManifest(options: {
name: string;
shortName: string;
themeColor: string;
backgroundColor: string;
}) {
return {
name: options.name,
short_name: options.shortName,
start_url: '/',
display: 'standalone',
theme_color: options.themeColor,
background_color: options.backgroundColor,
icons: [
{
src: '/icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: '/icons/icon-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
};
}
// ManifestProvider.tsx
export function ManifestProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
const manifest = generateManifest({
name: '我的应用',
shortName: 'App',
themeColor: '#2196F3',
backgroundColor: '#ffffff'
});
const manifestBlob = new Blob([JSON.stringify(manifest)], {
type: 'application/json'
});
const manifestURL = URL.createObjectURL(manifestBlob);
let link = document.querySelector('link[rel="manifest"]');
if (!link) {
link = document.createElement('link');
link.setAttribute('rel', 'manifest');
document.head.appendChild(link);
}
link.setAttribute('href', manifestURL);
return () => {
URL.revokeObjectURL(manifestURL);
};
}, []);
return <>{children}</>;
}10.2 主题颜色切换
tsx
// useThemeColor.ts
export function useThemeColor(color: string) {
useEffect(() => {
let meta = document.querySelector('meta[name="theme-color"]');
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name', 'theme-color');
document.head.appendChild(meta);
}
meta.setAttribute('content', color);
}, [color]);
}
// App.tsx
function App() {
const [darkMode, setDarkMode] = useState(false);
useThemeColor(darkMode ? '#1a1a1a' : '#2196F3');
return <div>...</div>;
}11. 验证和调试
11.1 Manifest验证
typescript
// 验证manifest
async function validateManifest(manifestUrl: string) {
const response = await fetch(manifestUrl);
const manifest = await response.json();
const errors: string[] = [];
const warnings: string[] = [];
// 必需字段检查
if (!manifest.name) {
errors.push('缺少name字段');
}
if (!manifest.icons || manifest.icons.length === 0) {
errors.push('缺少icons字段');
}
if (!manifest.start_url) {
warnings.push('建议设置start_url');
}
// 图标尺寸检查
const requiredSizes = ['192x192', '512x512'];
const iconSizes = manifest.icons?.map((icon: any) => icon.sizes) || [];
requiredSizes.forEach(size => {
if (!iconSizes.includes(size)) {
warnings.push(`建议包含${size}尺寸图标`);
}
});
// display模式检查
if (!['fullscreen', 'standalone', 'minimal-ui', 'browser'].includes(manifest.display)) {
warnings.push('display值无效');
}
return { errors, warnings, valid: errors.length === 0 };
}11.2 Chrome DevTools
typescript
const debugManifest = {
steps: [
'打开Chrome DevTools',
'切换到Application面板',
'选择Manifest',
'查看解析的manifest内容',
'检查错误和警告',
'测试添加到主屏幕'
],
checks: [
'manifest文件可访问',
'所有图标可加载',
'start_url有效',
'显示模式正确',
'主题颜色显示'
]
};12. 最佳实践
typescript
const manifestBestPractices = {
naming: [
'name: 完整应用名称(最多45字符)',
'short_name: 主屏幕显示(最多12字符)',
'提供描述性名称',
'避免使用通用词汇',
'考虑多语言支持'
],
icons: [
'提供192x192和512x512图标',
'使用maskable图标',
'支持多种格式',
'优化图标大小',
'安全区域内设计'
],
display: [
'大多数应用使用standalone',
'游戏使用fullscreen',
'提供display_override降级',
'测试不同显示模式',
'考虑用户体验'
],
colors: [
'theme_color与品牌一致',
'background_color避免闪烁',
'支持深色模式',
'确保对比度',
'测试不同主题'
],
urls: [
'start_url包含跟踪参数',
'scope限制应用范围',
'确保URL有效',
'测试离线访问',
'处理深层链接'
]
};13. 总结
Web App Manifest的核心要点:
- 基本配置: name, icons, start_url, display
- 图标: 多尺寸, maskable, 多格式
- 显示: standalone模式最常用
- 颜色: theme_color和background_color
- 快捷方式: 提供常用功能入口
- 分享: 接收系统分享内容
- 验证: 使用工具检查配置
正确配置Manifest是PWA体验的关键。