打包延伸模組
打包 Visual Studio Code 擴充功能的第一個原因是為了確保它能在任何平台上使用 VS Code 的使用者都能正常運作。只有打包後的擴充功能才能在 Visual Studio Code for Web 環境(如 github.dev 和 vscode.dev)中使用。當 VS Code 在瀏覽器中執行時,它只能為您的擴充功能載入一個檔案,因此擴充功能程式碼必須被打包成單一且適用於網頁的 JavaScript 檔案。這同樣適用於 Notebook 輸出渲染器 (Notebook Output Renderers),VS Code 對於您的渲染器擴充功能也同樣只會載入一個檔案。
此外,擴充功能的規模和複雜度可能會迅速增加。它們可能由多個原始碼檔案組成,並依賴來自 npm 的模組。解構和重用是開發的最佳實踐,但在安裝和執行擴充功能時卻需要付出代價。載入 100 個小檔案的速度遠比載入一個大檔案慢。這就是我們建議打包的原因。打包是將多個小原始碼檔案組合成單一檔案的過程。
對於 JavaScript,有各種不同的打包工具可供選擇。常見的有 rollup.js、Parcel、esbuild 和 webpack。
使用 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:esbuild 和 watch:tsc 指令碼。您需要在 package.json 的 devDependencies 區段中加入 npm-run-all。
compile 和 watch 指令碼用於開發,它們會產生包含來源對應檔的打包檔案。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檔案。resolve和module/rules設定旨在支援 TypeScript 和 JavaScript 輸入檔案。externals設定用於宣告排除項目,例如不應包含在套件中的檔案和模組。vscode模組不應被打包,因為它不存在於磁碟上,而是在需要時由 VS Code 即時建立的。根據擴充功能所使用的 Node 模組,可能需要排除更多項目。
最後,您會需要更新您的 .vscodeignore 檔案,以便將編譯後的檔案包含在已發佈的擴充功能中。請查閱 發佈 (Publishing) 章節以了解詳細資訊。
執行 webpack
建立了 webpack.config.js 檔案後,就可以呼叫 webpack 了。您可以從命令列執行 webpack,但為了減少重複作業,使用 npm 指令碼會很有幫助。
將這些項目合併到 package.json 的 scripts 區段中:
"scripts": {
"compile": "webpack --mode development",
"watch": "webpack --mode development --watch",
"vscode:prepublish": "npm run package",
"package": "webpack --mode production --devtool hidden-source-map",
},
compile 和 watch 指令碼用於開發,它們會產生打包檔案。vscode:prepublish 由 vsce 使用,這是一個 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.json 的 scripts 區段中:
"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。
您可以在其中看到:
- 將
esbuild或webpack、webpack-cli和ts-loader加入為devDependencies。 - 更新 npm 指令碼以使用如上所示的打包工具。
- 更新任務設定
tasks.json檔案。 - 加入並調整
esbuild.js或webpack.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。
後續步驟
- 擴充功能市集 (Extension Marketplace) - 了解更多關於 VS Code 公共擴充功能市集的資訊。
- 測試擴充功能 (Testing Extensions) - 為您的擴充功能專案加入測試,以確保高品質。
- 持續整合 (Continuous Integration) - 了解如何在 Azure Pipelines 上執行擴充功能 CI 建置。