本文约 5,656 字,预计阅读 15 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:jQuery 被加载了三遍,滑块插件时好时坏
header.php 里手写了一段 <script src="...jquery.js">,主题又引了一次,某个插件又注册了一次。前台源码里 jQuery 出现三遍,导致绑定在 document 上的事件被重复注册,滑块点一次翻两页。更麻烦的是两个版本不一致,一个 3.6、一个 1.12,插件报 $(...).on is not a function。
WordPress 有一套完整的资源登记系统(WP_Dependencies),专门用来避免这类冲突。手写标签等于绕过了它。
原理分析:注册(register)与入队(enqueue)是两步
资源系统的核心是两个类:WP_Scripts 和 WP_Styles,各自维护一个”已登记资源列表”和”待输出队列”。
wp_register_script() → 只登记:告诉系统"有这个资源,依赖谁,版本号多少"
wp_enqueue_script() → 登记 + 加入输出队列(未注册的会顺带注册)
wp_script_is( $handle, 'enqueued' ) → 查询状态
分开两步的意义在于:可以提前注册好一批资源,由不同条件决定要不要入队,且同一个 handle 只会输出一次,重复 wp_enqueue_script() 同一 handle 不会产生两份 <script>。
依赖系统是第二层价值。声明 array( 'jquery' ) 后,系统会自动做拓扑排序,保证 jQuery 排在前面;如果依赖的 handle 从未注册,你的脚本就不会被输出(而不是报错或乱序执行)。
两个函数的完整签名:
wp_enqueue_style( string $handle, string $src = '', array $deps = array(), string|bool|null $ver = false, string $media = 'all' )
wp_enqueue_script( string $handle, string $src = '', array $deps = array(), string|bool|null $ver = false, array|bool $args = array() )
第五个参数在 WP 6.3 起了变化:原来只接受布尔值 $in_footer,现在接受数组 array( 'in_footer' => true, 'strategy' => 'defer' ),旧的布尔值仍兼容。
保姆级实操
第 1 步:按钩子加载,不要写在模板里
add_action( 'wp_enqueue_scripts', function () {
$theme = wp_get_theme();
$uri = get_stylesheet_directory_uri();
$path = get_stylesheet_directory();
// 主样式
wp_enqueue_style(
'mmf-main',
get_stylesheet_uri(),
array(),
$theme->get( 'Version' )
);
// 主题脚本:依赖 jQuery,放页脚
wp_enqueue_script(
'mmf-main',
$uri . '/assets/js/main.js',
array( 'jquery' ),
$theme->get( 'Version' ),
true
);
} );
要点:wp_enqueue_scripts(前台)、admin_enqueue_scripts(后台)、enqueue_block_editor_assets(块编辑器)、login_enqueue_scripts(登录页)四个钩子各管一段,不要混用。
第 2 步:依赖声明与版本号
// 滑块库依赖 jQuery;主题脚本依赖滑块库 → 输出顺序自动为 jquery → slick → theme
wp_register_script( 'slick', $uri . '/assets/js/slick.min.js', array( 'jquery' ), '1.8.1', true );
wp_enqueue_script( 'mmf-home', $uri . '/assets/js/home.js', array( 'slick' ), '1.0.0', true );
常用内置 handle 一览(不必自己带文件):
| handle | 内容 |
|---|---|
jquery |
WordPress 内置 jQuery(含 jQuery.noConflict() 模式) |
jquery-ui-core / jquery-ui-sortable 等 |
jQuery UI 组件 |
underscore / backbone |
前端工具库 |
wp-util |
内含 wp.ajax 等工具 |
wp-api-fetch |
REST 请求封装 |
wp-i18n |
前端翻译 |
版本号传 false 时会退化为 WordPress 版本号,等于长期缓存;传 null 则完全不输出 ?ver=,只在有外部缓存策略时使用。
第 3 步:WP 6.3+ 用 strategy 控制 defer/async
wp_enqueue_script(
'mmf-main',
$uri . '/assets/js/main.js',
array(),
'1.0.0',
array(
'in_footer' => true,
'strategy' => 'defer', // 输出 <script defer>
)
);
wp_register_script(
'mmf-analytics',
$uri . '/assets/js/analytics.js',
array(),
'1.0.0',
array( 'strategy' => 'async' ) // 输出 <script async>
);
注意:strategy 为 async 时,脚本之间不保证执行顺序,任何”必须先执行 A 再执行 B”的组合都不要用 async;defer 会保持顺序,是可以放心用的默认项。有依赖关系的脚本会被强制降级为阻塞加载,这是核心的保护行为。
第 4 步:给内联脚本传数据
需要把 PHP 变量传给 JS 时,不要用 echo '<script>var x=...' 拼接字符串,用 wp_add_inline_script() 或 wp_localize_script():
wp_enqueue_script( 'mmf-main', $uri . '/assets/js/main.js', array( 'jquery' ), '1.0.0', true );
wp_add_inline_script(
'mmf-main',
'window.MMF = ' . wp_json_encode( array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'mmf_nonce' ),
'homeUrl' => home_url( '/' ),
'isMobile'=> wp_is_mobile(),
) ) . ';',
'before' // 插在 main.js 之前,保证脚本一执行就能读到
);
wp_json_encode() 会做正确的转义,比手拼字符串安全得多。前端直接 MMF.ajaxUrl 即可。
第 5 步:条件加载,只在该出现的页面加载
add_action( 'wp_enqueue_scripts', function () {
// 只在单篇文章加载评论回复脚本
if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
wp_enqueue_script( 'comment-reply' );
}
// 只在联系页加载地图脚本
if ( is_page( 'contact' ) ) {
wp_enqueue_script( 'mmf-map', get_stylesheet_directory_uri() . '/assets/js/map.js', array(), '1.0.0', true );
}
// 首页不需要的插件资源,直接注销
if ( is_front_page() && ! is_user_logged_in() ) {
wp_dequeue_style( 'contact-form-7' );
wp_dequeue_script( 'contact-form-7' );
}
} );
第 6 段这种”按需卸载”是前端性能优化里性价比最高的一招,前提是知道插件注册的 handle 名——翻插件源码里的 wp_enqueue_* 调用即可。
第 6 步:核对输出
wp eval 'wp_enqueue_script("jquery"); do_action("wp_enqueue_scripts"); global $wp_scripts; print_r($wp_scripts->queue);'
前台查看源码确认:每个 handle 只出现一次、?ver= 正确、页脚脚本确实在 </body> 前而非 <head> 里。
常见踩坑
- 在
header.php里手写<script>/<link>。 绕过依赖系统,无法去重、无法条件卸载,也无法让插件调整顺序。 - 直接
wp_enqueue_script('jquery')换成 CDN 版本。 改的是核心 bundle,jQuery.noConflict()模式与jquery-migrate的配套关系会乱,第三方插件大面积报错。要换版本请用wp_register_script覆盖jqueryhandle 并保留同样的 bundle 结构。 - 把
wp_enqueue_scripts写成wp_enqueue_script。 少一个s是动作名与函数名的经典混淆,钩子写错等于代码从不执行。 - 在
functions.php顶层直接调用wp_enqueue_script()。 此时还没到wp_enqueue_scripts阶段,多数情况下不生效或状态异常。 - 内联脚本里直接
echoPHP 变量。echo 'var id = ' . $_GET['id'];是典型的 XSS 入口,一律用wp_add_inline_script()+wp_json_encode(),转义规范见输出转义规范 esc_html esc_attr esc_url。 - 案例: 某站在
footer.php里加了一段统计代码,又在插件里加载了同一套 SDK,导致 PV 翻倍、事件重复上报,数据部门排查了两周。后来统一改成在插件里wp_register_script()注册、主题只负责wp_enqueue_script(),重复问题自然消失——因为 handle 相同只会输出一次。 defer之后脚本报$ is not defined。 依赖没声明,脚本跑到时 jQuery 还没执行。永远通过第三个参数声明依赖,而不是靠加载顺序侥幸。版本号建议跟随主题版本自动变化,做法见主题版本号与子主题版本号的关系。
总结
所有前端资源都必须走 wp_enqueue_style() / wp_enqueue_script(),把依赖、版本、位置三件事交给核心去管。落地清单:钩子用对(wp_enqueue_scripts / admin_enqueue_scripts / enqueue_block_editor_assets);版本号要么用主题版本要么用 filemtime();有依赖就声明,别靠顺序;新项目统一用 array( 'in_footer' => true, 'strategy' => 'defer' );传数据用 wp_add_inline_script() 配 wp_json_encode();用不上的插件资源按页面 wp_dequeue_*() 卸载。做到这六条,前端资源冲突基本绝迹。
