ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VSIX 插件打包内置 VSCode 安装程序全链路

VSIX 插件打包内置 VSCode 安装程序全链路

手上有几个自己写的扩展,想让整组同事开箱即用,结果发现每个人都要手动拖一次 vsix 进来,还有人装错版本、装完忘了重启——这个场景我遇到过不止一次。后来干脆把这批 vsix 插件直接打包进 VSCode 安装程序中,新机器装完编辑器,扩展已经躺在那儿了,默认配置也跟着走。听起来是个小技巧,实际链路比想象的长:VSIX 的打包规范、vsce 的参数取舍、扩展目录的扫描顺序、内置扩展和用户扩展的优先级、安装器的写权限,任何一环没对上,装完就是满屏"扩展被禁用"的黄条。

这篇内容写给两类人看。一类是刚写完第一个扩展,想搞清楚 vsce 除了 package 还能干什么;另一类是要给团队或客户交付一套定制版编辑器,需要把插件、默认设置甚至品牌信息一起塞进安装包。下面围绕"vsix 插件打包到 vscode 安装程序中"这条完整链路展开,从工程自查、打包出包、验包,到内置进安装程序、排查那些莫名其妙的失效问题,一步步拆开讲,能直接抄的部分我都把命令和配置写全。

1. 先把概念掰开:VSIX 到底是个什么东西

1.1 一个 VSIX 内部到底装着什么

很多人第一次接触.vsix会以为它是个二进制安装包,其实它就是个改了后缀的 zip。你把这个文件复制一份、后缀改成.zip、解压,会看到里面结构非常朴素:根目录下一个extension/文件夹,加上extension.vsixmanifest和[Content_Types].xml两个描述文件。extension/里面就是你项目根目录被打包进去的那一堆东西——package.json、编译后的out/或dist/、README、图标、语言包,甚至连node_modules都可能在里面。

这个认知很关键,因为它决定了后面所有排错的方向。你打包出来的东西本质上就是"一个目录 + 一份清单",编辑器安装扩展时做的事就是把这个目录解压到扩展目录下,然后读取package.json决定要不要激活。所以"打包失败"和"打包出来的包能用"完全是两件事,vsce package只做了前半段。

extension.vsixmanifest这个文件值得单独看一眼,它声明了扩展的标识信息、目标编辑器版本范围、支持的平台,以及资源清单。你自己不需要手写它,vsce 会从package.json生成,但排查"为什么安装时说版本不兼容"这类问题时,打开它对照一下是最快的手段。

注意:不要试图手改 VSIX 解压后的内容再重新压缩,压缩顺序和文件清单对不上,会出现扩展能装但加载失败的情况。要改就改源码重新打包。

1.2 为什么"打包"和"内置进安装程序"是两件事

标题里其实藏了两个动作。第一个动作是打包:把源码工程变成一个可分发的.vsix文件,这一步由vsce完成,产物是单个文件,可以扔到内部制品库或者直接发邮件。第二个动作是内置:把这个.vsix装进 VSCode 安装程序中,让编辑器一装好就自带这个扩展,用户不需要任何操作。

这两件事的目标不同,所以技术路线也不一样。打包是标准动作,工具链成熟,一条命令就完事;内置则属于"定制发行版"的范畴,官方安装程序并不提供"请把插件塞进安装包"的勾选项,你需要自己拆包、放文件、重打包,或者换一条完全不同的路线(比如用--extensions-dir独立管理扩展目录)。

我见过最多的一种误解是:以为把 vsix 丢进安装包的某个目录就会自动被安装。实际上 VSCode 的扩展加载逻辑是扫描固定路径下的子目录,而不是扫描.vsix文件。也就是说,安装包里放一个foo.vsix是没用的,必须解压后以目录形式放进去,并且目录名要符合规范。这一点如果不提前搞清楚,很容易在打包安装器这一步白折腾半天。

1.3 两种形态的扩展:内置扩展与用户扩展

VSCode 的扩展在运行时分成两类。一类是内置扩展(built-in),随编辑器一起分发,位于程序安装目录下的resources/app/extensions,在扩展面板里显示为"内置",通常没有卸载按钮,也不会被市场更新覆盖。另一类是用户扩展(user),位于用户目录,比如 Windows 下的%USERPROFILE%\.vscode\extensions,可以随意装卸、升级、降级。

把 vsix 内置进安装程序,本质就是让一个第三方扩展获得"内置扩展"的身份。好处很直接:用户装完就有,不会因为误操作卸载,也不会被网络环境卡住装不上。代价是版本管理变重了——用户想升级得等你的新安装包,没法自己从市场拉新版本。这个取舍得提前想明白,如果这个扩展是团队内部工具,内置完全合理;如果是生态型插件,那还是老实走市场分发,内置只作为离线兜底。

另外有个细节很多人没注意:内置扩展的engines.vscode字段会跟当前编辑器版本做匹配校验。你把一个要求^1.85.0的扩展内置到一个 1.80 的编辑器里,它会被标记为不兼容,虽然在目录里但不会激活。所以在选编辑器版本和扩展版本时必须拉齐,这个后面会详细讲。

2. 打包前的工程自查:这些配置不改,打包必踩坑

2.1 package.json 里必须交代清楚的字段

vsce对package.json的校验是偏严格的,缺字段直接报错退出。下面这几个是硬性要求,缺一个都过不去:name(全小写、不能有空格、建议用连字符)、version(必须符合 semver)、publisher(发布者标识,临时的也要写)、engines.vscode(最低兼容版本)。

除了硬性字段,还有几个强烈建议补上,直接影响用户体验和后续维护成本。displayName是扩展面板里展示的名字,不填就是name;description是一句话简介,扩展列表里会显示;categories决定你在市场上的归类,也影响内置后在扩展面板的筛选结果;icon必须是 PNG,官方推荐 128×128,尺寸不对不会报错但显示会糊。

{ "name": "internal-toolkit", "displayName": "内部工具集", "description": "团队内部使用的代码片段与快捷命令集合", "version": "1.2.0", "publisher": "team-internal", "engines": { "vscode": "^1.80.0" }, "categories": ["Other", "Snippets"], "icon": "assets/icon.png", "main": "./out/extension.js", "activationEvents": [], "contributes": { "commands": [ { "command": "toolkit.run", "title": "运行工具集命令" } ] } }

上面这段里activationEvents是空数组,这不是偷懒。从 1.74 版本开始,contributes.commands、contributes.languages这些声明会自动生成对应的激活事件(比如onCommand:toolkit.run),不需要手写了。但如果你的编辑器版本低于这个范围,还是得老老实实写全,否则会出现"命令存在但点不动"的诡异现象——菜单里能看到,点下去毫无反应,日志里也没有任何报错。我第一次遇到这个坑排查了快两个小时,最后发现就是激活事件没声明。

提示:publisher名字虽然可以随便写,但如果你后续要把它内置进安装程序,最好用一个不会和市场同名扩展撞车的名字,避免出现"内置版本和用户手动安装的版本互相打架"的情况。

2.2 依赖处理:node_modules 到底要不要带上

这是打包环节里最容易出问题的一块。vsce默认会执行npm list --production来收集生产依赖,也就是你dependencies里的东西,然后把它们一起打进包里。所以如果你的扩展在运行时require了第三方库,正常流程下是没问题的。

但有两个坑。第一个是依赖写错了位置。有些库只在编译阶段用,比如 TypeScript 类型包、打包工具,这些东西应该放在devDependencies。如果你不小心写进了dependencies,它们就会被完整打进 VSIX,包体积可能从几百 KB 膨胀到几十 MB。我见过一个扩展因为把一个构建工具写进了生产依赖,最终包体 40MB,内置进安装包后整个安装程序大了一圈。

第二个是原生模块。如果你的依赖里包含.node二进制文件,这些文件是跟平台绑定的,Windows 上编译的在 Linux 上跑不了。这种情况下你必须针对每个目标平台单独打包,用--target参数指定平台,产物会带上平台后缀。内置进安装程序时也要按平台分发对应的包,混用会直接导致扩展加载失败,报错信息通常是"找不到模块"或者更隐晦的段错误。

# 只装生产依赖,避免 devDependencies 污染 npm ci --omit=dev # 确认最终会打进包里的文件清单 npx vsce ls # 带平台标识打包(原生模块场景) npx vsce package --target win32-x64

vsce ls这个命令非常值得养成习惯。它不会真的生成文件,只是把将要被打包的路径全部列出来。我一般会在打包前跑一次,肉眼扫一遍,看看有没有src/、test/、.git/这类不该出现的目录。这一步能省掉大量"包太大"的返工。

2.3 体积与文件过滤:.vscodeignore 的正确写法

.vscodeignore的语法和.gitignore基本一致,但它的作用是反过来的——默认打包全部文件,你在里面写的规则会被排除掉。这跟.npmignore的思路一样,所以很多人第一次写会搞反,以为写了什么就打包什么。

我的习惯是先写一个最小排除集,把明显不该进去的干掉,再根据vsce ls的结果微调:

.vscode/** .vscode-test/** src/** test/** out/**/*.map **/*.ts !**/*.d.ts .gitignore .eslintrc.json tsconfig.json webpack.config.js node_modules/.cache/** **/__tests__

这里有两个细节值得说。out/**/*.map排除的是 sourcemap,开发时有用,分发时纯属浪费体积,一个中等规模的扩展 sourcemap 能占到一半体积。!**/*.d.ts是反向规则,意思是"排除所有.ts,但类型声明文件要保留",因为有些扩展在运行时需要读取类型信息,全删了会出问题。

还有一个隐蔽的坑:.vscodeignore里如果写了node_modules/**,但你的扩展又确实依赖运行时库,那就完蛋了。打包时不会报错,安装后才会在激活时抛出"模块未找到"。所以排除规则不要写得太激进,改完一定用vsce ls确认关键目录还在。

2.4 版本号与预发布标签的取舍

内置进安装程序的扩展,版本号策略跟市场发布不太一样。市场发布讲究语义化版本,每次改动都要升版本;内置场景下,扩展版本其实跟着安装包版本走,用户感知不到扩展单独升级这回事。

我一般会这么处理:扩展的version保持正常递增,同时在安装包的版本号里体现扩展批次。比如安装包 3.5.0 里内置的扩展是 1.2.0,这样出问题时能快速定位是哪一批次的组合。另外vsce package --pre-release可以打出带预发布标记的包,这类包在被内置后有可能会因为没有匹配的稳定版本而被标记为不兼容,生产环境别用。

3. 用 vsce 打出可交付的 VSIX

3.1 环境准备与命令参数逐条说明

工具链就一个:@vscode/vsce。建议全局装,因为内置进安装程序这个流程你多半会跑很多次,每次npx都去解析一遍太慢。

npm i -g @vscode/vsce vsce --version

打包命令本身很简单:vsce package。但参数用对了能省很多事,下面这几个是我日常真正在用的。

参数实际作用什么时候用
--out <path>指定产物路径和文件名需要按版本归档产物时
--no-dependencies跳过依赖检查依赖已手动处理好,或用了打包器
--allow-missing-repository允许缺少 repository 字段内部项目没有公开仓库
--skip-license跳过 LICENSE 文件检查内部工具不打算公开分发
--target <platform>指定目标平台含原生模块或需要平台优化
--pre-release打预发布标记内部测试分发

--allow-missing-repository和--skip-license这两个经常一起用。内部项目通常没有公开仓库,也懒得放 LICENSE,不加参数会直接被拦下来。但要注意,这两个参数在某些 vsce 版本里行为不完全一致,如果加了还报错,看看提示是"repository"还是"LICENSE",按提示补一个空字段或者一个文本文件都能过。

vsce package \ --allow-missing-repository \ --skip-license \ --out ./dist/internal-toolkit-1.2.0.vsix

3.2 打包时的签名与来源可信问题

现在新版编辑器对扩展签名的要求越来越严格,尤其是从市场下载的包会做来源校验。你自己打的内部 VSIX 不走市场渠道,安装时不会被强制校验签名,所以命令行安装、UI 拖拽安装、内置进安装目录这三种方式都不会因为"未签名"被拦。

但有一种情况需要留意:如果目标环境的策略里禁用了非市场来源的扩展安装,UI 上的"从 VSIX 安装"可能会变灰,这时候命令行--install-extension通常还是能过的。内置进安装目录则完全绕开了安装流程,直接从扫描阶段生效,是最稳的一条路。所以如果你交付的环境策略比较严,内置反而是首选方案,不是因为更方便,是因为更少受阻。

3.3 产物体检:解压看一眼再安装

打完包我强烈建议做一次手动体检,成本很低但收益很高。把.vsix复制一份改成.zip,解压,重点看三个地方:extension/package.json里的main指向的文件是否存在、extension/下有没有node_modules且里面有你要的库、extension.vsixmanifest里的版本范围是不是你预期的。

# 快速查看包内文件清单(不解压) unzip -l internal-toolkit-1.2.0.vsix | head -40

然后再装一次做真机验证。这里有个小技巧:装之前先清掉旧版本,避免缓存干扰。

code --uninstall-extension team-internal.internal-toolkit code --install-extension ./dist/internal-toolkit-1.2.0.vsix --force

--force是覆盖安装,用于版本号没变但你重新打包了的情况。不加这个参数,同版本号会被跳过,你会以为自己装成功了,实际跑的还是老代码——这个坑我踩过,排查了半小时以为打包没生效,其实就是没加--force。

4. 把 VSIX 装进编辑器:三条路线的取舍

4.1 路线一:命令行安装与独立扩展目录

最轻量的做法是命令行安装,适合开发调试和脚本化部署。缺点是它装在用户目录下,用户能自己卸载,且换台机器就没了。

真正值得讲的是--extensions-dir参数,它能把扩展目录指到任意位置。这个参数必须配合启动参数使用,仅在命令行装的时候指定没用。

# Windows 下用 code.cmd 带参数启动 code --extensions-dir "D:\team-ext\extensions"

用法上有个细节:这个目录必须是已存在且有写权限的,编辑器不会帮你创建。另外通过这种方式启动后,用户从 UI 安装的扩展也会落到这个目录里,跟默认目录完全隔离。对团队交付来说这是个很好的隔离方案——扩展跟着项目走,不污染用户个人的编辑器环境。

代价是每次启动都得带参数,所以实际落地时要改快捷方式。Windows 上右键快捷方式、改目标,把参数拼在后面;macOS 上得写个包装脚本或者用open -a带参数。这一步如果交给非技术同事做,大概率会漏,所以我一般会做成批处理文件或者.command脚本一起发下去。

4.2 路线二:UI 安装与离线分发

把 vsix 文件发给同事,让他们在扩展面板右上角点三个点、选"从 VSIX 安装",这条路线几乎零成本,适合一次性分发。缺点是没有任何版本控制,谁装了什么版本你完全不知道,出了问题也没法复现。

做离线分发时有个细节容易被忽略:vsix 文件名里的版本号要保留,别改成"最新版.vsix"这种。用户在扩展面板里看到的版本来自package.json,但文件系统里同名文件会被后下载的覆盖,出问题时你连他手上是哪个文件都判断不了。

还有一点,从市场下载 vsix 做离线分发的做法在需要联网校验的扩展上会失效。有些扩展会做激活时的在线校验,离线环境下直接拒绝启动。内置进安装程序同样绕不过这一点,所以如果你的依赖链里有这类扩展,得提前确认它的离线行为。

4.3 路线三:内置进安装程序

这条路把扩展变成"内置扩展",装完就有,用户不可卸载、不可降级,也不会被市场版本覆盖。代价是每次更新扩展都要重打一次安装程序,分发包体积也会增加。

三种路线没有绝对优劣,看你的交付形态:

路线用户感知可维护性适用场景
命令行安装无感,但需重装高开发调试、CI 环境
UI 装 vsix需要手动操作低一次性分发、少量用户
内置进安装程序装完即有中,需重打包团队统一交付、离线环境

从我的实践看,十人以内的团队用路线二就够了,超过二十人或者交付给外部客户,路线三的价值才真正体现出来。因为人数一多,版本对齐的成本会指数级上升。

5. 定制安装程序的实操细节

5.1 先搞清楚安装程序把文件放到了哪

内置扩展的第一步不是改代码,是找到编辑器安装后的目录结构。不同安装方式、不同平台的路径差别很大,弄错了就是把文件放到了没人读的地方。

Windows 用户级安装的典型路径是%LOCALAPPDATA%\Programs\<编辑器目录>\resources\app\extensions;系统级安装通常在C:\Program Files\<编辑器目录>\resources\app\extensions。macOS 在应用包内部,路径形如/Applications/<应用名>.app/Contents/Resources/app/extensions。Linux 发行版包管理安装的一般在/usr/share/<包名>/resources/app/extensions或/usr/lib/<包名>/extensions。

判断对不对的最快方法是打开这个目录看一眼:如果里面有一堆你已经认识的官方扩展目录(比如语言支持、主题、Git 集成这些),那就是对的。这个extensions目录就是内置扩展的扫描根目录,编辑器启动时会遍历它的每个子目录,读取子目录里的package.json判断是否加载。

注意:系统级安装目录在 Windows 上需要管理员权限才能写入,这也是为什么定制安装程序时最好用管理员权限运行安装器,或者干脆改用户级安装路径。

5.2 把 VSIX 变成目录放进去

前面说过,扩展加载扫的是目录不是文件,所以这一步的核心动作是解压。把foo.vsix解压,取里面的extension/目录,整个重命名为一个规范的名字放进去,比如team-internal.internal-toolkit-1.2.0。

命名规则不是死的,编辑器读取的是目录里的package.json的name和publisher,目录名只是为了人看着方便。但我建议按<publisher>.<name>-<version>这个形式来命名,一是能一眼看出装的是哪个版本,二是避免两个扩展目录同名互相覆盖。

# 解压到临时目录 unzip internal-toolkit-1.2.0.vsix -d /tmp/kit # 把 extension 目录挪到内置扩展目录下 cp -r /tmp/kit/extension "<编辑器安装目录>/resources/app/extensions/team-internal.internal-toolkit-1.2.0"

放进去之后,检查一下package.json里的main字段指向的文件在这个目录下是否存在。如果main是./out/extension.js,那目录里就必须有out/extension.js。这一步看着废话,但真有人打包时用了打包器,产物在dist/而main还写着out/,本地开发能跑,因为开发时out/存在,打包时被.vscodeignore排掉了,内置后就加载失败。

5.3 让安装程序自动完成这一步

手工拷目录只适合验证,真正的交付要交给安装器。不管你用的是 Inno Setup、NSIS 还是别的打包工具,思路都一样:在安装阶段把预置的扩展目录复制到目标路径下,卸载时再删掉。

以 Inno Setup 为例,大致是这么写的:

[Files] ; 预置扩展目录,递归复制 Source: "payload\extensions\*"; DestDir: "{app}\resources\app\extensions"; \ Flags: ignoreversion recursesubdirs createallsubdirs [UninstallDelete] Type: filesandordirs; Name: "{app}\resources\app\extensions\team-internal.internal-toolkit-1.2.0"

recursesubdirs createallsubdirs这两个标志很关键,少了它们只会复制第一层文件,扩展里的node_modules或者dist子目录会被漏掉。我见过一次就是因为漏了这两个标志,装完之后扩展显示但命令点了没反应,查了半天才发现子目录没复制过去。

NSIS 的写法类似,用File /r递归拷贝即可。要注意的是顺序:必须先安装编辑器本体,再复制扩展目录,因为安装编辑器本体时可能会整个覆盖resources/app目录。如果你把扩展复制写在前面,会被后来的安装动作冲掉。

5.4 定制发行版时顺手能改的几个配置

既然已经在改安装程序,有些配套的东西可以一起处理掉,省得用户装完还要手工配。编辑器在resources/app/product.json里有一批可定制的字段,改完就变成你自己的发行版。

比较实用的几个:configurationDefaults可以设置默认配置项,比如关掉遥测、设置默认字体、指定默认终端,用户装完就是你要的状态;dataFolderName改的是用户数据目录的名字,能让定制版和用户自己装的版本完全隔离,不会互相干扰;nameShort、nameLong改的是显示名称,任务栏、开始菜单里看到的就是你的品牌。

{ "nameShort": "团队编辑器", "nameLong": "团队编辑器 定制版", "dataFolderName": ".team-editor", "configurationDefaults": { "telemetry.telemetryLevel": "off", "editor.fontSize": 14, "workbench.startupEditor": "none" } }

这里有个必须提醒的点:dataFolderName一旦改了,用户原来的扩展和配置就不会被继承,因为那是存在旧目录里的。定制版和标准版并存时这是好事,但如果用户是从标准版迁移过来,得手动把.vscode目录下的extensions和User/settings.json搬过去。这个迁移脚本我建议一并提供,不然第一批用户会炸锅。

6. 常见问题与排查实录

6.1 打包阶段报错速查表

vsce 的报错信息通常比较直白,但有几个容易被误读。下面这张表是我这些年攒下来的高频问题,遇到先对一遍。

报错关键词真实原因处理方式
Missing publisherpackage.json没有publisher补一个,随便起个内部名
Missing repository没有repository字段加--allow-missing-repository
LICENSE not found没有 LICENSE 文件加--skip-license或补一个空文件
Icon should be a PNG图标格式不对转成 PNG,128×128
README.md not found缺 README建一个,哪怕只有一行
Extension entrypoint(s) missingmain指向的文件不存在检查产物路径是否被 ignore 掉

最后一条特别值得展开。这个报错说的是package.json里的main(或者browser)字段指向的文件在打包后的目录里找不到。原因通常是构建产物没生成、或者被.vscodeignore误排除、或者main路径写错了大小写。Windows 上大小写不敏感,Linux 上可敏感得很,本地打包没问题,内置到 Linux 环境就报错,这类问题特别隐蔽。

6.2 内置后不生效的四层排查链路

扩展放进了目录但没生效,是最让人头大的情况,因为没有任何显式报错。我一般按下面这个顺序查,基本四步内能定位。

第一层,路径对不对。确认编辑器实际读的是哪个目录。最直接的办法是打开扩展面板,任意找一个内置扩展,右键看有没有"在文件资源管理器中显示"之类的选项;或者用命令行code --list-extensions --show-versions看列表里有没有你的扩展。

第二层,目录结构对不对。内置扩展目录下应该是extensions/<扩展目录>/package.json,而不是extensions/<扩展目录>/extension/package.json。如果你是从 vsix 里直接拷的extension目录但没改名,路径就多了一层,编辑器扫不到。

第三层,版本范围对不对。打开扩展目录下的package.json,看engines.vscode的值。如果是^1.85.0而你内置的编辑器是 1.80,扩展会出现在列表里但被标记为不兼容,不会激活。这个在扩展面板里能看出来——会有一条"由于版本不兼容已被禁用"的提示。

第四层,激活时机对不对。前三层都过了,扩展也显示了,但命令就是没反应,那多半是激活事件的问题。打开"输出"面板看扩展宿主日志,或者在命令面板执行"开发人员: 显示运行中的扩展",确认扩展有没有被激活。没激活就是activationEvents没声明对,加上对应的onCommand或者onLanguage再验一次。

6.3 内置扩展与用户扩展的冲突处理

如果用户已经从市场装了同一个扩展,你再内置一个同名同 publisher 的版本进去,会发生什么?答案是两个都会存在,但只有一个会生效,而且哪个生效不保证稳定。现在编辑器一般倾向于加载版本更高的那个,但这属于实现细节,不能依赖。

正确的做法是让内置版本的标识和用户版本完全区分开。有两条路:一条是把publisher改成内部专用的名字,这样两个扩展互不相干,用户手动装的继续用,内置的也继续用,但会出现两个几乎一样的命令;另一条是干脆在内置版里把name也改掉,配上不同的命令前缀,彻底避免冲突。

我一般选第一条路,因为改名成本低(只改一个字段),而且如果命令 ID 不变,用户旧的习惯还能沿用。代价是扩展面板里会出现两条记录,得在交付文档里说清楚"内置那条不用管"。这个体验上的小瑕疵,比命令冲突引发的问题要小得多。

6.4 卸载与升级的坑

内置扩展在 UI 上是没有卸载按钮的,用户想"临时关掉"只能禁用。禁用状态存在用户配置里,重装安装程序后如果dataFolderName没变,禁用状态会被保留,用户会以为新版本还是坏的。这个细节交付时最好在说明里写一句"如果装完不生效,先检查扩展是否被禁用"。

升级则更麻烦。新的安装程序覆盖安装时,旧的内置扩展目录不会被自动删除,如果版本号变了目录名也变了,就会变成新旧两份并存。所以安装器里要么在复制前先删掉旧的扩展目录(按前缀匹配),要么保持目录名不带版本号、原地覆盖。后者更简单,但代价是没法定向排查版本问题。我现在的做法是目录名带版本号,安装器里加一步"删除同前缀的旧目录",多写两行逻辑,省掉后面无数麻烦。

6.5 一个真实踩过的坑:文件权限

最后说一个特别容易被忽略的问题。Windows 下如果用系统级安装路径,安装程序需要管理员权限。如果安装程序请求了提权但用户点了"否",界面可能仍然显示安装成功,只是扩展目录没写进去。这种情况最难查,因为用户坚信自己装成功了。

我的处理办法是在安装器最后加一个校验步骤:装完之后检测目标扩展目录下的package.json是否存在,不存在就弹窗提示"扩展预置失败,请以管理员身份重新运行安装程序"。多写这十几行代码,能省掉大量来回沟通。

7. 我在实际项目中留下的几条经验

这套流程我大概完整走过七八次,从最开始的手工拷目录到后来的脚本文档化,有几条经验是一直沿用的。

第一,把"打 vsix"和"做安装包"拆成两个独立步骤。前者产出带版本号的 vsix 文件并归档,后者只负责把归档目录里的内容复制进去。这样任何一批安装包出问题,都能追溯到具体是哪一版扩展,也能随时单独替换某一个扩展重打包,不用从头跑一遍。

第二,内置扩展的目录名一定带版本号,但package.json里的version必须和目录名一致。不一致的时候,编辑器显示的版本和实际代码对不上,排查问题时会被误导,这个错误我犯过。

第三,在安装包的分发包里放一份"扩展清单说明"。不一定要给最终用户,给 IT 支持的人就够了,写清楚这批安装包内置了哪几个扩展、各自版本、对应的编辑器版本范围。这个东西在跨部门交接的时候价值极高,比任何口头交代都管用。

第四,不要在内置扩展里做需要联网激活的逻辑。内置的意义就是离线可用,一旦某个扩展在启动时去请求网络,超时会拖慢整个编辑器的启动速度,用户第一反应就是"这个定制版好卡"。如果确实需要联网能力,做成按需触发,别放在激活阶段。

第五,留一条"用户可覆盖"的口子。内置扩展的配置项尽量都暴露成settings,让用户能在设置里关掉不想要的行为。一刀切的内置最容易引发抵触情绪,加几个开关的成本很低,接受度高很多。

最后再补一个小技巧:如果你要内置的扩展超过三个,建议在extensions目录旁边留一个.builtin-manifest.json之类的清单文件,把你的扩展列表和版本记下来。这个文件编辑器不会读,纯粹是给运维脚本和你自己用的。等到某天需要批量升级或者排查"到底装了什么",翻一眼清单比翻目录快得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进