You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Django管理员跨设备调用指定设备的Service Worker函数?

跨设备触发Service Worker函数实现方案

要实现管理员点击按钮后,在指定设备的Service Worker中调用needsToBeCalled(a,b),需要通过Web Push API完成后端到前端Service Worker的消息推送,以下是具体实现步骤:

1. 配置Web Push环境

安装依赖与生成VAPID密钥

在Django项目中安装django-webpush:

pip install django-webpush

生成VAPID密钥对(用于推送鉴权):

webpush generate_vapid_keys

将生成的公钥和私钥添加到Django项目的settings.py:

INSTALLED_APPS = [
    # ... 其他应用
    'webpush',
]

WEBPUSH_SETTINGS = {
    "VAPID_PUBLIC_KEY": "你的公钥",
    "VAPID_PRIVATE_KEY": "你的私钥",
    "VAPID_ADMIN_EMAIL": "管理员邮箱"
}

2. 存储设备的Push订阅信息

创建订阅存储模型

在应用的models.py中创建模型,保存用户设备的Push订阅信息:

from django.db import models
from django.contrib.auth.models import User

class DeviceSubscription(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    subscription = models.JSONField()
    device_name = models.CharField(max_length=100, blank=True)  # 可选:用于标记设备

    def __str__(self):
        return f"{self.user.username} - {self.device_name or '未知设备'}"

执行迁移:

python manage.py makemigrations
python manage.py migrate

前端注册Push订阅并提交到后端

在用户访问的页面中添加JS代码,注册Push订阅并发送到后端:

// 注册Service Worker后,请求Push订阅
async function registerPushSubscription() {
    const swRegistration = await navigator.serviceWorker.ready;
    const subscription = await swRegistration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array("你的VAPID公钥")
    });

    // 将订阅信息提交到Django后端
    await fetch('/save-subscription/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCookie('csrftoken')  // 需实现获取CSRF Token的函数
        },
        body: JSON.stringify({
            subscription: subscription,
            device_name: prompt('请输入设备名称:')  // 可选:让用户标记设备
        })
    });
}

// 辅助函数:将Base64公钥转换为Uint8Array
function urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
    const rawData = window.atob(base64);
    return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
}

// 获取CSRF Token的函数
function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

// 页面加载完成后注册订阅
window.addEventListener('load', () => {
    if ('serviceWorker' in navigator && 'PushManager' in window) {
        registerPushSubscription();
    }
});

后端接收订阅的视图

在应用的views.py中添加保存订阅的视图:

from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from .models import DeviceSubscription
import json

@require_POST
@csrf_exempt
def save_subscription(request):
    data = json.loads(request.body)
    subscription = data.get('subscription')
    device_name = data.get('device_name', '')
    user = request.user

    # 检查是否已存在该订阅,存在则更新,否则创建
    DeviceSubscription.objects.update_or_create(
        user=user,
        subscription=subscription,
        defaults={'device_name': device_name}
    )

    return JsonResponse({'status': 'success'})

在urls.py中添加路由:

from django.urls import path
from . import views

urlpatterns = [
    # ... 其他路由
    path('save-subscription/', views.save_subscription, name='save_subscription'),
]

3. 管理员触发推送的后端API与前端按钮

后端推送视图

在views.py中添加处理管理员推送请求的视图:

from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from .models import DeviceSubscription
from webpush import send_user_notification
import json

@require_POST
@csrf_exempt
def trigger_function(request):
    data = json.loads(request.body)
    a = data.get('a')
    b = data.get('b')
    selected_device_ids = data.get('selected_device_ids', [])  # 选中的设备ID列表

    # 获取选中的设备订阅
    subscriptions = DeviceSubscription.objects.filter(id__in=selected_device_ids)

    # 向每个订阅发送推送消息,携带参数a和b
    for sub in subscriptions:
        payload = json.dumps({
            'type': 'call_function',
            'a': a,
            'b': b
        })
        send_user_notification(
            user=sub.user,
            subscription=sub.subscription,
            payload=payload,
            title='触发函数执行'  # 可选:通知标题,若不需要可省略
        )

    return JsonResponse({'status': 'success', 'count': len(subscriptions)})

添加路由到urls.py:

path('trigger-function/', views.trigger_function, name='trigger_function'),

管理员页面的触发按钮

在管理员页面添加按钮和JS代码,用于选择设备并触发推送:

<!-- 设备选择下拉框 -->
<select id="deviceSelect" multiple>
    {% for device in devices %}
        <option value="{{ device.id }}">{{ device.user.username }} - {{ device.device_name }}</option>
    {% endfor %}
</select>

<!-- 触发按钮 -->
<button id="triggerBtn">触发函数执行</button>

<script>
document.getElementById('triggerBtn').addEventListener('click', async () => {
    const selectedDevices = Array.from(document.getElementById('deviceSelect').selectedOptions).map(opt => opt.value);
    const a = prompt('请输入参数a:');
    const b = prompt('请输入参数b:');

    if (!selectedDevices.length || !a || !b) {
        alert('请选择设备并输入参数');
        return;
    }

    const response = await fetch('/trigger-function/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCookie('csrftoken')
        },
        body: JSON.stringify({
            a: a,
            b: b,
            selected_device_ids: selectedDevices
        })
    });

    const result = await response.json();
    alert(`已向${result.count}台设备发送触发指令`);
});

// 复用之前的getCookie函数
function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}
</script>

注意:管理员页面需要获取所有设备订阅数据,可通过视图查询DeviceSubscription.objects.all()并传递到模板。

4. 修改Service Worker监听Push事件

更新你的serviceworker.js,添加push事件监听,收到推送消息后调用needsToBeCalled:

var staticCacheName = 'djangopwa-v1';

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(staticCacheName).then(function(cache) {
      return cache.addAll([
        '/base_layout'
      ]);
    })
  );
});

self.addEventListener('fetch', function(event) {
  var requestUrl = new URL(event.request.url);
    if (requestUrl.origin === location.origin) {
      if ((requestUrl.pathname === '/')) {
        event.respondWith(caches.match('/base_layout'));
        return;
      }
    }
    event.respondWith(
      caches.match(event.request).then(function(response) {
        return response || fetch(event.request);
      })
    );
});

// 监听Push事件
self.addEventListener('push', function(event) {
  if (!event.data) return;

  const payload = event.data.json();
  // 判断消息类型为触发函数
  if (payload.type === 'call_function') {
    const { a, b } = payload;
    // 调用目标函数
    needsToBeCalled(a, b);
  }
});

function needsToBeCalled(a, b) {
    console.log(a + b);
    // 可添加其他业务逻辑,比如显示通知
    self.registration.showNotification('函数已执行', {
        body: `参数a: ${a}, 参数b: ${b}, 结果: ${a + b}`
    });
}

注意事项

  • 确保网站使用HTTPS(localhost可例外),否则Service Worker和Push API无法正常工作
  • 设备需允许浏览器通知权限,否则无法完成Push订阅
  • 若不需要显示通知,可移除self.registration.showNotification部分,但userVisibleOnly需保持true(浏览器要求推送消息必须对用户可见)

内容的提问来源于stack exchange,提问作者miaa26

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 15:55:04