Service Worker

87 阅读8分钟

Service Worker 是一种在浏览器后台运行的 JavaScript 线程,它能够独立于网页和用户交互进行操作。它的主要作用是为 Web 应用程序提供离线功能、推送通知、后台数据同步等能力

他和worker的区别在于:

  • Worker 主要用于后台执行计算任务,避免阻塞主线程;而 Service Worker 主要用于离线功能、缓存管理、推送通知等与 Web 应用的网络交互相关的任务。
  • Worker 的生命周期短暂,通常用于即时任务;Service Worker 是长期驻留在后台的,能够在应用关闭后继续运行。

主要功能:

  1. 离线支持:

    • Service Worker 可以缓存网络请求和响应,从而实现离线访问。即使用户没有网络连接,应用仍然可以通过缓存的数据来显示内容。
  2. 拦截和处理网络请求:

    • Service Worker 可以拦截浏览器发出的 HTTP 请求,修改请求或响应数据,甚至返回缓存的资源。这样可以减少请求时间、提高应用的性能。
  3. 推送通知:

    • Service Worker 允许 Web 应用通过浏览器发送推送通知,即使应用处于后台或完全关闭状态。这在移动设备上尤其有用,用来提醒用户有新消息或更新。
  4. 后台同步:

    • Service Worker 支持后台同步(Background Sync),可以在网络恢复时自动同步数据,比如自动上传用户离线时产生的数据。
  5. 精细化的缓存管理:

    • Service Worker 提供了对缓存的精细化控制,你可以选择性地缓存某些资源或根据条件删除缓存的内容。

app.vue

<script setup lang="ts">
import { onMounted ,ref} from 'vue';

if ('serviceWorker' in navigator && 'SyncManager' in window) {
  navigator.serviceWorker.register('sw.js').then(function(registration) {
	 console.log('Service Worker registered with scope:', registration.scope);
	  
  }).catch(function(error) {
	console.error('Service Worker registration failed:', error);
  });
} else {
  console.log('浏览器不支持 PushManager 或 ServiceWorker');
}
//双向通信
const provideMessage = () => {
	if ('serviceWorker' in navigator) {
		navigator.serviceWorker.ready.then(function(registration) {
			return registration.controller.postMessage('Hello, Service Worker!');
		}).then(function(registration) {
			console.log('Background sync registered:', registration);
		}).catch(function(error) {
			console.log('Background sync registration failed:', error);
		});
	}
	// 确保 Service Worker 已经激活并控制页面
   if (navigator.serviceWorker.controller) {
		// 向 Service Worker 发送消息
		navigator.serviceWorker.controller.postMessage('Hello, Service Worker!');
	}
}

//后台同步
const submitFun = () => {
	if ('serviceWorker' in navigator) {
		navigator.serviceWorker.ready.then(function(registration) {
			return registration.sync.register('mySyncTag');
		}).then(function(registration) {
			console.log('Background sync registered:', registration);
		}).catch(function(error) {
			console.log('Background sync registration failed:', error);
		});
	}
}
//消息推送
function requestPermission() {
   Notification.requestPermission().then(permission => {
	 if (permission === "granted") {
	   console.log("用户已授予通知权限");
	 } else {
	   console.log("用户拒绝了通知权限");
	 }
   });	
}
onMounted(()=>{
	requestPermission();		
})

const subscriptionVal = ref<any>();
const tsFun = () =>{
	if ('serviceWorker' in navigator) {
		//先取消上一次次的
		getSubscription();
		navigator.serviceWorker.ready.then(function(registration) {
			// 使用 VAPID 公钥(从服务器获取)进行订阅
			// const vapidKeys = webpush.generateVAPIDKeys();
			// const vapidPublicKey = 'BCskj78qyd73k9k3df7kbskd9sks3kks9n0a9c4f7k';  // 从生成的公钥填入
			const vapidPublicKey = 'BNoZ0H7iJ5YGDLq_2vKNVMIJhGfhVgWzWk2zYVk90BnCJlM-MMtww1eL-VzbzSxBb4xLoYTvu_jKbHWTC9yxyLs';  // 从生成的公钥填入
			registration.pushManager.subscribe({
							 userVisibleOnly: true,
							 applicationServerKey: vapidPublicKey//urlB64ToUint8Array(vapidPublicKey)
			}).then(function(subscription) {
				subscriptionVal.value = subscription;
				console.log('订阅成功:', subscription);
			}).catch(function(error) {
				console.error('订阅时出错:', error);
			});
		})
	}
	
}
const getSubscription = () =>{
	// 获取当前的订阅
	navigator.serviceWorker.getRegistration().then(function(registration) {
	  return registration.pushManager.getSubscription();
	}).then(function(subscription) {
	  if (subscription) {
		// 取消订阅
		return subscription.unsubscribe();
	  }
	}).catch(function(error) {
	  console.error('取消订阅时出错:', error);
	});
}
const startNotification = async () => {
	await fetch('/subscribe', {
	  method: 'POST',
	  body: JSON.stringify(subscriptionVal.value),
	  headers: {
		'Content-Type': 'application/json',
	  },
	});
}
</script>

<template>
  <img alt="Vue logo" src="./assets/avatar.png" /><br/>
  <button @click="provideMessage">信息双向传递</button>
  <button @click="submitFun">点我开始后台同步</button>
  <button @click="tsFun">点我开始订阅消息推送</button>
  <button @click="startNotification" :disabled='!subscriptionVal'>点我开始消息推送</button>
</template>

sw.js

const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/",'/src/assets/avatar.png'];

self.addEventListener("install", event => {
	self.skipWaiting(); // 激活等待中的 Service Worker
	event.waitUntil(
		caches.open(CACHE_NAME).then(cache => {
		  console.log("[Service Worker]", urlsToCache);
		  return cache.addAll(urlsToCache);
		})
	);
});

self.addEventListener("activate", event => {
  // 不在白名单的`CACHE_NAME`就清理
  const cacheWhitelist = ["service-worker-demo"];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
  // 查看一下缓存
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
  );
  event.waitUntil(self.clients.claim()); // 控制当前页面的客户端
});

// service-worker.js
self.addEventListener('message', event => {
  console.log('Received message from main thread:', event.data);
  
  // 回复主线程
  event.source.postMessage('Hello from Service Worker!');
});

//后台推送
self.addEventListener('sync', function(event) {
  if (event.tag === 'mySyncTag') {
    event.waitUntil(syncData());
  }
});

async function syncData(){
	console.log("执行后台任务开始=====>",111)
}

//消息推送
self.addEventListener('push', function(event) {
    var title = '推送通知';
    const options = {
		body: event.data ? event.data.text() : '没有通知内容',
		icon: 'images/icon.png', // 你可以提供一个图标
		badge: 'images/badge.png', // 你可以提供一个徽章图标
	};
    event.waitUntil(self.registration.showNotification(title, options));
});
self.addEventListener('notificationclick', function (event) {
    event.notification.close();
    event.waitUntil(
        clients.openWindow('https://www.example.com')
    );
});

self.addEventListener("fetch", event => {
  const url = new URL(event.request.url);
  if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
    event.respondWith(
      caches.match(event.request).then(resp => {
        if (resp) {
          console.log("fetch ", event.request.url, "有缓存,从缓存中取");
          return resp;
        } else {
          console.log("fetch ", event.request.url, "没有缓存,网络获取");
          return fetch(event.request);
        }
      })
    );
  }
});

server.js 消息发送


const webPush = require('web-push');

// 引入 express
const express = require('express');

// 创建应用对象
const app = express();
// 使用从前端获取的订阅对象
const subscription = {
    "endpoint": "https://fcm.googleapis.com/fcm/send/fcm2EcW8mNE:APA91bHRNP_8z-Zm-3WWZ1v9Hx1lbF8wtQ7cELlLWGmRJLdH2fBguYhxquO0SGFcWk7PTXUb7admJ9PjNxBOXbLzVQDEqPZLcQkNXPI2IlazUvP2zE-rV5i17hpE9aa43qtoDLaBYJ4W",
    "expirationTime": null,
    "keys": {
        "p256dh": "BGbWsBR0q6idsgc9VxRL9yvstQdYTEf-8MpBpUU9h1mdncu9iYTcJx6XTtfOIOG3jFBVpj30--K3ftQU2Nk5fSE",
        "auth": "lAtncP-QOG8nlIOXdtvYPA"
    }
}
// 使用你的 VAPID 公钥和私钥生成授权
const vapidKeys = webPush.generateVAPIDKeys();
webPush.setVapidDetails(
  'mailto:shili@genomics.cn',
  'BDRMFE1zMf1S_uMgh6zC6nnv3tOFOUl9pZoxgy4VA3LHzHcsCQgp5N2FlwfgEJfOEjk_B_oILMGeZVsVo1NSbPA',
  '0YPvfGShlE3UftCffO9sTSUVXpykXXxJkJvg_i4snzk'
  // vapidKeys.publicKey,
  // vapidKeys.privateKey
);
// 创建路由
app.get('/subscribe', (req, res) => {
	webPush.sendNotification(subscription, JSON.stringify({ title: '测试通知', message: '这是一个测试通知' }))
	.then(response => {
		console.log('推送通知发送成功:', response);
	})
	.catch(err => {
		console.error('推送通知发送失败:', err);
	});
    res.end('hello express');
})

// 监听端口
app.listen(30000, () => {
    console.log('服务已启动,端口 30000 正在监听中...');
})

实现效果

功能:双向传递,离线缓存,后台同步均在控制台体现 Service Worker 是一种在浏览器后台运行的 JavaScript 线程,它能够独立于网页和用户交互进行操作。它的主要作用是为 Web 应用程序提供离线功能、推送通知、后台数据同步等能力

他和worker的区别在于:

  • Worker 主要用于后台执行计算任务,避免阻塞主线程;而 Service Worker 主要用于离线功能、缓存管理、推送通知等与 Web 应用的网络交互相关的任务。
  • Worker 的生命周期短暂,通常用于即时任务;Service Worker 是长期驻留在后台的,能够在应用关闭后继续运行。

主要功能:

  1. 离线支持:

    • Service Worker 可以缓存网络请求和响应,从而实现离线访问。即使用户没有网络连接,应用仍然可以通过缓存的数据来显示内容。
  2. 拦截和处理网络请求:

    • Service Worker 可以拦截浏览器发出的 HTTP 请求,修改请求或响应数据,甚至返回缓存的资源。这样可以减少请求时间、提高应用的性能。
  3. 推送通知:

    • Service Worker 允许 Web 应用通过浏览器发送推送通知,即使应用处于后台或完全关闭状态。这在移动设备上尤其有用,用来提醒用户有新消息或更新。
  4. 后台同步:

    • Service Worker 支持后台同步(Background Sync),可以在网络恢复时自动同步数据,比如自动上传用户离线时产生的数据。
  5. 精细化的缓存管理:

    • Service Worker 提供了对缓存的精细化控制,你可以选择性地缓存某些资源或根据条件删除缓存的内容。

app.vue

<script setup lang="ts">
import { onMounted ,ref} from 'vue';

if ('serviceWorker' in navigator && 'SyncManager' in window) {
  navigator.serviceWorker.register('sw.js').then(function(registration) {
	 console.log('Service Worker registered with scope:', registration.scope);
	  
  }).catch(function(error) {
	console.error('Service Worker registration failed:', error);
  });
} else {
  console.log('浏览器不支持 PushManager 或 ServiceWorker');
}
//双向通信
const provideMessage = () => {
	if ('serviceWorker' in navigator) {
		navigator.serviceWorker.ready.then(function(registration) {
			return registration.controller.postMessage('Hello, Service Worker!');
		}).then(function(registration) {
			console.log('Background sync registered:', registration);
		}).catch(function(error) {
			console.log('Background sync registration failed:', error);
		});
	}
	// 确保 Service Worker 已经激活并控制页面
   if (navigator.serviceWorker.controller) {
		// 向 Service Worker 发送消息
		navigator.serviceWorker.controller.postMessage('Hello, Service Worker!');
	}
}

//后台同步
const submitFun = () => {
	if ('serviceWorker' in navigator) {
		navigator.serviceWorker.ready.then(function(registration) {
			return registration.sync.register('mySyncTag');
		}).then(function(registration) {
			console.log('Background sync registered:', registration);
		}).catch(function(error) {
			console.log('Background sync registration failed:', error);
		});
	}
}
//消息推送
function requestPermission() {
   Notification.requestPermission().then(permission => {
	 if (permission === "granted") {
	   console.log("用户已授予通知权限");
	 } else {
	   console.log("用户拒绝了通知权限");
	 }
   });	
}
onMounted(()=>{
	requestPermission();		
})

const subscriptionVal = ref<any>();
const tsFun = () =>{
	if ('serviceWorker' in navigator) {
		//先取消上一次次的
		getSubscription();
		navigator.serviceWorker.ready.then(function(registration) {
			// 使用 VAPID 公钥(从服务器获取)进行订阅
			// const vapidKeys = webpush.generateVAPIDKeys();
			// const vapidPublicKey = 'BCskj78qyd73k9k3df7kbskd9sks3kks9n0a9c4f7k';  // 从生成的公钥填入
			const vapidPublicKey = 'BNoZ0H7iJ5YGDLq_2vKNVMIJhGfhVgWzWk2zYVk90BnCJlM-MMtww1eL-VzbzSxBb4xLoYTvu_jKbHWTC9yxyLs';  // 从生成的公钥填入
			registration.pushManager.subscribe({
							 userVisibleOnly: true,
							 applicationServerKey: vapidPublicKey//urlB64ToUint8Array(vapidPublicKey)
			}).then(function(subscription) {
				subscriptionVal.value = subscription;
				console.log('订阅成功:', subscription);
			}).catch(function(error) {
				console.error('订阅时出错:', error);
			});
		})
	}
	
}
const getSubscription = () =>{
	// 获取当前的订阅
	navigator.serviceWorker.getRegistration().then(function(registration) {
	  return registration.pushManager.getSubscription();
	}).then(function(subscription) {
	  if (subscription) {
		// 取消订阅
		return subscription.unsubscribe();
	  }
	}).catch(function(error) {
	  console.error('取消订阅时出错:', error);
	});
}
const startNotification = async () => {
	await fetch('/subscribe', {
	  method: 'POST',
	  body: JSON.stringify(subscriptionVal.value),
	  headers: {
		'Content-Type': 'application/json',
	  },
	});
}
</script>

<template>
  <img alt="Vue logo" src="./assets/avatar.png" /><br/>
  <button @click="provideMessage">信息双向传递</button>
  <button @click="submitFun">点我开始后台同步</button>
  <button @click="tsFun">点我开始订阅消息推送</button>
  <button @click="startNotification" :disabled='!subscriptionVal'>点我开始消息推送</button>
</template>

sw.js

const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/",'/src/assets/avatar.png'];

self.addEventListener("install", event => {
	self.skipWaiting(); // 激活等待中的 Service Worker
	event.waitUntil(
		caches.open(CACHE_NAME).then(cache => {
		  console.log("[Service Worker]", urlsToCache);
		  return cache.addAll(urlsToCache);
		})
	);
});

self.addEventListener("activate", event => {
  // 不在白名单的`CACHE_NAME`就清理
  const cacheWhitelist = ["service-worker-demo"];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
  // 查看一下缓存
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
  );
  event.waitUntil(self.clients.claim()); // 控制当前页面的客户端
});

// service-worker.js
self.addEventListener('message', event => {
  console.log('Received message from main thread:', event.data);
  
  // 回复主线程
  event.source.postMessage('Hello from Service Worker!');
});

//后台推送
self.addEventListener('sync', function(event) {
  if (event.tag === 'mySyncTag') {
    event.waitUntil(syncData());
  }
});

async function syncData(){
	console.log("执行后台任务开始=====>",111)
}

//消息推送
self.addEventListener('push', function(event) {
    var title = '推送通知';
    const options = {
		body: event.data ? event.data.text() : '没有通知内容',
		icon: 'images/icon.png', // 你可以提供一个图标
		badge: 'images/badge.png', // 你可以提供一个徽章图标
	};
    event.waitUntil(self.registration.showNotification(title, options));
});
self.addEventListener('notificationclick', function (event) {
    event.notification.close();
    event.waitUntil(
        clients.openWindow('https://www.example.com')
    );
});

self.addEventListener("fetch", event => {
  const url = new URL(event.request.url);
  if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
    event.respondWith(
      caches.match(event.request).then(resp => {
        if (resp) {
          console.log("fetch ", event.request.url, "有缓存,从缓存中取");
          return resp;
        } else {
          console.log("fetch ", event.request.url, "没有缓存,网络获取");
          return fetch(event.request);
        }
      })
    );
  }
});

server.js 消息发送


const webPush = require('web-push');

// 引入 express
const express = require('express');

// 创建应用对象
const app = express();
// 使用从前端获取的订阅对象
const subscription = {
    "endpoint": "https://fcm.googleapis.com/fcm/send/fcm2EcW8mNE:APA91bHRNP_8z-Zm-3WWZ1v9Hx1lbF8wtQ7cELlLWGmRJLdH2fBguYhxquO0SGFcWk7PTXUb7admJ9PjNxBOXbLzVQDEqPZLcQkNXPI2IlazUvP2zE-rV5i17hpE9aa43qtoDLaBYJ4W",
    "expirationTime": null,
    "keys": {
        "p256dh": "BGbWsBR0q6idsgc9VxRL9yvstQdYTEf-8MpBpUU9h1mdncu9iYTcJx6XTtfOIOG3jFBVpj30--K3ftQU2Nk5fSE",
        "auth": "lAtncP-QOG8nlIOXdtvYPA"
    }
}
// 使用你的 VAPID 公钥和私钥生成授权
const vapidKeys = webPush.generateVAPIDKeys();
webPush.setVapidDetails(
  'mailto:shili@genomics.cn',
  'BDRMFE1zMf1S_uMgh6zC6nnv3tOFOUl9pZoxgy4VA3LHzHcsCQgp5N2FlwfgEJfOEjk_B_oILMGeZVsVo1NSbPA',
  '0YPvfGShlE3UftCffO9sTSUVXpykXXxJkJvg_i4snzk'
  // vapidKeys.publicKey,
  // vapidKeys.privateKey
);
// 创建路由
app.get('/subscribe', (req, res) => {
	webPush.sendNotification(subscription, JSON.stringify({ title: '测试通知', message: '这是一个测试通知' }))
	.then(response => {
		console.log('推送通知发送成功:', response);
	})
	.catch(err => {
		console.error('推送通知发送失败:', err);
	});
    res.end('hello express');
})

// 监听端口
app.listen(30000, () => {
    console.log('服务已启动,端口 30000 正在监听中...');
})

实现效果

0e8d47dc-3f01-4873-995d-e52c0e1dcb4b.png 功能:双向传递,离线缓存,后台同步均在控制台体现

image.png