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 中还定义了事件和监听器之间的映射。具体实施起来比想象中更容易:
- 在 EventServiceProvider 中定义事件和监听器的映射EventServiceProvider.php
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
'App\Events\PublishArticle' => [
'App\Listeners\GenSiteMapListener',
],
];
- 执行 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。
发表回复