在WordPress站点中展示阅读量等流量分析数据(超详细实现)

这篇文章也可以在我的博客中查看

关于本文

专业的流量统计系统能够相对真实地反应网站的访问情况。
这些数据可以在后台很好地进行分析统计,但有时我们希望在网站前端展示一些数据

最常见的情景就是:展示页面的浏览量
这简单的操作当然也可以通过简单的计数器实现,但可能会造成重复统计(比如同一个用户点击10次)

目标

流量分析工具所提供的准确性是不可比拟的
因此这篇文章我们就来实现如何将流量分析数据搬到网站展示,做到:

  1. 同步流量分析工具数据到网站前端
    • 显示页面的阅读量
  2. 不影响页面加载
    • 用户不会感知到同步任务进行
  3. 不频繁访问分析工具API
    • 减少网络资源、API次数消耗

准备

为完成这些目标,需要一些前提准备:

  1. 配置好带有数据访问API的流量分析工具
    • Google AnalyticsUmami(本文将以Umami为例)
    • 这是我们的真实数据来源
  2. 配置好WordPress后台进程(Background Process)支持
    • 如Action-Scheduler(本文将以此为例)
    • 这是我们非阻塞运行的基础

分析问题

Analytics类

分析问题

API访问频率

阅读量实时性并不强,我们无须(也不可能)每次页面访问都从远程分析工具获取数据
频繁访问很有可能会被禁止访问API,(自建的相当于DDoS攻击自己😅)
在获取数据后,应该在短时间内缓存起来

WordPress中的跨请求缓存API是transient

处理缓存未命中

但如果缓存未命中怎么办?是立刻访问远程分析工具吗?
不可能,这样同步执行会使页面加载阻塞
特别是:如果你一次展示多篇文章,你需要等待它们全部完成才能加载出页面!

因此我们必须在本地数据库也持久化存储阅读量
这个冗余数据是缓存未命中时的唯一可行数据来源

在WordPress中,我们可以使用post_meta存储它

与此同时,这也可作为数据过时的标志:
我们应该触发更新阅读量的后台进程
非阻塞地将第三方分析工具的数据同步到本地上

小结

Analytics.php的是用于页面获取数据的接口。它的数据来源是:

  1. 内存缓存
    • 减少短期重复访问,减少服务器压力
  2. 本地数据库
    • 缓存未命中时的保底数据
  3. 远程分析工具
    • 数据更新的途径

它的职责是:

  1. 读写本地数据
  2. 发出更新请求

实现

注意组织文件结构,本文将/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作为唯一标识符
如果你希望实现任何页面的阅读量展示,你需要:

  1. 使用url[path]md5 hash作为唯一标识符
  2. 使用自定义数据库表存储阅读量:(url_md5, page_view)

需要做什么?
当访客来访时,需要展示阅读量,此时:

  1. 我们需要获取目标地址的WP_Post实例
    • 以获取url等信息
  2. 有缓存读缓存
  3. 无缓存读数据库
    1. (不阻塞执行)请求第三方流量分析API,更新记录
    2. 马上使用旧数据刷新缓存

前面提到了缓存过期是发出数据同步请求的标志,但我们不希望重复发起请求,
因此缓存未命中时需要马上再次写入缓存。

虽然数据是旧的,但不急。我们可以在数据同步时强制刷新它


大部分都好处理,异步请求比较麻烦,先卖个关子
同时我们还为阅读量定义了缓存键值和在数据库的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可能会很沉重,这里支持传入一个返回AnalyticsProviderClosure
以实现懒加载,只有需要使用它的时候才会生成

接下来再看看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

主要完成两件事:

  1. 完成任务提交逻辑
  2. 封装处理参数

以下我以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);}}
}
  1. Umami API获取阅读量必须提供页面的path,因此我重写pushUpdatePostViews并按id获取了它的path
  2. submitTask先检测了是否真有待提交任务数据,如有,提交
  • 具体提交逻辑见下文

后台任务

万事俱备,只欠东风
我们只剩下后台任务需要解决了,但你先别急
这篇文章目前只到一半

本文将使用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执行

  1. 第一次,制定任务
    1. 使用as_schedule_recurring_action制定任务
    2. 此时eg_midnight_loghook无效
  2. 第二次,午夜时执行任务(可能由cron或其它机制触发)
    1. 它从数据库中检测到预定的任务,生成eg_midnight_loghook
    2. 执行eg_midnight_loghook的逻辑

所以坑点就在于add_action( 'eg_midnight_log', 'eg_log_action_data' );必须在执行任务时加入,在制定任务时加入是无效

而我们的目标,则是:

  1. 把2次php执行的代码尽可能地透明化,封装起来
  2. 使用面向对象的思想处理任务,使其模块化

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

用于提交“保证任务触发时正常执行”所需的一切数据,包括:

  1. 交给谁处理(给谁处理)
  2. 执行处理的指引(怎么处理)
  3. 需要处理的数据(处理什么)

因此它需要传入3个参数:

  1. $handlerType: 承载任务处理逻辑的类名
    • 后文会详细介绍,它的基类是Task,包含一个handleTask方法
  2. $taskMeta: 承载任务处理的元数据
    • 比如任务时限?重试次数?
    • 反正是与任务相关,但与任务执行主体无关
  3. $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);
}
  1. 使用Action Scheduler提供的as_enqueue_async_action,将任务数据移交至其托管。
  2. 所有$args参数将被Action Scheduler存储于数据库,当执行时取出
    • 有点像序列化
  3. $taskNameTask类的静态变量,表示任务名
    • 因为Task与任务直接关联,因此任务名就存在它那了
  4. 防止完全重复任务
    1. 标记为唯一任务(第四个参数unique:true
    2. 计算参数的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个参数:

  1. $handlerType: 任务处理逻辑的类名
    • 用于动态生成负责处理事件的handler对象$provider = new $handlerType();
    • 调用它的Task::handleTask方法
  2. $meta: 承载任务处理的元数据
    • 将其转交给handler
  3. $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提交函数的简单封装:

  1. 因为自身存储了$taskName,因此它可以省略TaskManager的第一个参数
  2. 元数据可以明确限定
    • 比如我只需要重试次数,我就只把它当做输入参数,然后封装成meta

具体编写为以下逻辑:

public static function submitTask(int $maxRetry, array $taskParams)
{$taskMeta = ['retry' => $maxRetry];TaskManager::submitTask(static::class, $taskMeta, $taskParams);
}

handleTask

前面也提到了,handleTask是最终用于处理任务的逻辑
它其实有两个作用:

  1. 准备、善后处理
    • 接受任务元数据,先进行准备
  2. 处理任务
    • 接受任务参数,真正处理任务

在这里,“准备、善后”部分我只用作处理重试逻辑
处理任务的逻辑我把它分割到另一个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;}}
}

这段代码因为比较简单,也直接给出了
需要提醒的是:

  1. 重要数据不要硬编码在代码中,在WordPress中可以使用控制台的设置功能
    • 不过这里用到的of_get_option是装了options framework插件
  2. 大部分参数都可以自身构造而来,真正从外部接受的参数其实就只有:$args['path']
  3. 我们在$responseWP_Error时抛出异常,以示意出错
    • 出错的主要原因是网络连接不佳,因此我们需要抛出错误,并重试
    • 返回401,404等不算出错,有返回的情况反而没有重试的必要
      • 因为试几次都是一样的
  4. 返回的处理取决于返回数据,这里是顺着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:重试1次
  2. 参数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();
  1. 记得设置Provider,当然你也可以传入Closure实现懒加载
    • e.g. fn() => new UmamiAnalyticsProvider();
  2. 记得注册(TaskManager::registerTask所有可能执行的任务
    • 注册开销并不大,不要省
    • 省了任务绝对执行不了
  3. 在最后,记得调用init(),否则不会进行任何实质初始化操作

小结

花了好久,写了这么多
包括代码,包括文章

这过程中不止一次问自己,至于吗?
我最终的答案是肯定的

至于把东西封装到类里吗?多绕啊

确实绕,甚至是俄罗斯套娃
但在理解了绕之后,带来的是可拓展性、可维护性

当然也可以直接一步步写下来
实不相瞒,我第一个版本就是一步步写下去的,根本就没有一个类

但这样做,怎么进行拓展?
不同的代码混在一起,怎么维护?

所以就算是花更多时间,在把这坨屎跑起来之后,都要给它框架化、规则化
消化了这坨小屎,才能避免整个程序变成大屎

框架本身增加复杂性,但它也带来了规则性:
有了框架,就很容易借用相似的逻辑
有了框架,一切东西都井然有序

现在这个版本,你可以随意增加更多的Task,逻辑都是一样的
多舒服啊?

至于把问题想那么复杂吗?

至于访问远程统计工具获取精准数据吗?
至于搞缓存吗?
至于搞后台进程吗?

没错,要实现“显示浏览量”可以很简单
甚至不精准的统计数据,可以增加我网站的显示访问量(草,现在全是个位数)

但当把程序当做一种艺术,它就不能容忍凑合
精益求精,才是工匠精神

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/38940.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(el-Form)操作(不使用 ts):Element-plus 中 Form 表单组件校验规则等的使用

Ⅰ、Element-plus 提供的 Form 表单组件与想要目标情况的对比&#xff1a; 1、Element-plus 提供 Form 表单组件情况&#xff1a; 其一、Element-plus 自提供的 Form 代码情况为(示例的代码)&#xff1a; // Element-plus 自提供的代码&#xff1a; // 此时是使用了 ts 语言环…

6.3 社会工程学攻击

数据参考&#xff1a;CISP官方 目录 社会工程学攻击概念社会工程学攻击利用的人性 “弱点”典型社会工程学攻击方式社会工程学攻击防护 一、社会工程学攻击概念 什么是社会工程学攻击 也被称为 "社交工程学" 攻击利用人性弱点 (本能反应、贪婪、易于信任等) 进…

萤石直播以及回放的接入和销毁

以下基于vue项目 1.安装 npm i ezuikit-js 2、导入 main.js中 import EZUIKit from "ezuikit-js"; //导入萤石Vue.use(EZUIKit); 3、创建容器 <div class"video"><div id"video-container"></div><!-- <iframe :src…

栈存储结构详解

目录 栈存储结构详解 进栈和出栈 栈的具体实现 栈的应用 什么是队列&#xff08;队列存储结构&#xff09; 栈存储结构详解 同顺序表和链表一样&#xff0c;栈也是用来存储逻辑关系为 "一对一" 数据的线性存储结构&#xff0c;如图 1 所示。 图 1 栈存储结构示意…

HTML5的介绍和基本框架

目录 HTML5 HTML5介绍 HTML5的DOCTYPE声明 HTML5基本骨架 html标签 head标签 body标签 title标签 meta标签 在vscode中写出第一个小框架 HTML5 HTML5介绍 HTML5是用来描述网页的一种语言&#xff0c;被称为超文本标记语言。用HTML5编写的文件&#xff0c;后缀以.ht…

设备加密狗

场景描述 随着科技的飞速发展&#xff0c;越来越多的智能设备走进生产加工车间。例如智能雕刻机、钣金机、 榫槽机、钻孔机、磨刀机等等。 目前市场的智能设备具有一个共同的特点&#xff0c;内置嵌入操作系统&#xff0c;如windows或者linux系统。设备制造商提供智能设备出…

20天学会rust(四)常见系统库的使用

前面已经学习了rust的基础知识&#xff0c;今天我们来学习rust强大的系统库&#xff0c;从此coding事半功倍。 集合 数组&可变长数组 在 Rust 中&#xff0c;有两种主要的数组类型&#xff1a;固定长度数组&#xff08;Fixed-size Arrays&#xff09;和可变长度数组&…

Ajax如何理解

什么是ajax ●认识前后端交互 ○就是 前端 与 后端的 一种通讯方式 ○主要使用的技术栈就是 ajax (async javascript and xml) ●ajax 特点 ○使用 ajax 技术网页应用能够快速的将新内容呈现在用户界面 ○并且不需要刷新整个页面, 也就是能够让页面有 "无…

Java技术整理(5)—— Spring篇

Spring是一个全面的全面的、企业应用开发一站式的解决方案&#xff0c;贯穿表现层、业务层、持久层。但是 Spring 仍然可以和其他的框架无缝整合。 1、Spring的核心组件 &#xff08;1&#xff09;数据层&#xff1a; JDBC、ORM、OXM、JMS、Transations &#xff08;2&#x…

Flutter 中,ListView 中需要放置 ListView 需要怎么处理才高效?

问题及场景 ListView 是 Flutter 开发者第一个学习到的 Widget&#xff0c;因为它可以滑动。一切都会运行得很好&#xff0c;直到 ListView 中的 Item 本身也是一个 ListView。你可能会看到 Flutter 建议你将内部的 ListView 的ShrinkWrap 属性设置为 True。虽然错误消除了&am…

连续两年增收不增利,比亚迪电子靠新能源汽车业务再次起飞?

在净利润连续两年下挫之后&#xff0c;比亚迪电子&#xff08;00285.HK&#xff09;终于迎来了好消息。 不久前比亚迪电子发布2023年中期盈利预告显示&#xff0c;上半年净利润同比增加115%-146%&#xff08;2022年上半年的净利润显示6.34亿元&#xff09;。 这主要受益于大客…

包管理工具 nvm npm nrm yarn cnpm npx pnpm详解

包管理工具 nvm npm yarn cnpm npx pnpm npm、cnpm、yarn、pnpm、npx、nvm的区别&#xff1a;https://blog.csdn.net/weixin_53791978/article/details/122533843 npm、cnpm、yarn、pnpm、npx、nvm的区别&#xff1a;https://blog.csdn.net/weixin_53791978/article/details/1…

【Freertos基础入门】2个Freertos的Delay函数

文章目录 前言一、vTaskDelay与vTaskDelayUntil二、示例代码总结 前言 本系列基于stm32系列单片机来使用freerots 任务管理是实时操作系统&#xff08;RTOS&#xff09;的核心功能之一&#xff0c;它允许开发者以并发的方式组织和管理多个任务。FreeRTOS 是一个流行的开源RTO…

嵌入式开发中常用且杂散的命令

1、mount命令 # 挂载linux系统 mkdir /tmp/share mount -t nfs 10.77.66.88:/share/ /tmp/share -o nolock,tcp cd /tmp/share# 挂载Windows系统 mkdir /tmp/windows mount -t nfs 10.66.77.88:/c/public /tmp/windows -o nolock,tcp cd /tmp/windows# 挂载vfat格式的U盘 mkdi…

强训第32

选择 D B A A 发送TCP意思应该是已经建立了连接&#xff0c;会超时重传。在未建立连接的时候&#xff0c;会放弃该链接 C A 80端口是http A 交换机攻击主要有五种&#xff1a;VLAN跳跃攻击 生成树攻击 MAC表洪水攻击 ARP攻击 VTP攻击 B A 2^(32-26)2^(32-27)2^(32-27)128 减去…

基于Java+SpringBoot+Vue+echarts健身房管理系统设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

maven Jar包反向install到本地仓库

maven Jar包反向install到本地仓库 需求实现 需求 项目打包时报错&#xff0c;缺少一个jar包。 但是在maven仓库都找不到此jar包&#xff0c;其他人提供了这个jar包。 需要把这个jar包install到本地仓库&#xff0c;使项目能正常打包运行。 实现 使用git bash命令执行以下脚…

16.3.4 【Linux】系统资源的观察

free &#xff1a;观察内存使用情况 系统当中有 2848MB 左右的实体内存&#xff0c;我的 swap 有 1GB 左右&#xff0c; 那我使用free -m 以 MBytes 来显示时&#xff0c;就会出现上面的信息。Mem 那一行显示的是实体内存的量&#xff0c;Swap 则是内存交换空间的量。 total 是…

C++多态

文章目录 &#x1f435;1. 什么是多态&#x1f436;2. 构成多态的条件&#x1f429;2.1 虚函数&#x1f429;2.2 虚函数的重写&#x1f429;2.3 final 和 override关键字&#x1f429;2.4 重载、重写、重定义对比 &#x1f431;3. 虚函数表&#x1f42f;4. 多态的原理&#x1f…

神经网络基础-神经网络补充概念-17-计算神经网络的输出

计算神经网络的输出通常涉及前向传播&#xff08;Forward Propagation&#xff09;的过程&#xff0c;其中输入数据通过网络的层级结构&#xff0c;逐步被传递并变换&#xff0c;最终生成预测结果。下面我将为你展示一个简单的神经网络前向传播的示例。 假设我们有一个具有以下…