离线应用程式食谱





朋友们,美好的一天!



我注意到了Jake Archibald出色的文章“离线食谱”的翻译,该文章专门介绍ServiceWorker API和Cache API的各种用例。



假定您熟悉这些技术的基础,因为将有大量的代码和少量的单词。



如果您不熟悉,请从MDN开始,然后再回来。这是另一篇关于服务人员的专门针对视觉的文章



没有更多的前言。



何时节省资源?



worker允许您独立于缓存处理请求,因此我们将单独考虑它们。



第一个问题是何时应该缓存资源?



作为依赖项安装时






安装程序事件是工作程序运行时发生的事件之一。此事件可用于准备处理其他事件。安装新工作程序后,旧工作程序将继续为该页面提供服务,因此处理install事件不应破坏它。通常



适用于缓存样式,图像,脚本,模板...,通常用于页面上使用的任何静态文件。



我们正在谈论那些文件,如果没有这些文件,应用程序将无法像本机应用程序的初始下载中包含的文件那样工作。



self.addEventListener('install', event => {
    event.waitUntil(
        caches.open('mysite-static-v3')
            .then(cache => cache.addAll([
                '/css/whatever-v3.css',
                '/css/imgs/sprites-v6.png',
                '/css/fonts/whatever-v8.woff',
                '/js/all-min-v4.js'
                //  ..
            ]))
    )
})


event.waitUntil接受承诺来确定安装的持续时间和结果。如果承诺被拒绝,则不会安装该工作程序。caches.open和cache.addAll返回承诺。如果其中一种资源不可用,则

对cache.addAll调用将被拒绝。



安装时不作为依赖项






这与前面的示例相似,但是在这种情况下,我们不等待安装完成,因此不会取消安装。



适用于当前不需要的大型资源,例如游戏后期版本的资源。



self.addEventListener('install', event => {
    event.waitUntil(
        caches.open('mygame-core-v1')
            .then(cache => {
                cache.addAll(
                    //  11-20
                )
                return cache.addAll(
                    //     1-10
                )
            })
    )
})


我们不会将11.-20级的cache.addAll承诺传递给event.waitUntil,因此,如果拒绝,游戏仍将离线运行。当然,您应该注意缓存第一级的可能问题,例如,如果出现故障,请再次尝试缓存。



可以在处理事件后停止工作程序,然后将其缓存到11-20级。这意味着这些级别将不会保存。将来,计划为工作人员添加后台加载界面以解决此问题,并下载大文件(例如电影)。



大约 每。:此接口于2018年底实现,被称为Background Fetch,但到目前为止它仅在Chrome和Opera中有效(根据CanIUse的占68%)。



激活后






适用于删除旧的缓存和迁移。



在安装了新的工作程序并停止了旧的工作程序之后,新的工作程序被激活,并且我们收到一个激活事件。这是替换资源和删除旧缓存的绝好机会。



self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys()
            .then(cacheNames => Promise.all(
                cacheNames.filter(cacheName => {
                    //  true, ,     ,
                    //  ,      
                }).map(cacheName => caches.delete(cacheName))
            ))
    )
})


在激活期间,其他事件(例如访存)将排队,因此从理论上讲,长时间的激活可能会阻塞页面。因此,仅将此阶段用于您无法与老工人一起完成的事情。



发生自定义事件时






适合当整个网站无法脱机。在这种情况下,我们使用户能够决定要缓存的内容。例如,Youtube视频,Wikipedia页面或Flickr上的图片库。



向用户提供“稍后阅读”或“保存”按钮。单击该按钮后,获取资源并将其写入缓存。



document.querySelector('.cache-article').addEventListener('click', event => {
    event.preventDefault()

    const id = event.target.dataset.id
    caches.open(`mysite-article ${id}`)
        .then(cache => fetch(`/get-article-urls?id=${id}`)
            .then(response => {
                // get-article-urls     JSON
                //  URL   
                return response.json()
            }).then(urls => cache.addAll(urls)))
})


页面上可以使用缓存界面,就像工作程序本身一样,因此我们无需调用后者即可节省资源。



收到回应时






适用于频繁更新的资源,例如用户的邮箱或文章内容。也适合化身等次要内容,但在这种情况下要小心。



如果请求的资源不在缓存中,我们将从网络上获取它,将其发送到客户端,然后将其写入缓存。



如果您请求多个URL(例如头像路径),请确保该地址不会溢出原始存储(原始-协议,主机和端口)-如果用户需要释放磁盘空间,则您不应是第一个。请注意删除不必要的资源。



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.open('mysite-dynamic')
            .then(cache => cache.match(event.request)
                .then(response => response || fetch(event.request)
                    .then(response => {
                        cache.put(event.request, response.clone())
                        return response
                    })))
    )
})


为了有效地使用内存,我们只读取一次响应主体。上面的示例使用clone方法创建响应的副本。这样做是为了同时向客户端发送响应并将其写入缓存。



在检查新颖性期间






适用于更新不需要最新版本的资源。这也可以适用于化身。



如果资源在缓存中,我们将使用它,但是在下一个请求上获得更新。



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.open('mysite-dynamic')
            .then(cache => cache.match(event.request)
                .then(response => {
                    const fetchPromise = fetch(event.request)
                        .then(networkResponse => {
                            cache.put(event.request, networkResponse.clone())
                            return networkResponse
                        })
                        return response || fetchPromise
                    }))
    )
})


当您收到推送通知时






Push API是对worker的抽象。它允许工作程序响应来自操作系统的消息而运行。此外,无论用户如何,都会发生这种情况(关闭浏览器选项卡时)。页面通常会向用户发送请求,以请求执行某些操作。



适用于依赖通知的内容,例如聊天消息,新闻提要,电子邮件。也用于同步内容,例如列表中的任务或日历中的标记。



结果是一个通知,单击该通知将打开相应的页面。但是,在发送通知之前节省资源非常重要。用户在收到通知时处于联机状态,但是在单击通知时很可能处于脱机状态,因此在那时内容可以脱机使用很重要。Twitter移动应用程序这样做有点错误。



没有网络连接,Twitter不会提供与通知相关的内容。但是,单击通知将其删除。不要那样做!



以下代码在发送通知之前更新缓存:



self.addEventListener('push', event => {
    if (event.data.text() === 'new-email') {
        event.waitUntil(
            caches.open('mysite-dynamic')
                .then(cache => fetch('/inbox.json')
                    .then(response => {
                        cache.put('/inbox.json', response.clone())
                        return response.json()
                    })).then(emails => {
                        registration.showNotification('New email', {
                            body: `From ${emails[0].from.name}`,
                            tag: 'new-email'
                        })
                    })
        )
    }
})

self.addEventListener('notificationclick', event => {
    if (event.notification.tag === 'new-email') {
        // ,   ,    /inbox/  ,
        // ,   
        new WindowClient('/inbox/')
    }
})


具有后台同步






背景同步是对工作人员的另一种抽象。它允许您请求一次性或定期后台数据同步。它也独立于用户。但是,也会向他发送许可请求。



适用于更新微不足道的资源,定期发送通知的频率太高,因此对于用户来说很烦,例如,社交网络中的新事件或新闻提要中的新文章。



self.addEventListener('sync', event => {
    if (event.id === 'update-leaderboard') {
        event.waitUntil(
            caches.open('mygame-dynamic')
                .then(cache => cache.add('/leaderboard.json'))
        )
    }
})


保存缓存



您的源提供了一定数量的可用空间。该空间在所有存储之间共享:本地和会话,索引数据库,文件系统,当然还有缓存。



存储大小不是固定的,并且会因设备和存储条件而异。您可以像这样检查它:



navigator.storageQuota.queryInfo('temporary').then(info => {
    console.log(info.quota)
    // : <  >
    console.log(info.usage)
    //  <    >
})


当此存储空间或该存储空间的大小达到限制时,将根据当前无法更改的某些规则清除该存储空间。



为了解决这个问题,提出了发送许可请求的接口(requestPersistent):



navigator.storage.requestPersistent().then(granted => {
    if (granted) {
        // ,       
    }
})


当然,用户必须为此授予权限。用户必须是此过程的一部分。如果用户设备上的内存已满,并且删除次要数据不能解决问题,则用户必须确定要保留和删除哪些数据。



为此,操作系统必须将浏览器存储区视为单独的项。



回答请求



缓存多少资源都没有关系,工作人员不会使用它,直到您告诉他什么时候使用什么。这是一些处理请求的模板。



现金支付






适用于页面当前版本的任何静态资源。您必须在工作程序设置阶段缓存这些资源,以便能够响应请求发送它们。



self.addEventListener('fetch', event => {
    //     ,
    //      
    event.respondWith(caches.match(event.request))
})


仅网络






适用于无法缓存的资源,例如分析数据或非GET请求。



self.addEventListener('fetch', event => {
    event.respondWith(fetch(event.request))
    //     event.respondWith
    //      
})


首先是高速缓存,然后是发生故障时的网络






适用于处理离线应用程序中的大多数请求。



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => response || fetch(event.request))
    )
})


保存的资源从缓存中返回,未保存的资源从网络中返回。



谁有时间他就吃






适用于小型资源,以追求低存储设备的更好性能。



旧的硬盘驱动器,防病毒软件和快速的Internet连接相结合,可以使从网络中获取数据的速度比从缓存中获取数据的速度更快。但是,在将数据存储在用户设备上的同时从网络检索数据会浪费资源。



// Promise.race   ,   
//       .
//   
const promiseAny = promises => new Promise((resolve, reject) => {
    //  promises   
    promises = promises.map(p => Promise.resolve(p))
    //   ,    
    promises.forEach(p => p.then(resolve))
    //     ,   
    promises.reduce((a, b) => a.catch(() => b))
        .catch(() => reject(Error('  ')))
})

self.addEventListener('fetch', event => {
    event.respondWith(
        promiseAny([
            caches.match(event.request),
            fetch(event.request)
        ])
    )
})


大约 Lane:现在您可以使用Promise.allSettled来实现此目的,但是其浏览器支持率为80%:-20%的用户可能太多了。



首先是网络,然后在出现故障时缓存






适用于经常更新且不会影响网站当前版本的资源,例如,文章,头像,社交网络上的新闻提要,播放器评分等。



这意味着您正在向在线用户提供新内容,向离线用户提供旧内容。如果来自网络的资源请求成功,则可能应该更新缓存。



这种方法有一个缺点。如果用户遇到连接问题或速度很慢,则他必须等待请求完成或失败,而不是立即从缓存中获取内容。此等待时间可能很长,导致糟糕的用户体验。



self.addEventListener('fetch', event => {
    event.respondWith(
        fetch(event.request).catch(() => caches.match(event.request))
    )
})


首先是缓存,然后是网络






适用于频繁更新的资源。



这要求页面发送两个请求,一个用于高速缓存,一个用于网络。这个想法是从缓存中返回数据,然后在从网络接收数据时刷新它。



有时您可以在收到新数据时替换当前数据(例如,播放器的等级),但这对于大块内容来说是有问题的。这可能导致用户当前正在阅读或与之交互的内容消失。



Twitter在保持滚动的同时在现有内容之上添加了新内容:用户在屏幕顶部看到新推文的通知。这归功于内容的线性顺序。我复制了此模板,以尽快显示来自缓存的内容,并添加从网络获取的新内容。



页面上的代码:



const networkDataReceived = false

startSpinner()

//   
const networkUpdate = fetch('/data.json')
    .then(response => response.json())
        .then(data => {
            networkDataReceived = true
            updatePage(data)
        })

//   
caches.match('/data.json')
    .then(response => {
        if (!response) throw Error(' ')
        return response.json()
    }).then(data => {
        //      
        if (!networkDataReceived) {
            updatePage(data)
        }
    }).catch(() => {
        //      ,  -   
        return networkUpdate
    }).catch(showErrorMessage).then(stopSpinner)


工作人员代码:



我们访问网络并更新缓存。



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.open('mysite-dynamic')
            .then(cache => fetch(event.request)
                .then(response => {
                    cache.put(event.request, response.clone())
                    return response
                }))
    )
})


安全网






如果尝试从缓存和网络获取资源失败,则必须进行回退。



适用于占位符(用虚拟图像替换图像),POST请求失败,“脱机时不可用”页面。



self.addEventListener('fetch', event => {
    event.respondWith(
        //     
        //   ,   
        caches.match(event.request)
            .then(response => response || fetch(event.request))
            .catch(() => {
                //    ,  
                return caches.match('/offline.html')
                //       
                //    URL   
            })
    )
})


如果您的页面提交了电子邮件,则工作人员可以在提交之前将其保存到索引数据库中,并通知页面提交失败,但是电子邮件已保存。



在工作端创建标记






适用于在服务器端呈现且无法缓存的页面。



服务器端页面渲染是一个非常快的过程,但是它使动态内容在缓存中的存储变得毫无意义,因为每个渲染可能有所不同。如果您的页面是由工作人员控制的,则您可以请求资源并在那里渲染页面。



import './templating-engine.js'

self.addEventListener('fetch', event => {
    const requestURL = new URL(event.request.url)

    event.respondWith(
        Promise.all([
            caches.match('/article-template.html')
                .then(response => response.text()),
            caches.match(`${requestURL.path}.json`)
                .then(response => response.json())
        ]).then(responses => {
            const template = responses[0]
            const data = responses[1]

            return new Response(renderTemplate(template, data), {
                headers: {
                    'Content-Type': 'text/html'
                }
            })
        })
    )
})


一起


您不必局限于一个模板。您很可能必须根据要求将它们组合在一起。例如,训练有素的使用以下方法:



  • 持久性UI元素的工作程序设置缓存
  • 在服务器响应上缓存Flickr图像和数据
  • 从高速缓存中检索数据,以及从网络中获取大多数请求失败时的数据
  • 从缓存中检索资源,然后从Web检索Flick搜索结果的资源


只需查看请求并决定如何处理它:



self.addEventListener('fetch', event => {
    //  URL
    const requestURL = new URL(event.request.url)

    //       
    if (requestURL.hostname === 'api.example.com') {
        event.respondWith(/*    */)
        return
    }

    //    
    if (requestURL.origin === location.origin) {
        //   
        if (/^\/article\//.test(requestURL.pathname)) {
            event.respondWith(/*    */)
            return
        }
        if (/\.webp$/.test(requestURL.pathname)) {
            event.respondWith(/*    */)
            return
        }
        if (request.method == 'POST') {
            event.respondWith(/*     */)
            return
        }
        if (/cheese/.test(requestURL.pathname)) {
            event.respondWith(
                // . .:    -   ?
                new Response('Flagrant cheese error', {
                //    
                status: 512
                })
            )
            return
        }
    }

    //  
    event.respondWith(
        caches.match(event.request)
            .then(response => response || fetch(event.request))
    )
})


希望本文对您有所帮助。感谢您的关注。



All Articles