Javascript:用Service Worker做一個離線網(wǎng)頁應用

參考資料
MDN --- Service Worker API
Service Workers: an Introduction
服務工作線程生命周期
Service Worker Cookbook(收集了Service Worker的一些實踐例子)
理解 Service Workers

溫馨提示

  1. 使用限制
    Service Worker由于權限很高,只支持https協(xié)議或者localhost。
    個人認為Github Pages是一個很理想的練習場所特占。
  2. 儲備知識
    Service Worker大量使用Promise吐句,不了解的請移步:Javascript:Promise對象基礎

兼容性

Service Worker的兼容性

一蟀给、 生命周期

個人覺得先理解一下它的生命周期很重要瓮增!之前查資料的時候巩步,很多文章一上來就監(jiān)聽install事件功咒、waiting事件愉阎、activate事件……反正我是一臉懵逼。

Service Worker的生命周期

1. Parsed

SW是一個JS文件力奋,如果我們要使用一個SW(Service Worker)榜旦,那么我們需要在我們的js代碼中注冊它,類似于:
navigator.serviceWorker.register('/sw-1.js')

現(xiàn)在并不需要知道這個方法各個部分的詳細含義景殷,只要知道我們現(xiàn)在在為我們的網(wǎng)頁注冊一個SW就可以了溅呢。

可以看到我們傳入的參數(shù)是一個JS文件的路徑澡屡,當瀏覽器執(zhí)行到這里的時候,就會到相應的路徑下載該文件藕届,然后對該腳本進行解析挪蹭,如果下載或者解析失敗,那么這個SW就會被舍棄休偶。

如果解析成功了梁厉,那就到了parsed狀態(tài)√ざ担可以進行下面的工作了词顾。

2. Installing

在installing狀態(tài)中,SW 腳本中的 install 事件被執(zhí)行碱妆。在能夠控制客戶端之前肉盹,install 事件讓我們有機會緩存我們需要的所有內容。

比如疹尾,我們可以先緩存一張圖片上忍,那么當SW控制客戶端之后,客戶點擊該鏈接的圖片纳本,我們就可以用SW捕獲請求窍蓝,直接返回該圖片的緩存。

若事件中有 event.waitUntil() 方法繁成,則 installing 事件會一直等到該方法中的 Promise 完成之后才會成功吓笙;若 Promise 被拒,則安裝失敗巾腕,Service Worker 直接進入廢棄(redundant)狀態(tài)面睛。

3. Installed / Waiting

如果安裝成功,Service Worker 進入installed(waiting)狀態(tài)尊搬。在此狀態(tài)中叁鉴,它是一個有效的但尚未激活的 worker。它尚未納入 document 的控制毁嗦,確切來說是在等待著從當前 worker 接手亲茅。

處于 Waiting 狀態(tài)的 SW,在以下之一的情況下狗准,會被觸發(fā) Activating 狀態(tài)。

  • 當前已無激活狀態(tài)的 worker
  • SW 腳本中的 self.skipWaiting() 方法被調用
  • 用戶已關閉 SW作用域下的所有頁面茵肃,從而釋放了此前處于激活態(tài)的 worker
  • 超出指定時間腔长,從而釋放此前處于激活態(tài)的 worker

4. Activating

處于 activating 狀態(tài)期間,SW 腳本中的 activate 事件被執(zhí)行验残。我們通常在 activate 事件中捞附,清理 cache 中的文件(清除舊Worker的緩存文件)。

SW激活失敗,則直接進入廢棄(redundant)狀態(tài)鸟召。

5. Activated

如果激活成功胆绊,SW 進入激活狀態(tài)。在此狀態(tài)中欧募,SW開始接管控制客戶端压状,并可以處理fetch(捕捉請求)、 push(消息推送)跟继、 sync(同步事件)等功能性事件:

// sw.js

self.addEventListener('fetch', function(event) {  
  // Do stuff with fetch events
});

self.addEventListener('message', function(event) {  
  // Do stuff with postMessages received from document
}); 
......

6. Redundant 廢棄

Service Worker 可能以下之一的原因而被廢棄(redundant)——

  • installing 事件失敗
  • activating 事件失敗
  • 新的 Service Worker 替換其成為激活態(tài) worker

?
我們已經(jīng)理解了SW的生命周期了种冬,那么現(xiàn)在就開始來做一個離線應用。

我們只實現(xiàn)最簡單的功能:用戶每發(fā)送一個http請求舔糖,我們就用SW捕獲這個請求娱两,然后在緩存里找是否緩存了這個請求對應的響應內容,如果找到了金吗,就把緩存中的內容返回給主頁面十兢,否則再發(fā)送請求給服務器。

二摇庙、 register 注冊

首先要注冊一個SW旱物,在index.js文件中:

// index.js

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    // 注冊一個service worker,這個例子中worker的路徑是根目錄中的跟匆,所以這個worker可以緩存這個項目中任意文件异袄。如果目錄是‘/js/sw.js‘,那么只能緩存目錄'/js'下的文件
    // 參數(shù)registration存儲了本次注冊的一些相關信息
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      // Registration was successful
      // registration.scope 返回的是這個service worker的作用域
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }).catch(function(err) {
      // registration failed :(
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}

知識點:

1. window.navigator

返回一個Navigator對象玛臂,該對象簡單來說就是允許我們獲取我們用戶代理(瀏覽器)的一些信息烤蜕。比如,瀏覽器的官方名稱迹冤,瀏覽器的版本讽营,網(wǎng)絡連接狀況,設備位置信息等等泡徙。

2. navigator.serviceWorker

返回一個 ServiceWorkerContainer對象橱鹏,該對象允許我們對SW進行注冊、刪除堪藐、更新和通信莉兰。

上面的代碼中首先判斷navigator是否有serviceWorker屬性(存在的話表示瀏覽器支持SW),如果存在礁竞,那么通過navigator.serviceWorker.register()(也就是ServiceWorkerContainer.register())來注冊一個新的SW糖荒,.register()接受一個 路徑 作為第一個參數(shù)。

ServiceWorkerContainer.register()返回一個Promise模捂,所以可以用.then().catch()來進行后續(xù)處理捶朵。

3. SW的作用域

如果沒有指定該SW的作用域蜘矢,那么它的默認作用域就是其所在的目錄。
比如综看,.register('/sw.js')中品腹,sw.js在根目錄中,所以作用域是整個項目的文件红碑。

如果是這樣:.register('/controlled/sw.js')舞吭,sw.js的作用域是/controlled。

我們可以手動為SW指定一個作用域:
.register('service-worker.js', { scope: './controlled' });

3. 為什么在load事件中進行注冊

為什么需要在load事件啟動呢句喷?因為你要額外啟動一個線程镣典,啟動之后你可能還會讓它去加載資源,這些都是需要占用CPU和帶寬的唾琼,我們應該保證頁面能正常加載完兄春,然后再啟動我們的后臺線程,不能與正常的頁面加載產生競爭锡溯,這個在低端移動設備意義比較大赶舆。

三、install 安裝

我們已經(jīng)注冊好了SW祭饭,如果 sw.js 下載并且解析成功芜茵,我們的SW就進入安裝階段了,這時候會觸發(fā)install事件倡蝙。我們一般在install事件中緩存我們想要緩存的靜態(tài)資源九串,供SW控制主頁面之后使用:

// sw.js

var CACHE_NAME = 'my-site-cache-v1'; // cache對象的名字
var urlsToCache = [ // 想要緩存的文件的數(shù)組
  '/',
  '/styles/main.css',
  '/script/main.js'
];

// 如果所有文件都成功緩存,則將安裝成功
self.addEventListener('install', function(event) {
  // 執(zhí)行安裝步驟
  // ExtendableEvent.waitUntil()方法延長了安裝過程寺鸥,直到其傳回的Promise被resolve之后才會安裝成功
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        // console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

知識點:

1. cache

Cache是允許我們管理緩存的 Request / Response 對象對的接口猪钮,可以通過這個接口增刪查改 Request / Response 對。

上面代碼中cache.addAll(urlsToCache)表示把數(shù)組中的文件都緩存在內存中胆建。
詳細了解請戳 : Cache

2. caches

caches是一個CacheStorage對象烤低,提供一個可被訪問的命名Cache對象的目錄,維護字符串名稱到相應Cache對象的映射笆载。

我們可以通過該對象打開某一個特定的Cache對象扑馁,或者查看該列表中是否有名字為“xxx”的Cache對象,也可以刪除某一個Cache對象凉驻。

四腻要、activate 激活

我們的SW已經(jīng)安裝成功了,它可以準備控制客戶端并處理 push 和 sync 等功能事件了涝登,這時闯第,我們獲得一個 activate 事件。

// sw.js

self.addEventListener("activate", function(event) {
    console.log("service worker is active");
});

如果SW安裝成功并被激活缀拭,那么控制臺會打印出"service worker is active"咳短。

如果我們是在更新SW的情況下,此時應該還有一個舊的SW在工作蛛淋,這時我們的新SW就不會被激活咙好,而是進入了 "Waiting" 狀態(tài)。

我們需要關閉此網(wǎng)站的所有標簽頁來關閉舊SW褐荷,使新的SW激活勾效。或者手動激活叛甫。

那么activate事件可以用來干什么呢层宫?假設我們現(xiàn)在換了一個新的SW,新SW需要緩存的靜態(tài)資源和舊的不同其监,那么我們就需要清除舊緩存萌腿。

為什么呢?因為一個域能用的緩存空間是有限的抖苦,如果沒有正確管理緩存數(shù)據(jù)毁菱,導致數(shù)據(jù)過大,瀏覽器會幫我們刪除數(shù)據(jù)锌历,那么可能會誤刪我們想要留在緩存中的數(shù)據(jù)贮庞。

這個以后會詳細講,現(xiàn)在只需要知道activate事件能用來清除舊緩存舊可以了究西。

五窗慎、 fetch事件

現(xiàn)在我們的SW已經(jīng)激活了,那么可以開始捕獲網(wǎng)絡請求卤材,來提高網(wǎng)站的性能了遮斥。

當網(wǎng)頁發(fā)出請求的時候,會觸發(fā)fetch事件商膊。

Service Workers可以監(jiān)聽該事件伏伐,'攔截' 請求,并決定返回內容 ———— 是返回緩存的數(shù)據(jù)晕拆,還是發(fā)送請求藐翎,返回服務器響應的數(shù)據(jù)。

下面的代碼中实幕,SW會檢測緩存中是否有用戶想要的內容吝镣,如果有,就返回緩存中的內容昆庇。否則再發(fā)送網(wǎng)絡請求末贾。

// sw.js

self.addEventListener('fetch', event => {
    const { request } = event; // 獲取request
    const findResponsePromise = caches.open(CACHE_NAME)
    // 在match的時候,需要請求的url和header都一致才是相同的資源
    // caches.match(event.request, {ignoreVary: true}) 表示只要請求url相同就認為是同一個資源整吆。
    .then(cache => cache.match(request)) // 查看cache對象中是否有匹配的項
    .then(response => {
        if (response) { // 如果response不為空拱撵,則返回response辉川,否則發(fā)送網(wǎng)絡請求
            return response;
        }

        return fetch(request);
    });
    // event.respondWith 是一個 FetchEvent 對象中的特殊方法,用于將請求的響應發(fā)送回瀏覽器拴测。它接收一個對響應(或網(wǎng)絡錯誤)resolve 后的 Promise 對象作為參數(shù)乓旗。
    event.respondWith(findResponsePromise);
});

箭頭函數(shù)真的很適合用于Promise對象,省略了一堆的function集索、return關鍵字屿愚,看著舒服多了……

關于緩存策略
不同的應用場景需要使用不同的緩存策略。

比如务荆,小紅希望她的網(wǎng)站在在線的時候總是返回緩存中的內容妆距,然后在后臺更新緩存;在離線的時候函匕,返回緩存的內容娱据。

比如,小明希望他的網(wǎng)站可以在在線的時候返回最新的響應內容浦箱,離線的時候再返回緩存中的內容吸耿。
……
如果想要研究一下各種緩存策略,可以參考下面的資料酷窥,這里就不詳述了咽安,不然文章就成裹腳布了……
The Service Worker Cookbook
離線指南
Service Worker最佳實踐

不過,既然標題是“做一個離線網(wǎng)頁應用”蓬推,那我們就做一個最簡單的緩存策略:如果緩存中保存著請求的內容妆棒,則返回緩存中的內容,否則沸伏,請求新內容糕珊,并緩存新內容。

self.addEventListener('fetch', function (event) {
    event.respondWith(
        caches.match(event.request)
        .then(response => {
            // Cache hit - return response
            if (response) {
                return response;
            }
            // 克隆請求毅糟。因為請求是一個“stream”红选,只能用一次。但我們需要用兩次姆另,一次用來緩存喇肋,一次給瀏覽器抓取內容,所以需要克隆
            var fetchRequest = event.request.clone();
            // 返回請求的內容
            return fetch(fetchRequest).then(
                response => {
                    // 檢查是否為有效的響應迹辐。basic表示同源響應蝶防,也就是說,這意味著明吩,對第三方資產的請求不會添加到緩存间学。
                    if (!response || response.status !== 200 || response.type !== 'basic') {
                        return response;
                    }
                    // 同request,response是一個“stream”,只能用一次低葫,但我們需要用兩次详羡,一次用來緩存一個返回給瀏覽器,所以需要克隆氮采。
                    var responseToCache = response.clone();
                    // 緩存新請求
                    caches.open(CACHE_NAME)
                        .then(cache => cache.put(event.request, responseToCache));
                    return response;
                }
            );
        })
    );
});

?
完成啦殷绍!我們簡陋的離線應用!
打開頁面鹊漠,看一下緩存中有什么內容:


offline1

然后點擊“Vue”的鏈接:


offline2

可以看到緩存中多了一張后綴為.png的圖片。
SW緩存了我們的新請求茶行!

打開chrome的開發(fā)者工具躯概,點擊offline,使標簽頁處于離線狀態(tài):


offline3

然后畔师,刷新頁面娶靡。


offline4

依然可以訪問頁面。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末看锉,一起剝皮案震驚了整個濱河市姿锭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伯铣,老刑警劉巖呻此,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異腔寡,居然都是意外死亡焚鲜,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進店門放前,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忿磅,“玉大人,你說我怎么就攤上這事凭语〈兴” “怎么了?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵似扔,是天一觀的道長吨些。 經(jīng)常有香客問我,道長虫几,這世上最難降的妖魔是什么锤灿? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮辆脸,結果婚禮上但校,老公的妹妹穿的比我還像新娘。我一直安慰自己啡氢,他們只是感情好状囱,可當我...
    茶點故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布术裸。 她就那樣靜靜地躺著,像睡著了一般亭枷。 火紅的嫁衣襯著肌膚如雪袭艺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天叨粘,我揣著相機與錄音猾编,去河邊找鬼。 笑死升敲,一個胖子當著我的面吹牛答倡,可吹牛的內容都是我干的。 我是一名探鬼主播驴党,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼瘪撇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了港庄?” 一聲冷哼從身側響起倔既,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鹏氧,沒想到半個月后渤涌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨居荒郊野嶺守林人離奇死亡度帮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年歼捏,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笨篷。...
    茶點故事閱讀 40,973評論 1 354
  • 序言:一個原本活蹦亂跳的男人離奇死亡瞳秽,死狀恐怖,靈堂內的尸體忽然破棺而出率翅,到底是詐尸還是另有隱情练俐,我是刑警寧澤,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布冕臭,位于F島的核電站腺晾,受9級特大地震影響,放射性物質發(fā)生泄漏辜贵。R本人自食惡果不足惜悯蝉,卻給世界環(huán)境...
    茶點故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望托慨。 院中可真熱鬧鼻由,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至狠轻,卻和暖如春奸例,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背向楼。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工查吊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蜜自。 一個月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓菩貌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親重荠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,982評論 2 361

推薦閱讀更多精彩內容