在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;


	// 读取数据库记录,这将是最后能够返回的值
	$pageViews = get_post_meta($post->ID, Analytics::$pageViewMetaKey, true) ?: 0;
	if (is_singular()) {
		// 记录更新请求
		// <-- ?? async call to update ?? -->
		// 重写缓存
		set_transient(Analytics::pageViewsCacheKey($post->ID), $pageViews, static::$pageViewCacheTime);
	}
	return $pageViews;
}

为了减少不必要的请求数,我们使用is_singular()确保当前访问的是文章页面
否则文章大概率是被输出为概要、列表,并不存在真的访问,没必要更新

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);
		else
			static::$_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篇文章
如果我们需要为每篇文章组织一个任务
此时需要组织100个任务

虽然我们这里使用is_singluar()限制了只在文章页面发起更新请求(见上文)
但这里讨论的问题是更一般化的
我们编写的代码可不只是为了显示访问量一种数据而已

因为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)
{
	// ...
	if (is_singular()) {
		// 记录更新请求
		// <-- ?? 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));
		} else
			throw 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,逻辑都是一样的
多舒服啊?

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

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WordPress 使用双数据库可以通过使用 WordPress 的多站点功能来实现。你需要在 WordPress 上安装和配置多站点功能,并为每个站点分别配置不同的数据库。 以下是如何在 WordPress 使用双数据库的步骤: 1. 在 WordPress 启用多站点功能。打开 wp-config.php 文件,找到下面这行代码: ``` /* That’s all, stop editing! Happy blogging. */ ``` 在这行代码前面添加以下代码: ``` /* Multisite */ define('WP_ALLOW_MULTISITE', true); ``` 保存并关闭文件。 2. 在 WordPress 管理后台启用多站点功能。进入“工具”→“网络设置”,按照提示完成多站点设置。 3. 为每个站点配置不同的数据库。在 WordPress 的 wp-config.php 文件,你可以为每个站点配置不同的数据库。例如: ``` define('DB_NAME', 'database1'); define('DB_USER', 'username1'); define('DB_PASSWORD', 'password1'); define('DB_HOST', 'localhost'); /* Multisite */ define('MULTISITE', true); define('SUBDOMAIN_INSTALL', false); define('DOMAIN_CURRENT_SITE', 'example.com'); define('PATH_CURRENT_SITE', '/'); define('SITE_ID_CURRENT_SITE', 1); define('BLOG_ID_CURRENT_SITE', 1); /* Another site */ define('DB_NAME', 'database2'); define('DB_USER', 'username2'); define('DB_PASSWORD', 'password2'); define('DB_HOST', 'localhost'); ``` 请注意,每个站点数据库名称、用户名、密码和主机地址都应该不同。 4. 将数据上传到不同的数据库。在 WordPress ,你可以使用插件或者手动将数据上传到不同的数据库。如果你使用的是插件,可以搜索“WordPress站点数据共享”等相关插件。如果你想手动上传数据,可以在多站点选择需要上传的站点,然后在该站点数据执行 SQL 语句将数据上传到该站点数据。 总之,使用 WordPress 的多站点功能可以方便地实现WordPress 使用双数据库。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值