首页
Preview

使用 Laravel 和 Webpush 进行推送通知

在本教程中,我们将使用WebPush在我们的Laravel应用程序中实现推送通知功能。我们将使用纯JavaScript,没有框架或库。推送通知是Service Workers的一个功能。Service Workers是在Web浏览器内运行的后台脚本。有许多功能,如缓存,后台同步,但本教程仅涉及推送通知。我们还将为访客用户实现推送通知。

注意:Service Workers使用HTTPS,除非你使用localhost。

入门

让我们使用composer创建一个新的Laravel项目:

composer create-project laravel/laravel webpush

在迁移表之前,在位于app\ProvidersAppServiceProvider.php的**boot()**方法中添加以下代码:

<?php

\Schema::defaultStringLength(191);

以上代码将把字符串列的默认长度设置为191。由于Laravel默认使用utf8mb4字符集,其唯一键的最大长度为191。如果超过了限制,则Laravel将生成错误并且不会运行迁移。如果你想指定不同的字符集,那么可以在位于config目录中的database.php配置文件中设置它。现在设置数据库并将数据库凭据添加到**.env文件中并运行migrate**命令,该命令将创建用于身份验证的所有必要表:

php artisan migrate

并生成身份验证脚手架:

php artisan make:auth

我们将使用Laravel dev服务器(对于localhost),而不是使用带有apache的虚拟主机:

php artisan serve

打开Chrome并粘贴你在终端中看到的URL。(我不建议使用除Chrome或Firefox之外的浏览器)。

订阅用户

为了开始使用我们的通知,我们需要从浏览器订阅用户,我们可以使用JavaScript完成。订阅用户将涉及两个步骤,即:

  • 从用户获取权限。
  • 获取PushSubscription并将其发送到服务器。

在订阅用户之前,我们将需要生成应用程序服务器密钥,称为VAPID Keys。VAPID Keys是用于识别用户的公钥和私钥,这些密钥确保是同一服务器向用户触发推送消息。有几种方法可以生成这些密钥,你可以访问此网站web-push-codelab.glitch.me生成这些密钥,或使用npm安装web-push cli从命令行生成它们:

$ npm install -g web-push
$ web-push generate-vapid-keys

由于我们使用一个名为Webpush的Laravel包,因此我们可以使用Artisan命令生成这些密钥。Webpush库也可用于其他语言,请参见此链接以获取所有库的列表。让我们安装该软件包:

composer require laravel-notification-channels/webpush

Laravel 5.6或更高版本使用软件包发现功能,因此我们不需要添加软件包提供程序。让我们将HasPushSubscription特性添加到User模型:

<?php

use NotificationChannels\WebPush\HasPushSubscriptions; //import the trait

class User extends Model
{

    use HasPushSubscriptions; // add the trait to your class

    //your model code...

}

添加特性后,我们将需要发布将创建push_subscriptions表的迁移:

php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="migrations"

当你运行迁移命令时,它将生成一个字符集错误,说指定的键太长,因此我们将为此表添加不同的字符集,并为端点列添加不同的长度。让我们更新**up()**方法:

<?php

public function up()
{
    Schema::create('push_subscriptions', function (Blueprint $table) {
        $table->charset ='utf8';
        $table->collation = 'utf8_unicode_ci';
        $table->increments('id');
        $table->integer('guest_id')->unsigned()->index();
        $table->string('endpoint', 255)->unique();
        $table->string('public_key')->nullable();
        $table->string('auth_token')->nullable();
        $table->timestamps();
    });
}

你还可以发布config文件(我们将不使用它):

php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="config"

现在我们可以生成VAPID Keys

php artisan webpush:vapid

此命令将生成VAPID公钥和私钥,并将它们存储到 .env 文件中。

让我们在public目录中创建一个名为sw.js的Service Worker文件,并在public/js目录中创建一个名为enable-push.js的JavaScript文件。将该脚本添加到位于resources/views/layoutsapp.blade.php布局文件的</body>结束标记之前:

@auth
    <script src="{{ asset('js/enable-push.js') }}" defer></script>
@endauth

仅在我们经过身份验证时包含此脚本。

让我们从客户端开始工作,打开enable-push.js文件。我们将通过定义一个名为 initSW() 的函数来注册我们的服务工作者:

function initSW() {
    if (!"serviceWorker" in navigator) {
        //service worker isn't supported
        return;
    }

    //don't use it here if you use service worker
    //for other stuff.
    if (!"PushManager" in window) {
        //push isn't supported
        return;
    }

    //register the service worker
    navigator.serviceWorker.register('../sw.js')
        .then(() => {
            console.log('serviceWorker installed!')
            initPush();
        })
        .catch((err) => {
            console.log(err)
        });
}

在上述函数中,我们首先检查服务工作者和推送是否受支持,然后注册我们的服务工作者,该服务工作者在一个目录之外(你不必在此处检查推送,这将停止服务工作者的注册,但我们仅出于推送目的而使用服务工作者,因此我们将保留它。)在then()内,我们调用initPush(),它将负责订阅用户。不要忘记在我们的enable-push.js中调用initSW()。只需将此行推到脚本的顶部:

initSW();

让我们在initSW()之后定义initPush()

function initPush() {
    if (!navigator.serviceWorker.ready) {
        return;
    }

    new Promise(function (resolve, reject) {
        const permissionResult = Notification.requestPermission(function (result) {
            resolve(result);
        });

        if (permissionResult) {
            permissionResult.then(resolve, reject);
        }
    })
        .then((permissionResult) => {
            if (permissionResult !== 'granted') {
                throw new Error('We weren\'t granted permission.');
            }
            subscribeUser();
        });
}

initPush()内,我们正在请求用户权限,如果用户点击允许,则可以获取PushSubscription。在我们被授予权限后,我们将调用subscribeUser(),该函数将订阅用户并将PushSubscrption发送到我们的服务器。让我们定义subscribeUser()

function subscribeUser() {
    navigator.serviceWorker.ready
        .then((registration) => {
            const subscribeOptions = {
                userVisibleOnly: true,
                applicationServerKey: urlBase64ToUint8Array(
                    'YOUR_VAPID_PUBLIC_KEY'
                )
            };

            return registration.pushManager.subscribe(subscribeOptions);
        })
        .then((pushSubscription) => {
            console.log('Received PushSubscription: ', JSON.stringify(pushSubscription));
            storePushSubscription(pushSubscription);
        });
}

navigator.serviceWorker.ready用于检查服务工作者是否已注册并处于活动状态。在第一个then()内,我们首先创建一个名为subscriptionOptions的对象,其中包含我们的VAPID public keyuserVisibleOnly选项。使用你从上述命令生成的公钥。将userVisibleOnly设置为true将不允许在不让用户知道的情况下在后台进行静默推送(如果未定义,则会出现错误,请阅读更多)。urlBase64ToUint8Array()函数将将我们的公钥转换为适当的格式。订阅后。它将返回一个PushSubscription,我们可以将其发送到服务器。在第二个then()内,我们使用storePushSubscription()函数将PushSubscription发送到我们的服务器。在定义它之前,我们应该定义urlBase64ToUint8Array()

function urlBase64ToUint8Array(base64String) {
    var padding = '='.repeat((4 - base64String.length % 4) % 4);
    var base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');

    var rawData = window.atob(base64);
    var outputArray = new Uint8Array(rawData.length);

    for (var i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
}

我们的 storePushSubscription() 函数如下所示:

function storePushSubscription(pushSubscription) {
    const token = document.querySelector('meta[name=csrf-token]').getAttribute('content');

    fetch('/push', {
        method: 'POST',
        body: JSON.stringify(pushSubscription),
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'X-CSRF-Token': token
        }
    })
        .then((res) => {
            return res.json();
        })
        .then((res) => {
            console.log(res)
        })
        .catch((err) => {
            console.log(err)
        });
}

上面的函数正在向 /push 路由发送 POST 请求。我们从 meta 标签中获取 CSRF Token,这对于我们的 ajax 请求非常重要。当我们创建身份验证脚手架时,就添加了该 meta 标签。

将 PushSubscription 存储到数据库中

让我们从路由开始(将其添加到你的 web.php 中,而不是 api.php,因为我们需要访问已验证的用户):


<?php

//store a push subscriber.
Route::post('/push','PushController@store');

让我们创建我们的 PushController:

php artisan make:controller PushController

现在打开 PushController 并添加 store 方法:

<?php

namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Notifications\PushDemo;
use App\User;
use Auth;
use Notification;

class PushController extends Controller
{

    public function __construct(){
      $this->middleware('auth');
    }

    /**
     * Store the PushSubscription.
     * 
     * @param \Illuminate\Http\Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function store(Request $request){
        $this->validate($request,[
            'endpoint'    => 'required',
            'keys.auth'   => 'required',
            'keys.p256dh' => 'required'
        ]);
        $endpoint = $request->endpoint;
        $token = $request->keys['auth'];
        $key = $request->keys['p256dh'];
        $user = Auth::user();
        $user->updatePushSubscription($endpoint, $key, $token);
        
        return response()->json(['success' => true],200);
    }
    
}

我们可以使用 updatePushSubscription() 来创建或更新 PushSubscriber(因为我们已经将其添加到我们的 User 模型中)。如果成功,我们将在控制台上获得成功消息。请注意,每当你访问 / 路由时,你将不会看到“服务工作程序已安装”消息,因为我们已经将脚本添加到了 app 布局文件中,而该文件不会被 welcome 视图使用。现在刷新页面,你会看到一个权限框:

当你点击 允许,你会在控制台看到 PushSubscription 和成功消息:

现在我们需要在 Laravel 中创建一个 Notification 来发送 Push 通知:

php artisan make:notification PushDemo

打开通知类,它位于 app/Notifcations 目录中,添加以下代码:

<?php

namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use NotificationChannels\WebPush\WebPushMessage;
use NotificationChannels\WebPush\WebPushChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;

class PushDemo extends Notification
{

    use Queueable;
    
    public function via($notifiable)
    {
        return [WebPushChannel::class];
    }

    public function toWebPush($notifiable, $notification)
    {
        return (new WebPushMessage)
            ->title('I\'m Notification Title')
            ->icon('/notification-icon.png')
            ->body('Great, Push Notifications work!')
            ->action('View App', 'notification_action');
    }
    
}

toWebPush() 方法中,我们有一个通知标题作为通知的标题,正文将是额外的文本,图标是在通知上显示的图像,操作将显示一个带有通知的按钮(你可以添加更多的 选项,并了解这些 选项)。现在我们可以使用 Notification::send() 从我们的控制器发送通知。让我们在我们的控制器中定义一个 push 方法:


<?php

/**
 * Send Push Notifications to all users.
 * 
 * @return \Illuminate\Http\Response
 */
public function push(){
    Notification::send(User::all(),new PushDemo);
    return redirect()->back();
}

web.php 中添加一个新路由:

<?php

//make a push notification.
Route::get('/push','PushController@push')->name('push');

我们需要一个按钮来发送通知,所以让我们在我们的 home.blade.php 视图中添加它。将其放置在带有 card-body 类的 div 中:

<a href="{{route('push')}}" class="btn btn-outline-primary btn-block">Make a Push Notification!</a>

现在最后一步是添加一个事件监听器,它将监听我们的服务工作者文件 sw.js 中的 push event

self.addEventListener('push', function (e) {
    if (!(self.Notification && self.Notification.permission === 'granted')) {
        //notifications aren't supported or permission not granted!
        return;
    }

    if (e.data) {
        var msg = e.data.json();
        console.log(msg)
        e.waitUntil(self.registration.showNotification(msg.title, {
            body: msg.body,
            icon: msg.icon,
            actions: msg.actions
        }));
    }
});

push event 监听器中,我们首先检查是否允许并支持通知,然后使用 e.datae 对象中访问通知数据,并使用 json() 方法将其转换为 json。在服务工作者中,waitUntil() 用于告诉浏览器任务正在运行,不应被终止。我们可以使用 serviceWorkerRegistration.showNotification(title,options) 显示通知。确保你在应用程序选项卡中检查了重新加载选项的更新。

现在我们可以通过点击该按钮来测试它。

每个浏览器和操作系统都有自己的通知显示方式。我在 Ubuntu 上使用 Chrome。Chrome 不显示图标,而 Firefox 则显示。

当你悬停在上面时,它会看起来像这样:

这是 Firefox 的显示方式(它不显示通知操作):

为访客用户提供通知(新增)

在实现访客通知之前,请阅读上面的教程

大多数时候,我们希望我们网站的所有访问者都能接收推送通知,因此让我们来实现这个功能。首先,我们需要更新迁移并创建一些新的迁移。打开 push_subscriptions 迁移并将 user_id 更新为 guest_id

<?php

$table->integer('guest_id')->unsigned()->index();

删除外键约束:

<?php

$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');

让我们创建一个 Guest 模型及其迁移,以便与推送订阅相关联:

php artisan make:model Guest -m

guests 表迁移中,我们有一个唯一的列 endpoint,它将用于标识每个推送订阅:

<?php

public function up()
{
    Schema::create('guests', function (Blueprint $table) {
        $table->charset ='utf8';
        $table->collation = 'utf8_unicode_ci';
        $table->increments('id');
        $table->string('endpoint',255)->unique();
        $table->timestamps();
    });
}

我们需要另一个迁移,它将在这些表之间创建外键约束:

php artisan make:migration add_foreign_to_guest_id_in_push_subscriptions_table --table=push_subscriptions

打开迁移并在 up() 方法中添加以下外键约束:

<?php

$table->foreign('guest_id')->references('id')->on('guests')->onDelete('cascade');

使用迁移命令创建所有表。现在打开 Guest 模型并添加以下内容:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use NotificationChannels\WebPush\HasPushSubscriptions;

class Guest extends Model
{
    use Notifiable,
        HasPushSubscriptions;

    protected $fillable = [
        'endpoint',
    ];

    /**
     * Determine if the given subscription belongs to this user.
     *
     * @param  \NotificationChannels\WebPush\PushSubscription $subscription
     * @return bool
     */
    public function pushSubscriptionBelongsToUser($subscription){
        return (int) $subscription->guest_id === (int) $this->id;
    }
}

我们正在导入 Notifiable trait,以便我们可以使用 Laravel 通知(它已经被导入到 User 模型中)。重写 pushSubscriptionBelongsToUser() 方法非常重要,因为我们不使用经过身份验证的用户。

最后一步将修改我们的 PushController,因此打开控制器并首先将所有 User 模型用法替换为 Guest 模型。之后,将 store() 方法中的以下行替换为:


<?php

$user = Auth::user();

用以下内容替换:


<?php

$user = Guest::firstOrCreate([
    'endpoint' => $endpoint
]);

当然,你需要删除阻止包含我们的 enable-push.js 脚本的 @auth 指令,然后我们就完成了。

你可能想将其部署到 heroku,它具有带有 https 的免费版本(如果你想在移动设备上测试它)。就用户体验而言,可能最好创建类似按钮的东西,它将触发 initPush()。一开始就要求用户授权通知并不是一个好主意,因为我们的用户不知道他们正在接收什么样的通知。上面是一个使用推送通知的基本示例。如果你想深入了解,可以在Google开发者文档中查找更多信息。

可以在这里找到源代码/Github仓库。访客通知的分支可以在这里找到。

译自:https://medium.com/@sagarmaheshwary31/push-notifications-with-laravel-and-webpush-446884265aaa

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
anko
宽以待人处事,严于律己修身。

评论(0)

添加评论