WordPress 上的 JavaScript 简直是地狱……这就是为什么
已发表: 2022-05-05近年来,WordPress 开发堆栈发生了很大变化。 自从 Gutenberg 出现以来,JavaScript 在我们最喜欢的 CMS 中的作用比以往任何时候都更加重要。 在这篇博客中,我们已经详细讨论了这对开发人员带来的好处(举几个例子,我们已经讨论了 Gutenberg 的扩展、关于 TypeScript 和 React 的建议、开发工具、插件示例等等),但是每个故事有其阴暗面……这就是我们今天要在这里讨论的内容。
在今天的帖子中,我将与您分享 WordPress 插件开发人员未来可能面临的 3 个主要问题。 有趣的是,他们每个人都有不同的罪魁祸首:要么是 WordPress 本身,要么是其他开发人员,要么是你自己。 所以我们开始吧:您可能会遇到的最常见的 JavaScript 令人头疼的问题,以及您可以采取哪些措施来避免/修复它们。
#1 WPO 插件会破坏你的插件和你的网站
让我们从在 Nelio 获得大量票证的问题开始:WPO 插件。
我相信您已经阅读了大量文章和博客文章,其中概述了拥有快速加载的轻量级网站的重要性。 我的意思是,我们也曾多次写过它! 在他们通常提供的提示中,您会发现诸如切换到更好的托管服务提供商、使用缓存插件和 CDN、使您的服务器和 WordPress 保持最新,或者(这是第一个问题)安装 WPO 插件。 后者的一些例子包括:
- W3 Total Cache 安装量超过 100 万
- SiteGround Optimizer 也有超过 100 万次安装(顺便说一下,我们在我们的网站中使用了其中之一)
- 我们的好朋友 Fernando Tellado 的 WordPress WPO 调整和优化
这些插件承诺通过一系列合理的优化来加速您的网站,一般来说,“任何 WordPress 网站都可以从中受益”。 这些优化包括:
- 在前端出列不必要的脚本,例如表情符号或 Dashicons
- 缓存页面和数据库查询
- 减少标题中包含的信息量
- 组合和缩小 JavaScript 脚本和 CSS 样式
- 缩小 HTML
- 从静态资产的 URL 中删除版本查询 arg
- 延迟 JavaScript 脚本和/或异步加载它们
- ETC
正如我所说,这些类型的优化通常可能是有益的。 但根据我们的经验,WordPress 网站上的所有 JS 优化往往会导致更多问题,从而使他们所谓的改进毫无用处。 这些年来我看到的真实例子是:
- 结合脚本。 您的浏览器必须请求的脚本越少越好。 这就是为什么将所有脚本合二为一的原因。 然而,这可能是有问题的。 一般来说,如果 JavaScript 脚本崩溃,它的执行就会结束,并且错误会在浏览器的控制台中报告。 但只有该脚本的执行被停止; 您的其他脚本将正常运行。 但是如果你把它们全部结合起来……好吧,一旦一个脚本失败,其他脚本(可能包括你的)就不会运行,你的用户会认为是你的插件没有按预期工作。
- 缩小脚本。 我见过一些缩小过程,不管你信不信,它们破坏了正则表达式并导致 JS 脚本出现语法错误。 当然,距离我上次遇到这个已经有一段时间了,但是...... :-/
- 查询参数。 当你在 WordPress 中加入一个脚本时,你可以使用它的版本号(顺便说一下,它可能是由
@wordpress/scripts
自动生成的)。 版本号非常有用:如果您更新插件并且更改了脚本,则此新版本号将确保所有访问者看到不同的 URL,因此他们的浏览器将请求新版本。 不幸的是,如果 WPO 插件删除了查询字符串,您的访问者可能不会意识到脚本已更改,他们将使用所述脚本的缓存副本……这可能会也可能不会导致意想不到的后果。 该死!
一场彻底的灾难,不是吗? 但是等到你听到下一个:
推迟脚本
在 Nelio,我们实施了一个 A/B 测试插件,用于跟踪您的访问者并发现哪些设计和内容获得了最多的转化。 可以想象,我们的跟踪脚本类似于:
window.NelioABTesting = window.NelioABTesting || {}; window.NelioABTesting.init = ( settings ) => { // Add event listeners to track visitor events... console.log( settings ); };
也就是说,它公开了一个我们必须调用的init
函数,以便让脚本知道当前正在运行的测试。 为了调用这个方法,我们在 PHP 中加入了一个内联脚本,如下所示:
function nab_enqueue_tracking_script() { wp_enqueue_script( 'nab-tracking', ... ); wp_add_inline_script( 'nab-tracking', sprintf( 'NelioABTesting.init( %s );', wp_json_encode( nab_get_tracking_settings() ) ) ); } add_action( 'wp_enqueue_scripts', 'nab_enqueue_tracking_script' );
这会产生以下 HTML 标记:
<head> ... <script type="text/javascript" src="https://.../dist/tracking.js" ></script> <script type="text/javascript" > NelioABTesting.init( {"experiments":[...],...} ); </script> ... </head> <body> ...
但是当您的 WPO 插件将defer
属性添加到我们的脚本时会发生什么?
<head> ... <script defer <!-- This delays its loading... --> type="text/javascript" src="https://.../dist/tracking.js" ></script> <script type="text/javascript" > NelioABTesting.init( {"experiments":[...],...} ); </script> ... </head> <body> ...
好吧,脚本现在被推迟了……这意味着前面的代码片段等同于这个:
<head> ... <script type="text/javascript" > NelioABTesting.init( {"experiments":[...],...} ); </script> ... </head> <body> ... <script type="text/javascript" src="https://.../dist/tracking.js" ></script> </body> </html>
并且,因此, nab-tracking-js
不再在它应该加载的时候加载,因此,在它之后并依赖它的内联脚本将简单地失败: nab-tracking-js-after
使用NelioABTesting.init
哪个,由于defer
指令,目前还不可用。 可怕!
解决方案
最有效的解决方案很明确:告诉您的用户禁用脚本优化并收工。 毕竟,JavaScript 中的依赖管理通常非常复杂(特别是如果我们使用defer
和async
指令),WordPress 也不例外。 看看这个关于这个话题的 12 岁讨论吧!
但是,如果这不可行(我知道不可行),我建议您做与我们相同的事情:摆脱init
方法并颠倒您的常规和内联脚本的职责。 也就是说,在常规脚本之前添加一个内联脚本,并使用它来定义一个具有要求设置的全局变量:
function nab_enqueue_tracking_script() { wp_enqueue_script( 'nab-tracking', ... ); wp_add_inline_script( 'nab-tracking', sprintf( 'NelioABTestingSettings = %s;', wp_json_encode( nab_get_tracking_settings() ) ), 'before' ); } add_action( 'wp_enqueue_scripts', 'nab_enqueue_tracking_script' );
这样生成的 HTML 如下所示:
<head> ... <script type="text/javascript" > NelioABTestingSettings = {"experiments":[...],...}; </script> <script type="text/javascript" src="https://.../dist/tracking.js" ></script> ... </head> <body> ...
因此外部脚本的执行是否延迟并不重要——它总是出现在内联脚本之后,从而满足它们之间的依赖关系。
最后,如果您想确保没有人会修改您的设置,请将变量声明为const
并使用Object.freeze
冻结其值:
... sprintf( 'const NelioABTestingSettings = Object.freeze( %s );', wp_json_encode( nab_get_tracking_settings() ) ), ...
所有现代浏览器都支持它。
#2 WordPress 中可能会或可能不会起作用的依赖项……
依赖管理在 WordPress 中也可能存在问题,尤其是在谈论 WordPress 的内置脚本时。 让我解释。
例如,想象一下,我们正在为 Gutenberg 创建一个小型扩展,正如我们在此处解释的那样。 我们插件的源代码可能会有一些像这样的import
语句:
import { RichTextToolbarButton } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { registerFormatType } from '@wordpress/rich-text'; // ...
当这个 JS 源代码被转译后,Webpack(或你使用的工具)会将所有依赖项和你自己的源代码打包成一个 JS 文件。 这是您稍后将从 WordPress 中排入队列的文件,以便一切按您的预期工作。
如果您使用@wordpress/scripts
创建这样的文件,则某些依赖项将不会包含在输出文件中,因为内置过程假定包将在全局范围内可用。 这意味着以前的导入将被转译成类似这样的东西:
const { RichTextToolbarButton } = window.wp.blockEditor; const { __ } = window.wp.i18n; const { registerFormatType } = window.wp.richText; // ...
为了确保您不会错过任何脚本的依赖项, @wordpress/scripts
不仅会转译您的 JS 代码,还会生成一个包含其 WordPress 依赖项的 PHP 文件:
<?php return array( 'dependencies' => array('wp-block-editor','wp-i18n','wp-rich-text'), 'version' => 'a12850ccaf6588b1e10968124fa4aba3', );
很整洁吧? 那么,有什么问题呢? 好吧,这些 WordPress 软件包正在持续开发中,并且它们经常更改,添加新功能和改进。 因此,如果您使用最新版本的 WordPress 开发您的插件,您最终可能会无意中使用该最新版本中可用的功能或特性(因此一切正常),但在“旧”WordPress 版本中却没有……
你怎么知道?
解决方案
我的建议很简单:使用最新的 WordPress 版本开发你的插件,但在旧版本上测试你的版本。 特别是,我建议您至少使用您的插件应该支持的最低 WordPress 版本来测试您的插件。 您可以在插件的readme.txt
中找到最低版本:
=== Nelio Content === ... Requires PHP: 7.0 Requires at least: 5.4 Tested up to: 5.9 ...
从一个 WordPress 版本切换到另一个版本就像运行以下 WP CLI 命令一样简单:
wp core update --version=5.4 --force
#3 箭头函数比你想象的要复杂
最后,让我分享一下我几天前遇到的最新问题之一,这让我发疯了。 简而言之,我们有一个类似于这个的 JavaScript 文件:
import domReady from '@wordpress/dom-ready'; domReady( () => [ ...document.querySelectorAll( '.nelio-forms-form' ) ] .forEach( initForm ) ); // Helpers // ------- const initForm = ( form ) => { ... } // ...
它在前端初始化您的 Nelio Forms。 剧本很简单,不是吗? 它定义了一个在 DOM 准备就绪时调用的匿名函数。 此函数使用名为initForm
的辅助(箭头)函数。 好吧,事实证明,这样一个简单的例子可能会崩溃! 但仅在某些特定情况下(即,如果脚本由 WPO 插件使用defer
属性“优化”)。
下面是 JS 如何执行前面的脚本:
-
domReady
里面的匿名函数是定义的 domReady
运行- 如果 DOM 尚未准备好(通常在加载脚本时未准备好),
domReady
不会运行回调函数。 相反,它只是跟踪它,以便以后可以调用它 - JavaScript 继续解析文件并加载
initForm
函数 - 一旦 DOM 准备好,回调函数最终会被调用
现在,如果当我们到达第三步时,DOM 已经准备好,因此domReady
直接调用匿名函数怎么办? 好吧,在那种情况下,脚本会触发一个未定义的错误,因为initForm
仍然是undefined
。
事实上,这一切最令人好奇的是,这两种解决方案是等价的:
domReady( aux ); const aux = () => {};
domReady( () => aux() ); const aux = () => {}
JavaScript linter 只会在第一个错误上抛出错误,而不是最新的。
解决方案
有两种可能的解决方案:要么使用function
关键字定义辅助函数而忘记箭头函数,要么在定义所有辅助函数后将domReady
语句移到最后:
domReady( aux ); function aux() { // ... }
const aux = () => { // ... }; domReady( aux );
如果您想知道为什么第一个解决方案显然与我们的原始解决方案等效,那么它完全是关于 JavaScript 提升的工作原理。 简而言之,在 JavaScript 中,您可以在定义function
之前使用函数(使用 function 定义),但不能对变量和常量(以及箭头函数)执行相同操作。
总之
JavaScript 中可能会出错的地方有很多。 幸运的是,他们都有解决方案,特别是如果我们注意我们所做的事情。 我希望你今天学到了一些新的东西,我相信由于我过去所犯的错误和错误,你将来能够避免在自己的肉体中遭受痛苦。
Ian Stauffer 在 Unsplash 上的特色图片。