使用 Laravel 事件和队列自动更新 sitemap

sitemap 是一种 xml 格式的文本文件,是提供给搜索引擎使用的。搜索引擎可以通过定期读取 sitemap 文件来更新相关网站的索引。因此,sitemap 文件实际上以 xml 格式包含了网站的页面信息。本站的 sitemap 文件是这个样子的,片段如下:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <url>
        <loc>https://www.gryen.com</loc>
        <lastmod>2020-03-31T00:00:00+08:00</lastmod>
        <changefreq>daily</changefreq>
        <priority>0.1</priority>
    </url>
    <url>
        <loc>https://www.gryen.com/articles</loc>
        <lastmod>2020-03-31T00:00:00+08:00</lastmod>
        <changefreq>monthly</changefreq>
        <priority>0.1</priority>
    </url>
    <url>
        <loc>https://www.gryen.com/articles/show/10.html</loc>
        <lastmod>2017-05-23T10:47:31+08:00</lastmod>
        <changefreq>monthly</changefreq>
        <priority>0.2</priority>
    </url>
    ...
</urlset>

sitemap 应该是搜索引擎抓取时当下网站的状态的,反映整个网站当时的全貌,是整个网站的一个概览。

一种做法是在搜索引擎读取 sitemap 时动态生成内容,与动态网站用户访问时从数据库抓取内容填充到页面一样。

但是这样做有两个问题:一是网站内容比较多的时候,遍历所有数据生成 sitemap 比较慢,可能会影响收录;二是 sitemap 既然是提供当前网站网页概览的,那么只需要在网站发生变更,比如增加或者删除网页时更新就可以了,网站更新频率不高(低于搜索引擎读取 sitemap 的频率)的情况下,每次搜索引擎读取时都动态生成同样的 sitemap 文件,太浪费资源了。

比较好的做法是生成一份静态的 sitemap.xml 文件,在网站发生变更时,更新这个 sitemap.xml 文件就可以了。

利用 Laravel 事件和队列,实现在创建、删除文章等情况下更新 sitemap.xml

事件的使用

事件机制提供了不影响正常业务流程,做一些额外处理的能力。像是更新 sitemap.xml 文件,它不属于我正常发表一篇文章的流程,它的成功与失败,并不影响到我将一篇文章发表到网站上。所以,只需要在特定的条件下,触发这个事件就可以了,至于它什么时间执行完成,甚至成功与否,都不在发表文章的过程中关心。Laravel 也提供给了这样的一种事件处理机制。

Laravel 项目里面有两个文件夹看起来和事件有关系:Events 和 Listeners,一个负责事件定义,一个提供事件监听器。此外,EventServiceProvider 中还定义了事件和监听器之间的映射。具体实施起来比想象中更容易:

  1. 在 EventServiceProvider 中定义事件和监听器的映射EventServiceProvider.php
/**
  * The event listener mappings for the application.
  *
  * @var array
  */
 protected $listen = [
     Registered::class => [
         SendEmailVerificationNotification::class,
     ],
     'App\Events\PublishArticle' => [
         'App\Listeners\GenSiteMapListener',
     ],
 ];
  1. 执行 php artisan event:generate 事件和监听器就都生成好了。PublishArticle.php
<?php

namespace App\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class PublishArticle { 
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
    *   Create a new event instance.*
    *   @return void
    */
    public function __construct()
    { 
        //
    }

    /**
    *   Get the channels the event should broadcast on.
    *   @return \Illuminate\Broadcasting\Channel|array
    */
    public function broadcastOn()
    { 
        return [];
    }
}

GenSiteMapListener.php

<?php

namespace App\Listeners;

use App\Article;
use App\Events\PublishArticle;
use Carbon\Carbon;
use Illuminate\Contracts\Queue\ShouldQueue;
use Spatie\Sitemap\Sitemap;
use Spatie\Sitemap\Tags\Url;

class GenSiteMapListener implements ShouldQueue {

    /**
    *   Create the event listener.*
    *   @return void
    */
    public function __construct()
    { 
        //
    }

    /**
    *   Handle the event.
    *   @param PublishArticle $event
    *   @return void
    */
    public function handle(PublishArticle $event)
    { 
        $publicPath = public_path('sitemap.xml');
        $siteMap = Sitemap::create();

        $siteMap
             ->add(Url::create(action('HomeController@index'))
             ->setLastModificationDate(Carbon::yesterday())
             ->setChangeFrequency(\Spatie\Sitemap\Tags\Url::CHANGE_FREQUENCY_DAILY)
             ->setPriority(0.1))
             ->add(Url::create(action('ArticlesController@index'))
             ->setLastModificationDate(Carbon::yesterday())
             ->setChangeFrequency(\Spatie\Sitemap\Tags\Url::CHANGE_FREQUENCY_MONTHLY)
             ->setPriority(0.1));

        Article::all()->each(function ($article) use ($siteMap) {
            if ($article->status === 1) {
                $siteMap->add(Url::create(action('ArticlesController@show', [$article->id]))
                    ->setLastModificationDate($article->updated_at)
                    ->setChangeFrequency(\Spatie\Sitemap\Tags\Url::CHANGE_FREQUENCY_MONTHLY)
                    ->setPriority(0.2));
            }
        });

        $siteMap->writeToFile($publicPath);
        $siteMap = null;
    }
}

上面代码中包含了生成 sitemap.xml 具体逻辑的实现,用到了 spatie/laravel-sitemap 这个库。

触发事件只一行代码:

event(new PublishArticle());

把它放到创建文章或者删除文章等需要更新 sitemap.xml 的操作里面,保证能触发就可以了,参考:ArticlesController.php#L103

队列的使用

实现了事件之后,实际上只是将代码从正常发表文章的逻辑中抽离了,实际执行起来,它还是同步的。也就是说,如果生成 sitemap.xml 的过程需要 1 分钟,那么,在发表文章的时候,就需要多等上 1 分钟,这显然还是对我们发表文章的过程产生了影响。我们想要的是让生成 sitemap.xml 的过程异步去执行,我们只是在发表文章时触发它,它自己选择合适的时机去执行,我们不用多等 1 分钟就能将文章发表了。

这就用到了队列,我们将这个任务放入队列中,队列来决定什么时候执行这个任务。

Laravel 中使用队列只需要在事件监听器中实现 ShouldQueue 接口就可以了,代码见上面 GenSiteMapListener.php,然后我们还要对队列做一些配置工作,配置 Laravel 采用哪种方式实现队列和采取什么样的策略。

队列的配置

除了同步执行的队列 sync 以外,Laravel 支持 database、beanstalkd、sqs 以及 redis 多种队列实现策略,这里只介绍 redis 策略的配置,其他配置方式可参考 Laravel 的队列配置文件 config/queue.php 和官方相关说明文档。使用 redis 方式的队列,需要在 .env 文件中配置:

QUEUE_CONNECTION=redis

config/queue.php 中 redis 的配置节点保持默认就可以了,需要注意的是失败队列的配置,也就是以下部分;

'failed' => [
        'driver' => env('QUEUE_FAILED_DRIVER', 'database'),
        'database' => env('DB_CONNECTION', 'mysql'),
        'table' => 'failed_jobs',
    ],

这里面的配置也不需要更改,需要留意的是 table 中的 failed_jobs 这个表,laravel 默认是不会生成的,需要我们执行 php artisan queue:failed-table 生成 migration,然后执行 php artisan migrate 来生成 failed_jobs 表。

以上,采用 redis 的 laravel 队列配置完成。要让它开始工作,还得执行:

php artisan queue:work --tries=3

其中,--tries=3 参数给出了默认尝试的次数,失败后会记录到上一步生成的 failed-table 表中,否则队列将不停尝试。

另外,这是个需要在后台一直执行的任务,需要我们给它配置守护进程。laravel 提供了使用 Supervisor 实现守护进程的方法,当然也可以使用其他方式实现,比如 ubuntu 系统的 service


评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注