PWA 是一种 Web 应用的新范式,它将 Web 应用和原生应用的体验结合在一起。PWA 可以让 Web 应用离线缓存、全屏模式、桌面快捷方式等,提供与原生应用类似的用户体验。此外,PWA 还能够通过 Service Worker 技术实现增量更新,提高 Web 应用的性能。
PWA 的优势不仅在于它可以提供类似于原生应用的体验,更重要的是它可以通过一系列技术手段提高 Web 应用的性能。PWA 可以在应用启动时预加载资源,这可以大大缩短应用的启动时间。PWA 还支持离线缓存,这可以让用户在没有网络连接的情况下继续使用应用,提高用户的满意度。
PWA 还可以使用本地通知、推送通知等功能,让用户更方便地接收到应用的消息和提醒,提高应用的互动性和用户留存率。此外,PWA 还支持桌面快捷方式,用户可以在桌面上直接启动应用,减少寻找应用的时间,提高用户的使用频率。
在使用PWA时,开发者需要注意一些细节,以确保应用的性能和用户体验。首先,需要考虑应用的加载速度,保证应用能够在最短时间内启动。其次,需要优化应用的缓存策略,确保用户能够在没有网络连接的情况下继续使用应用。最后,需要注意用户体验,尽可能提供与原生应用相似的体验,以吸引更多的用户。
以下是一个简单的 PWA 应用的 demo,它是一个待办事项应用,用户可以添加和管理自己的待办事项列表,并在需要时将它们标记为已完成。
首先,在 index.html 文件中,我们需要添加一个 manifest.json 文件的链接,以及一个 Service Worker 的注册脚本:
<!DOCTYPE html> <html> <head> <title>PWA Todo App</title> <link rel="manifest" href="/manifest.json"> </head> <body> <h1>Todo List</h1> <ul id="todo-list"></ul> <form> <input type="text" id="new-todo" placeholder="Add a new todo..."> <button type="submit">Add</button> </form> <script src="/sw.js"></script> <script src="/app.js"></script> </body> </html>
接下来,在 manifest.json 文件中,我们可以定义应用的名称、图标、主题色等信息:
{ "name": "PWA Todo App", "short_name": "Todo App", "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" } ], "start_url": "/index.html", "display": "standalone", "background_color": "#fff", "theme_color": "#3367D6" }
然后,在 app.js 文件中,我们可以实现待办事项的添加、删除和标记为已完成等功能,同时使用 IndexedDB 存储数据:
const todoList = document.getElementById('todo-list'); const newTodoInput = document.getElementById('new-todo'); let db; window.addEventListener('load', async () => { if ('serviceWorker' in navigator) { try { await navigator.serviceWorker.register('/sw.js'); } catch (e) { console.log('SW registration failed'); } } const request = indexedDB.open('todo-db', 1); request.onupgradeneeded = event => { db = event.target.result; db.createObjectStore('todos', { keyPath: 'id', autoIncrement: true }); }; request.onsuccess = event => { db = event.target.result; loadTodos(); }; async function loadTodos() { const transaction = db.transaction(['todos'], 'readonly'); const store = transaction.objectStore('todos'); const todos = await store.getAll(); for (const todo of todos) { addTodoToList(todo); } } function addTodoToList(todo) { const li = document.createElement('li'); li.dataset.id = todo.id; li.innerText = todo.title; if (todo.completed) { li.classList.add('completed'); } li.addEventListener('click', event => { const completed = !li.classList.contains('completed'); updateTodoStatus(todo.id, completed); li.classList.toggle('completed'); }); todoList.appendChild(li); } function addNewTodo() { const title = newTodoInput.value; if (title.trim() === '') { return; } const transaction = db.transaction(['todos'], 'readwrite'); const store = transaction.objectStore('todos'); const request = store.add({ title, completed: false }); request.onsuccess = event => { const todo = { id: event.target.result, title, completed: false }; addTodoToList(todo); newTodoInput.value = ''; }; } function deleteTodo(id) { const transaction = db.transaction(['todos'], 'readwrite'); const store = transaction.objectStore('todos'); store.delete(id); } function updateTodoStatus(id, completed) { const transaction = db.transaction(['todos'], 'readwrite'); const store = transaction.objectStore('todos'); store.get(id).onsuccess = event => { const todo = event.target.result; todo.completed = completed; store.put(todo); }; } document.querySelector('form').addEventListener('submit', event => { event.preventDefault(); addNewTodo(); }); todoList.addEventListener('contextmenu', event => { event.preventDefault(); const li = event.target.closest('li'); if (li) { deleteTodo(parseInt(li.dataset.id)); li.remove(); } });
最后,在 sw.js 文件中,我们可以缓存应用所需的资源,以便在离线时仍可访问:
const CACHE_NAME = 'todo-cache-v1'; const urlsToCache = [ '/', '/index.html', '/app.js', '/manifest.json', '/icons/icon-72x72.png', '/icons/icon-96x96.png', '/icons/icon-128x128.png', '/icons/icon-144x144.png', '/icons/icon-152x152.png', '/icons/icon-192x192.png', '/icons/icon-384x384.png', '/icons/icon-512x512.png' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(urlsToCache)) ); }); self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => { if (response) { return response; } return fetch(event.request); }) ); });
以上就是一个简单的 PWA 应用的 demo,它能够在离线时正常工作,并且可以像本地应用一样被用户添加到主屏幕上。
我们再来详细解释一下上述代码的逻辑:
首先,我们在 index.html 中定义了应用的基本结构和样式,包括一个输入框用于添加新任务,以及一个任务列表。
然后,我们在 app.js 中编写了 PWA 应用的核心逻辑,它包括:
1.判断浏览器是否支持 Service Worker,并注册 sw.js。
if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js'); }); }
2.使用 IndexedDB 存储任务列表。
const request = indexedDB.open('todo-db', 1); request.onupgradeneeded = event => { const db = event.target.result; const store = db.createObjectStore('todos', { keyPath: 'id', autoIncrement: true }); store.createIndex('completed', 'completed'); }; request.onsuccess = event => { db = event.target.result; loadTodos(); }; async function loadTodos() { const transaction = db.transaction(['todos'], 'readonly'); const store = transaction.objectStore('todos'); const todos = await store.getAll(); for (const todo of todos) { addTodoToList(todo); } }
3.将任务添加到任务列表中。
function addTodoToList(todo) { const li = document.createElement('li'); li.dataset.id = todo.id; li.innerText = todo.title; if (todo.completed) { li.classList.add('completed'); } li.addEventListener('click', event => { const completed = !li.classList.contains('completed'); updateTodoStatus(todo.id, completed); li.classList.toggle('completed'); }); todoList.appendChild(li); }
4.从任务列表中删除任务。
function deleteTodo(id) { const transaction = db.transaction(['todos'], 'readwrite'); const store = transaction.objectStore('todos'); store.delete(id); }
5.更新任务的完成状态。
function updateTodoStatus(id, completed) { const transaction = db.transaction(['todos'], 'readwrite'); const store = transaction.objectStore('todos'); store.get(id).onsuccess = event => { const todo = event.target.result; todo.completed = completed; store.put(todo); }; }
6.使用 IndexedDB 存储新任务,并将其添加到任务列表中。
function addNewTodo() { const title = newTodoInput.value; if (title.trim() === '') { return; } const transaction = db.transaction(['todos'], 'readwrite'); const store = transaction.objectStore('todos'); const request = store.add({ title, completed: false }); request.onsuccess = event => { const todo = { id: event.target.result, title, completed: false }; addTodoToList(todo); newTodoInput.value = ''; }; }
7.监听表单提交事件,并调用 addNewTodo 函数。
{ "name": "PWA Todo App", "short_name": "Todo App", "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" } ], "start_url": "/index.html", "display": "standalone", "background_color": "#fff", "theme_color": "#3367D6" }0
8.监听任务列表中任务的右键点击事件,并调用 deleteTodo 函数。
最后,我们在 sw.js 中编写 Service Worker 的逻辑,包括:
1.缓存应用的静态资源。
{ "name": "PWA Todo App", "short_name": "Todo App", "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" } ], "start_url": "/index.html", "display": "standalone", "background_color": "#fff", "theme_color": "#3367D6" }1
2.从缓存中读取静态资源,如果没有则从网络中获取。
{ "name": "PWA Todo App", "short_name": "Todo App", "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" } ], "start_url": "/index.html", "display": "standalone", "background_color": "#fff", "theme_color": "#3367D6" }2
3.删除旧的缓存,保留最新的缓存。
{ "name": "PWA Todo App", "short_name": "Todo App", "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" } ], "start_url": "/index.html", "display": "standalone", "background_color": "#fff", "theme_color": "#3367D6" }3
到此为止,我们的 PWA 应用就完成了。用户可以使用它添加、完成和删除任务,应用还具有离线缓存、添加到主屏幕等 PWA 特性,大大提升了应用的性能和用户体验。
如果你想体验一下这个应用,可以将上述代码保存到本地,然后在浏览器中打开 index.html 文件即可。
综上所述,PWA 是一种非常有前途的技术,可以提高 Web 应用的性能和用户体验。使用 PWA 可以让 Web 应用更像原生应用,更加快速、可靠和易用。在未来,PWA 将会成为 Web 应用的重要发展方向,带来更多的创新和变革。
本文系前端老赵独家发表,未经许可,不得转载。
评论列表
发表评论