一、Service Worker 是什么 ?
以下几种流行的解释:
-
Service Worker 通过向典型的 Web 应用程序添加后台同步、离线渲染和推送通知等功能,缩小了本机应用程序和 Web 应用程序之间的差距,主要任务之一是充当代理。
-
Service Worker 本质上是浏览器在后台运行的脚本,它是完全独立于它正在处理或服务的网页。它们充当了 web 应用程序、浏览器和网络之间的代理服务器。Service Worker 赋予 Web 应用程序像原生应用程序一样工作的能力。
-
Service Worker 定义了由事件驱动的生命周期,这使得页面上任何网络请求事件都可以被其拦截并加以处理,同时还能访问缓存和IndexedDB,这就可以让开发者指定自定义更高的缓存管理策略,从而提高弱网络环境下的Web 运行体验。
-
Service Worker,是现代web开发的关键部分,在最近几年获得了关注,这都要归功于 PWA(渐进式 Web 应用程序) 的流行。
二、Service Worker的作用 ?
- 离线缓存(重点)
- 消息推送(重点)
- 后台数据同步
- 响应来自其他源的资源请求
- 集中接收计算机成本比较高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据
- 在客户端进行CoffeeScript, LESS, CJS/AMD等模块编译和依赖管理(用于开发目的)
- 后台服务钩子
- 自定义模版用于特定URL模式
- 性能增强,比如预取用户可能需要的资源,比如相册中的后面数张图片
三、Service Worker的特征
- Service Worker不能直接访问DOM,使用一种通过 postMessage 接口发送的消息机制来响应不使用时终止,这意味着它们是事件驱动的;
- Service Worker 使用 ES6 承诺机制;
- 使用Service Worker,则需要 HTTPS 的支持,如果在本地主机上可以在没有 HTTPS 支持的情况下使用它们;然而,如果上传到远程服务器,将需要 HTTPS 的支持。
四、Service worker的生命周期
Service Worker生命周期的反应: installing(安装) -> installed(已安装) -> activating(激活) -> activated(已激活)
install用来缓存文件, activate用来缓存更新
五、启用 Service Worker
// html中的script代码
if ("serviceWorker" in navigator) {
//开始注册serivice workers
navigator.serviceWoker
.register("./sw.js", {
scope: "./",
})
.then((registration) => {
console.log("注册成功");
let serviceWorker;
if (registration.installing) {
serviceWorker = registration.installing;
console.log("安装installing");
} else if (registration.waiting) {
serviceWorker = registration.waiting;
console.log("等待waiting");
} else if (registration.active) {
serviceWorker = registration.active;
console.log("激活active");
}
})
.catch((error) => {
console.log("注册没有成功");
});
} else {
console.log("浏览器不支持service worker");
}
//sw.js
const version = 1;
const staticName = `staticCache-${version}`;
// 缓存
self.addEventListener("install", (event) => {
console.log("Service Worker: Installed");
event.waintUntil( //caches 是个全局变量,可以操作缓存空间
caches.open(staticName).then((cache) => {
return cache.addAll(["./index.html"]);
})
);
});
// 缓存更新
self.addEventListener("activate", (event) => {
console.log("Service Worker activate");
event.wainUntill(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// 如果当前版本和缓存不一致,删除缓存
if (cacheName !== staticName) {
return caches.delete(cacheName);
}
})
);
})
);
});
// 捕获请求并返回缓存数据
self.addEventListener("fetch", (event) => {
console.log("Service Worker: Fetching");
ev.respondWith(
caches.open(staticName).then((cache) => {
return cache.match(event.request).then((response) => {
if (response) {
//有缓存
return response;
}
// 无缓存
return fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
在Service Worker上下文中:
- 第一: 不能访问DOM
- 第二:不能访问 window,localStorage等对象
- 第三: 这是个全新的上下文,只有一些特有的对象能访问,比如 self 代表全局作用域的对象
- 第四: 关于Service Worker生命周期一共有三种监听事件
Service Worker 让离线缓存成为可能,offline 情况下也可以访问页面~
图1-1 Service Worker安装成功,本地可以手动卸载
图1-2 Cache Storage 存储了哪些静态资源
图1-3 Storage 的存储情况,手动清空Storage
图1-3 离线状态刷新页面,去 Service Worker 请求资源
六、IndexedDB 存储
IndexedDB是一种事务型数据库系统,其事物型类似基于SQL的关系数据库管理系统(RDBMS),但其并不像RDBMS使用固定列表,而是基于一种Javascript面向对象的数据库,更接近与NoSQL。
1. IndexedDB具有以下特点:
-
储存空间大: IndexedDB的储存空间比LocalStorage大得多,一般来说不少于250MB,甚至没有上限;
-
支持事务: IndexedDB支持事务(trasaction), 这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况;
-
键值对储存: IndexedDB 内部采用对象仓库(object store)存放数据.所有类型的数据都可以直接存入,包括javascript对象,对象仓库中,数据以“键值对”的形式保存,每一条数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误;
-
支持二进制储存: IndexedDB不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer对象和Blob对象);
-
同源限制: IndexedDB受到同源限制,每一个数据库对应创建它的域名.网页只能访问自身域名下的数据库,而不能访问跨域的数据库;
-
异步: IndexedDB操作时不会锁死浏览器,用户依然可以进行其他操作,这与Localstorage形成对比,后者的操作是同步的;异步设计是为了防止大量数据的读写,拖慢网页的表现。
2. 打开或者新建数据库
使用IndexedDB的第一步就是打开或新建一个数据库对象,具体如下:
//根据所指定数据库名称去打开数据库,若不存在则会新建数据库,第二个参数为数据库版本号
const request = window.indexedDB.open('my_indexeddb', '1.0');
// 数据库打开后的操作请求对象,通过三种事件处理打开操作的结果
request.onerror = function(event){console.log('数据库打开失败');}
// 数据库打开成功后,便可通过request对象中的result属性拿到数据库对象
request.onsuccess = function(event) {
//数据库对象
const db = request.result;
}
/**
* 如果打开数据库时指定的版本号大于实际版本号,则会触发数据库升级事件
* 而新数据库必然会触发该事件,通常新建数据库之后第一件事便是创建对象仓库对象,即创建表
*/
request.onupgradeneeded = function(event) {
const db = event.target.result;
let objectStore;
// 判断所要创建的person表对象是否存在,不存在再进行创建
if(!db.obejctStoreNames.contains('person')) {
objectStore = db.createObjectStore('person', {keyPath: 'id'})
}
}
3. 数据的增删改查
新增数据是指的是向对象仓库写入数据记录, 这需要通过事务完成:
function add() {
var request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.add({id: 1, name: '张三', age: 24, email: 'zhangsan@example.com'})
request .onsuccess =. function(event) {
console.log('数据写入成功')
}
request.onerror = function(event) {
console.log('数据写入失败')
}
}
add()
上面的代码中,写入数据需要新建一个事务.新建时必须指定表格名称和操作模式(“只读”或“读写”). 新建事务以后,通过 IDBTransaction.objectStore(name)方法,拿到IDBObjectStore对象,再通过表格对象的add()方法,向表格写入一条记录。
写入操作是一个异步操作,通过监听连接对象的success事件和error事件,了解是否写入成功。
读取数据也是通过事务完成:
function read() {
var transaction = db.transaction(['person'])
var objectStore = transaction.objectStore('person')
var request = objectStore.get(1)
request.onerror = function(event) {
console.log('事务失败')
}
request.onsuccess = function(event) {
if(request.result) {
console.log('Name: ' + request.result.name);
console.log('Age: ' + request.result.age);
console.log('Email: ' + request.result.email);
} else {
console.log('未获得数据记录')
}
}
}
read()
上面代码中, objectStore.get()方法用于读取数据,参数是主键的值。
遍历数据表格的所有记录,要使用指针对象 IDBCursor:
function readAll() {
var objectStore = db.transaction('person').objectStore('person');
objectStore.openCursor().onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
console.log('Id: ' + cursor.key);
console.log('Name: ' + cursor.value.name);
console.log('Age: ' + cursor.value.age);
console.log('Email: ' + cursor.value.email);
cursor.continue();
} else {
console.log('没有更多数据了!');
}
};
}
readAll();
上面代码中,新建指针对象的openCursor()方法是一个异步操作,所以要监听success事件。
更新数据要使用IDBObject.put()方法:
function update() {
var request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.put({id: 1, name: '李四', age: 35, email: 'lisi@example.com'})
request.onsuccess =. function(event) {
console.log('数据更新成功')
}
request.onerror = function(event) {
console.log('数据更新失败')
}
}
update()
上面的代码中, put()方法自动更新了主键为1的记录。
IDBObjectStore.delete()方法用于删除记录:
function remove() {
var request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.delete(1)
request.onsuccess = function(event) {
console.log('数据删除成功')
}
}
remove()
4. 使用索引
索引的意义在于,可以让你搜索任意字段,也就是说从任意字段拿到数据记录。如果不建立索引,默认只能搜索主键(即从主键取值)。
假定新建表格的时候,对name字段建立了索引。
objectStore.createIndex('name', 'name', { unique: false });
现在,就可以从name找到对应的数据记录了。
var transaction = db.transaction(['person'], 'readonly');
var store = transaction.objectStore('person');
var index = store.index('name');
var request = index.get('李四');
request.onsuccess = function (e) {
var result = e.target.result;
if (result) {
// ...
} else {
// ...
}
}
IndexedDB存储这块,阮一峰老师讲解的比较具体,有兴趣的可以查看老师的博客: www.ruanyifeng.com/blog/2018/0…
Service Worker 和 IndexedDB 的基本概念、用法理的差不多了,概念讲得太多不如实战来着实在!
接下来我们以一个todo-list的实战案例,来看看怎怎么在Service Worker中做IndexedDB存储~
七、实战案例
Service Worker 中如何做 IndexedDB 存储? 接下来是一个添加fruits todo-list案例介绍,具体代码比较多,请耐心看完~
// index.html 页面内容
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="./main.css" />
<title>IndexDB in Service Worker</title>
</head>
<body>
<header>
<h1>IndexDB in Service Worker</h1>
</header>
<main>
<form id="form" name="colorFoem">
<p>
<label for="name">Fruits Name</label>
<input type="text" id="name" name="name" />
</p>
<p>
<label for="color">Fruits Color</label>
<input type="color" id="color" name="color" />
</p>
<p>
<button id="btnSave">Save</button>
</p>
</form>
<ul id="fList">
<!-- TODO: list of fruits and their colors -->
</ul>
</main>
<script type="text/javascript" src="./app.js"></script>
</body>
</html>
index.html 包含提交表单和list展示
// app.js
const APP = {
SW: null,
DB: null,
version: 3,
init() {
// called after DOMContentLoaded
// register our service worker
APP.registerSW();
document.getElementById("form").addEventListener("submit", APP.saveFruits);
APP.opnenDB();
},
registerSW() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("./sw.js", {
scope: "./",
})
.then(
(registration) => {
APP.SW =
registration.installing ||
registration.waiting ||
registration.active;
},
(error) => {
console.log("Service worker registration failed:", error);
}
);
//listen for the latest sw
navigator.serviceWorker.addEventListener("controllerchange", async () => {
APP.SW = navigator.serviceWorker.controller;
});
//listen for message from the service worker
navigator.serviceWorker.addEventListener("message", APP.onMessage);
} else {
console.log("Service workers are not supported.");
}
},
saveFruits(ev) {
ev.preventDefault();
let name = document.getElementById("name");
let color = document.getElementById("color");
let strName = name.value.trim();
let strColor = color.value.trim();
if (strName && strColor) {
let fruits = {
id: Date.now(),
name: strName,
color: strColor,
};
console.log("Save", fruits);
APP.sendMessage({
addFruits: fruits,
});
}
},
sendMessage(msg) {
console.log("APP.SW", APP.SW);
if (APP.SW) {
console.log("msg", msg);
APP.SW.postMessage(msg);
}
},
onMessage({ data }) {
//got a message from the service worker
console.log("Web page receving", data);
//TODO: check for saveFruits and build the list and clear the from
if ("saveFruits" in data) {
APP.showFruits();
document.getElementById("name").value = "";
}
},
showFruits() {
console.log("showFruits", APP.DB);
//TODO: check for DB
if (!APP.DB) {
APP.opnenDB();
}
//TODO: start transaction to read names and build the list
try {
let tx = APP.DB.transaction("fruitsStore", "readonly");
let store = tx.objectStore("fruitsStore");
let req = store.getAll();
req.onsuccess = (ev) => {
console.log("onsuccess");
let list = document.getElementById("fList");
let data = ev.target.result;
list.innerHTML = data
.map((fruits) => {
console.log("show", fruits);
return `<li data-id="${fruits.id}">
${fruits.name}
<input type="color" value="${fruits.color}" disabled />
</li>`;
})
.join("\n");
};
} catch (err) {
console.log("colorStore is no create");
}
},
opnenDB() {
let req = window.indexedDB.open("fruitsDB", APP.version);
req.onsuccess = (ev) => {
APP.DB = ev.target.result;
APP.showFruits();
};
req.onupgradeneeded = (ev) => {
console.log("onupgradeneeded");
let db = ev.target.result;
if (!db.objectStoreNames.contains("fruitsStore")) {
db.createObjectStore("fruitsStore", {
keyPath: "id",
});
}
};
},
};
document.addEventListener("DOMContentLoaded", APP.init);
app.js 注册Service Worker, 收集表单内容,通过postMessage将表单内容通知Service Worker去添加IndexedDB存储数据,并接收Service Worker的添加成功消息指令,查询IndexedDB的内容,展示到列表中~
// sw.js
const version = 3;
let staticName = `staticCache-${version}`;
let dynamicName = `dynamicChache`;
let imageName = `imageCache-${version}`;
// starter html add css js files
let assets = ["/", "./index.html", "./main.css", "./app.js", "./404.html"];
let DB = null;
self.addEventListener("install", (ev) => {
console.log("Service Worker: Installed");
ev.waitUntil(
caches.open(staticName).then((cache) => {
console.log("Service Worker: Caching Files");
cache.addAll(assets);
})
);
});
self.addEventListener("activate", (ev) => {
console.log("Service Worker activate");
ev.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys
.filter((key) => {
if (key != staticName && key != imageName) {
return true;
}
})
.map((key) => caches.delete(key))
).then((empties) => {
console.log("empties", empties);
openDB();
});
})
);
});
self.addEventListener("fetch", (ev) => {
console.log("Service Worker: Fetching");
ev.respondWith(
caches.open(staticName).then((cache) => {
return cache.match(ev.request).then((response) => {
if (response) {
//有缓存
return response;
}
// 无缓存
return fetch(ev.request).then((response) => {
cache.put(ev.request, response.clone());
return response;
});
});
})
);
});
self.addEventListener("message", (ev) => {
console.log("message", ev);
let data = ev.data;
let clientId = ev.source.id;
if ("addFruits" in data) {
if (DB) {
saveFruits(data.addFruits, clientId);
} else {
openDB(() => {
saveFruits(data.addFruits, clientId);
});
}
}
if ("otherAction" in data) {
let msg = "Hello";
sendMessage({
code: 0,
message: msg,
});
}
});
const saveFruits = (fruits, clientId) => {
if (fruits && DB) {
let tx = DB.transaction("fruitsStore", "readwrite");
tx.onerror = (err) => {
//failed transaction
};
tx.oncomplete = (ev) => {
let msg = "Thanks. The data was saved.";
sendMessage(
{
code: 0,
message: msg,
saveFruits: fruits,
},
clientId
);
};
let store = tx.objectStore("fruitsStore");
let req = store.put(fruits);
req.onsuccess = (ev) => {};
} else {
let msg = "No data was provied.";
sendMessage(
{
code: 0,
message: msg,
},
clientId
);
}
};
const sendMessage = async (msg, clientId) => {
const client = await clients.get(clientId);
if (!client) return;
client.postMessage(msg);
};
const openDB = (callback) => {
console.log("openDB");
let req = self.indexedDB.open("fruitsDB", version);
req.onerror = (err) => {
//could not open db
console.warn(err);
DB = null;
};
req.onupgradeneeded = (ev) => {
console.log("onupgradeneeded");
let db = ev.target.result;
if (!db.objectStoreNames.contains("fruitsStore")) {
db.createObjectStore("fruitsStore", {
keyPath: "id",
});
}
};
req.onsuccess = (ev) => {
DB = ev.target.result;
console.log("db opened and upgraded as needed");
if (callback) {
callback();
}
};
};
sw.js 监听Service Worker的几个生命周期函数,用Cache缓存静态资源及清空版本不一样的缓存,并且接收来自app应用的消息内容, 打开IndexedDB 添加存储数据,并将添加结果通过postMessage 通知 app应用~
图2-1 表单添加
图2-2 添加结果展示
图2-3 IndexedDB中的数据
八、结束语
上述的todo-list案例,已经涉及到Service Worker的注册安装、监听周期函数、缓存静态资源、离线访问、消息推送 等场景,然后在app主线程和Service Worker单独线程都去打开IndexedDB进行存储和查询数据,对IndexedDB在window和Service Worker中的应用场景基本上都有涉及到了;如果小伙伴们也能自己敲一下实例的代码,对Service Worker和IndexedDB的基本用法也能有个大概的了解了。
篇幅比较长,感谢阅读,前端攻城的路上大家一起加油~