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.