网站不备案备案,网站建设功能评价指标,小猪会飞网站建设,华为官方网站手机商城这篇文章也可以在我的博客中查看
关于本文
专业的流量统计系统能够相对真实地反应网站的访问情况。 这些数据可以在后台很好地进行分析统计#xff0c;但有时我们希望在网站前端展示一些数据
最常见的情景就是#xff1a;展示页面的浏览量 这简单的操作当然也可以通过简单…这篇文章也可以在我的博客中查看
关于本文
专业的流量统计系统能够相对真实地反应网站的访问情况。 这些数据可以在后台很好地进行分析统计但有时我们希望在网站前端展示一些数据
最常见的情景就是展示页面的浏览量 这简单的操作当然也可以通过简单的计数器实现但可能会造成重复统计比如同一个用户点击10次
目标
流量分析工具所提供的准确性是不可比拟的 因此这篇文章我们就来实现如何将流量分析数据搬到网站展示做到
同步流量分析工具数据到网站前端 显示页面的阅读量 不影响页面加载 用户不会感知到同步任务进行 不频繁访问分析工具API 减少网络资源、API次数消耗
准备
为完成这些目标需要一些前提准备
配置好带有数据访问API的流量分析工具 如Google Analytics、Umami本文将以Umami为例这是我们的真实数据来源 配置好WordPress后台进程Background Process支持 如Action-Scheduler本文将以此为例这是我们非阻塞运行的基础
分析问题
Analytics类
分析问题
API访问频率
阅读量实时性并不强我们无须也不可能每次页面访问都从远程分析工具获取数据 频繁访问很有可能会被禁止访问API自建的相当于DDoS攻击自己 在获取数据后应该在短时间内缓存起来 WordPress中的跨请求缓存API是transient 处理缓存未命中
但如果缓存未命中怎么办是立刻访问远程分析工具吗 不可能这样同步执行会使页面加载阻塞 特别是如果你一次展示多篇文章你需要等待它们全部完成才能加载出页面
因此我们必须在本地数据库也持久化存储阅读量 这个冗余数据是缓存未命中时的唯一可行数据来源 在WordPress中我们可以使用post_meta存储它 与此同时这也可作为数据过时的标志 我们应该触发更新阅读量的后台进程 非阻塞地将第三方分析工具的数据同步到本地上
小结
Analytics.php的是用于页面获取数据的接口。它的数据来源是
内存缓存 减少短期重复访问减少服务器压力 本地数据库 缓存未命中时的保底数据 远程分析工具 数据更新的途径
它的职责是
读写本地数据发出更新请求
实现 注意组织文件结构本文将/App文件夹作为根目录 在/App/Services/Analytics/创建Analytics.php文件
编写Analytics类它主要包含一些静态函数
namespace App\Services\Analytics {class Analytics{public static function getPageViews(WP_Post|int $post){}public static function setPageViews(WP_Post|int $postId, $newViews){}}
}getPageViews 本文实现需要依赖$post-ID作为唯一标识符 如果你希望实现任何页面的阅读量展示你需要 使用url[path]的md5 hash作为唯一标识符使用自定义数据库表存储阅读量(url_md5, page_view) 需要做什么 当访客来访时需要展示阅读量此时
我们需要获取目标地址的WP_Post实例 以获取url等信息 有缓存读缓存无缓存读数据库 不阻塞执行请求第三方流量分析API更新记录马上使用旧数据刷新缓存
前面提到了缓存过期是发出数据同步请求的标志但我们不希望重复发起请求 因此缓存未命中时需要马上再次写入缓存。
虽然数据是旧的但不急。我们可以在数据同步时强制刷新它 大部分都好处理异步请求比较麻烦先卖个关子 同时我们还为阅读量定义了缓存键值和在数据库的meta键值
protected static string $pageViewMetaKey page_views;
protected static int $pageViewCacheTime HOUR_IN_SECONDS;
protected static function pageViewsCacheKey(int $postId)
{return static::$pageViewMetaKey . _ . $postId;
}public static function getPageViews(WP_Post|int $post)
{if (!($post instanceof WP_Post))$post get_post($post);if (empty($post)) return 0;// 尝试获取缓存$pageViews get_transient(Analytics::pageViewsCacheKey($post-ID));if ($pageViews ! false) return $pageViews;// 记录更新请求// -- ?? async call to update ?? --// 读取数据库记录这将是最后能够返回的值$pageViews get_post_meta($post-ID, Analytics::$pageViewMetaKey, true) ?: 0;// 重写缓存set_transient(Analytics::pageViewsCacheKey($post-ID), $pageViews, static::$pageViewCacheTime);return $pageViews;
}setPageViews
这个函数用于写入本地的数据存储包括缓存和数据库 注意它并不包含异步更新的过程只是异步更新的结果需要借助它写入
public static function setPageViews(WP_Post|int $postId, $newViews)
{if ($postId instanceof WP_Post)$postId $postId-ID;// 更新缓存set_transient(Analytics::pageViewsCacheKey($postId), $newViews, static::$pageViewCacheTime);// 写到数据库update_post_meta($postId, Analytics::$pageViewMetaKey, $newViews);
}Provider
好了该想想怎么访问远程API了 Analytics因为大多为固定操作我们实现为静态 但是更新数据来源的逻辑呢
不同的流量分析工具会提供不同的API因此我们也需要为它们编写各自的处理逻辑 我们需要根据设置为Analytics注入一个恰当的数据来源实例这里称为Provider
先关注Analytics类中需要如何支持注入Provider 没使用任何框架我只能纯手工注入 以下代码是额外增加内容需要与上文合并 class Analytics
{private static Closure|AnalyticsProvider $_provider;public static function setProvider(callable|AnalyticsProvider $provider){if (is_callable($provider))static::$_provider Closure::fromCallable($provider);elsestatic::$_provider $provider;}protected static function getProvider(): AnalyticsProvider{if (static::$_provider instanceof Closure)static::$_provider (static::$_provider)();return static::$_provider;}
}我们需要先setProvider设置使用的数据源后续使用getProvider获取它
因为某些provider可能会很沉重这里支持传入一个返回AnalyticsProvider的Closure 以实现懒加载只有需要使用它的时候才会生成
接下来再看看provider需要怎么编写
AnalyticsProvider类
不同的provider有不同的访问逻辑但至少有没有些共性 还真有
需要未雨绸缪的问题
Provider负责组织后台任务但每次请求更新都立刻组织一个后台任务还是很恐怖的。
比如一个页面有100篇文章 每当Analytics::getPageViews缓存未命中时就组织后台任务 此时需要组织100个任务
因为php无守护进程每个后台任务其实需要通过写数据库进行任务信息持久化 因此组织100个后台任务意味着访问数据库上百次
而组织任务这个过程是同步的、阻塞的 用户会看着页面转十秒加载不出来
但说到底有没有必要把它视为100个任务不能批处理一下吗 当然可以而且这就是不同AnalyticsProvider的一个共性。
实现
在/App/Services/Analytics/创建AnalyticsProvider.php文件
编写Analytics类
namespace App\Services\Analytics {abstract class AnalyticsProvider{}
}pushUpdatePostViews
这是登记更新任务的逻辑 上文说了我们不希望立刻生成后台任务而是记录它
protected array $updatesList [];/*** 将目标加入浏览量更新任务队列* param array $args 查询需要的参数与具体实现有关*/
public function pushUpdatePostViews(WP_Post $post, array $args [])
{$this-updatesList[$post-ID] $args;
}$args主要是请求API时的参数比如时间段目标地址国家…… 这与具体数据源的实现有关但总之我们需要把这些可能用到的数据存到$updatesList里
$updatesList记录了本次请求中所有需要请求阅读量更新的文章和相应参数 但我们如何把它加到后台任务
submitTasks()
submitTasks由子类负责给出任务提交的逻辑 父类只需要给出约束
abstract public function submitTasks();没完我们需要有人在最后调用这个函数才能完成所有任务一次性提交 可以利用WordPress的shutdownhook
public function __construct()
{add_action(shutdown, [$this, submitTasks]);
}因为shutdown是WordPress最后一个hook因此不用担心之后还会有新的任务提交请求 注意WordPress hook的回调必须是public函数 调用
还记得Analytics::getPageViews的空缺位置吗 它应该调用AnalyticsProvider
public static function getPageViews(WP_Post|int $post)
{// ...// -- ?? async call to update ?? --static::getProvider()-pushUpdatePostViews($post);// ...
}注意static在上下文中就是Analytics
具体的AnalyticsProvider
主要完成两件事
完成任务提交逻辑封装处理参数 以下我以Umami为例 在/App/Services/Analytics/Umami创建UmamiAnalyticsProvider.php文件 编写UmamiAnalyticsProvider类
namespace App\Services\Analytics\Umami {use WP_Post;use App\Services\Analytics\AnalyticsProvider;class UmamiAnalyticsProvider extends AnalyticsProvider{public function submitTasks(){if ($this-updatesList) {// -- ?? submit this background task ?? --}}public function pushUpdatePostViews(WP_Post $post, array $args []){$args[path] parse_url(get_permalink($post))[path];parent::pushUpdatePostViews($post, $args);}}
}Umami API获取阅读量必须提供页面的path因此我重写pushUpdatePostViews并按id获取了它的pathsubmitTask先检测了是否真有待提交任务数据如有提交
具体提交逻辑见下文
后台任务 万事俱备只欠东风 我们只剩下后台任务需要解决了但你先别急 这篇文章目前只到一半 本文将使用Action Scheduler作为后台任务的驱动 但不管你是否使用它后文的task结构都可以给你一点灵感
Action-Scheduler
Action Scheduler基本上是WordPress中支持后台进程的唯一选择了 它的官方例子如下
require_once( plugin_dir_path( __FILE__ ) . /libraries/action-scheduler/action-scheduler.php );/*** Schedule an action with the hook eg_midnight_log to run at midnight each day* so that our callback is run then.*/
function eg_schedule_midnight_log() {if ( false as_has_scheduled_action( eg_midnight_log ) ) {as_schedule_recurring_action( strtotime( tomorrow ), DAY_IN_SECONDS, eg_midnight_log, array(), , true );}
}
add_action( init, eg_schedule_midnight_log );/*** A callback to run when the eg_midnight_log scheduled action is run.*/
function eg_log_action_data() {error_log( It is just after midnight on . date( Y-m-d ) );
}
add_action( eg_midnight_log, eg_log_action_data );这个例子将在每天午夜输出一个log
但这例子其实有个坑Action Scheduler的执行机制事实上跨越了2次php执行
第一次制定任务 使用as_schedule_recurring_action制定任务此时eg_midnight_loghook无效 第二次午夜时执行任务可能由cron或其它机制触发 它从数据库中检测到预定的任务生成eg_midnight_loghook执行eg_midnight_loghook的逻辑
所以坑点就在于add_action( eg_midnight_log, eg_log_action_data );必须在执行任务时加入在制定任务时加入是无效的
而我们的目标则是
把2次php执行的代码尽可能地透明化封装起来使用面向对象的思想处理任务使其模块化
TaskManager类
TaskManager主要用于负责所有任务的提交和触发我的实现主要针对Action Scheduler如果使用其它后台任务库该类需要做对应修改。 在阅读前建议先了解Action Scheduler的基本操作 实现
在/App/Services/Task创建TaskManager.php文件 编写TaskManager类
namespace App\Services\Task {class TaskManager{protected static array $taskList;public static function init(){}public static function registerTask($taskName){static::$taskList[] $taskName;}public static function submitTask(string $handlerType, array $taskMeta, array $taskParams): int{}}
}registerTask用于记录所有需要管理的任务名它的作用只是将名字加入$taskList列表
submitTask
用于提交“保证任务触发时正常执行”所需的一切数据包括
交给谁处理给谁处理执行处理的指引怎么处理需要处理的数据处理什么
因此它需要传入3个参数
$handlerType: 承载任务处理逻辑的类名 后文会详细介绍它的基类是Task包含一个handleTask方法 $taskMeta: 承载任务处理的元数据 比如任务时限重试次数反正是与任务相关但与任务执行主体无关的 $taskParams: 任务执行所需的数据 比如我们需要访问api那可能就是api参数等等
因此可以写出这样的代码
public static function submitTask(string $handlerType, array $taskMeta, array $taskParams): int
{if (!$handlerType) return 0;$args [handler $handlerType, meta $taskMeta, params $taskParams];return as_enqueue_async_action($handlerType::$taskName, $args, md5(json_encode($args)), true);
}使用Action Scheduler提供的as_enqueue_async_action将任务数据移交至其托管。所有$args参数将被Action Scheduler存储于数据库当执行时取出 有点像序列化 $taskName是Task类的静态变量表示任务名 因为Task与任务直接关联因此任务名就存在它那了 防止完全重复任务 标记为唯一任务第四个参数unique:true计算参数的md5作为分组用于识别重复任务
init
init需要在每次执行、所有registerTask调用结束后调用它用于监听后台任务是否已触发如果是则分配到相应的处理函数
public static function init()
{require_once(get_template_directory() . /vendor/woocommerce/action-scheduler/action-scheduler.php);/*** 监听事件触发并转交给handler*/foreach (static::$taskList as $taskName) {add_action($taskName, function (string $handlerType, array $meta, array $params) {$provider new $handlerType();$provider-handleTask($meta, $params);}, 10, 3);}
}首先需要引入Action Scheduler文件然后对每个注册的任务名都使用监听函数这里实现为匿名函数订阅它的action hook
当事件触发时这个函数将获得我们从TaskManager::submitTaask()中传入的3个参数
$handlerType: 任务处理逻辑的类名 用于动态生成负责处理事件的handler对象$provider new $handlerType();调用它的Task::handleTask方法 $meta: 承载任务处理的元数据 将其转交给handler $params: 任务执行所需的数据 将其转交给handler
当某个任务真正触发时其对应的action hook就会被触发然后由监听函数转发至真正的执行逻辑
Task类-任务处理类
Task代表了一个任务它包括 任务名、任务提交逻辑、任务执行逻辑
实现
在/App/Services/Task创建Task.php文件 编写Task类
namespace App\Services\Task {use Exception;abstract class Task{public static string $taskName;/*** 提交一个该类型的任务需要提供必要元数据和执行参数*/public static function submitTask(int $maxRetry, array $taskParams){}/*** 对应任务触发时的执行逻辑* param mixed $taskMeta 任务元数据* param mixed $taskParams 任务处理数据* throws Exception 若任务未全部完成抛出异常*/public function handleTask(array $taskMeta, array $taskParams){// ...$this-handle($taskParams);// ...}/*** 任务逻辑主体* param mixed $taskParams 传入给该任务的参数* return mixed */protected abstract function handle($taskParams);}
}submitTask
submitTask()是对TaskManager提交函数的简单封装
因为自身存储了$taskName因此它可以省略TaskManager的第一个参数元数据可以明确限定 比如我只需要重试次数我就只把它当做输入参数然后封装成meta
具体编写为以下逻辑
public static function submitTask(int $maxRetry, array $taskParams)
{$taskMeta [retry $maxRetry];TaskManager::submitTask(static::class, $taskMeta, $taskParams);
}handleTask
前面也提到了handleTask是最终用于处理任务的逻辑 它其实有两个作用
准备、善后处理 接受任务元数据先进行准备 处理任务 接受任务参数真正处理任务
在这里“准备、善后”部分我只用作处理重试逻辑 处理任务的逻辑我把它分割到另一个handle方法由子类实现
handleTask应在成功时返回假失败时返回需要任务再次执行所需的参数
public function handleTask(array $taskMeta, array $taskParams)
{$pushBacks $this-handle($taskParams);/*** 任务失败了需要重新push任务* 1. 有需要执行的东西* 2. 有retry的定义且不为0*/if (!empty($pushBacks)) {if (!empty($taskMeta[retry])) {$taskMeta[retry] - 1;TaskManager::submitTask(static::class, $taskMeta, $pushBacks);throw new Exception(Retries have been scheduled for some uncompleted tasks. params are: . var_export($pushBacks, true));} elsethrow new Exception(Some of tasks failed. params are: . var_export($pushBacks, true));}
}exception将由Action Scheduler处理并显示在控制台中 PageViewTask-具体的任务类
真正的功能类继承自Task类这里需要编写访问远程分析工具并返回页面浏览量的逻辑 因此命名为PageViewTask
同样地具体的PageViewTask依靠于具体的远程分析工具API 但在这层抽象中我们只关注它们的共性都需要失败重试
实现
在/App/Services/Analytics创建PageViewTask.php文件 编写PageViewTask类
namespace App\Services\Analytics {use App\Services\Task\Task;use Excecption;abstract class PageViewTask extends Task{public static string $taskName nova_page_view_task;protected function handle($updatesList){foreach ($updatesList as $postId $args) {try {$views $this-getPostView($args);Analytics::setPageViews($postId, $views);// 删掉unset($updatesList[$postId]);} catch (\Exception $e) {// 无视}}return $updatesList;}abstract protected function getPostView($args): int;}
}首先别忘了我们需要给任务起名$taskName php的静态多态太爽了 C#什么时候能站起来 handle()这段逻辑呼应了我们远古时代实现的AnalyticsProvider::$updatesList逻辑 我们为了节省开销将多次阅读量更新捆绑成一次提交 因此$updatesList包含的是一个列表的待更新文章
我们在foreach循环中分割成单个更新再次踢皮球到getPostView交给子类处理
然后更新过程中的try ctach就有点秀了
如果没出意外我们把它从列表中移除意为不再需要如果出了意外将被catch并跳转到foreach下个循环
所以一顿操作后最终执行失败的参数会保留在$updateList中 将它返回则会触发父类的重试逻辑再次压入后台进程队列 妙妙妙妙妙 具体的PageViewTask
每个远程统计工具实现不同所以这层是必须的 这里还是以Umami为例其它的也差不多只是需要修改访问的参数
在/App/Services/Analytics/Umami创建UmamiPageViewTask.php文件 编写UmamiPageViewTask类
namespace App\Services\Analytics\Umami {use Exception;use App\Services\Analytics\PageViewTask;class UmamiPageViewTask extends PageViewTask{protected function getPostView($args): int{// 获取secret$baseUrl of_get_option(analytics_api_domain, );$authToken of_get_option(analytics_api_token, );// header$headers array(Authorization Bearer $authToken,Content-Type application/json,Accept application/json,);// 向umami发送请求$umami_url trailingslashit($baseUrl) . stats . ? . http_build_query([startAt 0,endAt time() . 000,url $args[path],]);$response wp_remote_get($umami_url, [headers $headers]);if (is_wp_error($response))throw new Exception($response-get_error_message());if (!empty($response[body]))$data json_decode($response[body], true);return \intval($data[uniques][value]) ?? 0;}}
}这段代码因为比较简单也直接给出了 需要提醒的是
重要数据不要硬编码在代码中在WordPress中可以使用控制台的设置功能 不过这里用到的of_get_option是装了options framework插件 大部分参数都可以自身构造而来真正从外部接受的参数其实就只有$args[path]我们在$response为WP_Error时抛出异常以示意出错 出错的主要原因是网络连接不佳因此我们需要抛出错误并重试返回401,404等不算出错有返回的情况反而没有重试的必要 因为试几次都是一样的 返回的处理取决于返回数据这里是顺着Umami的返回写的
化身为神的最后一块拼图 ruaaaaaaaaaaaaaaaaaaaaa 还记得吗之前的代码有一段空了一块 在UmamiAnalyticsProvider提交任务时没有给出具体的操作代码
因为当时还没引入后面的一堆 但现在我们都是懂哥了 加入这句代码让这个系统运作起来
class UmamiAnalyticsProvider extends AnalyticsProvider
{public function submitTasks(){if ($this-updatesList) {// -- ?? submit this background task ?? --UmamiPageViewTask::submitTask(1, $this-updatesList);}}
}调用UmamiPageViewTask::submitTask()
参数1重试1次参数2更新若干文章的必要数据
初始化
最后我们需要初始化TaskManager如果不初始化没有任务会被监听 不管需不需要加入新任务请确保每次php执行都会执行以下语句
use App\Services\Analytics as Analytics;
use App\Services\Task\TaskManager;Analytics\Analytics::setProvider(new Analytics\Umami\UmamiAnalyticsProvider());
TaskManager::registerTask(Analytics\PageViewTask::$taskName);
TaskManager::init();记得设置Provider当然你也可以传入Closure实现懒加载 e.g. fn() new UmamiAnalyticsProvider(); 记得注册TaskManager::registerTask所有可能执行的任务 注册开销并不大不要省省了任务绝对执行不了 在最后记得调用init()否则不会进行任何实质初始化操作
小结 花了好久写了这么多 包括代码包括文章 这过程中不止一次问自己至于吗 我最终的答案是肯定的
至于把东西封装到类里吗多绕啊
确实绕甚至是俄罗斯套娃 但在理解了绕之后带来的是可拓展性、可维护性
当然也可以直接一步步写下来 实不相瞒我第一个版本就是一步步写下去的根本就没有一个类
但这样做怎么进行拓展 不同的代码混在一起怎么维护
所以就算是花更多时间在把这坨屎跑起来之后都要给它框架化、规则化 消化了这坨小屎才能避免整个程序变成大屎
框架本身增加复杂性但它也带来了规则性 有了框架就很容易借用相似的逻辑 有了框架一切东西都井然有序
现在这个版本你可以随意增加更多的Task逻辑都是一样的 多舒服啊
至于把问题想那么复杂吗
至于访问远程统计工具获取精准数据吗 至于搞缓存吗 至于搞后台进程吗
没错要实现“显示浏览量”可以很简单 甚至不精准的统计数据可以增加我网站的显示访问量草现在全是个位数
但当把程序当做一种艺术它就不能容忍凑合 精益求精才是工匠精神