打包扩展
打包 Visual Studio Code 扩展的首要原因,是确保它能够适用于在任何平台上使用 VS Code 的所有用户。只有打包后的扩展才能在网页版 VS Code 环境(如 github.dev 和 vscode.dev)中使用。当 VS Code 在浏览器中运行时,它只能为你的扩展加载一个文件,因此扩展代码需要被打包成一个对网页友好的单个 JavaScript 文件。这也适用于 Notebook 输出渲染器(Notebook Output Renderers),在这些渲染器中,VS Code 也只会为你的渲染器扩展加载一个文件。
此外,扩展的体积和复杂性可能会迅速增长。它们可能是由多个源文件编写的,并依赖于来自 npm 的模块。模块化分解和重用是开发过程中的最佳实践,但在安装和运行扩展时会带来性能成本。加载 100 个小文件的速度要远远慢于加载一个大文件。这就是我们推荐进行打包(Bundling)的原因。打包是将多个小型源文件合并为一个单独文件的过程。
对于 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问题匹配器(problem matcher)能够检测到的格式输出错误,该问题匹配器也需要作为扩展安装。 - 如果传入了
--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 脚本。你需要将 npm-run-all 添加到 package.json 的 devDependencies 部分中。
compile 和 watch 脚本用于开发阶段,它们会生成带有源码映射(source maps)的打包文件。package 脚本由 vscode:prepublish 脚本调用,后者由 VS Code 打包与发布工具 vsce 在发布扩展之前运行。向 esbuild 脚本传递 --production 标志会使其压缩代码并创建一个较小的包,但这也会使调试变得困难,因此在开发过程中会使用其他标志。要运行上述脚本,请打开终端并输入 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.js.cn/configuration/target/#target
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.cn/configuration/entry-context/
output: {
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.cn/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.js.cn/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.js.cn/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 和桌面版 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 由 VS Code 打包与发布工具 vsce 使用,并在发布扩展之前运行。它们的区别在于 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 严重依赖警告 (Critical dependencies)
运行 webpack 时,你可能会遇到诸如 Critical dependencies: the request of a dependency is an expression(严重依赖:对依赖项的请求是一个表达式)的警告。必须认真对待此类警告,因为你的打包文件很可能无法正常工作。此消息意味着 webpack 无法静态确定如何打包某些依赖项。这通常是由动态 require 语句引起的,例如 require(someDynamicVariable)。
要解决此警告,你应该采取以下措施之一
- 尝试将依赖项改为静态的,以便可以对其进行打包。
- 通过
externals配置排除该依赖项。同时,通过在.vscodeignore中使用带否定号的 glob 模式(例如!node_modules/mySpecialModule),确保这些 JavaScript 文件不会被从打包的扩展中排除。
下一步计划
- 扩展市场 (Extension Marketplace) - 了解更多关于 VS Code 公共扩展市场的信息。
- 测试扩展 (Testing Extensions) - 为你的扩展项目添加测试以确保高品质。
- 持续集成 (Continuous Integration) - 了解如何在 Azure Pipelines 上运行扩展 CI 构建。