WordPress源码深度解析之:`WordPress`的`Shortcode API`:`add_shortcode()`和`do_shortcode()`的源码实现。

各位观众老爷,早上好/下午好/晚上好!我是你们的老朋友,今天咱们来聊聊WordPress里一个相当实用,但又容易被忽略的小可爱——Shortcode API。

Shortcode API:化繁为简的魔法棒

你想想,咱在WordPress编辑器里,噼里啪啦敲了一堆HTML,CSS,甚至JS代码,就为了实现一个简单的功能,比如插入一个漂亮的按钮,或者展示一个动态的图库。这得多麻烦啊!而且,一旦主题换了,这些代码可能就得重新改一遍,简直是噩梦!

Shortcode API就是来拯救我们的!它就像一根魔法棒,能把复杂的功能封装成简单的标签(Shortcode),你只需要在文章或者页面里输入这些标签,就能轻松实现各种效果,而不用管背后的复杂逻辑。而且,Shortcode和主题是分离的,换主题也不怕!

今天咱们就来扒一扒add_shortcode()do_shortcode()这两个核心函数的源码,看看WordPress是怎么实现这个魔法的。

add_shortcode():注册你的魔法标签

add_shortcode()函数的作用很简单:就是把一个Shortcode标签和一个对应的处理函数关联起来。当我们使用这个标签时,WordPress就会调用这个处理函数,生成最终的内容。

先来看一下add_shortcode()函数的定义(位于wp-includes/shortcodes.php):

function add_shortcode( $tag, $callback ) {
    global $shortcode_tags;

    if ( is_callable( $callback ) ) {
        $shortcode_tags[ $tag ] = $callback;
    }
}

是不是超级简单?

  1. $tag: 这是你要注册的Shortcode标签的名字,比如'my_button'。注意,标签名只能包含字母、数字和下划线,而且必须以字母开头。
  2. $callback: 这是一个函数名或者一个类方法,它负责处理Shortcode标签,并返回最终的内容。

这个函数做了啥呢?它只是简单地检查一下$callback是不是一个有效的可调用函数,如果是,就把它存到一个全局数组$shortcode_tags里,key就是$tag,value就是$callback

$shortcode_tags 是一个全局数组,存储着所有已注册的 Shortcode 标签和对应的处理函数。

举个栗子

假设我们要创建一个Shortcode,用来显示当前日期:

function my_date_shortcode( $atts, $content = null ) {
    return date('Y-m-d');
}

add_shortcode( 'my_date', 'my_date_shortcode' );

这里,我们定义了一个名为my_date_shortcode的函数,它返回当前的日期。然后,我们用add_shortcode()函数把'my_date'标签和my_date_shortcode函数关联起来。

现在,你就可以在文章或者页面里输入[my_date],WordPress就会把它替换成当前的日期了。

do_shortcode():施展魔法,替换标签

do_shortcode()函数才是真正施展魔法的地方。它负责扫描文章内容,找到所有的Shortcode标签,然后调用对应的处理函数,把标签替换成最终的内容。

do_shortcode()函数的定义(位于wp-includes/shortcodes.php):

function do_shortcode( $content, $ignore_html = false ) {
    global $shortcode_tags;

    if ( false === strpos( $content, '[' ) ) {
        return $content;
    }

    if ( empty( $shortcode_tags ) || ! is_array( $shortcode_tags ) ) {
        return $content;
    }

    // Avoid nesting this function when the content is recursively filtered.
    static $recursion_level = 0;
    if ( $recursion_level > 3 ) {
        return $content;
    }

    $recursion_level++;

    // If the input is ALL html, return the input.
    if ( $ignore_html && ! preg_match( '/<[^>]*>[/', $content ) ) {
        return $content;
    }

    preg_match_all(
        '/' . get_shortcode_regex() . '/',
        $content,
        $matches,
        PREG_SET_ORDER
    );

    if ( empty( $matches ) ) {
        $recursion_level--;
        return $content;
    }

    $replace = array();
    $process = true;

    foreach ( $matches as $match ) {
        if ( $match[1] == '[' && $match[6] == ']' ) {
            $replace[ $match[0] ] = '';
            continue;
        }

        if ( $match[1] != '[' ) {
            $shortcode = $match[2];

            if ( isset( $shortcode_tags[ $shortcode ] ) && is_callable( $shortcode_tags[ $shortcode ] ) ) {
                $atts = shortcode_parse_atts( $match[3] );

                if ( false === $atts ) {
                    $atts = array();
                }

                $output = call_user_func(
                    $shortcode_tags[ $shortcode ],
                    $atts,
                    $match[5],
                    $shortcode
                );

                if ( null === $output ) {
                    $output = '';
                }

                $replace[ $match[0] ] = $output;
            } else {
                $replace[ $match[0] ] = $match[0];
            }
        } else {
            $replace[ $match[0] ] = substr( $match[0], 1, -1 );
        }
    }

    $content = strtr( $content, $replace );
    $recursion_level--;

    return $content;
}

这段代码稍微有点长,但咱们一步一步来分析:

  1. $content: 这是要处理的文章内容,也就是包含Shortcode标签的字符串。
  2. $ignore_html: 一个布尔值,表示是否忽略HTML标签。如果设置为true,并且文章内容全部是HTML,那么do_shortcode()函数会直接返回原始内容。

接下来,我们来看看do_shortcode()函数的主要逻辑:

  • 快速退出: 首先,函数会快速检查文章内容里是否包含[字符。如果没有,说明没有Shortcode标签,直接返回原始内容,省时省力。
  • 检查Shortcode标签: 然后,函数会检查全局数组$shortcode_tags是否为空。如果为空,说明没有注册任何Shortcode标签,也直接返回原始内容。
  • 避免递归: 为了防止无限递归,函数会用一个静态变量$recursion_level来记录递归的深度。如果递归深度超过3,就直接返回原始内容。
  • 忽略HTML: 如果$ignore_htmltrue,并且文章内容全部是HTML,就直接返回原始内容。
  • 正则匹配: 这是最关键的一步。函数使用preg_match_all()函数,用一个正则表达式来匹配文章内容里的所有Shortcode标签。这个正则表达式是由get_shortcode_regex()函数生成的。
  • 循环处理: 循环遍历所有匹配到的Shortcode标签,然后根据标签名,调用对应的处理函数,把标签替换成最终的内容。
  • 字符串替换: 最后,使用strtr()函数,把文章内容里的所有Shortcode标签替换成最终的内容。

重点解析

  • get_shortcode_regex(): 这个函数负责生成匹配Shortcode标签的正则表达式。这个正则表达式相当复杂,但它的作用就是匹配各种格式的Shortcode标签,包括带属性的,带内容的,自闭合的等等。
function get_shortcode_regex( $tagnames = null ) {
    global $shortcode_tags;

    if ( empty( $tagnames ) ) {
        $tagnames = array_keys( $shortcode_tags );
    }
    if ( empty( $tagnames ) ) {
        return false;
    }
    $tagregexp = join( '|', array_map( 'preg_quote', $tagnames ) );

    // WARNING! Do not change this regex without changing do_shortcode_tag() and strip_shortcodes()
    // Also, see shortcode_unautop() and shortcode.js.
    return
        '\['                              // Opening bracket
        . '(\[?)'                           // 1: Optional second opening bracket for escaping shortcodes: [[tag]]
        . "($tagregexp)"                     // 2: Shortcode name
        . '(?![\w-])'                       // Word boundary
        . '('                                // 3: Unroll the loop: Inside the opening shortcode tag
        .     '[^\]\/]*'                   // Not a closing bracket or forward slash
        .     '(?:'
        .         '\/(?!\])'               // A forward slash not followed by a closing bracket
        .         '[^\]\/]*'               // Not a closing bracket or forward slash
        .     ')*?'
        . ')'
        . '(?:'
        .     '(\/)'                        // 4: Self closing tag ...
        .     '\]'                          // ... and closing bracket
        . '|'
        .     '\]'                          // Closing bracket
        .     '(?:'
        .         '('                        // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags
        .             '[^\[]*+'             // Not an opening bracket
        .             '(?:'
        .                 '\[(?!\/\2\])' // An opening bracket not followed by the closing shortcode tag
        .                 '[^\[]*+'         // Not an opening bracket
        .             ')*+'
        .         ')'
        .         '\[\/\2\]'             // Closing shortcode tag
        .     ')?'
        . ')'
        . '(\]?)';                          // 6: Optional second closing brocket for escaping shortcodes: [[tag]]
}

这个函数根据已经注册的Shortcode标签,生成一个正则表达式,用于匹配文章内容里的Shortcode标签。正则表达式的细节这里就不展开了,有兴趣的可以自行研究。

  • shortcode_parse_atts(): 这个函数负责解析Shortcode标签的属性。比如,对于[my_button color="red" size="large"]这个标签,shortcode_parse_atts()函数会把属性解析成一个数组:array('color' => 'red', 'size' => 'large')
function shortcode_parse_atts( $text ) {
    $atts    = array();
    $pattern = '/(w+)s*=s*"([^"]*)"(?:s|$)|(w+)s*=s*'([^']*)'(?:s|$)|(w+)s*=s*([^s'"]+)(?:s|$)|"([^"]*)"(?:s|$)|(S+)(?:s|$)/';
    $text    = preg_replace( '/[x00-x20x7f-xff]+/', ' ', $text );
    $text    = trim( $text );
    if ( empty( $text ) ) {
        return $atts;
    }

    preg_match_all( $pattern, $text, $match, PREG_SET_ORDER );
    foreach ( $match as $m ) {
        if ( ! empty( $m[1] ) ) {
            $atts[ strtolower( $m[1] ) ] = stripcslashes( $m[2] );
        } elseif ( ! empty( $m[3] ) ) {
            $atts[ strtolower( $m[3] ) ] = stripcslashes( $m[4] );
        } elseif ( ! empty( $m[5] ) ) {
            $atts[ strtolower( $m[5] ) ] = stripcslashes( $m[6] );
        } elseif ( ! empty( $m[7] ) ) {
            $atts[] = stripcslashes( $m[7] );
        } elseif ( ! empty( $m[8] ) ) {
            $atts[] = stripcslashes( $m[8] );
        }
    }

    // Clean up any keys that are integers because of the regex matching any attributes that do not have a key.
    foreach ( $atts as $key => $value ) {
        if ( is_numeric( $key ) ) {
            unset( $atts[ $key ] );
            $atts[] = $value;
        }
    }

    if ( ! empty( $atts ) ) {
        return $atts;
    }
    return false;
}

这个函数也使用了正则表达式来解析属性,支持单引号、双引号和无引号的属性值。

  • call_user_func(): 这是PHP的一个内置函数,可以动态地调用一个函数。在这里,call_user_func()函数会调用我们用add_shortcode()函数注册的处理函数,并把属性数组、Shortcode内容和标签名作为参数传递给它。

Shortcode的参数

Shortcode处理函数通常接受三个参数:

  1. $atts: 一个数组,包含Shortcode标签的属性。
  2. $content: Shortcode标签的内容,也就是位于开始标签和结束标签之间的内容。如果Shortcode是自闭合的,那么$contentnull
  3. $tag: Shortcode标签的名字。

带属性和内容的Shortcode

除了简单的Shortcode,我们还可以创建带属性和内容的Shortcode。

比如,我们可以创建一个Shortcode,用来显示一个带颜色的文本框:

function my_box_shortcode( $atts, $content = null ) {
    $atts = shortcode_atts( array(
        'color' => 'gray',
    ), $atts );

    $color = esc_attr( $atts['color'] );
    $content = do_shortcode( $content ); // 允许嵌套 Shortcode

    return '<div style="background-color: ' . $color . '; padding: 10px;">' . $content . '</div>';
}

add_shortcode( 'my_box', 'my_box_shortcode' );

这里,我们用shortcode_atts()函数来设置属性的默认值。然后,我们用esc_attr()函数来转义属性值,防止XSS攻击。最后,我们把Shortcode的内容用do_shortcode()函数处理一遍,这样就可以在Shortcode的内容里嵌套其他的Shortcode了。

现在,你就可以在文章或者页面里输入[my_box color="red"]这是一段红色的文本[/my_box],WordPress就会把它替换成一个红色的文本框。

总结

add_shortcode()do_shortcode()函数是WordPress Shortcode API的核心。add_shortcode()函数负责注册Shortcode标签和对应的处理函数,do_shortcode()函数负责扫描文章内容,找到所有的Shortcode标签,然后调用对应的处理函数,把标签替换成最终的内容。

理解了这两个函数的源码,你就可以轻松地创建自己的Shortcode,实现各种各样的功能,让你的WordPress网站更加强大和灵活。

一些建议

  • 使用shortcode_atts()函数来设置属性的默认值。
  • 使用esc_attr()函数来转义属性值,防止XSS攻击。
  • 在Shortcode的处理函数里,把Shortcode的内容用do_shortcode()函数处理一遍,这样就可以在Shortcode的内容里嵌套其他的Shortcode了。
  • 注意性能问题。如果你的Shortcode处理函数比较复杂,或者你的文章内容里有很多Shortcode标签,可能会影响网站的性能。

希望今天的讲解对大家有所帮助!下次再见!

发表回复

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