社区新闻

@wordpress/build:下一代 WordPress 插件构建工具

WordPress 的构建工具即将发生变化。但并非颠覆性的。如今大多数使用 @wordpress/scripts 的开发者,在过渡发生时无需更改任何内容。但底层的引擎、理念和开发者体验都在转变,而影响这一方向的窗口现在正敞开着。

@wordpress/build 是推动这一转变的工具。它用一个显著更快的构建引擎取代了 webpack 和 Babel 流水线,根据 package.json 约定自动生成 PHP 注册文件,并一次性处理脚本、脚本模块和样式。它通过基于约定的文件夹发现机制工作:一个用于 JavaScript 包的 packages/ 目录和一个用于管理页面路由的 routes/ 目录,每个目录都无需配置即可自动发现。Gutenberg 已经用它来构建其所有 100 多个包。长期计划是让它成为 @wordpress/scripts 的底层引擎,这样每个插件开发者都能受益于相同的工具,而无需改变他们的工作流程。

@wordpress/build 尚未准备好应对所有用例。特别是,注册区块的插件(WordPress 插件开发者的常见入口点)仍然存在需要手动解决的空白。但正如 Riad Benguella 在介绍愿景时指出的:

“仅靠 Gutenberg 的工作,我无法找到所有边缘情况。”

本文解释了 @wordpress/build 的功能、它目前在 Gutenberg 内部的工作原理、它对插件开发者的意义,以及至关重要的是,在 API 仍在塑造过程中,项目需要您反馈的地方。

愿景

自区块编辑器推出以来,@wordpress/scripts 一直很好地服务于插件开发者。它预配置了 webpack,通过 @wordpress/dependency-extraction-webpack-plugin 处理依赖提取,并生成 .asset.php 文件。但多年来它积累了复杂性:自定义 Babel 插件、灵活(也因此复杂)的 webpack 配置。并且它有一个明显的局限:Gutenberg 本身从未用它来构建自己的包。核心依赖于单独的自定义工具。

2025年10月,Riad Benguella 开启了议题 #72032 “WordPress Scripts: A vision for a v2 version”:

如果我们不是追求灵活性,而是将复杂性吸收到工具中,使插件开发变得像它本应那样简单,会怎样?你想要一个新区块,只需添加一个包含区块列表的“blocks”文件夹。你想要一个 wp-admin 中的新页面,只需添加一个包含该页面 index 文件的文件夹。但构建命令始终相同——wp-scripts build,没有参数,什么都没有。

该提议:用约定取代配置。将你的代码放在已知的文件夹中,packages/ 用于 JavaScript 包或 routes/ 用于管理页面,通过 package.json 字段声明每个包是什么,然后运行一个单一的 wp-build 命令,该命令会发现一切并处理其余事项。没有 webpack 配置,没有入口点列表,没有 Babel 流水线。并且关键的是:PHP 注册自动生成,而非手写。

PR #72743 于 2025 年 10 月引入了 @wordpress/build 作为一个独立的包,并成为 Gutenberg 的内部构建工具。下一步是让它为所有人工作。

更快更简单的构建过程

JavaScript 生态系统已经远远超越了 webpack 和 Babel 时代。原生速度的打包器(用 Go、Rust 和其他编译语言编写的工具)可以在几分之一的时间内完成解析、转译和打包,通常通过单次处理一切,而不是通过多工具流水线。

@wordpress/build 利用了这一点。它当前内部引擎是 esbuild,一个基于 Go 的打包器,取代了独立的 webpack 打包和 Babel 转译步骤。对于像 Gutenberg(100 多个包)这样的大型项目,原来需要几分钟的完整构建现在只需几秒钟。在监视模式下,wp-build --watch 使用增量重建,只有更改的包及其依赖项会被重新编译,使得反馈循环近乎即时。

使用 esbuild 作为打包器是一个内部实现细节:不在 API 中暴露,并且可以在不影响你使用工具方式的情况下更改。

速度是图景的一部分,但它并不能解释为什么 @wordpress/build 几乎不需要配置。这源于该工具固执己见的特性。@wordpress/scripts 积累复杂性不是因为 webpack,而是因为选择将其保持为一个薄而灵活的包装器——一个你可以根据需求自定义的包装器。@wordpress/build 做出了相反的赌注:将复杂性吸收到工具中,并为常见情况提供一套约定。

工作原理

@wordpress/build 使用基于约定的发现模型。将你的代码放在已知的顶级文件夹中,工具会自动找到它:

  • packages/ — JavaScript 包。每个子目录都是一个拥有自己 package.json 的包。工具扫描 packages/*/package.json 以发现要构建的内容。
  • routes/ (实验性) — 管理页面路由。每个子目录定义一个路由,其 package.json 将其映射到一个页面和 URL 路径。组件和生命周期钩子会自动打包。需要在根 package.jsonwpPlugin.pages 中声明页面。

遵循相同模式的 blocks/ 文件夹已在议题 #74542 中提出(见下文区块插件结构和注册)。

不需要单独的构建配置文件。发现由文件夹结构驱动;一小部分 package.json 字段处理其余事项。

这些文件夹名称是固定的约定——packages/routes/blocks/ 不能通过配置或 CLI 标志重命名或指向其他地方。如果你的项目已经将这些目录名称用于其他目的,在采用该工具之前你需要重新组织。这是一个已知的限制,如果它影响了你的工作流程,值得提出

配置

无法从文件夹结构推断的自定义设置存在于两个层面:

  • package.json(插件级别)— 插件范围的设置:命名空间、全局变量名、句柄前缀,以及来自其他插件的任何外部命名空间。这是 wpPlugin
  • 单元 package.json(每个发现的单元一个,位于 packages/routes/ (实验性) 或类似文件夹内)— 每个单元的设置:是注册为脚本(wpScript)还是脚本模块(wpScriptModuleExports)。需要 Web Workers 的包使用 wpWorkers 来定义自包含的 worker 包,这些包在编译时内联所有依赖项,并可通过 Blob URL 加载。

以下是Gutenberg 的根配置

{
  "wpPlugin": {
    "name": "gutenberg",
    "scriptGlobal": "wp",
    "packageNamespace": "wordpress",
    "handlePrefix": "wp"
  }
}

以及一个简单工具包的包级配置:

{
  "name": "@wordpress/api-fetch",
  "wpScript": true,
  "wpScriptModuleExports": {
    ".": "./build-module/index.mjs"
  }
}

两个字段取代了原本需要的 webpack 配置、Babel 插件和手写 PHP:

自动生成的 PHP 注册

@wordpress/build 为脚本、模块和样式生成 WordPress 注册层。在你的插件文件中只需一行:

require_once plugin_dir_path( __FILE__ ) . 'build/build.php';

这会加载 scripts.phpmodules.phpstyles.php,这些生成的文件会挂接到 wp_default_scriptswp_default_styles,以注册你的包产生的每个脚本、模块和样式。.asset.php 文件根据你的导入自动跟踪依赖关系,包括 WordPress 用于优化导入映射的 staticdynamic 模块导入之间的区别。

命名空间模型

如果你使用过 @wordpress/scripts,你会熟悉 @wordpress/dependency-extraction-webpack-plugin 如何外部化 @wordpress/* 导入:import { __ } from '@wordpress/i18n' 在运行时变为对 window.wp.i18n 的引用,而不是打包该包(参见webpack 和 WordPress 包如何交互)。@wordpress/build 将其内置并进行了扩展。

你的 wpPlugin 命名空间配置 决定了你自己的包如何被外部化。一个名为 @acme/editor 且设置了 wpScript: true 的包,会被打包为 window.acme.editor 处的 IIFE,句柄为 acme-editor。当你的插件中的另一个包从 @acme/editor 导入时,该导入会自动被外部化:打包文件引用全局变量而不是复制模块,并且 acme-editor 会声明在 .asset.php 的依赖项中。

相同的模型适用于跨插件。将另一个插件的命名空间声明为外部:

{
  "wpPlugin": {
    "externalNamespaces": {
      "woo": { "global": "woo", "handlePrefix": "woocommerce" }
    }
  }
}

现在 import { Cart } from '@woo/cart' 在运行时解析为 window.woo.cart,并且 woocommerce-cart 会自动出现在你的 .asset.php 依赖项中,这样 WordPress 就会在你的脚本之前加载 WooCommerce 的脚本,无需任何手动依赖声明。

谁应该现在参与

Gutenberg 贡献者。 当你在 Gutenberg 仓库 中运行 npm start 时,执行的就是 @wordpress/build。理解 wpPlugin 配置、wpCopyFiles 转换和外部依赖解析,有助于你调试构建问题并理解 Gutenberg 如何将代码交付给 WordPress 核心。

构建具有多个脚本或包的插件的开发者。 @wordpress/build 不仅限于 monorepo。每个 JavaScript 入口点都可以是自己的包:只需在其 package.json 中添加 "private": true 并将其放在 packages/ 下。这意味着该工具适用于大多数插件架构,而不仅仅是那些构建为多包 npm 工作区的架构。如果你的插件有多个脚本、模块或管理页面,目前需要单独的 webpack 入口点或手写 PHP 注册,那么基于约定的模型值得评估。

希望帮助塑造该工具的开发者。 API 目前是可塑的。如果你关心 WordPress 构建工具的工作方式,现在就是参与的时刻。尝试该工具,遇到粗糙的边缘,报告你的发现。

其他人:请继续使用 @wordpress/scripts 对于单区块插件,@wordpress/create-block 可以让你通过一个命令获得一个可工作的区块。对于维护稳定 webpack 设置的插件,与 @wordpress/scripts 的融合将带来好处,而无需迁移。对于大多数开发者来说,等待这种融合是阻力较小的路径,不过如果你的插件需求与当前约定相符,提前迁移也是一个合理的选择。

帮助塑造工具

以下是设计尚未确定,你的经验可以直接影响结果的领域。

区块插件结构和注册

@wordpress/build 已经支持项目根目录下的实验性 routes/ 文件夹。将其与 packages/ 放在一起,在根 package.jsonwpPlugin.pages 中声明你的页面,每个路由通过其自身 package.json 中的 route.page 字段映射到已声明的页面。议题 #74542 为区块提出了相同的自动发现模式:

my-plugin/
├── packages/        # JS 包(已支持)
├── routes/          # 管理页面路由(实验性,已支持)
└── blocks/          # <span aria-hidden="true" class="wp-exclude-emoji"></span> 提议:区块在此自动发现
    ├── notice-box/
    │   ├── block.json
    │   ├── index.js
    │   ├── edit.js
    │   ├── save.js
    │   ├── render.php
    │   └── style.scss
    └── progress-bar/
        └── ...

blocks/ 中放置一个文件夹,运行 wp-build,就会得到一个生成的 build/blocks.php 来处理所有注册:无需 wpCopyFiles 配置,无需手动 PHP 加载器,无需样式注册样板代码。该提议还旨在使其成为 @wordpress/create-block 的默认设置,取代 @wordpress/scripts 作为脚手架目标。

对于区块插件开发者的问题是:这种结构是否适用于你今天构建插件的方式?根目录下的 blocks/ 文件夹是正确的约定吗,还是你需要不同的东西?该议题有八条评论,但需要来自 Gutenberg 团队之外开发者的更广泛意见。如果区块开发是你的主要用例,#74542 是最有价值的贡献场所。

此外,相关议题 #75832 涵盖了清单是否应使用 WP_Block_Metadata_Registry 注册区块(WordPress 6.8 中引入的 wp_register_block_types_from_metadata_collection() 方法,参见 WordPress 6.8 开发说明)。

在 monorepo 之外的设置体验

在独立插件中设置 @wordpress/build 目前需要:npm 工作区(用于跨包导入解析)、物理安装的 @wordpress/* 包(外部依赖插件读取它们的元数据),以及当 @wordpress/components 在你的依赖树中时,@babel/core 作为隐藏依赖。这些都没有文档记录。

尝试该工具。你报告的每一个摩擦点都有助于弥合“适用于 Gutenberg”和“适用于所有人”之间的差距。

外部依赖和命名空间模型

构建工具读取已安装的 @wordpress/* 包元数据,以决定如何外部化每个导入:作为经典脚本全局变量还是脚本模块。该模型还通过 externalNamespaces 实现了跨插件互操作性。它是否涵盖了你的插件架构?是否存在它不支持的依赖模式?

围绕此主题开启了议题 #75196 — Build: Update tools to recognize module dependencies of scripts

你遇到的任何摩擦或改进建议都可以在 Gutenberg 仓库 中报告。

融合路线图

@wordpress/build 旨在成为 @wordpress/script 的底层引擎,而不是在外部取代它,而是从内部为其提供动力:

  1. 今天:独立包,由 Gutenberg 内部使用。第三方采用需要解决上述一些空白。
  2. 下一步@wordpress/scripts 采用 @wordpress/build 作为其构建引擎。wp-scripts build 继续工作;转译和打包在内部转向使用 wp-build。
  3. 最终:webpack 和 Babel 从 @wordpress/scripts 中弃用。使用默认设置的开发者将看到更快的构建和自动生成的 PHP 注册。拥有自定义 webpack 配置的开发者将获得迁移指南。

@wordpress/build 中现有的功能,如 PHP 转换、支持静态/动态跟踪和 RTL 生成的脚本模块支持,将通过这条路径惠及每个 wp-scripts build 用户。这次过渡有多顺利,取决于在 API 稳定之前有多少粗糙的边缘被磨平。

议题 #72032 中跟踪愿景。如果你从事 WordPress 构建工具相关工作,现在是参与的时候了。

资源