背景
service worker要解決的問題是用戶丟失網絡連接,換句話說使用service worker後,當用戶在離線狀態也可以獲得很好的用戶體驗。
基本概念
Service worker是一個註冊在指定源和路徑下的事件驅動worker。它採用JavaScript控制關聯的頁面或者網站,攔截並修改訪問和資源請求,細粒度地緩存資源。你可以完全控制應用在特定情形(最常見的情形是網絡不可用)下的表現。
Service workers 本質上充當Web應用程序與瀏覽器之間的代理服務器,也可以在網絡可用時作爲瀏覽器和網絡間的代理。它們旨在(除其他之外)使得能夠創建有效的離線體驗,攔截網絡請求並基於網絡是否可用以及更新的資源是否駐留在服務器上來採取適當的動作。他們還允許訪問推送通知和後臺同步API。
Service Worker 是獨立於當前頁面的一段運行在瀏覽器後臺進程裏的腳本。它是異步運行,所以不會對我們的js主線程造成阻塞,我們可以基於service worker實現消息推送、更新一集地理圍欄等服務。它是漸進增強,使用特性嗅探瀏覽器來漸進增強,對不支持的瀏覽器不會產生影響。並且可以離線工作,讓存儲的數據在離線時使用。
create-react-app中的serviceWorker.js就是一個很好的例子:
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read http://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit http://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
從上面的例子我們也可以知道使用service worker時要先註冊,並且service worker可以註銷,稍後我們繼續講解它的完整生命週期。
使用前的設置
在已經支持 serivce workers 的瀏覽器的版本中,很多特性沒有默認開啓。你可能需要開啓一下瀏覽器的相關配置:
- Firefox Nightly: 訪問
about:config
並設置dom.serviceWorkers.enabled
的值爲 true; 重啓瀏覽器; - Chrome Canary: 訪問
chrome://flags
並開啓experimental-web-platform-features
; 重啓瀏覽器 (注意:有些特性在Chrome中沒有默認開放支持); - Opera: 訪問
opera://flags
並開啓ServiceWorker 的支持
; 重啓瀏覽器。
另外,你需要通過 HTTPS 來訪問你的頁面 — 出於安全原因,Service Workers 要求必須在 HTTPS 下才能運行。Github 是個用來測試的好地方,因爲它就支持HTTPS。爲了便於本地開發,localhost
也被瀏覽器認爲是安全源。
service worker的完整生命週期
1、註冊 serviceWorkerContainer.register()
,如果註冊成功,service worker就會被下載到客戶端並嘗試安裝或激活,這將作用於整個域內用戶可訪問的URL,或者其特定子集,並且至少每隔24小時會被下載一次。
2、如果註冊成功,service worker 就在 ServiceWorkerGlobalScope
環境中運行; 這是一個特殊類型的 woker 上下文運行環境。此時,service worker 現在就可以處理事件了。
3、受 service worker 控制的頁面打開後會嘗試去安裝(當 oninstall
事件的處理程序執行完畢後,可以認爲 service worker 安裝完成了)並激活service worker。
4、頁面不得不重新加載以讓 service worker 獲得完全的控制。
一個簡單的示例
爲了演示 service worker 的基本的註冊和安裝,MDN做了一個簡單的例子 sw-test,這是一個簡單的 Star wars Lego 圖片庫。採用了基於 promise 的函數從一個 JSON 對象來讀取圖片內容,在顯示圖片到頁面上之前,採用 Ajax 來加載圖片。頁面非常簡單,而且是靜態的,但也註冊、安裝和激活了 service worker,當瀏覽器支持的時候,它將緩存所有依賴的文件,它可以在離線的時候訪問!https://github.com/mdn/sw-test
我們可以通過chrome面板來查看效果:當我們第一次打開頁面時,service worker會註冊、安裝並激活,此時我們勾選上面板上的Offline,意味着離線狀態,重新刷新頁面後,頁面仍能正常打開。
總結
1、service worker是異步運行,並獨立於主線程,不會對現有程序造成渲染性能上的影響。
2、service worker不能操作DOM,多以沒有多線程問題(即不會產生和主線程同時操作DOM產生的衝突),但是可以通過postMessage來和web頁面通信。
3、service worker是一個可編程的網絡代理,允許開發者控制頁面上處理的網絡請求。
4、瀏覽器可能隨時回收service worker,在不被使用的時候,它會自己終止,而當它再次被用到的時候,會被重新激活。
5、service worker的生命週期是由事件驅動的而不是通過Client。
6、基於promise,且只支持https。因爲通過service worker可以劫持連接,僞造和過濾響應,爲了避免這些問題,只能在HTTPS的網頁上註冊service workers,防止加載service worker的時候不被壞人篡改。