打包延伸模組

打包 Visual Studio Code 擴充功能的第一個原因是為了確保它能在任何平台上使用 VS Code 的使用者都能正常運作。只有打包後的擴充功能才能在 Visual Studio Code for Web 環境(如 github.devvscode.dev)中使用。當 VS Code 在瀏覽器中執行時,它只能為您的擴充功能載入一個檔案,因此擴充功能程式碼必須被打包成單一且適用於網頁的 JavaScript 檔案。這同樣適用於 Notebook 輸出渲染器 (Notebook Output Renderers),VS Code 對於您的渲染器擴充功能也同樣只會載入一個檔案。

此外,擴充功能的規模和複雜度可能會迅速增加。它們可能由多個原始碼檔案組成,並依賴來自 npm 的模組。解構和重用是開發的最佳實踐,但在安裝和執行擴充功能時卻需要付出代價。載入 100 個小檔案的速度遠比載入一個大檔案慢。這就是我們建議打包的原因。打包是將多個小原始碼檔案組合成單一檔案的過程。

對於 JavaScript,有各種不同的打包工具可供選擇。常見的有 rollup.jsParcelesbuildwebpack

使用 esbuild

esbuild 是一個速度快且易於設定的 JavaScript 打包工具。若要取得 esbuild,請開啟終端機並輸入:

npm i --save-dev esbuild

執行 esbuild

您可以從命令列執行 esbuild,但為了減少重複作業並啟用問題回報,使用建置指令碼 esbuild.js 會很有幫助。

const esbuild = require('esbuild');

const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');

async function main() {
  const ctx = await esbuild.context({
    entryPoints: ['src/extension.ts'],
    bundle: true,
    format: 'cjs',
    minify: production,
    sourcemap: !production,
    sourcesContent: false,
    platform: 'node',
    outfile: 'dist/extension.js',
    external: ['vscode'],
    logLevel: 'warning',
    plugins: [
      /* add to the end of plugins array */
      esbuildProblemMatcherPlugin
    ]
  });
  if (watch) {
    await ctx.watch();
  } else {
    await ctx.rebuild();
    await ctx.dispose();
  }
}

/**
 * @type {import('esbuild').Plugin}
 */
const esbuildProblemMatcherPlugin = {
  name: 'esbuild-problem-matcher',

  setup(build) {
    build.onStart(() => {
      console.log('[watch] build started');
    });
    build.onEnd(result => {
      result.errors.forEach(({ text, location }) => {
        console.error(`✘ [ERROR] ${text}`);
        if (location == null) return;
        console.error(`    ${location.file}:${location.line}:${location.column}:`);
      });
      console.log('[watch] build finished');
    });
  }
};

main().catch(e => {
  console.error(e);
  process.exit(1);
});

建置指令碼的功能如下:

  • 它使用 esbuild 建立一個建置上下文 (build context)。該上下文配置為:
    • src/extension.ts 中的程式碼打包成單一檔案 dist/extension.js
    • 如果傳入了 --production 旗標,則會壓縮 (minify) 程式碼。
    • 除非傳入 --production 旗標,否則會產生來源對應檔 (source maps)。
    • 從套件中排除 'vscode' 模組(因為它是由 VS Code 執行時期提供的)。
  • 使用 esbuildProblemMatcherPlugin 外掛程式來回報導致打包器無法完成的錯誤。此外掛程式會以 esbuild 問題比對器能偵測到的格式發出錯誤,該比對器也需要作為擴充功能進行安裝。
  • 如果傳入了 --watch 旗標,它會開始監視原始碼檔案的變更,並在偵測到變更時重新打包。

esbuild 可以直接處理 TypeScript 檔案。然而,esbuild 只是簡單地移除所有型別宣告,而不進行任何型別檢查。只有語法錯誤會被回報,且可能導致 esbuild 失敗。

因此,我們會另外執行 TypeScript 編譯器 (tsc) 來檢查型別,但不發出任何程式碼(使用 --noEmit 旗標)。

package.json 中的 scripts 區段現在看起來像這樣:

"scripts": {
    "compile": "npm run check-types && node esbuild.js",
    "check-types": "tsc --noEmit",
    "watch": "npm-run-all -p watch:*",
    "watch:esbuild": "node esbuild.js --watch",
    "watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
    "vscode:prepublish": "npm run package",
    "package": "npm run check-types && node esbuild.js --production"
}

npm-run-all 是一個 Node 模組,用於並行執行名稱符合給定字首的指令碼。對我們而言,它執行 watch:esbuildwatch:tsc 指令碼。您需要在 package.jsondevDependencies 區段中加入 npm-run-all

compilewatch 指令碼用於開發,它們會產生包含來源對應檔的打包檔案。package 指令碼由 vscode:prepublish 指令碼使用,該指令碼由 VS Code 的封裝與發佈工具 vsce 使用,並在發佈擴充功能前執行。將 --production 旗標傳遞給 esbuild 指令碼會使其壓縮程式碼並建立較小的套件,但這也會使偵錯變得困難,因此在開發期間會使用其他旗標。要執行上述指令碼,請開啟終端機並輸入 npm run watch,或從命令選擇區選取 Tasks: Run Task (⇧⌘P (Windows, Linux Ctrl+Shift+P))。

如果您依照下列方式設定 .vscode/tasks.json,您將會為每個監視任務獲得一個獨立的終端機。

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "watch",
      "dependsOn": ["npm: watch:tsc", "npm: watch:esbuild"],
      "presentation": {
        "reveal": "never"
      },
      "group": {
        "kind": "build",
        "isDefault": true
      }
    },
    {
      "type": "npm",
      "script": "watch:esbuild",
      "group": "build",
      "problemMatcher": "$esbuild-watch",
      "isBackground": true,
      "label": "npm: watch:esbuild",
      "presentation": {
        "group": "watch",
        "reveal": "never"
      }
    },
    {
      "type": "npm",
      "script": "watch:tsc",
      "group": "build",
      "problemMatcher": "$tsc-watch",
      "isBackground": true,
      "label": "npm: watch:tsc",
      "presentation": {
        "group": "watch",
        "reveal": "never"
      }
    }
  ]
}

此監視任務依賴於擴充功能 connor4312.esbuild-problem-matchers 進行問題比對,您需要安裝它才能讓任務在問題檢視中回報問題。必須安裝此擴充功能才能完成啟動。

為了避免遺忘,請在工作區中加入一個 .vscode/extensions.json 檔案:

{
  "recommendations": ["connor4312.esbuild-problem-matchers"]
}

最後,您會需要更新您的 .vscodeignore 檔案,以便將編譯後的檔案包含在已發佈的擴充功能中。請查閱 發佈 (Publishing) 章節以了解詳細資訊。

跳至 測試 (Tests) 章節繼續閱讀。

使用 webpack

Webpack 是一個可從 npm 取得的開發工具。若要取得 webpack 及其命令列介面,請開啟終端機並輸入:

npm i --save-dev webpack webpack-cli

這將會安裝 webpack 並更新您擴充功能的 package.json 檔案,將 webpack 加入至 devDependencies

Webpack 是一個 JavaScript 打包工具,但許多 VS Code 擴充功能是用 TypeScript 編寫並僅編譯為 JavaScript。如果您的擴充功能使用 TypeScript,您可以使用 ts-loader 加載器,以便讓 webpack 能理解 TypeScript。請使用下列指令安裝 ts-loader

npm i --save-dev ts-loader

所有檔案都可以在 webpack-extension 範例中找到。

設定 webpack

安裝好所有工具後,現在可以設定 webpack。按照慣例,webpack.config.js 檔案包含指示 webpack 打包您的擴充功能的設定。下方的範例設定適用於 VS Code 擴充功能,應能提供一個良好的起點:

//@ts-check

'use strict';

const path = require('path');
const webpack = require('webpack');

/**@type {import('webpack').Configuration}*/
const config = {
  target: 'webworker', // vscode extensions run in webworker context for VS Code web 📖 -> https://webpack.nodejs.com.tw/configuration/target/#target

  entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.nodejs.com.tw/configuration/entry-context/
  output: {
    // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.nodejs.com.tw/configuration/output/
    path: path.resolve(__dirname, 'dist'),
    filename: 'extension.js',
    libraryTarget: 'commonjs2',
    devtoolModuleFilenameTemplate: '../[resource-path]'
  },
  devtool: 'source-map',
  externals: {
    vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.nodejs.com.tw/configuration/externals/
  },
  resolve: {
    // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
    mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules
    extensions: ['.ts', '.js'],
    alias: {
      // provides alternate implementation for node module and source files
    },
    fallback: {
      // Webpack 5 no longer polyfills Node.js core modules automatically.
      // see https://webpack.nodejs.com.tw/configuration/resolve/#resolvefallback
      // for the list of Node.js core module polyfills.
    }
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'ts-loader'
          }
        ]
      }
    ]
  }
};
module.exports = config;

該檔案作為 webpack-extension 範例的一部分提供。Webpack 設定檔是普通的 JavaScript 模組,必須匯出一個設定物件。

在上述範例中,定義了以下內容:

  • target 指示您的擴充功能將在何種環境下執行。我們建議使用 webworker,這樣您的擴充功能就能同時在 VS Code for Web 和 VS Code 桌面版本中運作。
  • Webpack 應使用的進入點。這類似於 package.json 中的 main 屬性,但您提供給 webpack 的是一個「原始碼」進入點(通常是 src/extension.ts),而非「輸出」進入點。Webpack 打包器能理解 TypeScript,因此不需要額外的 TypeScript 編譯步驟。
  • output 設定告訴 webpack 將產生的打包檔案放置在何處。按照慣例,這是 dist 資料夾。在此範例中,webpack 將產生一個 dist/extension.js 檔案。
  • resolvemodule/rules 設定旨在支援 TypeScript 和 JavaScript 輸入檔案。
  • externals 設定用於宣告排除項目,例如不應包含在套件中的檔案和模組。vscode 模組不應被打包,因為它不存在於磁碟上,而是在需要時由 VS Code 即時建立的。根據擴充功能所使用的 Node 模組,可能需要排除更多項目。

最後,您會需要更新您的 .vscodeignore 檔案,以便將編譯後的檔案包含在已發佈的擴充功能中。請查閱 發佈 (Publishing) 章節以了解詳細資訊。

執行 webpack

建立了 webpack.config.js 檔案後,就可以呼叫 webpack 了。您可以從命令列執行 webpack,但為了減少重複作業,使用 npm 指令碼會很有幫助。

將這些項目合併到 package.jsonscripts 區段中:

"scripts": {
    "compile": "webpack --mode development",
    "watch": "webpack --mode development --watch",
    "vscode:prepublish": "npm run package",
    "package": "webpack --mode production --devtool hidden-source-map",
},

compilewatch 指令碼用於開發,它們會產生打包檔案。vscode:prepublishvsce 使用,這是一個 VS Code 的封裝與發佈工具,並在發佈擴充功能前執行。其差異在於 mode,它控制了最佳化的程度。使用 production 會產生最小的套件,但耗時較長,因此其他情況下會使用 development。要執行上述指令碼,請開啟終端機並輸入 npm run compile,或從命令選擇區選取 Tasks: Run Task (⇧⌘P (Windows, Linux Ctrl+Shift+P))。

執行擴充功能

在執行擴充功能之前,package.json 中的 main 屬性必須指向打包後的檔案,對於上述設定而言,即為 "./dist/extension"。進行此項變更後,擴充功能現在即可執行與測試。

測試

擴充功能開發人員通常會為其原始碼編寫單元測試。透過正確的架構分層,讓擴充功能原始碼不依賴於測試,webpack 和 esbuild 產生的套件就不會包含任何測試程式碼。若要執行單元測試,只需要進行簡單的編譯即可。

將這些項目合併到 package.jsonscripts 區段中:

"scripts": {
    "compile-tests": "tsc -p . --outDir out",
    "pretest": "npm run compile-tests",
    "test": "vscode-test"
}

compile-tests 指令碼使用 TypeScript 編譯器將擴充功能編譯至 out 資料夾。有了這些中間 JavaScript 檔案後,下列 launch.json 程式碼片段就足以執行測試。

{
  "name": "Extension Tests",
  "type": "extensionHost",
  "request": "launch",
  "runtimeExecutable": "${execPath}",
  "args": [
    "--extensionDevelopmentPath=${workspaceFolder}",
    "--extensionTestsPath=${workspaceFolder}/out/test"
  ],
  "outFiles": ["${workspaceFolder}/out/test/**/*.js"],
  "preLaunchTask": "npm: compile-tests"
}

這種執行測試的設定與未打包的擴充功能相同。沒有必要打包單元測試,因為它們不是擴充功能發佈部分的一部分。

發佈

在發佈之前,您應該更新 .vscodeignore 檔案。所有已打包到 dist/extension.js 檔案中的內容都可以排除,通常是 out 資料夾(以防您尚未刪除它),以及最重要的一點:node_modules 資料夾。

典型的 .vscodeignore 檔案看起來像這樣:

.vscode
node_modules
out/
src/
tsconfig.json
webpack.config.js
esbuild.js

遷移現有的擴充功能

將現有的擴充功能遷移至使用 esbuild 或 webpack 非常簡單,且與上述的入門指南相似。一個採用了 webpack 的真實範例是 VS Code 的 References view,您可以參考這個 pull request

您可以在其中看到:

  • esbuildwebpackwebpack-clits-loader 加入為 devDependencies
  • 更新 npm 指令碼以使用如上所示的打包工具。
  • 更新任務設定 tasks.json 檔案。
  • 加入並調整 esbuild.jswebpack.config.js 建置檔案。
  • 更新 .vscodeignore 以排除 node_modules 和中間輸出檔案。
  • 享受安裝和載入速度快得多的擴充功能!

疑難排解

壓縮 (Minification)

production 模式下打包也會執行程式碼壓縮。壓縮透過移除空白字元和註解,並將變數和函式名稱改為簡短且難以辨識的字串來縮減原始碼。如果原始碼使用了 Function.prototype.name,其運作方式會有所不同,因此您可能必須停用壓縮功能。

Webpack 關鍵依賴項目

執行 webpack 時,您可能會遇到類似 Critical dependencies: the request of a dependency is an expression 的警告。這類警告必須嚴肅看待,且您的套件很可能無法正常運作。此訊息表示 webpack 無法靜態判斷如何打包某些依賴項目。這通常是由動態的 require 陳述式所引起的,例如 require(someDynamicVariable)

若要處理此警告,您應該:

  • 嘗試使該依賴項目靜態化,以便能被打包。
  • 透過 externals 設定排除該依賴項目。同時請確保這些 JavaScript 檔案沒有被排除在已封裝的擴充功能之外,請在 .vscodeignore 中使用否定的 glob 模式,例如 !node_modules/mySpecialModule

後續步驟

© . This site is unofficial and not affiliated with Microsoft.