我迁移到 Hugo 的最后一步是实现一个渐进式 Web 应用(简称 PWA)。我想要实现 PWA 的原因有几个:
它允许用户(并提示他们)将网站作为应用程序安装到他们的移动设备上。
将来,我可以使用推送通知来告知用户新内容。
它支持离线模式,因此用户在互联网中断时仍然可以浏览和阅读。
它缓存内容以提供更快、响应更灵敏的体验。
终身应用开发者
如果您对这些功能感兴趣,那么 PWA 可能就是您正在寻找的!
什么是 PWA?
PWA最初是为移动设备设计的(之所以说最初,是因为现在已经支持桌面 PWA),它是一种使用 HTML、CSS 和 JavaScript 等传统 Web 技术构建的特殊移动应用。所有现代浏览器都支持 PWA。它们之所以被称为“渐进式”,是因为它们本质上就像浏览器中的普通网页一样,但一旦安装,就可以逐步添加新功能,例如与硬件交互和管理推送通知。PWA 的最低要求是包含清单和一个 Service Worker。
宣言
这是终身开发者的宣言。
{"name":"Developer for Life","short_name":"dev4life","icons":[{"src":"/appicons/favicon-128.png","sizes":"128x128","type":"image/png"},{"src":"/appicons/apple-touch-icon-144x144.png","sizes":"144x144","type":"image/png"},{"src":"/appicons/apple-touch-icon-152x152.png","sizes":"152x152","type":"image/png"},{"src":"/appicons/favicon-196x196.png","sizes":"196x196","type":"image/png"},{"src":"/appicons/splash.png","sizes":"512x512","type":"image/png"}],"start_url":"/","display":"standalone","orientation":"portrait","background_color":"#FFFFFF","theme_color":"#FFFFFF"}
它包含一些基本信息,例如应用安装时显示的图标、使用的颜色、起始页以及默认方向。它安装在您网站的根目录下。此链接将下载 Developer for Life 的清单:manifest.json。
PWA 最关键的部分是相关的Service Worker。这是一个特殊的 JavaScript 应用,由浏览器或移动设备注册,用于管理网站。出于安全考虑,Service Worker 的作用域仅限于其所在的域。您无法为 Service Worker 引用其他域的 JavaScript,并且 Service Worker 被阻止直接修改页面。相反,它们充当代理来帮助编组请求。如果您将 Service Worker 放置在 ,mydomain.com/serviceworker/code.js它将只能访问在其下提供服务的页面mydomain.com/serviceworker。因此,它通常安装在根目录下。
我创建了一个部分模板,并在页脚中引用。它包含以下代码:
if ('serviceWorker'innavigator){navigator.serviceWorker.register('/sw.js',{scope:'/'}).then(()=>{console.info('Developer for Life Service Worker Registered');},err=>console.error("Developer for Life Service Worker registration failed: ",err));navigator.serviceWorker.ready.then(()=>{console.info('Developer for Life Service Worker Ready');});}
JavaScript 会注册 Service Worker 的源代码 ( sw.js ),并在准备就绪时发出控制台消息。我实现的 Service Worker 主要充当网络代理。它主要完成以下几项任务:
# simple wget-snippet or do it manually# cd /your-projects-root-directory/
wget https://raw.githubusercontent.com/wildhaber/offline-first-sw/master/sw.js
并使用以下代码片段启动 Service Worker:
<script>
if('serviceWorker' in navigator) {/** * Define if <link rel='next|prev|prefetch'> should * be preloaded when accessing this page */constPREFETCH=true;/** * Define which link-rel's should be preloaded if enabled. */constPREFETCH_LINK_RELS=['index','next','prev','prefetch'];/** * prefetchCache */functionprefetchCache(){
constCACHE_VERSIONS={assets:'assets-v'+CACHE_VERSION,content:'content-v'+CACHE_VERSION,offline:'offline-v'+CACHE_VERSION,notFound:'404-v'+CACHE_VERSION,};// Define MAX_TTL's in SECONDS for specific file extensionsconstMAX_TTL={'/':3600,html:43200,json:43200,js:86400,css:86400,};
functioninstallServiceWorker(){returnPromise.all([caches.open(CACHE_VERSIONS.assets).then((cache)=>{returncache.addAll(BASE_CACHE_FILES);},err=>console.error(`Error with ${CACHE_VERSIONS.assets}`,err)),caches.open(CACHE_VERSIONS.offline).then((cache)=>{returncache.addAll(OFFLINE_CACHE_FILES);},err=>console.error(`Error with ${CACHE_VERSIONS.offline}`,err)),caches.open(CACHE_VERSIONS.notFound).then((cache)=>{returncache.addAll(NOT_FOUND_CACHE_FILES);},err=>console.error(`Error with ${CACHE_VERSIONS.notFound}`,err))]).then(()=>{returnself.skipWaiting();},err=>console.error("Error with installation: ",err));}
cleanupLegacyCache当检测到新版本时,会调用该方法。它会查找较旧的缓存并将其删除。
functioncleanupLegacyCache(){letcurrentCaches=Object.keys(CACHE_VERSIONS).map((key)=>{returnCACHE_VERSIONS[key];});returnnewPromise((resolve,reject)=>{caches.keys().then((keys)=>{returnlegacyKeys=keys.filter((key)=>{return!~currentCaches.indexOf(key);});}).then((legacy)=>{if (legacy.length){Promise.all(legacy.map((legacyKey)=>{returncaches.delete(legacyKey)})).then(()=>{resolve()}).catch((err)=>{console.error("Error in legacy cleanup: ",err);reject(err);});}else{resolve();}}).catch((err)=>{console.error("Error in legacy cleanup: ",err);reject(err);});});}
最复杂的代码是 Service Worker 的核心。该应用基本上会拦截fetch浏览器加载内容的事件,并将其替换为 JavaScript 代理。以下伪代码解释了它的工作原理。
Intercept request for content
Is content in cache?
Yes, is content expired?
Yes, fetch fresh content.
If fetch was successful, store it in cache and return it
If fetch was not successful, just serve cached content
No, serve cached content
No, fetch the content for the first time
If fetch had OK status, store in cache and return
Otherwise show and store "not found" page
If fetch throws exception, show offline page
Done.