剖析 `get_current_screen()` 函数的源码,它在 WordPress 后台是如何确定当前页面的?

好嘞,各位观众老爷们,欢迎来到“扒光 WordPress 后台小秘密”系列讲座。今天咱们要聊的是一个非常关键的函数:get_current_screen(),看看它到底是怎么摸清我们现在身处 WordPress 后台的哪个角落。

一、开场白:get_current_screen() 的重要性

在 WordPress 后台开发中,我们经常需要知道当前页面是文章编辑页、用户管理页、还是小工具设置页等等。get_current_screen() 函数就像一个老侦探,负责收集线索、分析证据,最终确定“案发现场”。

为什么要知道当前页面?原因很多:

  • 加载特定页面的 CSS/JavaScript: 只有在特定页面才需要加载的脚本和样式,避免全局加载造成性能浪费。
  • 修改特定页面的行为: 根据当前页面,调整表单验证规则、添加自定义按钮等等。
  • 权限控制: 判断用户是否有权限访问当前页面,进行相应的权限检查。
  • 追踪用户行为: 记录用户在特定页面的操作,用于分析和优化。

总之,get_current_screen() 是后台开发的基石,理解它的工作原理至关重要。

二、源码剖析:步步为营,抽丝剥茧

让我们深入 wp-includes/screen.php 文件,看看 get_current_screen() 的真面目。 为了方便讲解,我简化了一些代码,保留了核心逻辑。

<?php

/**
 * Retrieve the current screen object.
 *
 * @global WP_Screen $current_screen
 *
 * @return WP_Screen|null WP_Screen object. Null if not set.
 */
function get_current_screen() {
    global $current_screen;

    if ( ! is_a( $current_screen, 'WP_Screen' ) ) {
        set_current_screen();
    }

    return $current_screen;
}

/**
 * Sets the current screen object.
 *
 * @global WP_Screen $current_screen
 * @global string    $pagenow
 * @global WP_Admin_Bar $wp_admin_bar
 *
 * @param WP_Screen|string|null $hook_name Optional. A hook name.
 */
function set_current_screen( $hook_name = '' ) {
    global $current_screen, $pagenow, $wp_admin_bar;

    if ( ! is_a( $current_screen, 'WP_Screen' ) ) {
        $current_screen = new WP_Screen();
    }

    // Populate admin menu variables.
    require_once ABSPATH . 'wp-admin/includes/admin.php';

    // Get the hook name.
    if ( empty( $hook_name ) ) {
        $hook_name = get_current_screen_hook();
    }

    $current_screen->set_hookname( $hook_name );
    $current_screen->populate_network_path();
    $current_screen->populate_base();
    $current_screen->populate_id();
    $current_screen->populate_key();
    $current_screen->populate_taxonomy();
    $current_screen->populate_post_type();

    // Add screen help tabs and help sidebar.
    do_action( 'load-' . $current_screen->id );
    do_action( 'current_screen', $current_screen );

    if ( is_admin_bar_showing() ) {
        require_once ABSPATH . 'wp-admin/includes/admin-bar.php';
        wp_admin_bar_init();
    }
}

/**
 * Get the current screen hook.
 *
 * @global string $pagenow
 * @global string $typenow
 * @global string $taxnow
 *
 * @return string|null The hook name of the current screen, or null if not set.
 */
function get_current_screen_hook() {
    global $pagenow, $typenow, $taxnow;

    $hook_name = false;

    if ( isset( $_GET['import'] ) ) {
        $hook_name = 'import';
    } elseif ( 'plugins.php' == $pagenow ) {
        $plugin = isset( $_REQUEST['plugin'] ) ? trim( sanitize_text_field( wp_unslash( $_REQUEST['plugin'] ) ) ) : '';
        if ( $plugin ) {
            $hook_name = 'plugin-install';
        } else {
            $hook_name = 'plugins';
        }
    } elseif ( 'themes.php' == $pagenow ) {
        $hook_name = 'themes';
    } elseif ( $typenow ) {
        $hook_name = get_plugin_page_hookname( 'edit.php?post_type=' . $typenow, '' );
    } elseif ( $taxnow ) {
        $hook_name = get_plugin_page_hookname( 'edit-tags.php?taxonomy=' . $taxnow, '' );
    } elseif ( isset( $_GET['page'] ) ) {
        $hook_name = get_plugin_page_hookname( $_GET['page'], '' );
    } elseif ( in_array( $pagenow, array( 'post.php', 'post-new.php' ) ) ) {
        $hook_name = 'post';
    } elseif ( in_array( $pagenow, array( 'media-upload.php', 'async-upload.php' ) ) ) {
        $hook_name = 'media-upload';
    } elseif ( in_array( $pagenow, array( 'index.php', 'wp-activate.php' ) ) ) {
        $hook_name = 'dashboard';
    } else {
        $hook_name = $pagenow;
    }

    return $hook_name;
}

class WP_Screen {
    public $id = '';
    public $taxonomy = '';
    public $post_type = '';
    public $base = '';
    public $action = '';
    public $parent_base = '';
    public $parent_file = '';
    public $screen_icon = '';
    public $help_tabs = array();
    public $help_sidebar = '';
    public $is_network = false;
    public $is_user = false;
    public $in_admin = true;
    private $key = null;
    private $hook_suffix;

    public function __construct( $hook_name = '', $parent_file = '' ) {
        if ( ! empty( $hook_name ) ) {
            $this->set_hookname( $hook_name );
        }

        if ( ! empty( $parent_file ) ) {
            $this->parent_file = $parent_file;
        }
    }

    public function set_hookname( $hook_name ) {
        $this->hook_suffix = preg_replace( '#W#', '', strtolower( $hook_name ) );
    }

    public function populate_network_path() {
        if ( ! function_exists( 'is_network_admin' ) ) {
            return;
        }

        $this->is_network = is_network_admin();
        $this->is_user    = is_user_admin();
    }

    public function populate_base() {
        $base = 'admin';

        if ( 'appearance_page_custom-header' === $this->id ) {
            $base = 'appearance_page_header';
        }

        if ( 'appearance_page_custom-background' === $this->id ) {
            $base = 'appearance_page_background';
        }

        $this->base = $base;
    }

    public function populate_id() {
        global $pagenow, $typenow, $taxnow;

        if ( $typenow ) {
            $this->id = "edit-{$typenow}";
        } elseif ( $taxnow ) {
            $this->id = "edit-{$taxnow}";
        } else {
            $this->id = $pagenow;
        }
    }

    public function populate_key() {
        $this->key = md5( serialize( array(
            'id'         => $this->id,
            'base'       => $this->base,
            'action'     => $this->action,
            'post_type'  => $this->post_type,
            'taxonomy'   => $this->taxonomy,
            'is_network' => $this->is_network,
            'is_user'    => $this->is_user,
        ) ) );
    }

    public function populate_taxonomy() {
        global $taxnow;

        if ( $taxnow ) {
            $this->taxonomy = $taxnow;
        }
    }

    public function populate_post_type() {
        global $typenow;

        if ( $typenow ) {
            $this->post_type = $typenow;
        }
    }

}

2.1 核心流程

  1. get_current_screen(): 这是我们调用的函数。它首先检查全局变量 $current_screen 是否存在并且是一个 WP_Screen 对象。如果不是,它会调用 set_current_screen() 来初始化 $current_screen。最后,返回 $current_screen 对象。

  2. set_current_screen(): 这个函数负责创建和初始化 WP_Screen 对象。

    • 它首先创建一个新的 WP_Screen 对象(如果 $current_screen 还没有被创建)。
    • 然后,它调用 get_current_screen_hook() 来获取当前页面的 "hook name"。这才是确定当前页面的关键步骤。
    • 接下来,它使用 WP_Screen 对象的各种 populate_*() 方法,根据 hook name 以及全局变量(如 $pagenow, $typenow, $taxnow)来设置 WP_Screen 对象的各种属性,例如 id, base, post_type, taxonomy 等。
    • 最后,它触发两个 action hook:load-$current_screen->idcurrent_screen,允许其他插件或主题在当前页面加载时执行自定义代码。
  3. get_current_screen_hook(): 这个函数是整个流程中最复杂的部分,也是决定当前页面的核心逻辑所在。它通过检查各种全局变量和 $_GET 参数,来确定当前页面的 "hook name"。这个 hook name 最终会被用作 WP_Screen 对象的 id 属性的一部分。

2.2 get_current_screen_hook() 的逻辑

get_current_screen_hook() 函数使用一系列 if...elseif...else 语句,逐步缩小范围,确定当前页面。

条件 页面类型 Hook Name
isset( $_GET['import'] ) 导入页面 'import'
$pagenow == 'plugins.php' 插件页面 如果有 $_REQUEST['plugin'] 参数,则是 'plugin-install',否则是 'plugins'
$pagenow == 'themes.php' 主题页面 'themes'
$typenow (当前文章类型) 文章类型列表页面 'edit.php?post_type=' . $typenow 的钩子名称 (通过 get_plugin_page_hookname)
$taxnow (当前分类法) 分类法列表页面 'edit-tags.php?taxonomy=' . $taxnow 的钩子名称 (通过 get_plugin_page_hookname)
isset( $_GET['page'] ) 插件创建的页面 (通过 add_menu_page) $_GET['page'] 的钩子名称 (通过 get_plugin_page_hookname)
$pagenow'post.php''post-new.php' 编辑或新建文章页面 'post'
$pagenow'media-upload.php''async-upload.php' 媒体上传页面 'media-upload'
$pagenow'index.php''wp-activate.php' 仪表盘或激活页面 'dashboard'
其他情况 其他页面 $pagenow

2.3 WP_Screen 对象的属性

WP_Screen 类定义了许多属性,用于描述当前页面的各种信息。

属性 描述
id 页面的唯一标识符。通常是 $pagenowedit-$post_typeedit-$taxonomy
taxonomy 当前页面的分类法名称,如果适用。
post_type 当前页面的文章类型名称,如果适用。
base 页面的基本类型。通常是 'admin'
action 页面的动作,例如 'edit''add'
parent_base 父页面的基本类型。
parent_file 父页面的文件名。
is_network 是否是网络管理页面(在 Multisite 环境中)。
is_user 是否是用户管理页面(在 Multisite 环境中)。

三、实战演练:代码示例,手把手教学

现在,让我们通过一些代码示例,看看如何使用 get_current_screen() 来实现一些常见的任务。

3.1 只在文章编辑页面加载 JavaScript

add_action( 'admin_enqueue_scripts', 'my_admin_enqueue_scripts' );

function my_admin_enqueue_scripts() {
    $screen = get_current_screen();

    if ( $screen && $screen->id == 'post' ) {
        wp_enqueue_script( 'my-custom-script', plugin_dir_url( __FILE__ ) . 'js/my-custom-script.js', array( 'jquery' ), '1.0', true );
    }
}

这段代码会在文章编辑页面加载 my-custom-script.js 文件。 admin_enqueue_scripts 是一个在后台加载脚本和样式时触发的 action hook。 我们首先获取当前屏幕对象,然后检查它的 id 属性是否等于 'post'。如果是,就加载我们的自定义脚本。

3.2 在特定插件页面添加自定义 Meta Box

假设我们有一个名为 "my-awesome-plugin" 的插件,并且它创建了一个名为 "my-awesome-page" 的后台页面。 我们想在这个页面上添加一个自定义 Meta Box。

add_action( 'add_meta_boxes', 'my_add_meta_boxes' );

function my_add_meta_boxes() {
    $screen = get_current_screen();

    if ( $screen && $screen->id == 'my-awesome-page' ) {
        add_meta_box(
            'my_meta_box',
            'My Awesome Meta Box',
            'my_meta_box_callback',
            $screen->id,
            'normal',
            'default'
        );
    }
}

function my_meta_box_callback( $post ) {
    // Meta Box 的内容
    echo '<p>This is my awesome meta box!</p>';
}

这段代码会在 "my-awesome-page" 页面上添加一个名为 "My Awesome Meta Box" 的 Meta Box。 add_meta_boxes 是一个在添加 Meta Box 时触发的 action hook。 我们首先获取当前屏幕对象,然后检查它的 id 属性是否等于 'my-awesome-page'。如果是,就添加我们的自定义 Meta Box。 my_meta_box_callback 函数负责渲染 Meta Box 的内容。

3.3 根据文章类型显示不同的帮助文本

add_action( 'current_screen', 'my_add_help_tab' );

function my_add_help_tab( $screen ) {
    if ( 'edit-book' == $screen->id ) {
        $screen->add_help_tab( array(
            'id'      => 'book_help_tab',
            'title'   => 'Book Help',
            'content' => '<p>This is help content for the Book post type.</p>',
        ) );
    } elseif ( 'edit-movie' == $screen->id ) {
        $screen->add_help_tab( array(
            'id'      => 'movie_help_tab',
            'title'   => 'Movie Help',
            'content' => '<p>This is help content for the Movie post type.</p>',
        ) );
    }
}

这段代码会根据当前文章类型,在帮助选项卡中显示不同的帮助文本。 current_screen 是一个在当前屏幕对象被设置后触发的 action hook。 我们首先检查屏幕对象的 id 属性是否等于 'edit-book''edit-movie'。如果是,就添加相应的帮助选项卡。

四、注意事项:避坑指南,安全第一

  • 在正确的 action hook 中使用 get_current_screen(): get_current_screen() 的返回值依赖于 WordPress 的加载顺序。 通常,在 admin_enqueue_scriptsadd_meta_boxescurrent_screen 等 action hook 中使用它是安全的。 在更早的 action hook 中使用它可能会导致 $current_screen 对象尚未被初始化。

  • 检查返回值是否为 WP_Screen 对象: 在访问 $screen 对象的属性之前,务必检查它是否是 WP_Screen 对象,以避免潜在的错误。 可以使用 is_a( $screen, 'WP_Screen' ) 来进行检查。

  • 谨慎使用 $_GET$_POST 参数: 虽然 get_current_screen_hook() 函数会检查 $_GET 参数来确定当前页面,但在你的代码中直接使用 $_GET$_POST 参数时要小心,确保进行适当的验证和清理,以防止安全漏洞(例如 XSS 攻击)。

  • 了解不同页面的 id 属性: 不同的页面有不同的 id 属性。 可以使用 var_dump( $screen ) 来查看当前页面的完整 WP_Screen 对象,从而了解它的 id 属性和其他属性。

  • 插件冲突: 某些插件可能会修改 $current_screen 对象或者 get_current_screen_hook() 的行为,导致你的代码无法正常工作。 在出现问题时,尝试禁用其他插件来排查冲突。

五、总结:掌握核心,融会贯通

get_current_screen() 函数是 WordPress 后台开发中一个非常重要的工具。 通过深入了解它的工作原理,我们可以更好地控制后台页面的行为,为用户提供更好的体验。 记住,理解源码是提升技能的关键。 希望今天的讲座能帮助你更好地理解 get_current_screen(),并在实际开发中灵活运用。

好了,今天的讲座就到这里。 感谢各位的观看,我们下期再见! 如果有任何问题,欢迎留言讨论。 祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注