专业的流量统计系统能够相对真实地反应网站的访问情况。 这些数据可以在后台很好地进行分析统计,但有时我们希望在网站前端展示一些数据
最常见的情景就是:展示页面的浏览量 这简单的操作当然也可以通过简单的计数器实现,但可能会造成重复统计(比如同一个用户点击10次)
流量分析工具所提供的准确性是不可比拟的 因此这篇文章我们就来实现如何将流量分析数据搬到网站展示,做到:
- 同步流量分析工具数据到网站前端
- 显示页面的阅读量
- 不影响页面加载
- 用户不会感知到同步任务进行
- 不频繁访问分析工具API
- 减少网络资源、API次数消耗
为完成这些目标,需要一些前提准备:
- 配置好带有数据访问API的流量分析工具
- 如、(本文将以Umami为例)
- 这是我们的真实数据来源
- 配置好WordPress后台进程(Background Process)支持
- 如Action-Scheduler(本文将以此为例)
- 这是我们非阻塞运行的基础
API访问频率
阅读量实时性并不强,我们无须(也不可能)每次页面访问都从远程分析工具获取数据 频繁访问很有可能会被禁止访问API,(自建的相当于DDoS攻击自己😅) 在获取数据后,应该在短时间内缓存起来
WordPress中的跨请求缓存API是
处理缓存未命中
但如果缓存未命中怎么办?是立刻访问远程分析工具吗? 不可能,这样同步执行会使页面加载阻塞 特别是:如果你一次展示多篇文章,你需要等待它们全部完成才能加载出页面!
因此我们必须在本地数据库也持久化存储阅读量 这个冗余数据是缓存未命中时的唯一可行数据来源
在WordPress中,我们可以使用存储它
与此同时,这也可作为数据过时的标志: 我们应该触发更新阅读量的后台进程 非阻塞地将第三方分析工具的数据同步到本地上
小结
的是用于页面获取数据的接口。它的数据来源是:
- 内存缓存
- 减少短期重复访问,减少服务器压力
- 本地数据库
- 缓存未命中时的保底数据
- 远程分析工具
- 数据更新的途径
它的职责是:
- 读写本地数据
- 发出更新请求
注意组织文件结构,本文将文件夹作为根目录
在创建文件
编写类,它主要包含一些静态函数
getPageViews
本文实现需要依赖$post->ID作为唯一标识符 如果你希望实现任何页面的阅读量展示,你需要:
- 使用的作为唯一标识符
- 使用自定义数据库表存储阅读量:
需要做什么? 当访客来访时,需要展示阅读量,此时:
- 我们需要获取目标地址的实例
- 以获取url等信息
- 有缓存读缓存
- 无缓存读数据库
- (不阻塞执行)请求第三方流量分析API,更新记录
- 马上使用旧数据刷新缓存
前面提到了缓存过期是发出数据同步请求的标志,但我们不希望重复发起请求, 因此缓存未命中时需要马上再次写入缓存。
虽然数据是旧的,但不急。我们可以在数据同步时强制刷新它
大部分都好处理,异步请求比较麻烦,先卖个关子 同时我们还为阅读量定义了缓存键值和在数据库的meta键值:
为了减少不必要的请求数,我们使用确保当前访问的是文章页面 否则文章大概率是被输出为概要、列表,并不存在真的访问,没必要更新
setPageViews
这个函数用于写入本地的数据存储,包括缓存和数据库 注意,它并不包含异步更新的过程,只是异步更新的结果需要借助它写入:
Provider
好了,该想想怎么访问远程API了 因为大多为固定操作,我们实现为静态 但是更新数据来源的逻辑呢?
不同的流量分析工具会提供不同的API,因此我们也需要为它们编写各自的处理逻辑 我们需要根据设置为注入一个恰当的数据来源实例,这里称为
先关注类中需要如何支持注入Provider
没使用任何框架,我只能纯手工注入 以下代码是额外增加内容,需要与上文合并
我们需要先设置使用的数据源,后续使用获取它
因为某些可能会很沉重,这里支持传入一个返回的 以实现懒加载,只有需要使用它的时候才会生成
接下来再看看需要怎么编写
不同的provider有不同的访问逻辑,但至少有没有些共性? 还真有!
Provider负责组织后台任务,但每次请求更新都立刻组织一个后台任务还是很恐怖的。
比如:一个页面有100篇文章 如果我们需要为每篇文章组织一个任务 此时需要组织100个任务
虽然我们这里使用限制了只在文章页面发起更新请求(见上文) 但这里讨论的问题是更一般化的 我们编写的代码可不只是为了显示访问量一种数据而已
因为php无守护进程,每个后台任务其实需要通过写数据库进行任务信息持久化 因此组织100个后台任务,意味着访问数据库上百次
而组织任务这个过程,是同步的、阻塞的 用户会看着页面转十秒加载不出来
但说到底,有没有必要把它视为100个任务?不能批处理一下吗? 当然可以,而且这就是不同的一个共性。
在创建文件
编写类
pushUpdatePostViews
这是登记更新任务的逻辑 上文说了,我们不希望立刻生成后台任务,而是记录它:
主要是请求API时的参数,比如:时间段?目标地址?国家?…… 这与具体数据源的实现有关,但总之,我们需要把这些可能用到的数据存到里
记录了本次请求中,所有需要请求阅读量更新的文章和相应参数 但我们如何把它加到后台任务?
submitTasks()
submitTasks由子类负责给出任务提交的逻辑 父类只需要给出约束
没完,我们需要有人在最后调用这个函数,才能完成所有任务一次性提交 可以利用WordPress的hook
因为是WordPress最后一个hook,因此不用担心之后还会有新的任务提交请求
注意,WordPress hook的回调必须是函数
还记得的空缺位置吗? 它应该调用!
注意:在上下文中就是
主要完成两件事:
- 完成任务提交逻辑
- 封装处理参数
以下我以为例
在创建文件 编写类:
- 获取阅读量必须提供页面的,因此我重写并按获取了它的
- 先检测了是否真有待提交任务数据,如有,提交
- 具体提交逻辑见下文
万事俱备,只欠东风 我们只剩下后台任务需要解决了,但你先别急 这篇文章目前只到一半
本文将使用作为后台任务的驱动 但不管你是否使用它,后文的结构都可以给你一点灵感
基本上是WordPress中支持后台进程的唯一选择了 它的官方例子如下:
这个例子将在每天午夜输出一个log
但这例子其实有个坑,的执行机制事实上跨越了2次php执行:
- 第一次,制定任务
- 使用制定任务
- 此时hook无效
- 第二次,午夜时执行任务(可能由cron或其它机制触发)
- 它从数据库中检测到预定的任务,生成hook
- 执行hook的逻辑
所以坑点就在于必须在执行任务时加入,在制定任务时加入是无效的
而我们的目标,则是:
- 把2次php执行的代码尽可能地透明化,封装起来
- 使用面向对象的思想处理任务,使其模块化
主要用于负责所有任务的提交和触发,我的实现主要针对,如果使用其它后台任务库,该类需要做对应修改。
在阅读前,建议先了解的基本操作
在创建文件 编写类:
用于记录所有需要管理的任务名,它的作用只是将名字加入列表
submitTask
用于提交“保证任务触发时正常执行”所需的一切数据,包括:
- 交给谁处理(给谁处理)
- 执行处理的指引(怎么处理)
- 需要处理的数据(处理什么)
因此它需要传入3个参数:
- : 承载任务处理逻辑的类名
- 后文会详细介绍,它的基类是,包含一个方法
- : 承载任务处理的元数据
- 比如任务时限?重试次数?
- 反正是与任务相关,但与任务执行主体无关的
- : 任务执行所需的数据
- 比如我们需要访问api,那可能就是api参数等等
因此可以写出这样的代码:
- 使用提供的,将任务数据移交至其托管。
- 所有参数将被存储于数据库,当执行时取出
- 有点像序列化
- 是类的静态变量,表示任务名
- 因为与任务直接关联,因此任务名就存在它那了
- 防止完全重复任务
- 标记为唯一任务(第四个参数)
- 计算参数的md5作为分组,用于识别重复任务
init
init需要在每次执行、所有调用结束后调用,它用于监听后台任务是否已触发,如果是,则分配到相应的处理函数
首先需要引入文件,然后对每个注册的任务名,都使用监听函数(这里实现为匿名函数)订阅它的
当事件触发时,这个函数将获得我们从中传入的3个参数:
- : 任务处理逻辑的类名
- 用于动态生成负责处理事件的handler对象
- 调用它的方法
- : 承载任务处理的元数据
- 将其转交给handler
- : 任务执行所需的数据
- 将其转交给handler
当某个任务真正触发时,其对应的就会被触发,然后由监听函数转发至真正的执行逻辑
Task代表了一个任务,它包括: 任务名、任务提交逻辑、任务执行逻辑
在创建文件 编写类:
submitTask
是对提交函数的简单封装:
- 因为自身存储了,因此它可以省略的第一个参数
- 元数据可以明确限定
- 比如我只需要重试次数,我就只把它当做输入参数,然后封装成
具体编写为以下逻辑:
handleTask
前面也提到了,是最终用于处理任务的逻辑 它其实有两个作用:
- 准备、善后处理
- 接受任务元数据,先进行准备
- 处理任务
- 接受任务参数,真正处理任务
在这里,“准备、善后”部分我只用作处理重试逻辑 处理任务的逻辑我把它分割到另一个方法,由子类实现
应在成功时返回假,失败时返回需要任务再次执行所需的参数
将由处理并显示在控制台中
真正的功能类继承自Task类,这里需要编写访问远程分析工具,并返回页面浏览量的逻辑 因此命名为
同样地,具体的依靠于具体的远程分析工具API 但在这层抽象中,我们只关注它们的共性:都需要失败重试
在创建文件 编写类:
首先别忘了我们需要给任务起名
php的静态多态太爽了 C#什么时候能站起来()
这段逻辑呼应了我们远古时代实现的逻辑 我们为了节省开销,将多次阅读量更新捆绑成一次提交 因此包含的是一个列表的待更新文章
我们在foreach循环中分割成单个更新,再次踢皮球到交给子类处理
然后更新过程中的就有点秀了:
- 如果没出意外,我们把它从列表中移除,意为不再需要
- 如果出了意外,将被catch,并跳转到foreach下个循环
所以一顿操作后,最终执行失败的参数会保留在中 将它返回,则会触发父类的重试逻辑,再次压入后台进程队列
妙妙妙妙妙
每个远程统计工具实现不同,所以这层是必须的 这里还是以为例,其它的也差不多,只是需要修改访问的参数
在创建文件 编写类:
这段代码因为比较简单,也直接给出了 需要提醒的是:
- 重要数据不要硬编码在代码中,在WordPress中可以使用控制台的设置功能
- 不过这里用到的是装了插件
- 大部分参数都可以自身构造而来,真正从外部接受的参数其实就只有:
- 我们在为时抛出异常,以示意出错
- 出错的主要原因是网络连接不佳,因此我们需要抛出错误,并重试
- 返回401,404等不算出错,有返回的情况反而没有重试的必要
- 因为试几次都是一样的
- 返回的处理取决于返回数据,这里是顺着的返回写的
ruaaaaaaaaaaaaaaaaaaaaa
还记得吗?之前的代码有一段空了一块 在提交任务时,没有给出具体的操作代码
因为当时还没引入后面的一堆 但现在,我们都是懂哥了 加入这句代码,让这个系统运作起来:
调用
- 参数1:重试1次
- 参数2:更新若干文章的必要数据
最后,我们需要初始化,如果不初始化,没有任务会被监听 不管需不需要加入新任务,请确保每次php执行都会执行以下语句:
- 记得设置,当然你也可以传入实现懒加载
- e.g. ;
- 记得注册()所有可能执行的任务
- 注册开销并不大,不要省
- 省了任务绝对执行不了
- 在最后,记得调用,否则不会进行任何实质初始化操作
花了好久,写了这么多 包括代码,包括文章
这过程中不止一次问自己,至于吗? 我最终的答案是肯定的
确实绕,甚至是俄罗斯套娃 但在理解了绕之后,带来的是可拓展性、可维护性
当然也可以直接一步步写下来 实不相瞒,我第一个版本就是一步步写下去的,根本就没有一个类
但这样做,怎么进行拓展? 不同的代码混在一起,怎么维护?
所以就算是花更多时间,在把这坨屎跑起来之后,都要给它框架化、规则化 消化了这坨小屎,才能避免整个程序变成大屎
框架本身增加复杂性,但它也带来了规则性: 有了框架,就很容易借用相似的逻辑 有了框架,一切东西都井然有序
现在这个版本,你可以随意增加更多的Task,逻辑都是一样的 多舒服啊?
至于访问远程统计工具获取精准数据吗? 至于搞缓存吗? 至于搞后台进程吗?
没错,要实现“显示浏览量”可以很简单 甚至不精准的统计数据,可以增加我网站的显示访问量(草,现在全是个位数)