让网页拥有App体验?PWA 将网页变为桌面应用的保姆级教程

1. 前言

PWA,即渐进式网络应用(Progressive Web App),是一种利用现代 Web 技术构建的应用程序,它融合了网页应用和原生应用的优点,能够为用户提供类似于原生应用的体验。PWA 具备以下显著特点:

  1. 渐进增强:可以根据不同浏览器和设备的能力,逐步增强功能,适配各种环境和用户需求。
  2. 响应式布局:能自适应不同设备的屏幕大小和分辨率,确保在桌面、移动设备和平板电脑等各种终端上都有良好的显示效果。
  3. 离线访问:借助 Service Worker 技术,将应用的关键资源缓存到本地,使用户在没有网络连接时也能访问部分功能和内容。
  4. 推送通知:具备推送通知功能,可向用户实时发送新内容、交互提示或重要信息等,保持与用户的互动。
  5. 安装性:用户能够将应用添加到设备主屏幕,实现快速访问,就像使用原生应用一样便捷。
  6. 链接分享:通过 URL 链接即可进行分享和访问,方便用户将应用分享给他人。

2. 准备工作

在开始将网页配置为 PWA 并添加到桌面之前,需要确保满足以下条件:

  • 使用 HTTPS 协议:除了本地开发环境(localhost)外,为了保证安全性和兼容性,网页必须使用 HTTPS 协议。因为 Service Worker 只能在安全的上下文环境中运行,这是 PWA 实现离线访问等功能的基础。
  • 准备应用图标:准备不同尺寸的应用图标,以适应各种设备和平台的显示需求。通常建议提供 192x192 像素、512x512 像素等常见尺寸的图标,格式一般为 PNG。

3. 配置 manifest.json 文件

manifest.json 文件,用于定义 PWA 的相关信息,包括应用名称、图标、启动页面、显示模式等。这些信息将决定用户将应用添加到桌面后的显示效果和行为。通常都放在项目根目录下面,并且在根 index.html 中引入。其常用配置如下:

json 复制代码
{
	// 应用的全名
	"name": "React.App",
	// 应用的短名称,当全名对于可用空间来说太长时可以使用它。
	"short_name": "R.App",
	// 应用描述,当用户在应用商店(Microsoft Store)中查看应用时,这些文本可以帮助他们理解应用的目的。
	"description": "一个React的示例App",
	// 应用的图标对应不同尺寸
	"icons": [
		{
			"src": "/logo/logo-192.png",
			"type": "image/png",
			"sizes": "192x192"
		},
		{
			"src": "/logo/logo-512.png",
			"type": "image/png",
			"sizes": "512x512"
		}
	],
	// 应用启动时应该打开的URL,这里打开了首页且带了个参数source=pwa
	"start_url": "/home?source=pwa",
	// 使页面显示效果为沉浸式 没有地址栏和底部状态栏
	"display": "standalone",
	// 导航界面的背景颜色
	"background_color": "#000000",
	// 手机最上方显示运营商及电量、时间等这一栏的颜色
	"theme_color": "#000000",
	// 应用方向 竖向:portrait 横向:landscape
	"orientation": "portrait",
	// 用于网站的首选显示模式 默认全屏 否则是独立的应用程序
	"display_override": ["fullscreen", "minimal-ui"]
}

更多详细配置,可参考:官方文档

然后在项目的 index.html 文件的标签内,添加如下代码,将 manifest.json 文件与网页关联起来

html 复制代码
<!-- 网页清单 定义应用的元数据(如名称、图标、启动 URL 等) -->
<link rel="manifest" href="/manifest.json" />
<!-- iOS的网页清单 定义应用的元数据(如名称、图标、启动 URL 等) -->
<link rel="manifest" href="/manifest.webmanifest" />

注意:iOS 的 manifest 文件,需要使用.webmanifest 后缀,直接复制一份 manifest.json 文件,并修改后缀为.webmanifest。

4. 配置 Service Worker

Service Worker 是 PWA 的核心技术之一,它允许网页在后台运行脚本,实现离线缓存、拦截网络请求等功能,从而提升应用的性能和用户体验,比如把缓存的资源保存到本地,当用户没有网络连接时,依然可以使用这些资源。如下图,资源来自Service Worker中获取到的:

  1. 在 body 标签内添加如下代码:
html 复制代码
<body oncontextmenu="return false" ondragstart="return false">
	<!-- 已有代码...... -->
	<script defer src="/register-sw.js"></script>
</body>
  1. 编写 register-sw.js 文件,用于注册 Service Worker,并设置其缓存策略。通常都放在项目根目录下面,并且在 body 标签内引入,如下:
javascript 复制代码
/**
 * @description 执行SW的register
 * @description 单独拿出来使用defer避免阻塞
 */
if ('serviceWorker' in navigator) {
	window.addEventListener('load', () => {
		navigator.serviceWorker
			.register('./service-worker.js')
			.then((registration) => {
				console.log('%cService Worker 注册成功:', 'color:green;', registration)
			})
			.catch((error) => {
				console.error('%cService Worker 注册失败:', 'color:red;', error)
			})
	})
}

上述代码首先检查浏览器是否支持 Service Worker,如果支持,则在页面加载完成后(load 事件触发时)尝试注册名为 service-worker.js 的 Service Worker 文件。如果注册成功,会在控制台输出相关信息;如果注册失败,也会在控制台打印错误信息,方便调试。

  1. 编写 service-worker.js 文件,用于定义 Service Worker 的行为,比如缓存静态资源、拦截网络请求等。通常都放在项目根目录下面,并且在 body 标签内引入,如下:
javascript 复制代码
/**
 * @description PWA 用于实现离线缓存和后台同步等功能
 * @description 安装时缓存静态资源
 */

// 缓存空间名称
const CACHE_NAME = 'test-pwa-cache'

// 缓存静态资源
const urlsToCache = [
	'/',
	'/index.html',
	'favicon.ico',
	'/logo/logo-192.png',
	'/logo/logo-192.jpg',
	'/logo/logo-512.png',
	'/logo/logo-512.jpg',
	'/video/loading.mp4',
]

// 安装事件 此时缓存静态资源
self.addEventListener('install', (event) => {
	event.waitUntil(
		caches.open(CACHE_NAME).then((cache) => {
			console.log('%cService Worker 缓存:', 'color:gray;', cache)
			return cache.addAll(urlsToCache)
		})
	)
})

// 激活事件​ 清理旧的缓存
self.addEventListener('activate', (event) => {
	event.waitUntil(
		caches.keys().then(function (keyList) {
			return Promise.all(
				keyList
					.filter(function (key) {
						return key.startsWith('test-pwa-cache-') && key !== CACHE_NAME
					})
					.map(function (key) {
						return caches.delete(key)
					})
			)
		})
	)
})

// 拦截网络请求 此时可以判断是否命中缓存内容并返回
self.addEventListener('fetch', (event) => {
	event.respondWith(
		caches.match(event.request).then((response) => {
			if (response) {
				console.log('%cService Worker 拦截成功,返回缓存内容:', 'color:blue;', response)
				return response
			}
			return fetch(event.request)
		})
	)
})

上述代码中:

  • 安装阶段:install 事件触发时,打开指定的缓存(CACHE_NAME),并将 PRE_CACHE 列表中的资源缓存到本地。任何资源路径出错都可能导致 Service Worker 安装失败。
  • 激活阶段:activate 事件触发时,清理旧的缓存。遍历所有缓存名称,删除那些以 cache - 开头且不等于当前缓存名称(CACHE_NAME)的缓存。
  • 请求拦截阶段:fetch 事件触发时,拦截所有网络请求。首先尝试从缓存中匹配请求,如果找到匹配的缓存响应,则直接返回;如果没有找到,则发起网络请求。在网络请求成功返回且状态码为 200 时,将响应克隆一份缓存到本地,以便下次可以直接从缓存中获取。

5. 将网页添加到桌面

当完成上述 Web App Manifest 和 Service Worker 的配置后,用户就可以将该网页作为 PWA 添加到桌面了。具体操作步骤因浏览器和设备而异:

  1. 手动添加:
  • 谷歌内核浏览器

    • 在电脑浏览器中打开网页

    • 点击浏览器地址栏右侧的 "+" 图标(如果网页满足 PWA 条件,该图标会显示),或者点击浏览器菜单(通常是三个点的图标),选择 "安装 [应用名称]" 选项。

    • 根据提示完成安装,应用图标将出现在桌面。

    • 或手机浏览器中打开网页

    • 在手机浏览器中,点击浏览器菜单(通常是三个点的图标),选择 "添加到主屏幕" 选项。

    • 用户可以自定义应用名称,然后点击 "添加",应用图标将添加到手机主屏幕。

  • iOS Safari浏览器

    • 在手机浏览器中打开网页
    • 点击浏览器底部的分享按钮(一个向上箭头的图标)。
    • 在弹出的分享菜单中,选择 "添加到主屏幕" 选项。
    • 可以对应用名称进行编辑,然后点击 "添加",应用图标将被添加到主屏幕。
  1. 自动添加(iOS不支持):

在页面加载的生命周期中,监听beforeinstallprompt事件,当事件触发后,阻止掉默认的安装行为,存储在变量中,并显示安装按钮。当用户点击按钮时,释放存储的事件,此时会弹出一个系统的安装对话框,用户可以点击安装应用,具体代码如下:

tsx 复制代码
// 使用此变量存储事件
const deferredPrompt = useRef<any>(null)

/**
 * @description 初始化生命周期
 */
useEffect(() => {
	// 监听 PWA安装事件
	window.addEventListener('beforeinstallprompt', (e) => {
		// 阻止系统自己弹窗
		e.preventDefault()
		// 保存事件,稍后触发安装
		deferredPrompt.current = e
	})
}, [])

/**
 * @description 判断是否在PWA中打开
 */
const isInStandaloneMode = () => {
	const is =
		window.matchMedia('(display-mode: standalone)').matches ||
		(window.navigator as T_Any).standalone ||
		document.referrer.includes('android-app://')
	console.log(`${is ? '在' : '不在'}PWA中打开`)
	return is
}

/**
 * @description 添加到主屏幕
 */
const handleAddPWA = () => {
	if (!isInStandaloneMode()) {
		if (deferredPrompt.current) {
			console.log('%c添加到主屏幕事件:触发成功', 'color: green;')
			// 触发PWA安装
			deferredPrompt.current.prompt()
			// 监听安装结果
			deferredPrompt.current.userChoice.then((choiceResult: T_Any) => {
				if (choiceResult.outcome === 'accepted') {
					console.log('成功!')
				} else {
					console.log('用户取消安装,该deferredPrompt.prompt仍可继续使用')
				}
        deferredPrompt.current = null
			})
		} else {
			console.log('%c添加到主屏幕事件:触发失败', 'color: red;')
			// 此时打开提示弹出,引导用户手动添加到屏幕
		}
	} else {
		console.log('已经成功添加!')
	}
}

上述手动添加的代码,本质上是拦截了系统的通知事件,因此不同浏览器和手机设备可能有不同的实现方式。并且开发者只能被动拦截,无法主动触发,成功率不稳定,所以需要做一个手动安装的引导弹窗,作为替补方案。

通过以上步骤,你可以将普通网页转化为 PWA 应用,并让用户方便地将其添加到桌面。这不仅提升了用户体验,使网页应用更接近原生应用的便捷性,还能增加用户对应用的访问频率和粘性。在实际应用中,你可以根据具体需求进一步优化 PWA 的功能和性能,例如优化缓存策略、实现推送通知功能等,为用户提供更优质的服务。

本文参考文档:


如果看了觉得有帮助的,我是@鹏多多,欢迎 点赞 关注 评论;

往期文章

个人主页

相关推荐
zhangxingchao26 分钟前
Flutter中的页面跳转
前端
前端风云志1 小时前
TypeScript实用类型之Omit
javascript
烛阴1 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝2 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇2 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军2 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加3 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript
Carlos_sam4 小时前
OpenLayers:封装一个自定义罗盘控件
前端·javascript
前端南玖4 小时前
深入Vue3响应式:手写实现reactive与ref
前端·javascript·vue.js
wordbaby5 小时前
React Router 双重加载器机制:服务端 loader 与客户端 clientLoader 完整解析
前端·react.js