🚀 在 VS Code 中

捆绑扩展

捆绑 Visual Studio Code 扩展的第一个原因是确保它适用于在任何平台上使用 VS Code 的每个人。只有捆绑的扩展才能在 VS Code for Web 环境中使用,例如 github.devvscode.dev。当 VS Code 在浏览器中运行时,它只能为你的扩展加载一个文件,因此扩展代码需要捆绑到一个单独的 Web 友好的 JavaScript 文件中。这也适用于 Notebook 输出渲染器,在其中 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 创建一个构建上下文。上下文配置为
    • src/extension.ts 中的代码捆绑到单个文件 dist/extension.js 中。
    • 如果传递了 --production 标志,则缩小代码。
    • 除非传递了 --production 标志,否则生成源映射。
    • 从捆绑包中排除 '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 脚本。你需要将 npm-run-all 添加到 package.json 中的 devDependencies 部分。

compilewatch 脚本用于开发,它们生成带有源映射的捆绑文件。 package 脚本由 vscode:prepublish 脚本使用,该脚本由 vsce(VS Code 打包和发布工具)使用,并在发布扩展之前运行。将 --production 标志传递给 esbuild 脚本将导致它压缩代码并创建一个小型捆绑包,但也会使调试变得困难,因此在开发期间使用其他标志。要运行上述脚本,请打开终端并键入 npm run watch 或从命令面板中选择 任务: 运行任务 (⇧⌘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 文件,以便编译后的文件包含在发布的扩展中。查看 发布 部分以获取更多详细信息。

跳转到 测试 部分继续阅读。

使用 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 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 文件,以便编译后的文件包含在发布的扩展中。查看 发布 部分以获取更多详细信息。

运行 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",
},

compilewatch 脚本用于开发,它们生成捆绑文件。 vscode:prepublishvsce(VS Code 打包和发布工具)使用,并在发布扩展之前运行。不同之处在于 mode,它控制优化级别。使用 production 会产生最小的捆绑包,但也需要更长的时间,因此其他情况下使用 development。要运行上述脚本,请打开终端并键入 npm run compile 或从命令面板中选择 任务: 运行任务 (⇧⌘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 很简单,并且类似于上面的入门指南。通过这个 pull request,VS Code 的“引用”视图采用了 webpack 的真实示例。

在那里你可以看到

  • 添加 esbuildwebpackwebpack-clits-loader 作为 devDependencies
  • 更新 npm 脚本以使用如上所示的捆绑器
  • 更新任务配置 tasks.json 文件。
  • 添加和调整 esbuild.jswebpack.config.js 构建文件。
  • 更新 .vscodeignore 以排除 node_modules 和中间输出文件。
  • 享受安装和加载速度更快的扩展!

故障排除

代码压缩

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

后续步骤

  • 扩展市场 - 了解有关 VS Code 公共扩展市场的更多信息。
  • 测试扩展 - 向你的扩展项目添加测试以确保高质量。
  • 持续集成 - 了解如何在 Azure Pipelines 上运行扩展 CI 构建。