DevTips — как эффективно компилировать ресурсы
Опубликовано: 2022-08-25Одной из наших целей на этот год был рефакторинг двух наших флагманских плагинов (Nelio Content и Nelio A/B Testing) для TypeScript и React Hooks. Что ж, прошло всего полгода, и мы уже можем сказать, что эта цель увенчалась полным успехом. Однако я должен признать: путь оказался немного сложнее, чем ожидалось… особенно если учесть, что после введения Время сборки плагина сократилось с нескольких секунд до более чем двух минут! Что-то было не так , и мы не знали, что.
Что ж, в сегодняшнем посте я хотел бы немного рассказать вам об этом опыте и о том, что мы сделали, чтобы его исправить. В конце концов, мы все знаем, что TypeScript всегда немного замедляет процесс сборки (проверка типов имеет свою цену), но это не должно быть так сильно! Что ж, спойлер: проблема не в TypeScript, а в моем конфиге. TypeScript только сделал это «очевидным». Итак, давайте начнем, не так ли?
Игрушечный проект
Чтобы помочь вам понять проблему, с которой мы столкнулись несколько недель назад, и то, как мы ее исправили, лучшее, что мы можем сделать, — это создать очень простой пример, которому вы можете следовать. Давайте создадим простой плагин WordPress, использующий TypeScript, и рассмотрим, как неправильная конфигурация может привести к очень медленной компиляции. Если вам нужна помощь, чтобы начать работу, ознакомьтесь с этим постом о средах разработки WordPress.
Создание плагина
Первое, что вам нужно сделать, это создать новую папку с именем вашего плагина (например, nelio
) в каталоге WordPress /wp-content/plugins
. Затем добавьте основной файл ( 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 будет готов, давайте настроим TypeScript, добавив файл tsconfig.json
:
{ "compilerOptions": { "target": "es5", "module": "esnext", "moduleResolution": "node", "outDir": "build", "lib": [ "es7", "dom" ] }, "exclude": [ "node_modules" ] }
Наконец, давайте напишем код TS. Мы хотим, чтобы это было очень просто, но «достаточно близко» к тому, что у нас было в Nelio A/B Testing и 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
(то есть базовый инструмент, который мы используем для создания нашего проекта) был разработан для работы с кодовой базой, подобной той, что используется в нашем примере. То есть, если у нас есть файл index.ts
в папке src
, он сгенерирует файл index.js
в папке build
вместе с файлом зависимостей index.asset.php
:
> ls build index.asset.php index.js
Почему два файла? Что ж, один — это скомпилированный файл JavaScript (ага), а другой — файл зависимостей с некоторой полезной информацией о нашем скрипте. В частности, он сообщает нам, на какие библиотеки JavaScript из включенных в WordPress он опирается. Например, наш index.ts
полагается на пакет @wordpress/i18n
для интернационализации строк, и это библиотека, включенная в WordPress, так что… да, wp-i18n
появится в index.asset.php
:
build/index.asset.php <?php return array( 'dependencies' => array( 'wp-i18n' ), 'version' => 'c6131c7f24df4fa803b7', );
К сожалению, конфигурация по умолчанию не идеальна, если вы спросите меня. Вот почему.
Если мы внесем ошибку в ваш код (например, давайте вызовем функцию min
со string
аргументом вместо number
):
const m = min( `${ a }`, b );
это должно вызвать ошибку. Но это не так. Собирается без проблем.
Проверка типов во время компиляции с помощью TypeScript
Чтобы устранить вышеупомянутое «ограничение», нам просто нужно создать собственный файл конфигурации веб-пакета и указать ему использовать tsc
(компилятор TypeScript) всякий раз, когда он сталкивается с кодом TS. Другими словами, нам нужен следующий файл 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
, а затем расширяет defaultConfig
, добавляя ts-loader
ко всем файлам .ts
. Очень просто!
А теперь вот:
> 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
. Как мы это делаем?
Во-первых, мы должны переименовать оператор import
в index.js
, чтобы он выглядел как настоящий пакет. Другими словами, вместо того, чтобы импортировать скрипт с использованием относительного пути ( ./utils
), было бы неплохо использовать имя вроде @nelio/utils
. Для этого все, что вам нужно сделать, это отредактировать файл package.json
проекта, чтобы добавить новую зависимость в dependencies
:
{ ... "dependencies": { "@nelio/utils": "./src/utils" }, "devDependencies": { "@wordpress/i18n": ..., "@wordpress/scripts": ... }, ... }
запустите npm install
, чтобы создать символическую ссылку в node_modules
указывающую на этот «новый» пакет, и, наконец, запустите npm init
в src/utils
, чтобы, с точки зрения 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.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 Testing, требуют нескольких минут для компиляции.
Почему это «так медленно» и что мы можем сделать, чтобы ускорить его? Насколько я мог судить, проблема заключалась в нашей конфигурации веб-пакета. Чем больше у вас экспортов в module.exports вашего module.exports
, тем медленнее становится время компиляции. Тем не менее, один экспорт намного быстрее.
Давайте немного рефакторим наш проект, чтобы использовать один экспорт. Прежде всего, создайте файл export.ts
в src/utils
со следующим содержимым:
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 имеют свою цену: компиляция кода становится немного медленнее.
В сегодняшнем посте мы увидели, что в зависимости от конфигурации вашего веб-пакета компиляция вашего проекта может быть намного быстрее (или медленнее). Золотая середина требует единственного экспорта... и это то, о чем мы говорили сегодня.
Надеюсь, вам понравился пост. Если это так, пожалуйста, поделитесь им. Если вы знаете другие способы оптимизации веб-пакета, сообщите мне об этом в разделе комментариев ниже. Хорошего дня!
Избранное изображение Саффу на Unsplash.