Service Worker和Fetch API开发离线应用

Service Worker和Fetch API开发离线应用

🗨

富离线应用程序、周期性背景同步、通知推出—以前这些功能只能在本地应用程序中实现,现在已经被引入到互联网世界中。Service Worker提供一些这些功能所依赖的技术基础。

什么是Service Worker?

Service Worker是在浏览器中运行的一段后台脚本,它与Web页面分离,开启一个不需要页面或与用户交互的特性。将来这些特性将包括消息推出与背景同步等等,但目前第一个特性是能够拦截与处理网络请求,包括以可编程的方式管理响应缓存。

该API之所以令人惊叹是因为它支持离线应用,给予开发者能够对于离线应用程序进行完全控制的能力。

在Service Worker被推出之前存在一个被称为App Cache的用于实现离线应用的API。该API所存在的主要问题是虽然它可以被运行在单页面Web应用程序中,但不能被很好地被运行在多页面环境中。Service Worker完美地解决了这些问题。

使用Service Worker时必须了解下述注意事项:

  • 它是一个JavaScript Worker,所以不能直接访问页面,但是可以通过postMessage方法与页面进行通信。
  • Service Worker是一个网络代理,允许开发者对从页面上发出的请求进行控制。
  • 当不被需要时它停止运行,直到下一次被需要时重新启动。所以在一个Service Worker的onfetch事件处理函数与onmessage事件处理函数中不存在全局状态。如果你有一些需要保持且在Service Worker下一次重新启动时重新使用的信息,需要将其保存在IndexedDB数据库中。
  • Service Worker充分利用Promise,所以如果你还不了解Promise API的话,需要首先补充这方面知识。

Service Worker的生命周期

Service Worker具有一个完全独立于页面的生命周期。

为了在网站中安装一个Service Worker,首先需要在页面上的JavaScript脚本代码中对其进行注册。注册一个Service Worker将会使浏览器在后台启动Service Worker安装过程。

通常在安装过程中,开发者想要缓存一些静态资源。如果所有文件都被成功缓存,Service Worker将被成功安装,如果任何文件下载或缓存失败,安装将失败,Service Worker也不会被成功启动。如果发生这种情况,不必担心,它将在下一次被重试。但这意味着如果安装Service Worker,你需要确保这些静态资源已经在缓存中(否则仍将失败)。

如果安装成功,将开始激活过程,这是一个对于老的缓存进行管理的最佳机会。我们将在后文中对此进行介绍。

在激活过程之后,Service Worker将会对处于其作用范围之内的所有页面进行控制,尽管首次注册Service Worker的页面将等到下次被加载时才会被控制。一旦一个Servie Worker发挥作用,它将处于两种状态之一:为了节约内存而暂停,或当一个网络请求被产生或接收到页面发出消息时执行fetch或message事件处理函数。

Sevice Worker首次安装时其生命周期的简化版如下图所示:

在开始之前

请首先从https://github.com/coonsta/cache-polyfill或从我们的网站中获取caches插件。

该插件添加目前Chrome浏览器中尚未支持的CacheStorage.match、Cache.add与Cache.addAll方法。

将下载目录中的serviceworker-cache-polyfill.js文件复制到网站中并且在一个Service Worker文件中使用importScripts方法引用该插件,代码如下所示:

importScripts("serviceworker-cache-polyfill.js");

需要使用HTTPS

尽管在部署期间可以通过localhost来使用Service Worker,但是在网络中的某个公开网站上正式运行时需要安装及使用HTTPS协议。

如果使用Service Worker,开发者可以拦截连接、修改与过滤响应,非常强大。开发者可以只在使用HTTPS协议的页面上注册Service Worker,这样可以确保浏览器接收到的Service Worker在网络上没有被篡改。

如果开发者想要在服务器中使用HTTPS协议,需要为服务器获取并安装一个TLS证书。

使用Service Worker

如何注册与安装Service Worker

为了安装一个Service Worker,开发者需要在页面上注册一个Service Worker以启动安装过程。这个注册通知浏览器Service Worker的JavaScript文件位置。

if("serviceWorker" in navigator){
    navigator.serviceWorker.register("/sw.js").then(function(registration)
    {
        //注册成功
        console.log("Service Worker注册成功,作用范围为:"+registration.scope);
    }).catch(function(err){
        console.log("注册失败:"+err);
    });
}
else
    console.log("浏览器不支持Service Worker");

上述代码检查浏览器是否支持Service API,如果支持,注册/sw.js文件中定义的Service Worker。

可以在每次页面加载时注册Service Worker,浏览器将自动检测Service Worker是否被注册,如果没有则注册该Service Worker。

一个值得注意的地方是Service Worker文件的位置。在上述代码中Service Worker文件位于网站根目录,这表示Service Worker的作用范围为整个域。换句话说,这个Servce Worker将接收到来自整个域的fetch事件。如果我们注册/example/sw.js文件中定义的Service Worker,该Service Worker将只接收到来自于以/example/这个URL地址开头的所有页面(例如/example/page1/,/example/page2/)中的fetch事件。

当Serive Worker被安装成功后,可以从chrome://seriveworker-internals页面中查看该Service Worker,如下图所示。

如果只想了解一个Service Worker的生命周期,这个查看页面将会是非常有用的。

你将发现在一个隐藏页面中测试Service Worker的方法是非常有用的,因为你可以在不为人知的情况下关闭与重新打开这个隐藏页面。来自于一个隐藏页面中的注册与缓存将在该页面被关闭时被立即清除。

Service Worker的安装步骤

接下来让我们看一个Service Worker的脚本代码,在脚本代码中可以处理安装事件。

作为最基础的示例,我们定义一个install事件回调函数并决定需要缓存哪些文件。

//我们想要缓存的文件
var urlsToCache=[
    '/',
    '/styles/main.css',
    '/script/main.js'
];
//为安装设置回调函数
self.addEventListener('install',function(event){
    //执行安装过程
});

在我们的回调函数中,我们需要执行以下步骤:

  1. 打开一个缓存
  2. 缓存我们的文件
  3. 确认是否所有的请求是否被缓存
importScripts("serviceworker-cache-polyfill.js");
var CACHE_NAME="my-site-cache-v1";
//我们想要缓存的文件
var urlsToCache=[
    '/',
    '/styles/main.css',
    '/script/main.js'
];

//为安装设置回调函数
self.addEventListener("install",function(event){
    //执行安装过程
    event.waitUntil(
        caches.open(CACHE_NAME).then(function(cache){
            console.log("被打开的缓存");
            return cache.addAll(urlsToCache);
        })
    );
});

此处你可以看见我们使用我们想要的缓存名来调用caches.open方法,在此之后我们调用cache.addAll方法并且传入我们的文件数组。event.waitUntil方法采用一个Promise对象作为参数并且被用来观察安装时间以及是否成功。

如果所有的文件被成功缓存,Service Worker将被成功安装。如果任何文件被下载失败,将导致安装失败。这可以使你确保所有文件被成功缓存,但也意味着你必须对需要在安装时进行缓存的所有文件进行小心确认。文件越多缓存失败,从而导致Service Worker安装失败的可能性也越大。

此处只是一个示例,你可以在install事件中执行其他处理或避免设置install事件处理函数。

如何缓存与返回请求

现在你已经安装了一个Service Worker,你可能想要返回一个被缓存的响应。

在一个Service Worker被安装之后,用户可能被导航到另一个页面或当前页面被刷新。Service Worker将要开始接收fetch事件,一个示例如下所示:

self.addEventListener("fetch",function(event){
    event.respondWith(
        caches.match(event.request).then(function(response){
            //缓存响应
            if(response)
                return response;
            return fetch(event.request);
        })
    )
});

此处我们定义了fetch事件回调函数,并且在其中定义了event.respondWith方法,我们在caches.match方法中传入一个Promise对象,catches.match方法将要根据请求查找是否在Service Worker创建的任何缓存中存在响应结果。

如果已存在匹配的响应结果,将该缓存值返回,否则返回一个fetch方法的调用结果,该方法将创建一个网络请求并返回其响应结果。这是一个简单示例,使用安装时指定的所有缓存文件。

如果我们想要继续缓存新的请求,我们可以通过如下所示的方法对响应进行处理并将其添加到缓存:

self.addEventListener("fetch",function(event){
    event.respondWith(
        caches.match(event.request).then(function(response){
            //缓存响应
            if(response)
                return response;
            //重要:复制请求。一个请求是一个流,只能被消耗一次。
            //因为我们在获取与缓存请求时已消耗该请求,所以我们需要复制该请求
            var fetchRequest=event.request.close();
            return fetch(fetchRequest).then(
                function(response){
                    //检查我们是否获取到一个有效响应
                    if(!response||response.status!=200||
                    response.type!=="basic"){
                        return response;
                    }
                    //重要:复制响应。一个响应是一个流
                    //因为我们想在缓存消耗响应的同时让浏览器消耗缓存,
                    //我们需要对其进行复制已得到两个流
                    var responseToCache=response.close();
                    caches.open(CACHE_NAME)
                    .then(function(cache){
                        caches.put(event.request,responseToCache);
                   });
                   return response;
            });
        })
    );
});

我们在上述代码中所做的处理为:

  1. 对fetch方法添加一个then回调方法。
  2. 一旦我们获取到响应,我们执行以下检查:
    1. 确认响应有效
    2. 检查响应状态是否为200
    3. 确认响应类型为basic,它表示请求来自我们的域,它也表示来自第三方的响应不会被缓存。
  3. 如果检查通过,我们对响应进行复制。原因是因为响应是一个流,只能被消耗一次。因为我们想要把响应返回给浏览器使用,同时也要将其缓存在缓存中,所以我们需要对其进行复制,以便将一个送往浏览器,另一个送往缓存。

如何更新Service Worker

随着开发的不断进行,我们可能想要更新Service Worker。这时,你需要执行以下步骤:

  1. 更新Service Worker JavaScript文件。当用户进行网站,浏览器尝试从后台下载定义Service Worker的脚本文件。如果已下载的文件与浏览器中的文件存在哪怕有一个字节的差异,这个Service Worker被视为一个新的Service Worker。
  2. 新的Service Worker将会被重新安装。
  3. 这时旧的Service Worker仍然在控制页面,所以新的Service Worker将处于等待状态。
  4. 当当前页面被用户关闭时,旧的Service Worker将会被杀死,新的Service Worker取而代之。
  5. 一旦新的Service Worker取代旧的Service Worker,它的active(激活)事件被触发。

一个激活事件回调函数中可以执行的常规处理是对缓存进行管理。你之所以想要在激活事件回调函数中进行这个处理是因为如果在安装时删除老的缓存,正在管理当前页面的所有旧的Service Worker将突然变得不能获取来自该缓存的文件。

假如我们有一个被称为“my-site-cache-v1”的缓存,我们想要将其分离为两个缓存,一个给一部分页面使用,另一个给另一部分页面使用。这意味着在安装过程中我们需要创建两个缓存:“cache-v1”与“cache-v2”。在激活步骤中我们想要删除我们原来的“my-site-cache-v1”缓存。

下述代码展示如何遍历Service Worker中管理的所有缓存并且清除不在当前缓存清单中的所有缓存。

self.addEventListener("activate",function(event){
    var cacheWhitelist=["cache-v1","cache-v2"];
    event.waitUntil(
        caches.keys().then(function(cacheNames){
            return Promise.all(
                cacheNames.map(function(cacheName){
                    if(cacheWhitelist.indexOf(cacheName)===-1){
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

注意事项:

目前Service Worker API为刚推出阶段,还存在很多问题。希望可以很快得到解决。目前需要对此引起注意:

  1. 如果安装失败,我们获取太多消息

    如果一个Service Worker被注册,但是并不出现在chrome://seriveworker-internals页面中,很可能是由于被抛出一个错误而导致安装失败,或者被传递给event.waitUntil方法的Promise对象所拒绝。

    为了避免这一问题,可以在chrome://seriveworker-internals页面中勾选“ Open DevTools window and pause JavaScript execution on Service Worker startup for debugging.”选项并且从Service Worker脚本代码中的install事件回调函数的开始处进行调试。

 


 

fetch()方法介绍

告别XMLHttpRequest

与XMLHttpRequest(XHR)类似,fetch()方法允许你发出AJAX请求。区别在于Fetch API使用Promise,因此是一种简洁明了的API,比XMLHttpRequest更加简单易用。

从Chrome 40开始,Fetch API可以被利用在Service Worker全局作用范围中,自Chrome 42开始,可以被利用在页面中。

如果你还不了解Promise,需要首先补充这方面知识。

基本的Fetch请求

让我们首先来比较一个XMLHttpRequest使用示例与fetch方法的使用示例。该示例向服务器端发出请求,得到响应并使用JSON将其解析。

XMLHttpRequest

一个XMLHttpRequest需要设置两个事件回调函数,一个用于获取数据成功时调用,另一个用于获取数据失败时调用,以及一个open()方法调用及一个send()方法调用。

function reqListener(){
    var data=JSON.parse(this.responseText);
    console.log(data);
}
function reqError(err){
    console.log("Fetch错误:"+err);
}
var oReq=new XMLHttpRequest();
oReq.onload=reqListener;
oReq.onerror=reqError;
oReq.open("get","/students.json",true);
oReq.send();
演示 -  http://www.html5online.com.cn/articles/2015051302.html

Fetch

一个fetch()方法的使用代码示例如下所示:

fetch("/students.json")
.then(
    function(response){
        if(response.status!==200){
            console.log("存在一个问题,状态码为:"+response.status);
            return;
        }
        //检查响应文本
        response.json().then(function(data){
            console.log(data);
        });
    }
)
.catch(function(err){
    console.log("Fetch错误:"+err);
});
演示 - http://www.html5online.com.cn/articles/2015051303.html

在上面这个示例中,我们在使用JSON解析响应前首先检查响应状态码是否为200。

一个fetch()请求的响应为一个Stream对象,这表示当我们调用json()方法,将返回一个Promise对象,因为流的读取将为一个异步过程。

响应元数据

在上一个示例中我们检查了Response对象的状态码,同时展示了如何使用JSON解析响应数据。我们可能想要访问响应头等元数据,代码如下所示:

fetch("/students.json")
.then(
    function(response){
        console.log(response.headers.get('Content-Type'));
        console.log(response.headers.get('Date'));
        console.log(response.status);
        console.log(response.statusText);
        console.log(response.type);
        console.log(response.url);
    }
)
演示 - http://www.html5online.com.cn/articles/2015051304.html

响应类型

当我们发出一个fetch请求时,响应类型将会为以下几种之一:“basic”、“cors”或“opaque”。这些类型标识资源来源,提示你应该怎样对待响应流。

当请求的资源在相同域中时,响应类型为“basic”,不严格限定你如何处理这些资源。

如果请求的资源在其他域中,将返回一个CORS响应头。响应类型为“cors”。“cors”响应限定了你只能在响应头中看见“Cache-Control”、“Content-Language”、“Content-Type”、“Expires”、“Last-Modified”以及“Progma”。

一个“opaque”响应针对的是访问的资源位于不同域中,但没有返回CORS响应头的场合。如果响应类型为“opaque”,我们将不能查看数据,也不能查看响应状态,也就是说我们不能检查请求成功与否。目前为止不能在页面脚本中请求其他域中的资源。

你可以为fetch请求定义一个模式以确保请求有效。可以定义的模式如下所示:

  • "same-origin":只在请求同域中资源时成功,其他请求将被拒绝。
  • "cors":允许请求同域及返回CORS响应头的域中的资源。
  • "cors-with-forced-preflight":在发出实际请求前执行preflight检查。
  • "no-cors"针对的是向其他不返回CORS响应头的域中的资源发出的请求(响应类型为“opaque”),但如前所述,目前在页面脚本代码中不起作用。

为了定义模式,在fetch方法的第二个参数中添加选项对象并在该对象中定义模式:

fetch("http://www.html5online.com.cn/cors-enabled/students.json",{mode:"cors"})
.then(
    function(response){
        console.log(response.headers.get('Content-Type'));
        console.log(response.headers.get('Date'));
        console.log(response.status);
        console.log(response.statusText);
        console.log(response.type);
        console.log(response.url);
    }
)
.catch(function(err){
    console.log("Fetch错误:"+err);
});

Promise方法链

Promise API的一个重大特性是可以链接方法。对于fetch来说,这允许你共享fetch请求逻辑。

如果使用JSON API,你需要检查状态并且使用JSON对每个响应进行解析。你可以通过在不同的返回Promise对象的函数中定义状态及使用JSON进行解析来简化代码,你将只需要关注于处理数据及错误:

function status(response){
    if(response.status>=200 && response.status<300){
        return Promise.resolve(response);
    }
    else{
        return Promise.reject(new Error(response.statusText));
    }
}
function json(response){
    return response.json();
}
fetch("/students.json")
.then(status)
.then(json)
.then(function(data){
    console.log("请求成功,JSON解析后的响应数据为:",data);
})
.catch(function(err){
    console.log("Fetch错误:"+err);
});
演示 - http://www.html5online.com.cn/articles/2015051305.html

在上述代码中,我们定义了status函数,该函数检查响应的状态码并返回Promise.resolve()方法或Promise.reject()方法的返回结果(分别为具有肯定结果的Promise及具有否定结果的Promise)。这是fetch()方法链中的第一个方法。如果返回肯定结果,我们调用json()函数,该函数返回来自于response.json()方法的Promise对象。在此之后我们得到了一个被解析过的JSON对象,如果解析失败Promise将返回否定结果,导致catch段代码被执行。

这样书写的好处在于你可以共享fetch请求的逻辑,代码容易阅读、维护及测试。

POST请求

在Web应用程序中经常需要使用POST方法提交页面中的一些数据。

为了执行POST提交,我们可以将method属性值设置为post,并且在body属性值中设置需要提交的数据。

fetch(url,{
    method:"post",
    headers:{
        "Content-type":"application:/x-www-form-urlencoded:charset=UTF-8"
    },
    body:"name=lulingniu&age=40"
})
.then(status)
.then(json)
.then(function(data){
    console.log("请求成功,JSON解析后的响应数据为:",data);
})
.catch(function(err){
    console.log("Fetch错误:"+err);
});

使用Fetch请求发送凭证

你可能想要使用Fetch发送带有诸如cookie之类的凭证的请求。你可以在选项对象中将credentials属性值设置为“include”:

fetch(url,{
credentials:"include"
})

频道:Web