├── .gitignore ├── index.js ├── yarn.lock ├── package.json ├── lib ├── notify.js ├── sw.js └── cache.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Notify from './lib/notify'; 2 | import SW from './lib/sw'; 3 | import WebCaches from './lib/cache'; 4 | 5 | export default SW; 6 | export {Notify,WebCaches}; 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | isarray@0.0.1: 6 | version "0.0.1" 7 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 8 | 9 | path-to-regexp@^1.7.0: 10 | version "1.7.0" 11 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" 12 | dependencies: 13 | isarray "0.0.1" 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-pwa", 3 | "version": "0.1.3", 4 | "description": "a simple repository for pwa, including caches,sw,notification", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/JimmyVV/web-pwa" 12 | }, 13 | "author": "JimmyVV", 14 | "license": "ISC", 15 | "dependencies": { 16 | "path-to-regexp": "^1.7.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/notify.js: -------------------------------------------------------------------------------- 1 | let Notify = {}; 2 | Notify.request = function () { 3 | if (!('Notification' in window)) { 4 | return new Promise((res, rej) => { 5 | rej('The browser do not support Notification'); 6 | }) 7 | } 8 | return Notification.requestPermission() 9 | .then(permission => { 10 | return new Promise((res, rej) => { 11 | if (permission === "granted") { 12 | res(permission); 13 | } else if (permission === "default") { 14 | rej('User cancel') 15 | } else { 16 | rej('User denied') 17 | } 18 | }) 19 | }) 20 | }; 21 | 22 | function spawnNotification(title, options) { 23 | var not = new Notification(title, options); 24 | Notify.currentNot = not; 25 | } 26 | 27 | 28 | Notify.show = function (title, body, icon) { 29 | if (Notification.permission === 'default') 30 | Notify.request().then(() => { 31 | spawnNotification(title, { 32 | body, 33 | icon 34 | }) 35 | }) 36 | else if (Notification.permission === 'granted') { 37 | spawnNotification(title, { 38 | body, 39 | icon 40 | }); 41 | } else {} 42 | return Notify; 43 | } 44 | 45 | // Why error? 46 | // ['onclick','onclose','onshow','onerror'].forEach(val=>{ 47 | // Notify[val] = function(cb){ 48 | // Notify.currentNot[val] = cb; 49 | // return Notify 50 | // } 51 | // }) 52 | 53 | Notify.onclick = function(cb){ 54 | Notify.currentNot.onclick = cb; 55 | return Notify; 56 | } 57 | Notify.onclose = function(cb){ 58 | Notify.currentNot.onclose = cb; 59 | return Notify; 60 | } 61 | Notify.onshow = function(cb){ 62 | Notify.currentNot.onshow = cb; 63 | return Notify; 64 | } 65 | Notify.onerror = function(cb){ 66 | Notify.currentNot.onerror = cb; 67 | return Notify; 68 | } 69 | Notify.hide = function(time){ 70 | var not = Notify.currentNot; 71 | setTimeout(not.close.bind(not),time); 72 | return Notify; 73 | } 74 | 75 | Notify.focus = function(){ 76 | window.focus(); 77 | return Notify; 78 | } 79 | 80 | Notify.open = function(url){ 81 | window.open(url); 82 | return Notify; 83 | } 84 | 85 | 86 | 87 | 88 | export default Notify; 89 | -------------------------------------------------------------------------------- /lib/sw.js: -------------------------------------------------------------------------------- 1 | let SW = {}; 2 | 3 | SW.init = function () { 4 | return new Promise((res, rej) => { 5 | 'serviceWorker' in navigator ? res() : rej('The browser does not support PWA/Service Worker'); 6 | }) 7 | } 8 | 9 | /** 10 | * 注册 service worker 11 | * @param href 12 | * @returns Promise 13 | */ 14 | 15 | 16 | SW.register = function (href) { 17 | return navigator.serviceWorker.register(href).then(reg => { 18 | return reg; 19 | }).catch(err => { 20 | console.error(err); 21 | }) 22 | }; 23 | 24 | SW.unregister = function () { 25 | SW.ready.then(reg => { 26 | reg.unregister(); 27 | }) 28 | } 29 | 30 | SW.update = function () { 31 | SW.ready.then(reg => { 32 | reg.update(); 33 | }) 34 | } 35 | 36 | SW.ready = navigator.serviceWorker.ready; 37 | 38 | /** 39 | * 可以用来监听是否返回相应的数据 40 | * @param msg 41 | * @returns {Promise} 42 | */ 43 | SW.postMessage = function (msg) { 44 | var sw = navigator.serviceWorker; 45 | return new Promise((res, rej) => { 46 | !sw.controller ? rej('Service Worker do not register') : sw.ready.then(reg => { 47 | var channel = new MessageChannel(); 48 | channel.port1.onmessage = event => { 49 | !event.data.error ? res(event.data) : rej(event.data.error); 50 | }; 51 | sw.controller.postMessage(msg, [channel.port2]); 52 | }) 53 | }) 54 | 55 | } 56 | 57 | 58 | 59 | /** 60 | * 接受 Service Woker 通过广播,或者使用 client.postMessage 直接返回的消息 61 | * @param cb 62 | */ 63 | SW.onmessage = function (cb) { 64 | navigator.serviceWorker.addEventListener('message', cb); 65 | } 66 | 67 | function urlBase64ToUint8Array(base64String) { 68 | const padding = '='.repeat((4 - base64String.length % 4) % 4); 69 | const base64 = (base64String + padding) 70 | .replace(/\-/g, '+') 71 | .replace(/_/g, '/'); 72 | 73 | const rawData = window.atob(base64); 74 | const outputArray = new Uint8Array(rawData.length); 75 | 76 | for (let i = 0; i < rawData.length; ++i) { 77 | outputArray[i] = rawData.charCodeAt(i); 78 | } 79 | return outputArray; 80 | } 81 | 82 | SW.subscribe = function (route, key) { 83 | return navigator.serviceWorker.ready.then(reg => { 84 | key = urlBase64ToUint8Array(key); 85 | return reg.pushManager.subscribe({ 86 | userVisibleOnly: true, 87 | applicationServerKey: key 88 | }) 89 | }).then(result => { 90 | return fetch(route, { 91 | method: 'POST', 92 | headers: { 93 | 'Content-Type': 'application/json' 94 | }, 95 | body: JSON.stringify(result) 96 | }) 97 | }) 98 | } 99 | 100 | export default SW; 101 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | import pathToRegexp from 'path-to-regexp'; 2 | // import clone from './cloneCache'; 3 | 4 | let Caches = {}; 5 | let Utils = {}; 6 | 7 | /** 8 | * 获得变量的具体类型/构造函数名 9 | * e.g. 10 | * a{"abc"} => string 11 | * b{{key:'value'}} => object 12 | * c{function(){}} => function 13 | */ 14 | Utils.getType = function (obj) { 15 | return Object.prototype.toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase() 16 | } 17 | 18 | /** 19 | * 原始数据查询字段内容 20 | * @param table.type[Number]: 21 | * 0: 完整的 tableName[String] // 目前只支持该种方式 22 | * 1: 正则 23 | * @param row.type[Number]: 24 | * 0: request[Object] 25 | * 1: 路径通配符 /path/name 26 | * 27 | */ 28 | 29 | let DATA = { 30 | table: { 31 | name: '', 32 | type: 0 33 | }, 34 | row: { 35 | name: '', 36 | type: 0 37 | } 38 | } 39 | 40 | /** 41 | * @param {string} cacheName 目前只支持完整 string 的形式. 42 | */ 43 | Caches.table = function (cacheName) { 44 | DATA.table = {}; 45 | if (Utils.getType(cacheName) === "string") { 46 | DATA.table.type = 0; 47 | } else { 48 | DATA.table.type = 1; 49 | } 50 | DATA.table.name = cacheName; 51 | return Caches.table; 52 | } 53 | 54 | /** 55 | * @param {string/request} name 用来接收 rowName 56 | * string: 只能为 pathname,不能为绝对路径 e.g. '/app/*.js' 57 | * request: 通过 new request() 得来的。 58 | */ 59 | Caches.row = function (name) { 60 | DATA.row = {}; 61 | if (Utils.getType(name) === 'request') { 62 | DATA.row.type = 0; 63 | } else { 64 | DATA.row.type = 1; 65 | } 66 | DATA.row.name = name; 67 | return Caches.row; 68 | } 69 | 70 | 71 | /** 72 | * 复制指定的 tablename,并且删除原有的 table 73 | * TODO 测试通过 get 74 | * @param {String} newName 新的名字 75 | */ 76 | Caches.table.rename = function (newName) { 77 | return caches.open(DATA.table.name) 78 | .then(cache => { 79 | // 获得所有记录 80 | return cache.keys(); 81 | }) 82 | .then(keys => { 83 | // 向新的 table 里面添加记录 84 | return caches.open(newName) 85 | .then(cache => { 86 | return cache.addAll(keys); 87 | }) 88 | }) 89 | .then(() => { 90 | // 删除原来的 table 91 | return caches.delete(DATA.table.name) 92 | }) 93 | .then(res => { 94 | DATA = {}; 95 | return res; 96 | }) 97 | } 98 | 99 | /** 100 | * @param {string} newName 复制为的 tableName 101 | */ 102 | Caches.table.copyTo = function (newName) { 103 | return caches.open(DATA.table.name) 104 | .then(cache => { 105 | // 获得所有记录 106 | return cache.keys(); 107 | }) 108 | .then(keys => { 109 | // 向新的 table 里面添加记录 110 | return caches.open(newName) 111 | .then(cache => { 112 | return cache.addAll(keys); 113 | }) 114 | }) 115 | .then(res => { 116 | DATA = {}; 117 | return res; 118 | }) 119 | } 120 | 121 | 122 | /** 123 | * 打开指定的 table。如果没有则自动创建 124 | */ 125 | Caches.table.open = function () { 126 | return caches.open(DATA.table.name) 127 | } 128 | 129 | /** 130 | * 删除指定的 table 131 | */ 132 | Caches.table.delete = function () { 133 | return caches.delete(DATA.table.name) 134 | .then(res => { 135 | DATA = {}; 136 | return res; 137 | }) 138 | } 139 | 140 | 141 | /** 142 | * 向 table 里面手动添加 row 143 | * 参数有三种接受方式 144 | * 1. @param1 {String||Request} url/request 145 | * 2. @param1 {url/request} @param2 {Response} response 146 | * 3. @param1 {Array} [url]/[request] 147 | * 148 | * @return {Promise} then: 成功。 catch: 失败。 149 | */ 150 | Caches.table.addRow = function (param1, param2) { 151 | return caches.open(DATA.table.name) 152 | .then(cache => { 153 | if (Utils.getType(param1) === 'array') { 154 | return cache.addAll(param1) 155 | } else { 156 | return param2 ? cache.put(param1, param2) : cache.add(param1); 157 | } 158 | }) 159 | .then(res => { 160 | DATA = {}; 161 | return res; 162 | }) 163 | } 164 | 165 | // 连接 table().row() 166 | // 使其可以进行链式调用 167 | Caches.table.row = Caches.row; 168 | 169 | /** 170 | * 删除指定的 row 171 | */ 172 | Caches.row.delete = function () { 173 | return caches.open(DATA.table.name) 174 | .then(cache => { 175 | return deleteRow(cache, DATA.row); 176 | }) 177 | .then(res => { 178 | DATA = {}; 179 | return res; 180 | }) 181 | } 182 | 183 | /** 184 | * 获得指定的所有的匹配到的 response 185 | */ 186 | Caches.row.get = function () { 187 | return caches.open(DATA.table.name) 188 | .then(cache => { 189 | return findRow(cache, DATA.row) 190 | .then(requests => { 191 | return Promise.all(requests.map(request => { 192 | return cache.match(request); 193 | })) 194 | }) 195 | }) 196 | .then(res=>{ 197 | DATA = {}; 198 | return res; 199 | }) 200 | } 201 | 202 | Caches.row.update = function (response) { 203 | return caches.open(DATA.table.name) 204 | .then(cache=>{ 205 | return findRow(cache, DATA.row) 206 | .then(requests=>{ 207 | return !!requests.length?cache.put(requests[0],response):null; 208 | }) 209 | }) 210 | } 211 | 212 | 213 | // TODO 待开发,实现查询条件的缓存 214 | // Caches.table.clone = Caches.row.clone = clone.bind(this); 215 | 216 | Caches.clone = function(table,row){ 217 | return function(){ 218 | if(table && row) return Caches.table(table).row(row); 219 | else if(table) return Caches.table(table) 220 | } 221 | } 222 | 223 | 224 | /** 225 | * 解析 url 对象 226 | * @param {string} href 路由值 227 | */ 228 | function resoveURL(href) { 229 | return new URL(href); 230 | } 231 | 232 | /** 233 | * 找到指定的 row 234 | * @param {Object} cache 指定打开的缓存 cache 235 | * @param {Object} row row.name && row.type 236 | * @return {Array} requests 返回找到的 requests Object。 237 | */ 238 | function findRow(cache, row) { 239 | return new Promise((res, rej) => { 240 | if (row.type === 0) { 241 | // request 类型 242 | res([row.name]); 243 | } else { 244 | // string 类型的 pathname。 245 | cache.keys() 246 | .then(requests => { 247 | res(matchRequest(requests, row.name)); 248 | }) 249 | .catch(rej) 250 | } 251 | }) 252 | } 253 | 254 | 255 | function deleteRow(cache, row) { 256 | return findRow(cache, row) 257 | .then(requests => { 258 | return Promise.all(requests.map(req => { 259 | return cache.delete(req); 260 | })) 261 | .then(res => { 262 | return !!res ? true : false; 263 | }) 264 | }) 265 | } 266 | 267 | function matchRequest(requests, matchPath) { 268 | var regPath = pathToRegexp(matchPath); 269 | return requests = requests.filter(req => { 270 | return !!regPath.exec(resoveURL(req.url).pathname); 271 | }) 272 | } 273 | 274 | export default Caches; 275 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-pwa 2 | 3 | 4 | > 该库是应对当前 Google 提出的 PWA 概念而写的。以链式 API 来完成 PWA 相关的操作。 5 | 6 | [![npm][1]][2] [![git][3]][4] 7 | 8 | ## 安装 9 | 10 | ``` 11 | npm install web-pwa 12 | // 或者使用 yarn 13 | yarn add web-pwa 14 | ``` 15 | 16 | 17 | ## DEMO 18 | 19 | 首先说明一下,我们要完成的目标: 20 | 21 | - 注册 sw 22 | - 添加 app.js 的缓存 23 | - 实现推送,并在用户点击后关闭,然后聚焦当前页面 24 | 25 | 整个代码如下: 26 | 27 | ``` 28 | import SW,{Notify,WebCaches} from 'web-pwa'; 29 | 30 | window.onload = function(){ 31 | SW.register('sw.js'); 32 | var tableName = 'prefetch-cache-v1'; 33 | WebCaches.table(tableName).addRow('/app.js') 34 | .then(res=>{ 35 | // res: 成功 36 | }) 37 | Notify.request() // 请求推送权限 38 | .then(permission=>{ 39 | // 用户同意 40 | Notify.show('villianhr','Hello Pwa') 41 | .onclick(event=>{ 42 | event.close(); // 关闭当前 Notification 43 | Notify.focus(); // 聚焦窗口 44 | }) 45 | }) 46 | } 47 | ``` 48 | 49 | ## 使用 50 | 51 | 基本使用可以分为三块: 52 | 53 | - SW: 主要处理主线程 JS Service Worker 的相关行为。例如:注册,发送消息等 54 | - WebCaches: 用来处理 `CacheStorage` 缓存的相关操作。 55 | - Notify: 根据 `new Notification()` 来完成主线程 JS 的消息推送 56 | 57 | 58 | ``` 59 | import SW,{WebCaches,Notify} from 'web-pwa'; 60 | ``` 61 | 62 | (重点推荐使用 WebCaches) 63 | 在内部细节中,处理了兼容性和权限请求的问题,这里我们具体落实到场景当中。 64 | 65 | ### SW 66 | 67 | SW 原意是 `Service Worker`。如果大家还不熟悉,推荐可以参考:[Service Worker 全面进阶][5]。 68 | 69 | #### 权限申请 70 | 71 | ``` 72 | SW.register('sw.js') 73 | .then(reg=>{ 74 | 75 | }) 76 | ``` 77 | 它返回的是 Promise 对象。 78 | 79 | #### 销毁 Service Worker 80 | 81 | ``` 82 | SW.unregister().then(res=>{ 83 | if(res)console.log('unregisteration, done!'); 84 | }) 85 | ``` 86 | 它返回的是 Promise 对象。 87 | #### Service Worker 更新 88 | 89 | ``` 90 | SW.update(); 91 | ``` 92 | 93 | #### 消息通信 94 | 95 | 我们了解 Service Worker 是继承 `Web Worker`。在 `Web Worker` 中,我们可以使用 `postMessage` 进行通信,那么在 SW(Service Worker)中同样是可以的。 96 | 97 | ``` 98 | SW.postMessage('a new message send to Service Worker'); 99 | ``` 100 | 101 | 如果你想接受此次 SW 回复的信息,可以直接加上 `Promise` 的写法。 102 | 103 | ``` 104 | // 接收 SW 回复的信息 105 | SW.postMessage('a new message send to Service Worker') 106 | .then(reply=>{ 107 | // doSth 108 | }) 109 | 110 | // SW 回复信息 111 | 112 | self.addEventListener('message', function(event){ 113 | console.log("SW Received Message: " + event.data); 114 | event.ports[0].postMessage("SW Says 'Hello back!'"); 115 | }); 116 | ``` 117 | 118 | 另外,SW 还可以通过 `clients` 挂载的 [postMessage][6] 向 client 发送信息。如果有这种需求,可以直接监听 `message` 事件。 119 | 120 | ``` 121 | SW.onmessage(event=>{ 122 | // 接收 SW 发送的消息 123 | // event.data 124 | }) 125 | ``` 126 | 127 | #### 推送订阅 128 | 129 | 当你想要使用 `Push` 相关的内容时,可以调用 `Notify.subscribe(route,key)` 方法。如果,你不是很理解 `Web Push` 的概念,可以参考: [Web Push 讲解][7] 130 | 131 | ``` 132 | // 下面的 key 根据自己生成进行替换 133 | SW.subscribe('/subscription','BPLISiRYgXzzLY_-mKahMBdYPeRZU-8bFVzgJMcDuthMxD08v0cEfc9krx6pG5VGOC31oX_QEuOSgU5CYJqpzf0'); 134 | ``` 135 | 136 | 137 | 138 | ### WebCaches 139 | 140 | 首先这里有两个概念,一个是 table(表),一个是 row(行)。每一个网站缓存可以有多个表,这完全取决于你自己的结构。该库是 `one-off` 形式,即,不能使用变量名来缓存表。例如: 141 | 142 | ``` 143 | var table = WebCaches.table('v1'); 144 | table.open(); // 正常执行没问题 145 | 146 | table.open(); // 第二次使用无效 147 | ``` 148 | 后面会介绍一种简便的方法进行简写。 149 | 150 | 缓存处理主要分为两块: 151 | 152 | - table 153 | - addRow: 添加行记录 154 | - delete: 删除表 155 | - copy: 复制整个表 156 | - rename: 重命名整个表 157 | - open: 打开表 158 | - row 159 | - get: 查询行 160 | - delete: 删除行 161 | - update: 更新行 162 | 163 | #### table 164 | 165 | table 本身就是一个函数,构造格式为: 166 | 167 | - table(cachesName): 打开某个具体的表 168 | - @param cachesName[String]: 具体打开的表名 169 | 170 | ##### 打开表 171 | 172 | 构造函数为: 173 | 174 | - open(): 执行打开操作 175 | - @return: promise 176 | 177 | ``` 178 | WebCaches.table('demo-v1').open() 179 | .then(cache=>{}) 180 | ``` 181 | 182 | ##### 添加行 183 | 184 | 向表中添加具体的缓存行,添加方式有三种: 185 | 186 | - addRow(request) 187 | - @param request: 可以为 url 或者通过 `new Request(url)` 实例化得来的。 188 | - addRow([request1,request2,...]) 189 | - @param Array: 里面就是 url/request 的数组。 190 | - addRow(request,response) 191 | - @param request: 和上面一样,没啥区别 192 | - @param response: 需要存储的结构。一般是通过 `new Response(res)` 生成,或者直接通过 `fetch().then(response=>{})` 获得的。 193 | 194 | 195 | ##### 重命名/复制表 196 | 197 | 重命名的格式为: 198 | 199 | - rename(newName) 200 | - @param newName[String]: 表的新名字 201 | - @return Promise 202 | 203 | ``` 204 | WebCaches.table('old-v1').rename('new-v2') 205 | .then(res=>{ 206 | // success 207 | }) 208 | .catch(err=>{ 209 | // fail 210 | }) 211 | ``` 212 | 213 | 复制表的格式为: 214 | 215 | - copyTo(targetTable) 216 | - @param targetTable[String]: 指定的表名 217 | - @return Promise 218 | 219 | ``` 220 | // 将 A 表复制给 B 221 | WebCaches.table('A').copyTo('B') 222 | .then(res=>{ 223 | // success 224 | }) 225 | .catch(err=>{ 226 | // fail 227 | }) 228 | ``` 229 | 230 | ##### 删除表 231 | 232 | 格式为: 233 | 234 | - delete() 235 | - @return: Promise 236 | 237 | 238 | ``` 239 | WebCaches.table('A').delete() 240 | .then(res=>{ 241 | // success 242 | }) 243 | .catch(err=>{ 244 | // fail 245 | }) 246 | ``` 247 | 248 | #### row 249 | 250 | row 本身也是一个函数: 251 | 252 | - row(request) 253 | - @param request[Request||String]: 该参数可以为 request,或者 pathname(注意不能带上 `origin`)。当为 request 时,是直接匹配对应的行记录,而为 `pathname` 时,则是使用 [path-to-regexp][8] 的格式,可以匹配多个或者模糊匹配。 254 | 255 | 256 | ``` 257 | // 只匹配 js 文件 258 | 259 | WebCaches.table(tableName).row('/*.js') 260 | .get().then(res=>{ 261 | console.log(res); 262 | }) 263 | ``` 264 | 265 | 通过 request 匹配: 266 | 267 | ``` 268 | var js = new Request('/app.js'); 269 | 270 | Caches.table(tableName).row(js) 271 | .get().then(res=>{ 272 | console.log(res); 273 | }) 274 | ``` 275 | 276 | ##### 简写 277 | 如果每次都 `WebCaches.table.row` 这样调用,会让人觉得比较冗长,那么有没有什么好的办法解决呢?这里提供了一个工具函数 `clone` 用来生成可重复使用的对象。 278 | 279 | ``` 280 | // 提取 table 281 | var table_v1 = WebCaches.clone('v1'); 282 | table_v1().open(); // first,OK 283 | 284 | table_v1().open(); // second,OK 285 | ``` 286 | 287 | 然后,可以提取 row 288 | 289 | ``` 290 | var table_row = WebCaches.clone('v1','/*.js'); 291 | 292 | table_row().get(); // first, OK 293 | 294 | table_row().get(); // second, OK 295 | ``` 296 | 297 | 298 | ##### 删除行 299 | 300 | ``` 301 | // 删除所有 js 文件 302 | WebCaches.table(tableName).row('/*.js') 303 | .delete() 304 | .then(()=>{ 305 | // success 306 | }) 307 | .catch(err=>{ 308 | // fail 309 | }) 310 | ``` 311 | 312 | ##### 更新行 313 | 314 | ``` 315 | fetch('/') 316 | .then(res=>{ 317 | // 更新根目录文件 318 | WebCaches.table(tableName).row('/') 319 | .update(res) 320 | .then(()=>{ 321 | WebCaches.table(tableName).row('/').get() 322 | .then(console.log.bind(console)) 323 | }) 324 | }) 325 | ``` 326 | 327 | 328 | 329 | ### Notify 330 | 331 | `Notify` 提供了 `Notification` 相关的 API。其主打的是链式调用,不需要过多的关注 `Notification` 内部细节。 332 | 333 | 334 | #### 权限申请 335 | 336 | ``` 337 | notify.request() 338 | .then(permission=>{ 339 | // permission === "granted" 340 | // permission === "denied" 341 | // permission === "default" 342 | }) 343 | ``` 344 | 345 | #### 消息推送 346 | 347 | 使用消息推送的时候,可以不用嵌套在 `request()` 里面,它内部已经做了权限的处理。 348 | 349 | ``` 350 | // 纯文字版 351 | Notify.show('demo','this is a demo') 352 | 353 | // 带 Icon 354 | Notify.show('demo','this is a demo','demo.png') 355 | ``` 356 | 357 | #### 推送后自动关闭 358 | 359 | ``` 360 | Notify.show('demo','this is a demo') 361 | .hide(3000); // 3s 后自动关闭 362 | ``` 363 | 364 | #### 推送点击 365 | 366 | ``` 367 | Notify.show('demo','this is a demo') 368 | .onclick(e=>{ 369 | e.target.close(); 370 | }); 371 | ``` 372 | 373 | Notify 还提供了其它的事件监听 374 | 375 | - onclick 376 | - onclose 377 | - onshow 378 | - onerror 379 | 380 | 上面这些方法都可以进行链式调用。 381 | 382 | ``` 383 | Notify.show('demo','this is a demo') 384 | .onclick(event=>{}) 385 | .onclose(event=>{}) 386 | .onshow(event=>{}) 387 | ``` 388 | 389 | 用户点击推送这一行为,我们可以加上额外的处理,例如,打开页面,聚焦页面等。 390 | 391 | ``` 392 | Notify.show('demo','this is a demo') 393 | .onclick(event=>{ 394 | event.target.close(); 395 | // 聚焦页面 396 | Notify.focus(); 397 | // 打开新的页面 398 | Notify.open('https://www.villainhr.com'); 399 | }) 400 | ``` 401 | 402 | ## License 403 | 404 | MIT 405 | 406 | ## Author 407 | 408 | - author: [villainhr][9] 409 | - email: villainthr@gmail.com 410 | 411 | 412 | 413 | [1]: https://img.shields.io/badge/npm-web--pwa-blue.svg 414 | [2]: https://www.npmjs.com/package/web-pwa 415 | [3]: https://img.shields.io/badge/git-web--pwa-blue.svg 416 | [4]: https://github.com/JimmyVV/web-pwa 417 | [5]: https://www.villainhr.com/page/2017/01/08/Service%20Worker%20%E5%85%A8%E9%9D%A2%E8%BF%9B%E9%98%B6 418 | [6]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage 419 | [7]: https://www.villainhr.com/page/2017/01/08/Web%20%E6%8E%A8%E9%80%81%E6%8A%80%E6%9C%AF#Push 420 | [8]: https://github.com/pillarjs/path-to-regexp 421 | [9]: https://www.villainhr.com/ --------------------------------------------------------------------------------