DevTips – 如何有效地编译你的资产
已发表: 2022-08-25我们今年的目标之一是将我们的两个旗舰插件(Nelio Content 和 Nelio A/B 测试)重构为 TypeScript 和 React Hooks。 好吧,我们刚刚完成了半年,我们已经可以说这个目标已经完全成功了但是,我必须承认:这条路比预期的要复杂一些……尤其是如果我们考虑到,在引入 TypeScript 之后,我们的插件构建时间从几秒钟到超过两分钟! 出了点问题,我们不知道是什么。
好吧,在今天的帖子中,我想告诉你一些关于那次经历以及我们为解决它所做的工作。 毕竟,我们都知道 TypeScript 总是会稍微减慢构建过程(类型检查是有代价的),但它不应该那么多! 好吧,剧透警告:问题从来都不是 TypeScript……这是我的配置。 TypeScript 只是让它“显而易见”。 那么让我们开始吧,好吗?
玩具项目
为了帮助您了解我们几周前遇到的问题以及我们如何解决它,我们能做的最好的事情就是创建一个非常简单的示例供您跟进。 让我们构建一个使用 TypeScript 的简单 WordPress 插件,以及错误配置如何导致编译时间极慢。 如果您需要一些帮助才能开始,请查看这篇关于 WordPress 开发环境的帖子。
插件创建
您应该做的第一件事是在您的 WordPress 的/wp-content/plugins
目录中使用您的插件名称(例如nelio
)创建一个新文件夹。 然后添加具有以下内容的主文件( nelio.php
):
<?php /** * Plugin Name: Nelio * Description: This is a test plugin. * Version: 0.0.1 * * Author: Nelio Software * Author URI: https://neliosoftware.com * License: GPL-2.0+ * License URI: http://www.gnu.org/licenses/gpl-2.0.txt * * Text Domain: nelio */ if ( ! defined( 'ABSPATH' ) ) { exit; }
如果你做得对,你会看到你现在可以在 WordPress 中激活插件:

当然,插件什么也没做……但至少它出现了
打字稿
让我们添加一些 TypeScript 代码! 我们要做的第一件事是在我们的插件文件夹中初始化 npm。 运行这个:
npm init
并按照屏幕上的说明进行操作。 然后,安装依赖项:
npm add -D @wordpress/scripts @wordpress/i18n
并编辑package.json
文件以添加@wordpress/scripts
所需的构建脚本:
{ ... "scripts": { "build": "wp-scripts build", "start": "wp-scripts start", }, ... }
一旦 npm 准备就绪,让我们通过添加tsconfig.json
文件来自定义 TypeScript:
{ "compilerOptions": { "target": "es5", "module": "esnext", "moduleResolution": "node", "outDir": "build", "lib": [ "es7", "dom" ] }, "exclude": [ "node_modules" ] }
最后,让我们编写一些 TS 代码。 我们希望这非常简单,但“足够接近”我们在 Nelio A/B 测试和 Nelio Content 中的内容,因此在我们的项目中创建一个src
文件夹,其中包含几个 TypeScript 文件: index.ts
和utils/index.ts
。
一方面,我们假设utils/index.ts
是一个实用程序包。 也就是说,它包含我们项目中其他文件可能需要的一些功能。 例如,假设它提供了经典的min
和max
函数:
export const min = ( a: number, b: number ): number => a < b ? a : b; export const max = ( a: number, b: number ): number => a > b ? a : b;
另一方面,让我们看一下我们应用程序的主文件: index.ts
。 出于测试目的,我们只需要一个使用我们的实用程序包和 WordPress 依赖项的简单脚本。 像这样的东西:
import { __, sprintf } from '@wordpress/i18n'; import { min } from './utils'; const a = 2; const b = 3; const m = min( a, b ); console.log( sprintf( /* translators: 1 -> num, 2 -> num, 3 -> num */ __( 'Min between %1$s and %2$s is %3$s', 'nelio' ), a, b, m ) );
@wordpress/scripts 默认设置
如果我们现在使用npm run build
项目,那么一切都可以开箱即用。 这仅仅是因为@wordpress/scripts
(即我们用于构建项目的底层工具)被设计为与我们示例中的代码库一起使用。 也就是说,如果我们在src
文件夹中有一个index.ts
文件,它将在build
文件夹中生成一个index.js
文件以及一个index.asset.php
依赖文件:
> ls build index.asset.php index.js
为什么是两个文件? 好吧,一个是编译后的 JavaScript 文件(duh),另一个是一个依赖文件,其中包含一些关于我们脚本的有用信息。 特别是,它告诉我们它依赖于 WordPress 中包含的那些 JavaScript 库。 例如,我们的index.ts
依赖于@wordpress/i18n
包来国际化字符串,这是一个包含在 WordPress 中的库,所以……是的, wp-i18n
将出现在index.asset.php
中:
build/index.asset.php <?php return array( 'dependencies' => array( 'wp-i18n' ), 'version' => 'c6131c7f24df4fa803b7', );
不幸的是,如果你问我,默认配置并不完美。 这就是为什么。
如果我们在您的代码中引入错误(例如,让我们使用string
arg 而不是number
调用min
函数):
const m = min( `${ a }`, b );
这应该会触发错误。 但事实并非如此。 它编译没有问题。
使用 TypeScript 编译期间的类型检查
为了解决前面提到的“限制”,我们只需要创建自己的 webpack 配置文件,并告诉它在遇到 TS 代码时使用tsc
(TypeScript 编译器)。 换句话说,我们需要以下webpack.config.json
文件:
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); const config = { ...defaultConfig, module: { ...defaultConfig.module, rules: [ ...defaultConfig.module.rules, { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, }; module.exports = { ...config, entry: './src/index', output: { path: __dirname + '/build', filename: 'index.js', }, };
如您所见,它首先加载包含在@wordpress/scripts
包中的默认 webpack 配置,然后通过将ts-loader
添加到所有.ts
文件来扩展defaultConfig
。 十分简单!
现在,看:
> npm run build ... ERROR in ...src/index.ts TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. webpack 5.74.0 compiled with 1 error in 4470 ms
编译我们的项目会导致错误。 欢呼! 当然,它会慢一点,但至少我们在将任何内容上传到生产环境之前进行了一些安全检查。
将脚本排队
好吧,既然您知道代码中存在问题,请修复它并再次编译插件。 这一切都奏效了吗? 凉爽的! 因为现在是时候将脚本及其对 PHP 的依赖项排入队列,以便我们可以在浏览器中试用它。
打开nelio.php
并附加以下代码段:
add_action( 'admin_enqueue_scripts', function() { $path = untrailingslashit( plugin_dir_path( __FILE__ ) ); $url = untrailingslashit( plugin_dir_url( __FILE__ ) ); $asset = require( $path . '/build/index.asset.php' ); wp_enqueue_script( 'nelio', $url . '/build/index.js', $asset['dependencies'], $asset['version'] ); } );
接下来,转到 WordPress 仪表板(任何页面都可以)并查看浏览器的 JavaScript 控制台。 您应该看到以下文本:
Min between 2 and 3 is 2
好的!
我的依赖项呢?
让我们谈谈 JavaScript/webpack/WordPress 中的依赖管理。 @wordpress/scripts
的配置方式是,默认情况下,如果您的项目使用打包在 WordPress 核心中的依赖项,它将在.asset.php
文件中如此列出。 例如,这解释了为什么@wordpress/i18n
被列在我们脚本的依赖文件中。
但是对“其他”包的依赖呢? 我们的utils
包发生了什么? 长话短说:默认情况下,webpack 编译所有依赖项并将其合并到输出脚本中只需查看生成的 JS 文件(使用npm run start
编译它以禁用缩小):
... var __webpack_modules__ = ({ "./src/utils/index.ts": ((...) => ... var min = function (a, b) { return a < b ? a : b; }; var max = function (a, b) { return a > b ? a : b; }; }), ...
看? 我们的utils
代码就在那里,嵌入在我们的输出脚本中。

@wordpress/i18n
呢? 好吧,这只是对全局变量的简单引用:
... var __webpack_modules__ = ({ "./src/utils/index.ts": ..., "@wordpress/i18n": ((module)) => { module.exports = window["wp"]["i18n"]; }) ...
正如我所说, @wordpress/scripts
带有一个插件, Dependency Extraction Webpack Plugin ,它从编译过程中“排除”某些依赖项并生成代码,假设它们将在全局范围内可用。 例如,在我们的示例中,我们可以看到@wordpress/i18n
在wp.i18n
中。 这就是为什么在将我们的脚本排入队列时,我们还需要将其依赖项排入队列。
自定义配置以生成两个单独的脚本
考虑到这一切,假设我们想用我们的utils
包实现同样的目标。 也就是说,我们不希望它的内容嵌入到index.js
中,而是应该将其编译到自己的.js
文件中,并作为对index.asset.php
的依赖项出现。 我们如何做到这一点?
首先,我们应该重命名index.js
中的import
语句,使它看起来像一个真正的包。 换句话说,与其使用相对路径( ./utils
)导入脚本,不如使用像@nelio/utils
这样的名称。 为此,您所要做的就是编辑项目的package.json
文件,在 dependencies 中添加一个新的dependencies
项:
{ ... "dependencies": { "@nelio/utils": "./src/utils" }, "devDependencies": { "@wordpress/i18n": ..., "@wordpress/scripts": ... }, ... }
运行npm install
在node_modules
中创建一个指向这个“新”包的符号链接,最后在src/utils
中运行npm init
,这样,从 npm 的角度来看, @nelio/utils
是一个有效的包。
然后,要将@nelio/utils
编译成它自己的脚本,我们需要编辑我们的webpack.config.js
配置并定义两个导出:
- 我们已经拥有的(
./src/index.ts
) - 另一个将
./src/utils
编译到不同文件的导出,将其导出暴露在名为nelio.utils
的全局变量中。
换句话说,我们想要这样:
module.exports = [ { ...config, entry: './src/index', output: { path: __dirname + '/build', filename: 'index.js', }, }, { ...config, entry: './src/utils', output: { path: __dirname + '/build', filename: 'utils.js', library: [ 'nelio', 'utils' ], libraryTarget: 'window', }, }, ];
再次编译代码并查看./build
文件夹——你会看到我们现在都有两个脚本。 看看./build/utils.js
,你会看到它是如何定义nelio.utils
的,正如预期的那样:
... var min = function (a, b) { return a < b ? a : b; }; var max = function (a, b) { return a > b ? a : b; }; (window.nelio = window.nelio || {}).utils = __webpack_exports__; ...
... var min = function (a, b) { return a < b ? a : b; }; var max = function (a, b) { return a > b ? a : b; }; (window.nelio = window.nelio || {}).utils = __webpack_exports__; ...
不幸的是,我们只完成了一半。 如果你也看一下./build/index.js
,你会发现src/utils
仍然嵌入其中……不应该是“外部依赖”并使用我们刚刚定义的全局变量吗?
自定义配置以创建外部依赖项
要将@nelio/utils
转换为实际的外部依赖项,我们需要进一步自定义我们的 webpack 并利用我们之前提到的依赖项提取插件。 只需重新打开webpack.config.js
文件并修改config
变量,如下所示:
const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' ); const config = { ...defaultConfig, module: { ...defaultConfig.module, rules: [ ...defaultConfig.module.rules, { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, plugins: [ ...defaultConfig.plugins.filter( ( p ) => p.constructor.name !== 'DependencyExtractionWebpackPlugin' ), new DependencyExtractionWebpackPlugin( { requestToExternal: ( request ) => '@nelio/utils' === request ? [ 'nelio', 'utils' ] : undefined, requestToHandle: ( request ) => '@nelio/utils' === request ? 'nelio-utils' : undefined, outputFormat: 'php', } ), ], };
这样所有对@nelio/utils
的引用在代码中都被翻译为nelio.utils
,并且对脚本处理程序nelio-utils
存在依赖关系。 如果我们看一下这两个脚本的依赖关系,我们会看到以下内容:
build/index.asset.php <?php return array( 'dependencies' => array('nelio-utils', 'wp-i18n')... ?> build/utils.asset.php <?php return array( 'dependencies' => array()... ?>
如果我们查看./build/index.js
,我们会确认@nelio/utils
依赖项现在确实是外部的:
... var __webpack_modules__ = ({ "@nelio/utils": ((module)) => { module.exports = window["nelio"]["utils"]; }), "@wordpress/i18n": ((module)) => { module.exports = window["wp"]["i18n"]; }) ...
不过,我们还需要解决最后一个问题。 转到您的浏览器,刷新仪表板页面,然后查看控制台。 什么都没有出现,对吧? 为什么? 好吧, nelio
现在依赖于nelio-utils
,但是这个脚本没有在 WordPress 中注册……所以它的依赖现在不能满足。 要解决此问题,请编辑nelio.php
并注册新脚本:
add_action( 'admin_enqueue_scripts', function() { $path = untrailingslashit( plugin_dir_path( __FILE__ ) ); $url = untrailingslashit( plugin_dir_url( __FILE__ ) ); $asset = require( $path . '/build/utils.asset.php' ); wp_register_script( 'nelio-utils', $url . '/build/utils.js', $asset['dependencies'], $asset['version'] ); } );
如何加快构建过程
如果我们多次运行构建过程并平均完成所需的时间,我们会看到构建运行大约 10 秒:
> yarn run build ... ./src/index.ts + 2 modules ... webpack 5.74.0 compiled successfully in 5703 ms ... ./src/utils/index.ts ... webpack 5.74.0 compiled successfully in 5726 m Done in 10.22s.
这可能看起来不多,但这是一个简单的玩具项目,正如我所说,像 Nelio Content 或 Nelio A/B 测试这样的真实项目需要几分钟来编译。
为什么它“这么慢”,我们可以做些什么来加快速度? 据我所知,问题在于我们的 webpack 配置。 您在 wepack 的module.exports
中的导出越多,编译时间就越慢。 但是,单个导出方式更快。
让我们稍微重构一下我们的项目以使用单个导出。 首先,在src/utils
中创建一个export.ts
文件,内容如下:
export * as utils from './index';
然后,编辑您的webpack.config.js
,使其具有包含两个条目的单个导出:
module.exports = { ...config, entry: { index: './src/index', utils: './src/utils/export', }, output: { path: __dirname + '/dist', filename: 'js/[name].js', library: { name: 'nelio', type: 'assign-properties', }, }, };
最后,再次构建项目:
> yarn run build ... built modules 522 bytes [built] ./src/index.ts + 2 modules ... ./src/utils/export.ts + 1 modules ... webpack 5.74.0 compiled successfully in 4339 ms Done in 6.02s.
只用了6秒,几乎是以前的一半! 很整洁吧?
概括
TypeScript 将帮助您提高代码质量,因为它可以让您在编译时检查类型是否正确并且没有不一致。 但就像生活中的一切一样,使用 TypeScript 的优势是有代价的:编译代码变得有点慢。
在今天的帖子中,我们已经看到,根据您的 webpack 配置,编译您的项目可能会更快(或更慢)。 甜蜜点需要一个单一的出口……这就是我们今天讨论的内容。
我希望你喜欢这篇文章。 如果有,请分享。 如果您知道其他优化 webpack 的方法,请在下面的评论部分告诉我。 祝你有美好的一天!
Saffu 在 Unsplash 上的精选图片。