如何用Spring Boot、Angular和Firebase Cloud Messaging发送通知

1,092 阅读15分钟

使用Spring Boot、Angular和Firebase云信息发送通知

通知是提高用户参与度的一个好方法。通过让用户了解你的应用中他们感兴趣的事件,你可以让他们不断地回来找你。

许多应用程序甚至依靠通知作为核心功能。例如,一些提醒应用程序帮助你记住重要事件。如果没有能够在不活动时通知你,它们会是什么?不管怎么说,作为一个开发者,发送通知是一项重要的技能。

在本指南中,我们将学习如何向Angular应用程序发送通知。为了实现这一点,我们将在Firebase Cloud Messaging的帮助下使用Spring Boot后端。

在本指南结束时,你应该对如何在你的下一个全栈应用程序中发送通知有一个很好的理解。

前提条件

要继续学习本教程,你需要具备以下条件。

  • 基本的Angular知识,包括CLI、HTTP客户端和基本的模板设计。
  • 基本的Spring Boot概念。这包括Spring MVC和基本的设计模式(即[Bean],和[定型注解])。
  • 最好有一些Kotlin经验,因为我们将使用它。但这并不是必须的,因为所有的概念对于一个纯Java开发者来说都是可以理解的。
  • 最好能掌握[构建器设计模式],因为我们将在后端大量使用它。

了解我们项目的高层架构

概述

首先,重要的是要从高层次上了解这个项目是如何工作的。

当用户第一次打开应用程序时,我们请求允许发送通知。如果他们同意,那么Firebase将发送一个令牌来识别他们的设备。然后,客户端将令牌发送给我们的Spring Boot应用,这样我们就可以用它来向该用户发送通知。

每当我们的后端想要发送通知时,它就会向Firebase提供关于所需通知的细节。从那里,Firebase后端会将通知发送到正确的设备上。

在客户端会发生什么?

在客户端,我们会在应用程序中显示消息,或者作为一个通知弹出窗口。前者是在我们的应用程序被打开时,后者是在我们的应用程序被关闭时。

虽然你可能会问:当我们的应用程序处于非活动状态时,我们如何显示通知?我们会在服务工作者的帮助下做到这一点。

什么是服务工作者?

服务工作者是一个特殊的脚本,它在与你的应用程序分开的线程上运行。它允许你拦截请求,缓存数据供离线使用,在我们的例子中,还可以发送通知。

由于服务工作者与我们的应用程序是分开的,因此我们可以使用它来发送通知,即使应用程序处于非活动状态。不过,当我们的应用程序处于活动状态时,我们将让我们的angular项目通过在页面上显示消息来处理它。

在服务器端会发生什么?

在服务器端,我们的Spring Boot应用程序将使用Firebase的SDK,称为Admin SDK。这个SDK允许我们的应用程序与Firebase交互,为我们发送通知。

当我们在Firebase控制台初始化Firebase时,他们会给我们一个特殊的JSON文件,我们将用它来授权我们的Spring Boot应用发送通知。

每当我们想发送通知时,我们必须创建一个Message 对象。这将包含关于我们要发送的通知的所有信息。

这将包括标题、描述、图标URL,以及任何平台特定的信息。

我们有两种方式来发送通知--主题或直接通知。

主题通知与直接通知

主题通知是一种带有指定标签的通知,称为主题。用户将订阅任何带有他们选择的主题的信息,以获得通知。

每当用户订阅一个主题时,他们会把他们的令牌和要订阅的主题的名称发送到我们的Spring Boot应用程序。利用这一点,我们可以告诉Firebase向他们发送关于该主题的通知。

此外,我们也可以选择发送直接通知。在这里,我们在Message 对象中,指定要通知的用户的token。然后,Firebase就会把通知发送给那个特定的用户。

说明整个架构

为了说明整个项目是如何工作的,这里有一张我制作的方便的流程图。希望这能消除你对架构的任何疑惑。

a flow chart to illustrate the architecture

设置我们的后端

初始化我们的Spring Boot应用程序

像往常一样,我们首先使用Spring初始化器生成一个Spring Boot项目。

我们将选择Kotlin 作为语言,Maven 作为依赖管理器,打包为jar ,而Java版本为11 。对于我们的依赖关系,这里我们唯一需要的是Spring Web依赖关系。

当然,要确保设置组和工件的ID,以及包名和项目名。

设置Firebase

  1. 用你的谷歌账户登录到Firebase网站
  2. 点击右上角的 "转到控制台"。
  3. 选择添加项目来创建一个新的Firebase项目。在那里,它将指导你创建项目,这应该是非常简单的。
  4. 按下左边项目概览按钮旁边的齿轮图标,然后选择项目设置
  5. 项目设置下的上部区域点击服务账户
  6. 下面,生成一个新的私钥。这个私钥就是我前面提到的JSON文件,用来授权我们的后端。
  7. application.properties 文件中添加一个属性,并注明私钥的文件路径。
app.firebase-config-file=firebase-config/[your-file-name-goes-here].json

现在我们有了私钥,我们就可以开始将Firebase与我们的Spring Boot应用整合起来。

首先,将JSON文件放在resources 文件夹中的一个名为firebase-config 的新文件夹下。接下来,我们需要使用Maven将Firebase admin SDK加入我们的项目。

pom.xml 文件的依赖项标签中插入以下依赖项。

<dependency>
    <groupId>com.google.Firebase</groupId>
    <artifactId>Firebase-admin</artifactId>
    <version>7.2.0</version>
</dependency>

然后,我们需要创建一个新的服务Bean,用来将Firebase添加到你的后端。使用@Value 注解,我们首先将私钥的文件路径注入到一个字段。

@Service
class FirebaseInitializer {
    @Value("\${app.Firebase-config-file}")
    lateinit var FirebaseConfigPath: String
}

如果你不知道的话,@Value 注解是将application.properties 文件中的值注入到一个字段中。

在这里,我们在$ 前面加了一个\ ,以逃避Kotlin的字符串插值。不要感到困惑,以为我们在向字符串中插值变量,这是一个原始字符串。

Spring boot将读取括号内的属性名称,并将该属性的值注入字段中。

在该类中,我们还需要创建一个用@PostConstruct 注解的函数,以获得对Firebase的访问。为了了解一些情况:@PostConstruct 告诉Spring在Bean的属性被初始化后运行这个函数。

@Value("\${app.Firebase-config-file}")
lateinit var FirebaseConfigPath: String

// creates a logger we can use to log messages to the console. This is just to format our console messages nicely.
var logger: Logger = LoggerFactory.getLogger(FirebaseInitializer::class.java)

@PostConstruct
fun initialize(){
   // Get our credentials to authorize this Spring Boot application.
   try {
       val options = FirebaseOptions.builder()
               .setCredentials(GoogleCredentials.fromStream(ClassPathResource(FirebaseConfigPath).inputStream)).build()
       // If our app Firebase application was not initialized, do so.
       if (FirebaseApp.getApps().isEmpty()) {
           FirebaseApp.initializeApp(options)
           logger.info("Firebase application has been initialized")
       }
   } catch (e: IOException) {
       logger.error(e.message)
   }

}

做完这些,我们的Spring Boot应用程序就应该配置完毕了。

创建一个Firebase云信息服务

首先,让我们创建一个服务来发送通知并将用户订阅到一个主题。

但首先,让我们创建几个模型类来表示一个通知。

abstract class AppNotification(open val title: String, open val message: String)

data class TopicNotification(val topic: String, override val title: String,
                             override val message: String): AppNotification(title, message)

data class DirectNotification(val target: String, override val title: String,
                              override val message: String): AppNotification(title, message)

使用这些类,我们可以在我们的服务中创建一个函数来直接发送通知。

package me.john.amiscaray.services

import org.springframework.stereotype.Service
import com.google.Firebase.messaging.*
import me.john.amiscaray.dtos.SubscriptionRequest
import me.john.amiscaray.dtos.DirectNotification
import me.john.amiscaray.dtos.TopicNotification

@Service
class FCMService {
   fun sendNotificationToTarget(notification: DirectNotification){
       val message = Message.builder()
                // Set the configuration for our web notification
               .setWebpushConfig(
                       // Create and pass a WebpushConfig object setting the notification
                       WebpushConfig.builder()
                               .setNotification(
                                       // Create and pass a web notification object with the specified title, body, and icon URL 
                                       WebpushNotification.builder()
                                               .setTitle(notification.title)
                                               .setBody(notification.message)
                                               .setIcon("https://assets.mapquestapi.com/icon/v2/circle@2x.png")
                                               .build()
                               ).build()
               )
                // Specify the user to send it to in the form of their token  
               .setToken(notification.target)
               .build()
       FirebaseMessaging.getInstance().sendAsync(message)
   }
}

正如你所看到的,如果你知道构建器的设计模式,创建通知是非常直接的。由于我们的平台是网络,我们向构建器传递一个WebpushConfig 对象。

最后,在设置完通知数据后,我们设置令牌来指定消息的对象。从那里,我们调用sendAsync 方法来发送消息。

同样,为了发送主题通知,我们在我们的服务中创建一个类似的函数。唯一的区别是我们指定一个主题而不是一个标记。

// Same code as above, the only difference is we call setTopic instead of setToken with the appropriate topic
fun sendNotificationToTopic(notification: TopicNotification){
    val message = Message.builder()
            .setWebpushConfig(
                    WebpushConfig.builder()
                            .setNotification(
                                    WebpushNotification.builder()
                                            .setTitle(notification.title)
                                            .setBody(notification.message)
                                            .setIcon("https://assets.mapquestapi.com/icon/v2/incident@2x.png")
                                            .build()
                            ).build()
            ).setTopic(notification.topic)
            .build()

    FirebaseMessaging.getInstance().sendAsync(message)
}

为了完成这项服务,让我们创建一个函数,将用户订阅到一个指定的主题。在这之前,让我们创建一个模型对象来表示一个订阅请求。

// The subscriber field specifies the token of the subscribing user
data class SubscriptionRequest(val subscriber: String, val topic: String)

有了这个类的创建,下面是将用户订阅到一个主题的函数。

fun subscribeToTopic(subscription: SubscriptionRequest){

   FirebaseMessaging.getInstance().subscribeToTopic(listOf(subscription.subscriber), subscription.topic)

}

通过REST控制器公开我们的服务

创建REST控制器

现在我们有了Firebase云消息服务,我们需要做的就是创建一个REST控制器来展示它。

例如,我们要让客户向这个控制器发送请求,向他们自己发送通知。一个真正的生产应用可能不会像这样建立。

假设你有Spring MVC的坚实背景,这应该是很简单的。

package me.john.amiscaray.controllers

import me.john.amiscaray.dtos.SubscriptionRequest
import me.john.amiscaray.dtos.DirectNotification
import me.john.amiscaray.dtos.TopicNotification
import me.john.amiscaray.services.FCMService
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
class NotificationController(private val fcm: FCMService) {
   @PostMapping("/notification")
   fun sendTargetedNotification(@RequestBody notification: DirectNotification){
       fcm.sendNotificationToTarget(notification)
   }

   @PostMapping("/topic/notification")
   fun sendNotificationToTopic(@RequestBody notification: TopicNotification){
       fcm.sendNotificationToTopic(notification)
   }

   @PostMapping("/topic/subscription")
   fun subscribeToTopic(@RequestBody subscription: SubscriptionRequest){
       fcm.subscribeToTopic(subscription)
   }
}

配置 CORS

我们需要做的最后一件事是配置CORS。这样,我们的客户端将被允许向我们的后端发送任何它想要的请求。

要做到这一点,请添加以下Bean。

@Bean
fun cors(): WebMvcConfigurer {
   return object : WebMvcConfigurer{
       override fun addCorsMappings(registry: CorsRegistry) {
           // Allow our client (on localhost:4200) to send requests anywhere in our backend
           registry.addMapping("/**").allowedOrigins("http://localhost:4200")
       }
   }
}

在我们的前端设置Firebase

现在我们已经创建了我们的后端,我们可以开始创建我们的前端。

像往常一样,我们首先使用CLI生成一个angular项目。然后,我们需要使用下面的命令将Firebase添加到我们的angular项目中。

ng add @angular/fire

接下来,进入你的Firebase控制台的项目设置。下面,你应该看到一个带有Javascript对象的代码片段,像这样。

firebase config object

你将需要把这个复制到你的环境文件中。

export const environment = {
  production: false,
  FirebaseConfig: {
    apiKey: "...",
    authDomain: "...",
    projectId: "...",
    storageBucket: "...",
    messagingSenderId: "...",
    appId: "...",
    measurementId: "..."
  }
};

我们需要这个数据来授权我们的angular应用程序使用我们的Firebase项目。

使用这些数据,我们可以在我们的应用程序模块文件中初始化Firebase。

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AngularFireModule } from '@angular/fire';
import { AngularFireMessagingModule } from "@angular/fire/messaging";
import { environment } from "../environments/environment";
import { HttpClientModule } from "@angular/common/http";

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    AngularFireModule.initializeApp(environment.FirebaseConfig),
    AngularFireMessagingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

为了完成我们的设置过程,我们需要在我们的Firebase控制台中找到发送者的ID。

首先,按下项目概览按钮旁边的齿轮,然后进入项目设置,点击上面的云信息。发件人ID可以在项目凭证下找到。

复制发件人ID并将其粘贴到一个名为manifest.json 的JSON文件中,像这样。

{
  "gcm_sender_id": "your-sender-ID"
}

这个文件应该在src 文件夹中,与index.html 文件处于同一级别。我们需要告诉Angular这个文件是一个资产文件,这样当我们构建项目时它就在正确的目录中。

要做到这一点,打开你的angular.json 文件,寻找任何名为assets的数组属性。在这些数组的末尾加上以下字符串。"src/manifest.json"。

最后,像这样在你的index.html 文件的头部标签中链接清单文件。

<link rel="manifest" href="manifest.json">

完成这些后,我们的Angular应用就应该配置完毕了。

请求发送通知的权限

配置好一切后,现在我们需要请求发送通知的权限。

一旦用户同意,Firebase就可以给我们发送一个令牌来识别他们。使用该令牌,我们将向后端发送HTTP请求,以发送通知并订阅一个主题。

为了实现这一点,首先,在我们的应用程序组件的构造函数中注入以下对象。

import {Component, OnInit} from '@angular/core';
import {AngularFireMessaging} from "@angular/fire/messaging";
import {HttpClient} from "@angular/common/http";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit{

    constructor(private msg: AngularFireMessaging, private http: HttpClient) { }

}

然后,在应用程序组件类中添加以下ngOnInit 方法。

import {Component, OnInit} from '@angular/core';
import {AngularFireMessaging} from "@angular/fire/messaging";
import {HttpClient} from "@angular/common/http";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit{

    constructor(private msg: AngularFireMessaging, private http: HttpClient) { }

    ngOnInit() {
    
     this.msg.requestToken.subscribe(token => {
    
       console.log(token);
       this.http.post('http://localhost:8080/notification', {
         target: token,
         title: 'hello world',
         message: 'First notification, kinda nervous',
       }).subscribe(() => {  });
    
       this.http.post('http://localhost:8080/topic/subscription', {
         topic: 'weather',
         subscriber: token
       }).subscribe(() => {  });
    
     }, error => {
    
       console.log(error);
    
     });
    
    }

}

首先,我们订阅一个可观察的对象,它代表了对Firebase的一个标记请求。

当用户第一次执行时,它将询问他们发送通知的权限。我们传递给subscribe 方法的第一个函数是用于当用户接受权限并且Firebase给了我们一个令牌。

同时,第二个函数是在他们拒绝许可或发生其他错误时使用的。在第一个函数中,我们向我们的服务器发送一个post请求,地址是http://localhost:8080/notification

POST 请求有一个请求体,代表一个DirectNotification 对象(回顾一下,在我们的后台,我们创建了一个名为DirectNotification 的类)。这个类映射到我们在这里发送的对象。我们还向http://localhost:8080/topic/subscription 发送了一个post请求。

这个请求代表我们订阅了以天气为主题的信息。

请求主体映射到我们后端定义的SubscriptionRequest 类。

订阅接收通知

当应用程序处于活动状态时接收通知

当应用程序处于活动状态时,我们将在页面本身显示通知。

为此,我们将创建一个Message 对象来存储通知的细节。每发送一条通知,我们将把它添加到一个Message 对象的数组中。这些信息将使用ngFor 指令显示在屏幕上。

首先,我们需要定义Message 类。

export class Message{

  constructor(public title: string, public body: string, public iconUrl: string) {  }

}

然后,我们需要在我们的应用程序组件类中声明我们的messages 数组。

import {Component, OnInit} from '@angular/core';
import {AngularFireMessaging} from "@angular/fire/messaging";
import {HttpClient} from "@angular/common/http";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit{
    messages: Array<Message> = [];
    // The rest of the code goes here...
}

接下来,在ngOnInit 方法的末尾添加以下代码。

this.msg.onMessage((payload) => {
 // Get the data about the notification
 let notification = payload.notification;
 // Create a Message object and add it to the array
 this.messages.push({title: notification.title, body: notification.body, iconUrl: notification.icon});
});

最后,在我们的app.component.html 文件中用这样的模板显示这些信息。

<h1>Hello World</h1>
<h3>These are your messages:</h3>
<ul>
 <li *ngFor="let message of messages">
   <h3>{{message.title}}</h3>
   <p>
     {{message.body}}
   </p>
   <img [src]="message.iconUrl" alt="message-icon">
 </li>
</ul>

当应用程序处于非活动状态时接收通知

正如我们在项目架构中提到的,我们需要在应用被关闭的情况下使用一个服务工作者。

Firebase从我们这里寻找一个名为:firebase-messaging-sw.js 的文件。Firebase会从我们这里获取这个文件,并使用它来生成服务工作者。

这个文件应该放在src 文件夹下,其内容如下。

importScripts('https://www.gstatic.com/Firebasejs/8.7.0/Firebase-app.js')
importScripts('https://www.gstatic.com/Firebasejs/8.7.0/Firebase-messaging.js')

// The object we pass as an argument is the same object we copied into the environment files
Firebase.initializeApp({
  apiKey: "...",
  authDomain: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
  measurementId: "..."
})

const messaging = Firebase.messaging();

首先,我们需要将Firebase和Firebase消息导入到服务工作者文件中。

你可能想知道为什么我们要用这个奇怪的importScripts 函数来做这件事。

服务工作者和其他类型的工作者的工作方式与普通的javascript文件不同。这就是为什么它们必须使用importScripts 函数来导入任何东西。

不要担心为什么会这样,我们不需要了解这些。

总之,在安装了这个之后,我们调用initializeApp 方法,传递我们复制到环境文件中的对象。然后,我们需要做的就是使用messaging 方法创建一个名为messaging的对象。

这应该可以为我们处理所有的显示通知的魔法!

创建了这个对象后,试着向我们的服务器发送一个POST 请求,以便在应用程序不活动时发送一个通知。

如果你不知道,你可以使用像Postman这样的工具来这样做。

你应该在显示器的角落里看到类似这样的东西。

a sample notification

总结

在本指南中,我们经历了在一个全栈的Spring Boot和Angular项目中发送通知的过程。

在后端,我们设置了一个REST API来告诉Firebase要发送什么通知以及在哪里发送。在前端,我们学习了如何订阅,以便在应用关闭时也能收到通知。

为了更好地使用本指南,请尝试制作你自己的全栈项目,使用这些概念。