搜索全站
主题 13 分钟阅读

主题自定义 JS 的安全添加方式:不改主题文件的注入方法

想在主题里加一段脚本却不敢改文件?本文讲清 wp_register_script / wp_enqueue_script / wp_add_inline_script 的标准链路,以及参数传递与条件加载的安全写法。

U
UFO_PF 2023-05-03 · 阅读 13 分钟
主题 主题
阅读提示
本文约 5,009 字,预计阅读 13 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:把 <script> 粘进 header.php 之后

最常见的做法是打开 header.php,在 </head> 前粘一段 <script>,或者干脆用某个”插入页头页脚”的插件贴进去。短期能跑,长期代价明显:主题一升级代码没了;脚本在 jQuery 之前执行,控制台满屏报错;同一个脚本被主题和插件各加载一次,事件被绑定两遍;想给脚本传一个 PHP 变量,只能用 echo 拼字符串,一不小心就写出 XSS。

规范的注入方式其实核心只涉及四个函数,而且它们互相配合能解决上面所有问题:wp_register_script() 注册、wp_enqueue_script() 排队、wp_add_inline_script() 追加内联代码或参数、wp_localize_script() 传递数据。

原理分析:排队机制解决了什么

WordPress 的脚本管理由 WP_Scripts(wp_scripts() 全局实例)负责,本质是”依赖图 + 去重”。wp_enqueue_script( $handle, $src, $deps, $ver, $args ) 里:

  • $handle 是唯一标识,同一个 handle 只会输出一次,天然去重;
  • $deps 声明依赖(如 array( 'jquery' )),核心会自动保证顺序;
  • $ver 用于缓存控制;
  • $args 或第五参为 true 时输出在 wp_footer() 处,避免阻塞渲染。

wp_add_inline_script( $handle, $data, $position ) 把一段内联代码挂在某个已注册脚本的前面(before)或后面(after),从而保证它在该脚本加载之后(或之前)执行——这比直接往 wp_head echo 安全得多,也不会因为脚本未加载而报错。

传参要避免字符串拼接。正确做法是用 wp_localize_script() 输出一个 JSON 对象(内部用 json_encode 并做转义),前端通过全局变量读取。

需求 推荐做法 不要这样做
加一段自己的脚本 wp_enqueue_script 指向子主题文件 粘进 header.php
依赖 jQuery $deps = array('jquery') 假设 jQuery 一定在
传 PHP 变量给 JS wp_localize_script echo 拼进 script
追加几行初始化代码 wp_add_inline_script(..., 'after') 再排一个空脚本
只在某页加载 is_singular() 等条件判断 全站无条件加载

保姆级实操

  1. 把脚本放进子主题目录:例如 assets/js/theme-extra.js,不要内联在模板里。
  2. 注册并排队:在 wp_enqueue_scripts 钩子上注册,声明依赖与版本,最后一个参数设为 true 输出到页脚。
  3. 不要在主题目录里改文件:即使只是加一段统计代码,也应通过子主题或钩子注入。
  4. 传参用 wp_localize_script:把 AJAX 地址、nonce、文章 ID 等一次性输出,前端从全局对象读取。
  5. 初始化代码用 wp_add_inline_script:挂在目标 handle 的 after 位置,保证顺序。
  6. 做条件加载:只在需要的页面排队,减少无关页面的请求与潜在冲突。
  7. 输出前转义:任何进入 JS 上下文的 PHP 值都要 esc_js() / wp_json_encode(),URL 用 esc_url(),nonce 用 wp_create_nonce()。
<?php
// 子主题 functions.php:注册 + 排队 + 传参 + 内联初始化
add_action( 'wp_enqueue_scripts', 'mimangfei_enqueue_js' );
function mimangfei_enqueue_js() {
    $file = get_stylesheet_directory() . '/assets/js/theme-extra.js';
    if ( ! file_exists( $file ) ) {
        return;
    }

    wp_register_script(
        'mf-theme-extra',
        get_stylesheet_directory_uri() . '/assets/js/theme-extra.js',
        array( 'jquery' ),          // 声明依赖,核心保证加载顺序
        filemtime( $file ),         // 内容变化即更新版本
        true                        // 输出到 wp_footer()
    );
    wp_enqueue_script( 'mf-theme-extra' );

    // 安全传参:不要 echo 拼接字符串
    wp_localize_script( 'mf-theme-extra', 'mfData', array(
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
        'nonce'   => wp_create_nonce( 'mf_ajax_nonce' ),
        'postId'  => is_singular() ? get_the_ID() : 0,
        'isHome'  => is_front_page(),
    ) );

    // 初始化代码挂在 after,保证脚本已加载
    wp_add_inline_script(
        'mf-theme-extra',
        'window.mfBoot = function(){ if ( window.mfThemeExtra ) { window.mfThemeExtra.init( window.mfData ); } };',
        'after'
    );
}
<?php
// 条件加载:只在文章详情页与商品页排队,其余页面不加载
add_action( 'wp_enqueue_scripts', 'mimangfei_conditional_js' );
function mimangfei_conditional_js() {
    if ( ! is_singular( array( 'post', 'product' ) ) ) {
        return;
    }
    wp_enqueue_script(
        'mf-single',
        get_stylesheet_directory_uri() . '/assets/js/single.js',
        array(),
        wp_get_theme()->get( 'Version' ),
        true
    );
}

// 需要给脚本标签加属性时(如 async),用 script_loader_tag 过滤
add_filter( 'script_loader_tag', 'mimangfei_async_single', 10, 2 );
function mimangfei_async_single( $tag, $handle ) {
    if ( 'mf-single' === $handle ) {
        return str_replace( ' src=', ' async src=', $tag );
    }
    return $tag;
}
// assets/js/theme-extra.js
// 注意:所有 PHP 数据统一从 mfData 读取,脚本里不出现 PHP 拼接
window.mfThemeExtra = ( function () {
    function init( data ) {
        if ( ! data ) {
            return;
        }
        document.querySelectorAll( '.mf-ajax-btn' ).forEach( function ( btn ) {
            btn.addEventListener( 'click', function () {
                fetch( data.ajaxUrl, {
                    method: 'POST',
                    credentials: 'same-origin',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: new URLSearchParams( {
                        action: 'mf_track',
                        post_id: data.postId,
                        nonce: data.nonce
                    } )
                } );
            } );
        } );
    }
    return { init: init };
} )();

常见踩坑

  • 直接在 header.php 里贴 <script>,主题升级后丢失,且无法控制加载顺序。
  • 没有声明 jquery 依赖,脚本先于库执行,报 jQuery is not defined 或 $ is not a function。
  • 用 echo 把 PHP 变量拼进 JS 字符串,值里带引号就破坏语法,严重时会造成 XSS。
  • 同一段脚本被主题和插件各加载一次,事件重复绑定(点击一次触发两次)。
  • 案例:某站在 footer.php 里直接 echo 了一段统计代码,并把文章标题拼进 JS 参数。某篇文章标题里带单引号,整个脚本语法错误,导致同页面后续所有脚本(含移动端菜单)全部失效。改用 wp_localize_script + wp_json_encode 传参后彻底解决。
  • 脚本无条件全站排队,落地页也加载商品页脚本,白白增加体积与报错面。
  • 用了缓存插件后,nonce 被缓存,导致 AJAX 请求一直返回 -1 或 403。

总结

添加自定义 JS 的标准链路是:子主题文件 → wp_register_script 声明依赖 → wp_enqueue_script 排队到页脚 → wp_localize_script 传参 → wp_add_inline_script 追加初始化。判断标准是”脚本在 Network 里只出现一次、在依赖之后执行、页面源码里没有 PHP 拼接的 JS 字符串”。行动清单:① 绝不在模板里贴 script 标签;② 显式声明依赖;③ 传参一律用 wp_localize_script;④ 按页面条件加载;⑤ 涉及 nonce 的页面排除缓存。延伸阅读 lumen-mimangfei 主题自定义 CSS 规范、主题自定义 CSS 放置位置 与 lumen-mimangfei 主题缓存兼容。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。