消息推送是 APP 开发中非常重要的功能,可以让不在前台运行的 APP,及时进行消息通知。在新闻内容、促销活动、产品信息、版本更新提醒、订单状态提醒等诸多场景中都有应用。

以下以 iOS 为例,介绍下消息推送的机制,以及实现一些基础代码。

1. 原理

APNs(英文全称:Apple Push Notification service),即苹果推送通知服务。

消息推送分为本地通知和远程通知,本地通知是由本地应用触发的,基于时间行为的一种通知形式,如闹钟定时、待办事项提醒,又或者一个应用在一段时间不使用通常会提示用户使用此应用等,都是本地通知。

因为本地通知没有服务器的参与,所以我们在此不展开讨论,主要来了解下远程通知。

远程通知的主要流程为:

  • 在 APNs 下载 push 证书,并设置在服务器中。
  • APP 获取 APNs 分配的 deviceToken(应用唯一标识,不同设备,不同应用的 deviceToken 都会不同),提交给服务器,服务器记录下 deviceToken。
  • 消息推送时,服务器将 deviceToken 和要发送的消息提交给 APNs,并使用 push 证书签名。
  • APNs 会在自己已注册的 iPhone 列表中根据 deviceToken 查找目标 iPhone。然后将消息发送给目标 iPhone。
  • iPhone 把消息发送给相应的程序,并且按已设定的方式弹出。

自己实现远程推送的功能会有很多的配置工作,再加上以后还有 Android 设备,有很多繁琐的工作要处理,所以我们一般使用第三方的推送服务,省去了很多开发工作。同时第三方推出的数据统计也满足了运营的需求。

2. 注册第三方推送账号

极光推送 是我们常用的第三方推送服务商,以下简称 Jpush。Jpush 同时支持 iOS 和 Android 平台的消息推送,服务器只需要实现一套代码即可。

注册 Jpush 账号并创建应用后,我们会得到 key 和 secret。

由于使用了第三方服务,远程通知的流程变为:

  • 服务器配置 Jpush 的 AppKey 和 Secret。
  • APP 获取 APNs 分配的 deviceToken,去 Jpush 注册并获取 RegistrationID。
  • APP 将 RegistrationID 提交到服务器,服务器记录下对应用户的 RegistrationID。
  • 消息推送时,服务器通过 Jpush sdk 提交 RegistrationID 及消息到 Jpush 服务器。
  • Jpush 负责之后的消息推送。

原来是将 deviceToken 提交到服务器,现在是将 RegistrationID 提交到服务器,RegistrationID 相当于设备在 Jpush 的唯一标识,服务器将 RegistrationID 与单个用户绑定,这样就能推送消息给指定用户。

3. 注册 RegistrationID

3.1 添加 registration_id 字段

users 表增加字段:

php artisan make:migration add_registration_id_to_users_table --table=users

database/migrations/XXX_add_registration_id_to_users_table.php

.
.
.
public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('registration_id')->nullable();
        });
    }

public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('registration_id');
        });
    }
.
.
.

执行迁移:

php artisan migrate

3.2 修改个人信息接口

app/Http/Controllers/Api/UsersController.php

.
.
.
public function update(UserRequest $request)
{
    $user = $this->user();

    $attributes = $request->only(['name', 'email', 'introduction', 'registration_id']);
.
.
.

修改模型可修改字段:

.
.
.
protected $fillable = [
    'name', 'phone', 'email', 'password', 'introduction', 'avatar',
    'weixin_openid', 'weixin_unionid', 'registration_id'
];
.
.
.

4. 推送消息通知

4.1 安装 Jpush SDK

composer require jpush/jpush

4.2 封装 SDK

为了方便使用,进行一下简单的封装:

php artisan make:provider JpushServiceProvider

app/Providers/JpushProvider.php

namespace App\Providers;

use JPush\Client;
use Illuminate\Support\ServiceProvider;

class JpushServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton(Client::class, function ($app) {
            return new Client(config('jpush.key'), config('jpush.secret'));
        });

        $this->app->alias(Client::class, 'jpush');
    }
}

config/app.php

.
.
.
App\Providers\EasySmsServiceProvider::class,
App\Providers\JpushServiceProvider::class,
.
.
.

创建配置文件:

touch config/jpush.php

config/jpush.php

return [
    'key' => env('JPUSH_KEY'),
    'secret' => env('JPUSH_SECRET'),
];

在 env 文件中填写 Jpush 的 key 和 secret

.env

# jpush
JPUSH_KEY=9c6f53edad67db7ec24bfe32
JPUSH_SECRET=deeb2a04669ab79******

这样我们就可以直接依赖注入 JPush\Client 或者 app('jpush') 来使用 Jpush 的 SDK。

4.3 监听消息通知

php artisan make:listener PushNotification

监听事件,这里我们可以在 DatabaseNotification 创建时,推送消息给客户端:

app/Providers/EventServiceProvider.php

protected $listen = [
    \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        // add your listeners (aka providers) here
        'SocialiteProviders\Weixin\WeixinExtendSocialite@handle'
    ],
    'eloquent.created: Illuminate\Notifications\DatabaseNotification' => [
        'App\Listeners\PushNotification',
    ],
];

app/Listeners/PushNotification.php

namespace App\Listeners;

use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\DatabaseNotification;
use JPush\Client;

class PushNotification implements ShouldQueue
{
    protected $client;

    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    /**
     * Handle the event.
     *
     * @param  NotificationSent  $event
     * @return void
     */
    public function handle(DatabaseNotification $notification)
    {
        // 本地环境默认不推送
        if (app()->environment('local')) {
            return;
        }

        $user = $notification->notifiable;

        // 没有 registration_id 的不推送
        if (!$user->registration_id) {
            return;
        }

        // 推送消息
        $this->client->push()
            ->setPlatform('all')
            ->addRegistrationId($user->registration_id)
            ->setNotificationAlert(strip_tags($notification->data['reply_content']))
            ->send();

    }
}

逻辑很简单,当通知存入数据库后,也就是监听 elequent.created:Illuminate\Notifications\DatabaseNotification 这个事件。如果用户已经有了 Jpush 的 registration_id,则使用 Jpush SDK 将消息内容推送到目标用户的 APP 中,注意我们使用了 strip_tags 去除了 notification 数据中的 HTML标签。