uni-app 通知系统

核心知识点讲解

1. 通知系统概述

通知系统是移动应用中非常重要的功能,它可以:

  • 及时向用户传递重要信息
  • 提高用户活跃度和留存率
  • 实现个性化的用户提醒
  • 支持应用内和应用外的消息触达

2. 通知类型

在 uni-app 中,通知主要分为以下几种类型:

2.1 本地通知

  • 定义:由应用本地触发的通知,不需要网络连接
  • 特点:实时性高,无需后端支持,适合定时提醒等场景
  • API:使用 uni.createLocalNotification() 创建

2.2 推送通知

  • 定义:由服务器通过推送服务发送的通知
  • 特点:需要后端支持,可以实现远程消息触达
  • 服务:支持极光推送、个推、Firebase Cloud Messaging 等

2.3 应用内通知

  • 定义:仅在应用内部显示的通知
  • 特点:不需要系统权限,实现灵活
  • 场景:应用内消息、更新提醒、活动通知等

3. 通知触发条件

通知的触发条件主要包括:

  • 时间触发:定时提醒、闹钟、日程安排
  • 事件触发:用户操作、系统事件、网络状态变化
  • 状态触发:订单状态更新、消息到达、任务完成
  • 位置触发:基于地理位置的通知(地理围栏)

4. 通知展示方式

通知的展示方式有多种形式:

  • 系统通知栏:显示在设备的通知中心
  • 应用内弹窗:在应用内部弹出的通知
  • 顶部通知条:显示在应用顶部的通知条
  • 徽章通知:应用图标上的数字徽章
  • 声音和振动:通过声音和振动提醒用户

5. 通知权限管理

在实现通知功能时,需要注意权限管理:

  • iOS 权限:需要在 info.plist 中配置通知权限
  • Android 权限:需要在 AndroidManifest.xml 中配置通知权限
  • 权限申请:使用 uni.requestSubscribeMessage() 申请订阅消息权限

实用案例分析

案例:实现完整的通知系统

功能需求

我们需要实现一个包含以下功能的通知系统:

  1. 本地定时通知
  2. 应用内通知
  3. 服务器推送通知
  4. 通知管理中心

实现方案

1. 本地通知实现
// 1. 检查通知权限
uni.getSetting({
  success: (res) => {
    if (!res.authSetting['notification']) {
      // 申请通知权限
      uni.requestSubscribeMessage({
        tmplIds: [], // 模板ID数组
        success: (res) => {
          console.log('订阅消息成功', res);
        },
        fail: (err) => {
          console.log('订阅消息失败', err);
        }
      });
    }
  }
});

// 2. 创建本地通知
function createLocalNotification(title, content, time) {
  uni.createLocalNotification({
    title: title,
    content: content,
    payload: {"key": "value"},
    // 定时通知,单位毫秒
    // 如果不设置,则立即发送
    // time: Date.now() + time
  });
}

// 3. 监听通知点击事件
uni.onLocalNotificationClick((res) => {
  console.log('点击了本地通知', res);
  // 可以根据通知的 payload 进行相应的操作
  uni.navigateTo({
    url: '/pages/detail/detail?id=' + res.payload.id
  });
});
2. 应用内通知组件
<template>
  <view class="notification-container">
    <!-- 顶部通知条 -->
    <view v-if="showTopNotification" class="top-notification">
      <text>{{ topNotification.content }}</text>
      <view class="close-btn" @click="closeTopNotification">×</view>
    </view>
    
    <!-- 通知中心 -->
    <view class="notification-center">
      <view class="notification-header">
        <text class="title">通知中心</text>
        <text class="clear-btn" @click="clearAllNotifications">清空</text>
      </view>
      <view class="notification-list">
        <view 
          v-for="(notification, index) in notifications" 
          :key="index"
          class="notification-item"
          :class="{ 'unread': !notification.read }"
          @click="markAsRead(index)"
        >
          <view class="notification-icon">
            <text>{{ getNotificationIcon(notification.type) }}</text>
          </view>
          <view class="notification-content">
            <text class="notification-title">{{ notification.title }}</text>
            <text class="notification-desc">{{ notification.content }}</text>
            <text class="notification-time">{{ formatTime(notification.time) }}</text>
          </view>
          <view v-if="!notification.read" class="unread-dot"></view>
        </view>
        <view v-if="notifications.length === 0" class="empty-notification">
          <text>暂无通知</text>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      showTopNotification: false,
      topNotification: {
        content: ''
      },
      notifications: [
        {
          id: 1,
          title: '系统通知',
          content: '您的账号已成功登录',
          type: 'system',
          time: Date.now(),
          read: false
        },
        {
          id: 2,
          title: '活动通知',
          content: '限时优惠活动开始了',
          type: 'activity',
          time: Date.now() - 3600000,
          read: true
        }
      ]
    };
  },
  methods: {
    // 显示顶部通知
    showTopNotification(content) {
      this.topNotification.content = content;
      this.showTopNotification = true;
      // 3秒后自动关闭
      setTimeout(() => {
        this.closeTopNotification();
      }, 3000);
    },
    
    // 关闭顶部通知
    closeTopNotification() {
      this.showTopNotification = false;
    },
    
    // 标记为已读
    markAsRead(index) {
      this.notifications[index].read = true;
    },
    
    // 清空所有通知
    clearAllNotifications() {
      this.notifications = [];
    },
    
    // 获取通知图标
    getNotificationIcon(type) {
      const icons = {
        system: '📢',
        activity: '🎉',
        message: '💬',
        order: '📦'
      };
      return icons[type] || '📢';
    },
    
    // 格式化时间
    formatTime(time) {
      const date = new Date(time);
      return date.toLocaleString();
    },
    
    // 添加通知
    addNotification(notification) {
      this.notifications.unshift({
        id: Date.now(),
        read: false,
        time: Date.now(),
        ...notification
      });
      // 显示顶部通知
      this.showTopNotification(notification.content);
    }
  }
};
</script>

<style scoped>
.notification-container {
  padding: 20rpx;
}

/* 顶部通知条样式 */
.top-notification {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  background-color: #007AFF;
  color: white;
  padding: 15rpx 20rpx;
  display: flex;
  justify-content: space-between;
  align-items: center;
  z-index: 9999;
  animation: slideDown 0.3s ease;
}

.close-btn {
  font-size: 32rpx;
  font-weight: bold;
}

/* 通知中心样式 */
.notification-center {
  margin-top: 20rpx;
}

.notification-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}

.title {
  font-size: 28rpx;
  font-weight: bold;
}

.clear-btn {
  font-size: 24rpx;
  color: #007AFF;
}

/* 通知列表样式 */
.notification-list {
  background-color: #F5F5F5;
  border-radius: 10rpx;
  overflow: hidden;
}

.notification-item {
  display: flex;
  align-items: center;
  padding: 20rpx;
  background-color: white;
  margin-bottom: 1rpx;
  position: relative;
}

.notification-item.unread {
  background-color: #F8F8F8;
}

.notification-icon {
  width: 60rpx;
  height: 60rpx;
  border-radius: 50%;
  background-color: #F0F0F0;
  display: flex;
  justify-content: center;
  align-items: center;
  margin-right: 20rpx;
  font-size: 32rpx;
}

.notification-content {
  flex: 1;
}

.notification-title {
  font-size: 26rpx;
  font-weight: 500;
  margin-bottom: 8rpx;
  display: block;
}

.notification-desc {
  font-size: 22rpx;
  color: #666;
  margin-bottom: 8rpx;
  display: block;
}

.notification-time {
  font-size: 20rpx;
  color: #999;
}

.unread-dot {
  position: absolute;
  top: 20rpx;
  right: 20rpx;
  width: 10rpx;
  height: 10rpx;
  border-radius: 50%;
  background-color: #FF3B30;
}

.empty-notification {
  padding: 60rpx 0;
  text-align: center;
  color: #999;
}

/* 动画效果 */
@keyframes slideDown {
  from {
    transform: translateY(-100%);
  }
  to {
    transform: translateY(0);
  }
}
</style>
3. 推送通知集成

以极光推送为例,实现服务器推送通知:

// 1. 安装极光推送插件
// 在 HBuilderX 中搜索并安装 "极光推送" 插件

// 2. 初始化极光推送
const jpush = uni.requireNativePlugin('JG-JPush');
jpush.init();

// 3. 注册设备
jpush.getRegistrationID({
  success: (res) => {
    console.log('RegistrationID:', res.registerID);
    // 将 RegistrationID 发送到服务器
    // 用于服务器向特定设备发送推送
  }
});

// 4. 监听推送消息
jpush.addNotificationListener({
  success: (res) => {
    console.log('收到推送消息:', res);
    // 处理推送消息
    if (res.notification) {
      // 显示通知
      uni.showToast({
        title: res.notification.alert,
        duration: 2000
      });
    }
  }
});

// 5. 监听点击通知事件
jpush.addReceiveOpenNotificationListener({
  success: (res) => {
    console.log('点击了推送通知:', res);
    // 处理点击通知后的跳转逻辑
    if (res.extras && res.extras.url) {
      uni.navigateTo({
        url: res.extras.url
      });
    }
  }
});
4. 通知管理中心
<template>
  <view class="notification-settings">
    <view class="setting-section">
      <text class="section-title">通知设置</text>
      
      <!-- 通知类型开关 -->
      <view class="setting-item">
        <text class="setting-label">系统通知</text>
        <switch 
          :checked="notificationSettings.system" 
          @change="toggleSetting('system', $event)"
        />
      </view>
      
      <view class="setting-item">
        <text class="setting-label">活动通知</text>
        <switch 
          :checked="notificationSettings.activity" 
          @change="toggleSetting('activity', $event)"
        />
      </view>
      
      <view class="setting-item">
        <text class="setting-label">消息通知</text>
        <switch 
          :checked="notificationSettings.message" 
          @change="toggleSetting('message', $event)"
        />
      </view>
      
      <view class="setting-item">
        <text class="setting-label">订单通知</text>
        <switch 
          :checked="notificationSettings.order" 
          @change="toggleSetting('order', $event)"
        />
      </view>
    </view>
    
    <view class="setting-section">
      <text class="section-title">通知样式</text>
      
      <!-- 通知样式设置 -->
      <view class="setting-item">
        <text class="setting-label">声音</text>
        <switch 
          :checked="notificationSettings.sound" 
          @change="toggleSetting('sound', $event)"
        />
      </view>
      
      <view class="setting-item">
        <text class="setting-label">振动</text>
        <switch 
          :checked="notificationSettings.vibration" 
          @change="toggleSetting('vibration', $event)"
        />
      </view>
      
      <view class="setting-item">
        <text class="setting-label">徽章</text>
        <switch 
          :checked="notificationSettings.badge" 
          @change="toggleSetting('badge', $event)"
        />
      </view>
    </view>
    
    <view class="setting-section">
      <text class="section-title">通知权限</text>
      <button type="primary" @click="checkNotificationPermission">检查通知权限</button>
      <button type="default" @click="requestNotificationPermission">申请通知权限</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      notificationSettings: {
        system: true,
        activity: true,
        message: true,
        order: true,
        sound: true,
        vibration: true,
        badge: true
      }
    };
  },
  mounted() {
    // 从本地存储加载设置
    this.loadSettings();
  },
  methods: {
    // 切换设置
    toggleSetting(key, event) {
      this.notificationSettings[key] = event.detail.value;
      // 保存设置到本地存储
      this.saveSettings();
    },
    
    // 保存设置
    saveSettings() {
      uni.setStorageSync('notificationSettings', this.notificationSettings);
    },
    
    // 加载设置
    loadSettings() {
      const settings = uni.getStorageSync('notificationSettings');
      if (settings) {
        this.notificationSettings = settings;
      }
    },
    
    // 检查通知权限
    checkNotificationPermission() {
      uni.getSetting({
        success: (res) => {
          const hasPermission = res.authSetting['notification'] || res.authSetting['notify'];
          uni.showToast({
            title: hasPermission ? '已获得通知权限' : '未获得通知权限',
            duration: 2000
          });
        }
      });
    },
    
    // 申请通知权限
    requestNotificationPermission() {
      uni.requestSubscribeMessage({
        tmplIds: [], // 模板ID数组
        success: (res) => {
          console.log('申请通知权限成功', res);
          uni.showToast({
            title: '申请通知权限成功',
            duration: 2000
          });
        },
        fail: (err) => {
          console.log('申请通知权限失败', err);
          uni.showToast({
            title: '申请通知权限失败',
            duration: 2000,
            icon: 'none'
          });
        }
      });
    }
  }
};
</script>

<style scoped>
.notification-settings {
  padding: 20rpx;
}

.setting-section {
  background-color: white;
  border-radius: 10rpx;
  padding: 20rpx;
  margin-bottom: 20rpx;
}

.section-title {
  font-size: 26rpx;
  font-weight: bold;
  margin-bottom: 20rpx;
  display: block;
}

.setting-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20rpx 0;
  border-bottom: 1rpx solid #F0F0F0;
}

.setting-item:last-child {
  border-bottom: none;
}

.setting-label {
  font-size: 24rpx;
  color: #333;
}

button {
  margin-top: 20rpx;
}
</style>

代码优化建议

1. 通知去重和合并

当有多个相似通知时,可以考虑去重和合并,避免通知轰炸:

// 通知去重函数
function deduplicateNotifications(notifications) {
  const uniqueNotifications = [];
  const seen = new Set();
  
  for (const notification of notifications) {
    const key = `${notification.type}-${notification.content}`;
    if (!seen.has(key)) {
      seen.add(key);
      uniqueNotifications.push(notification);
    }
  }
  
  return uniqueNotifications;
}

// 通知合并函数
function mergeSimilarNotifications(notifications) {
  const merged = [];
  const groups = {};
  
  for (const notification of notifications) {
    if (!groups[notification.type]) {
      groups[notification.type] = [];
    }
    groups[notification.type].push(notification);
  }
  
  // 合并同一类型的通知
  for (const type in groups) {
    const group = groups[type];
    if (group.length > 1) {
      // 创建合并通知
      merged.push({
        id: Date.now(),
        title: `${type}通知`,
        content: `您有 ${group.length} 条新${type}通知`,
        type: type,
        time: Date.now(),
        read: false,
        count: group.length
      });
    } else if (group.length === 1) {
      merged.push(group[0]);
    }
  }
  
  return merged;
}

2. 通知频率限制

为了避免过多的通知打扰用户,可以实现通知频率限制:

// 通知频率限制
const notificationLimits = {
  minInterval: 60000, // 最小通知间隔(毫秒)
  maxPerHour: 10, // 每小时最大通知数
  maxPerDay: 50 // 每天最大通知数
};

// 通知记录
const notificationHistory = [];

// 检查通知是否可以发送
function canSendNotification() {
  const now = Date.now();
  
  // 过滤出最近一小时的通知
  const recentNotifications = notificationHistory.filter(n => now - n.time < 3600000);
  
  // 过滤出今天的通知
  const today = new Date().setHours(0, 0, 0, 0);
  const todayNotifications = notificationHistory.filter(n => n.time >= today);
  
  // 检查最近一次通知的时间间隔
  const lastNotification = notificationHistory[notificationHistory.length - 1];
  const timeSinceLast = lastNotification ? now - lastNotification.time : Infinity;
  
  // 检查限制条件
  if (timeSinceLast < notificationLimits.minInterval) {
    return false;
  }
  
  if (recentNotifications.length >= notificationLimits.maxPerHour) {
    return false;
  }
  
  if (todayNotifications.length >= notificationLimits.maxPerDay) {
    return false;
  }
  
  return true;
}

// 发送通知前检查
function sendNotification(notification) {
  if (canSendNotification()) {
    // 发送通知
    console.log('发送通知:', notification);
    
    // 记录通知
    notificationHistory.push({
      time: Date.now(),
      type: notification.type
    });
    
    // 限制历史记录长度
    if (notificationHistory.length > 100) {
      notificationHistory.shift();
    }
    
    return true;
  } else {
    console.log('通知频率过高,跳过发送');
    return false;
  }
}

3. 通知优先级管理

实现通知优先级管理,确保重要通知能够及时送达:

// 通知优先级
const NOTIFICATION_PRIORITY = {
  HIGH: 3,
  MEDIUM: 2,
  LOW: 1
};

// 通知队列
const notificationQueue = [];

// 添加通知到队列
function addToNotificationQueue(notification, priority = NOTIFICATION_PRIORITY.MEDIUM) {
  notificationQueue.push({
    ...notification,
    priority,
    addedTime: Date.now()
  });
  
  // 按优先级排序
  notificationQueue.sort((a, b) => b.priority - a.priority);
  
  // 处理队列
  processNotificationQueue();
}

// 处理通知队列
function processNotificationQueue() {
  if (notificationQueue.length > 0 && canSendNotification()) {
    const notification = notificationQueue.shift();
    sendNotification(notification);
    
    // 继续处理队列
    setTimeout(processNotificationQueue, 1000);
  }
}

总结

本教程详细介绍了 uni-app 通知系统的实现方法,包括:

  1. 通知类型:本地通知、推送通知、应用内通知
  2. 触发条件:时间触发、事件触发、状态触发、位置触发
  3. 展示方式:系统通知栏、应用内弹窗、顶部通知条、徽章通知
  4. 权限管理:iOS 和 Android 的通知权限配置和申请
  5. 完整实现:本地通知、应用内通知、服务器推送通知、通知管理中心

通过本教程的学习,您应该能够:

  • 理解通知系统的核心概念和实现原理
  • 掌握本地通知和推送通知的实现方法
  • 开发功能完整的通知管理中心
  • 优化通知系统的用户体验

通知系统是移动应用中非常重要的功能,合理的通知设计可以提高用户活跃度和留存率,为用户提供更好的使用体验。在实现通知功能时,需要注意平衡通知的及时性和用户体验,避免过度打扰用户。

« 上一篇 uni-app 消息系统 下一篇 » uni-app 评论系统