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.tsutils/index.ts

一方面,我們假設utils/index.ts是一個實用程序包。 也就是說,它包含我們項目中其他文件可能需要的一些功能。 例如,假設它提供了經典的minmax函數:

 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/i18nwp.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 installnode_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 上的精選圖片。