Remote File —— VSCode 里的 FTP / SFTP 部署插件在 VSCode 侧边栏里浏览远程服务器上的文件,保存本地文件时自动上传,支持目录映射、 主动/被动模式的 FTP、SFTP 和 FTPS。配置就放在项目里的一个 JSON 文件里,跟着项目走。
目录它解决什么问题写完代码要传到服务器上看效果,一般得开个 FileZilla 拖来拖去。这个插件把这件事搬进 VSCode:按 Ctrl+S 就传上去了,想看远程有什么文件就在侧边栏点开看,不用切窗口。 大致支持这些事:
安装方式一:安装打包好的
|
| 步骤 | 问什么 | 默认值 / 校验 |
|---|---|---|
| 1 | 协议 | 快选:SFTP(推荐)/ FTP / FTPS |
| 2 | 主机地址 | 必填 |
| 3 | 端口 | 跟着协议走:SFTP→22,FTP/FTPS→21 |
| 4 | 用户名 | 必填 |
| 5 | 密码 | 隐藏输入,可留空(SFTP 用密钥时) |
| 6 | 远程目录 | 默认 /;必须 / 开头 |
| 7 | 本地目录 | 整个工作区 / 工作区子目录 / 选择其它文件夹 / 手动输入 |
写完会问一句「要现在测一下连接吗?」—— 连不上当场就知道。
端口默认值跟着协议走是特意做的:
protocol选 ftp 但端口留着 22 是最常见的配置错误, 而且要等到连接失败才发现。
不想让配置跟着项目提交到 git 的话,记得把
.vscode/sftp.json加进.gitignore—— 里面可能写着服务器密码。(宝塔的站点.gitignore通常已经包含.vscode/。)
第 2 步:想手写也行
最小可用配置就这么几行(不想用向导的话自己写也一样):
{
"protocol": "sftp", // sftp / ftp / ftps
"host": "192.168.1.100",
"port": 22, // 按协议填:sftp 用 22,ftp/ftps 用 21
"username": "root",
"password": "你的密码",
"remotePath": "/var/www/html" // 本地项目根目录对应到服务器上哪个目录
}
第 3 步:点侧边栏
点左侧的「远程文件」图标,会看到远程目录树。第一次展开时会自动连接服务器。
如果连不上,按 Ctrl+Shift+P → Remote File: 显示运行日志,日志里有完整的
协议交互过程,能看出卡在哪一步。
第 4 步:改个文件试试
随便改一个文件按 Ctrl+S,日志里会出现 ↑ 文件名 → /var/www/html/...。
去服务器上确认一下,文件已经更新了。
配置文件详解
配置文件默认在 .vscode/sftp.json,路径可以在 VSCode 设置里改(remoteFile.configFile)。
两种格式,随便挑
| 格式 | 文件 | 注释 | 说明 |
|---|---|---|---|
| JSONC | .vscode/sftp.json |
// 和 /* */ |
默认。插件会自动把配置文件切到 jsonc 语言模式,所以 // 注释不会报语法错误 |
| YAML | .vscode/sftp.yml / .yaml |
# |
扩展名是 .yml/.yaml 就按 YAML 解析 |
切换方式:把文件改名就行,不用改任何设置 —— 插件在找不到指定文件时会自动尝试同名的
其它扩展名(sftp.json ↔ sftp.yml ↔ sftp.yaml)。两个都存在时,remoteFile.configFile
指定的那个优先。
为什么 JSONC 不会报错:
.json文件 VSCode 默认按严格 JSON 解析,//会被标成 语法错误(就是那一堆波浪线)。插件在打开配置文件时会把它切到 JSONC 语言模式 —— 这只是换个语法解析器,不改文件内容。你直接用资源管理器打开.vscode/sftp.json也会自动切过去。
YAML 的一个坑我已经帮你关了:YAML 默认会把
password: 123456变成数字、password: yes变成布尔、09:00变成 540 秒 —— 密码当场就废了。插件用的是 不做隐式类型转换的解析模式,所有值一律按字符串读取,原样保留。
两种格式都支持 // / # 注释、行尾注释和尾逗号(JSON 的尾逗号也认)。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string | 服务器地址 | 显示名,多服务器时用来区分 |
protocol |
string | ftp |
sftp / ftp / ftps |
host |
string | 必填 | 服务器地址或域名 |
port |
number | 22 / 21 | SFTP 默认 22,FTP/FTPS 默认 21 |
username |
string | 必填 | 登录用户名(也认 user) |
password |
string | — | 登录密码 |
privateKeyPath |
string | — | SFTP 私钥文件路径,相对工作区或绝对路径 |
passphrase |
string | — | 私钥的密码 |
remotePath |
string | / |
远程根目录(也认 remote_path) |
context |
string | . |
本地根目录,相对工作区;. 表示整个工作区 |
uploadOnSave |
boolean | true |
保存时自动上传 |
uploadOnCreate |
boolean | true |
新建文件时自动上传 |
uploadOnlyWhenChanged |
boolean | true |
内容与上次成功上传的一致时跳过,不白跑一次网络。关掉就每次重传 |
deleteOnLocalDelete |
boolean | false |
本地删除时同步删除远程。危险,默认关闭 |
ftpMode |
string | passive |
passive(被动)或 active(主动) |
secure |
boolean | false |
用 protocol: "ftp" 时置 true 等价于 ftps |
ftpsMode |
string | explicit |
explicit(AUTH TLS,端口 21)或 implicit(隐式,一般 990) |
allowUnauthorizedTls |
boolean | false |
服务器用自签名证书时置 true |
connectTimeout |
number | 20000 |
连接超时(毫秒) |
concurrency |
number | 按协议 | 同一条连接上允许同时进行的操作数。不写就自动:SFTP 用 4,FTP 用 1。这不是连接数,插件始终只用一条连接 |
encoding |
string | utf8 |
远程文件名编码,中文乱码时改成 gbk |
ignore |
string[] | 见下 | 不上传的路径规则 |
mappings |
array | [] |
目录映射,见下文 |
为了兼容网上现成的 sftp.json,下面这些别名也能识别:
user → username、remote_path → remotePath、passive: false → ftpMode: "active"、
autoDelete → deleteOnLocalDelete、mapping → mappings、charset → encoding。
多服务器配置
一个项目要部署到测试机和正式机,就写成 profiles:
{
"defaultProfile": "正式机",
// 写在顶层的是所有服务器的公共配置,单个 profile 里可以覆盖
"uploadOnSave": true,
"ignore": [".git", "node_modules"],
"profiles": [
{
"name": "正式机",
"protocol": "sftp",
"host": "10.0.0.1",
"username": "deploy",
"password": "xxx",
"remotePath": "/srv/www"
},
{
"name": "测试机",
"protocol": "ftp",
"host": "192.168.1.50",
"username": "tester",
"password": "yyy",
"remotePath": "/htdocs",
"ftpMode": "passive"
}
]
}
侧边栏顶层会列出两台服务器,带「(当前)」的那台是正在用的。 切换到别的服务器:点它,或者状态栏左下角的服务器名字 → 「切换服务器」。
目录映射
默认情况下,本地 context 目录整体对应远程 remotePath:
<工作区>/ → /var/www/html
<工作区>/index.html → /var/www/html/index.html
<工作区>/css/app.css → /var/www/html/css/app.css
如果要让某个子目录去别的地方,用 mappings:
{
"context": ".",
"remotePath": "/var/www/html",
"mappings": [
{ "local": "dist", "remote": "/var/www/html" },
{ "local": "src/assets", "remote": "/var/www/static" }
]
}
规则是本地目录路径更长(更深)的优先匹配,所以上面这两条能覆盖默认根里的对应子目录:
<工作区>/dist/index.html → /var/www/html/index.html
<工作区>/src/assets/logo.png → /var/www/static/logo.png
<工作区>/src/app.ts → /var/www/html/src/app.ts
也支持简写 "mapping": { "dist": "/var/www/html" }。
忽略规则
ignore 用 glob 语法,判定方式和 .gitignore 的直觉一致:路径本身或它的任意祖先目录命中规则,就跳过。
{
"ignore": [
".git", // 任何位置叫 .git 的目录及其内容
".vscode", // 配置文件自己也不上传
"node_modules",
"**/*.log", // 任意层级的 .log 文件
"dist/**", // dist 目录及其下所有内容
"/build", // 只忽略根目录下的 build(带斜杠即从根匹配)
"*.tmp"
]
}
VSCode 全局设置 remoteFile.ignore 里的规则会和配置文件里的合并。
encoding、connectTimeout 这些也同理:配置文件优先,没有就用全局设置。
.vscode/sftp.json 本身永远不会被上传,不用特意排除。
不用手写:右键就能加/减
在资源管理器里右键一个文件或目录(在编辑器里右键也行)。菜单会按右键对象、以及它当前的状态变化:
| 右键对象 | 状态 | 菜单项 | 写进 ignore 的规则 |
|---|---|---|---|
| 目录 | 未忽略 | 忽略该目录 | 目录路径,例如 storage/cache(整棵子树都不传) |
| 文件 | 未忽略 | 忽略该文件 | 文件路径,例如 api/admin/main.go(只忽略这一个) |
| 文件(有扩展名) | 未忽略 | 忽略该类型文件 | 按扩展名,例如 **/*.go(所有层级的同类文件) |
| 任意 | 已忽略 | 从上传忽略规则中移除 | 删掉覆盖它的那条规则 |
也就是说:已经被忽略的路径上不会再出现「忽略该项」(点了也只是告诉你已经在里面了), 没被忽略的路径上也不会出现「移除」。
- 「忽略该类型文件」只在有扩展名的文件上出现(
Makefile、.gitignore不会显示这一项) - 「忽略该目录」只在目录上出现;编辑器/标签页右键时也不会出现(那里只会是文件)
- 从命令面板执行「从上传忽略规则中移除」会列出当前所有规则让你挑; 执行「忽略该目录」会弹一个文件夹选择框
两点说明:
- 写进去的是相对 context 目录的路径(和
ignore的语义一致)。带斜杠的规则从根匹配, 不带斜杠的匹配任意层级同名条目 —— 和手写规则完全一样。 - 插件改的是文本,所以你配置文件里的注释和排版一个字都不会动。 写盘前还会把结果重新解析一遍确认合法,宁可不改也不会把配置写坏。
各项功能怎么用
远程文件树(侧边栏)
完全按需加载,不会一连上就把整个项目扫一遍:
① 打开面板 → 拿到同步根节点,0 次请求
② 同步根默认展开 → 列 1 次,就是"第一层目录"
③ 第一层里的目录是折叠的 → 不点开就绝不去列它
④ 你点开 css/ → 只列 /site/css 这一次,然后缓存住
⑤ 折叠再展开、或重新渲染 → 命中缓存,0 次请求
每一层都是"点谁列谁、列完记住"。所以:
- 目录再多,打开面板也只花列第一层那一次
- 已经看过的目录来回点不会重复请求
- 只有点工具栏的刷新按钮才会清缓存重新列(这是显式的)
想展开更深,改 VSCode 设置 remoteFile.expandDepth
(0 全部折叠 / 1 只展开同步根,默认 / 2 再展开第一层目录 / 3 最多)。
- 右键菜单:见后面「右键菜单」一节
- 视图标题栏:刷新 / 重新连接 / 上传当前文件 / 上传整个项目 / 切换服务器 / 打开配置 / 显示日志
- 自动更新:上传、新建上传、删除同步、重命名同步之后,只有受影响的那些远程目录
会重新向服务器拉一次列表,其余目录直接命中缓存。所以按
Ctrl+S之后侧边栏会立刻 看到新文件(或更新后的大小),但不会为一次保存把整棵远程树重拉一遍。 - 内容没变不会白传:按
Ctrl+S时如果文件内容跟上次传上去的一模一样,会直接跳过, 日志里写内容没有变化,跳过上传:xxx。用户显式执行的上传命令(Ctrl+Alt+U、 右键上传、上传整个项目)不受此影响,一定会真的传上去。 想关掉这个行为(每次都重传):配置里写"uploadOnlyWhenChanged": false。
⚠️ 关于
expandDepth:每一个被自动展开的目录都会立刻向服务器发一次列目录请求。 默认的1只多花一次(列第一层);但设成3就可能在打开面板时同时打几十次请求过去, 在 FTP 上(一个目录约 3~4 次往返)会明显拖慢。层数越高越"开",代价也就越大。
打开远程文件:分栏 / 只看远程 / 差异对比
点远程文件树里的文件时,默认会分栏打开两份:
┌─────────────────────┬─────────────────────┐
│ (远程) index.html │ index.html │
│ ── 只读,来自服务器 │ ── 本地那份,可编辑 │
└─────────────────────┴─────────────────────┘
- 左边是远程内容:虚拟文档,只读,标签名带
(远程)前缀 - 右边是本地对应文件:真实文件,可以正常编辑保存
⚠️ 只有远程那份能加
(远程)前缀。本地那份不行 —— VSCode 没有给真实文件 重命名标签页的 API(改成虚拟文档就能标了,但那样本地那份就不能编辑了,得不偿失)。 本地那份靠"在右边那一栏"来区分。
本地还没有这个文件时,只会打开远程那一份。
想换行为,改 VSCode 设置 remoteFile.openMode:
| 值 | 行为 |
|---|---|
both(默认) |
分栏:左远程 + 右本地 |
remote |
只打开远程那一份 |
diff |
直接用差异视图对比(同右键的「与本地文件对比」) |
右键远程文件还有一项 「与本地文件对比」,用 VSCode 的差异视图并排显示两者的异同,
标签名是 (远程 ↔ 本地) 文件名。
右键菜单
一共三处。
① 远程文件树里右键(按节点类型不同)
| 右键的节点 | 菜单项 |
|---|---|
| 远程文件 | 查看远程文件、在本地打开对应文件、下载到本地、复制远程路径 |
| 远程文件夹 | 下载到本地、上传到远程服务器、新建远程文件夹、复制远程路径、删除远程文件/文件夹、刷新 |
| 同步根节点 | 下载到本地、上传到远程服务器、新建远程文件夹、复制远程路径、刷新 |
说明几点:
- 「查看远程文件」同时在行内图标和右键菜单里;直接单击文件名也能打开。
- 「在本地打开对应文件」会按目录映射找到本地那一份并打开;本地还没有的话会问你要不要 先下载下来。
- 同步根节点故意没有「删除」,避免右键一不小心把整个远程根目录清空。
- 删除文件夹/文件前会弹窗确认(VSCode 设置
remoteFile.confirmDelete,默认开)。
② 左侧本地文件浏览器里右键 —— 对本地文件或文件夹右键,有「上传到远程服务器」。
注意这个操作走的是目录映射,右键 dist/ 会传到 dist 映射到的那个远程目录。
③ 编辑器里右键 —— 「上传当前文件到远程」,快捷键 Ctrl+Alt+U 也是它。
上传
| 场景 | 操作 |
|---|---|
| 保存时自动传 | 直接 Ctrl+S |
| 传当前文件 | Ctrl+Alt+U,或编辑器右键 → 「上传当前文件到远程」 |
| 传某个文件/文件夹 | 资源管理器里右键 → 「上传到远程服务器」 |
| 传整个项目 | 侧边栏标题栏的上传图标,或命令面板 Remote File: 上传整个项目到远程 |
上传时会自动逐级创建远程目录。目录映射之外的文件会被跳过(日志里会写原因)。
命令面板 / 工具栏触发的上传(上传当前文件、上传文件夹、上传整个项目)会刷新整棵远程树; 保存和新建触发的自动上传只刷新落点所在的那个目录,避免高频操作下反复重拉。
一次保存多个文件会怎样
Ctrl+K S(全部保存)会为每个文件各触发一次保存事件,插件会逐个上传所有文件,不会漏。
它们走同一条连接、依次串行执行(不是并发),所以:
- 一条连接上不会有两个传输打架(FTP 尤其需要这样);
- 20 个文件就是 20 次往返,慢一点,但结果完整;
- 每个文件独立判定成败,其中一个失败不影响其它(右下角弹错误,日志里有明细)。
本地文件的读取是并发的,只有真正发往服务器的部分是排队的 —— 所以不用担心中间状态, 但大项目里"全部保存"后请留意日志,确认没有失败项。
哪些情况不会自动上传
自动上传只响应两件事:编辑器里的保存、文件新建。
文件"内容变化"事件是故意不监听的(否则每次保存都会被处理两遍,重复上传)。 所以下面这些情况保存时不会触发上传 —— 但它们会在下次打开项目时被补同步(见下):
- 用
npm run build、webpack --watch、tsc -w等工具生成的产物 git checkout/git pull/git clean引起的文件变化- 用别的编辑器或脚本改动的文件
想立刻同步:命令面板 → Remote File: 与远程增量同步(只传变化的)。
打开项目时补同步(增量同步)
场景:VSCode 关着的时候,文件被别的工具改过、加了、删了。再打开项目时把这些差异补到远程。
它靠一份同步账本工作 —— 账本记着「上次同步时本地每个文件是什么内容」(存内容指纹), 放在 VSCode 的工作区存储里,不进项目目录,也不会被上传。
打开项目时(或你手动执行命令时)它会:
- 遍历本地文件,比对账本 → 找出改动过和新增的
- 找出账本里记着、本地却已经不存在的 → 这些对应的远程文件需要删掉
- 有内容可做时,按设置决定是直接同步还是先问你一句
由 VSCode 设置 remoteFile.syncOnStartup 控制:
| 值 | 行为 |
|---|---|
off(默认) |
不自动跑,用命令手动触发 |
ask |
检测到变化时弹一次确认 |
auto |
直接同步,不询问 |
删除的安全边界
删除**只删「账本里记着、且本地已经不存在的」**远程文件,绝不是"远程上本地没有的就删"。
后者在两种情况下会清空你的远程目录:第一次启用(账本为空)、账本丢了。 所以规则是:
| 情况 | 行为 |
|---|---|
| 本地删了、账本里记着本插件传过 | 删远程 |
| 本地删了、只是建基线时记录过 | 删远程 —— 但先核对远程内容(见下) |
| 本地删了、账本里完全没有这个路径 | 不动 |
| 账本为空(首次 / 丢了) | 只建基线,一个都不传也不删 |
deleteOnLocalDelete 关着 |
绝不删远程 |
关于"核对远程内容":建基线时账本只记了"当时本地是什么内容",那些文件本插件并没有传过, 远程那份可能是别人放的。所以删除前会先读远程内容比对:
- 和基线记录一致 → 就是本地那份的对应物,删
- 不一致(或读不到)→ 跳过,只留一条日志,绝不动别人的文件
这一步只发生在"删本地文件"的时候,不影响正常的增量比对速度。
顺带一提:这样对早就部署好的项目也能正常同步删除了 —— 之前为了保护别人的文件, 把基线条目一律排除在删除之外,结果那些文件本地删掉后远程永远不动。
第一次同步会先建基线(会看远程到底有什么)
账本里还没有这台服务器的基线时,插件先列一遍远程,然后分开处理:
| 情况 | 行为 |
|---|---|
| 本地有、远程也有 | 按当前本地内容登记为基线,不传(早就部署好的项目不会被全量重传) |
| 本地有、远程没有 | 真传上去(VSCode 关着时新建的文件不会被吞掉) |
为什么不是"首次就假设远程和本地一致":那样会把远程上其实还没有的新文件一起登记成 "已同步",之后再也传不上去。而"首次"是按连接标识(协议+主机+端口+用户名)算的 —— 改过端口或账号就会重新触发一次,所以这个坑很容易踩到。
如果远程基本是空的(比如新站点),首次同步就会提议把整个项目传上去。手动执行时会问你一句:
- 「上传」= 把本地这些文件推到服务器
- 「只建基线」= 一个都不传,只登记远程已有的(想从远程拉代码时选这个,之后用「从远程下载整个项目」)
这个功能解决的是"打开项目时补上差异"。VSCode 没开的时候插件是不存在的, 所以做不到"改一下立刻自动上传" —— 那需要常驻服务,不是插件能做到的。
下载
单个文件/目录:在远程文件树上右键 → 「下载到本地」。
- 如果远程路径能对应到本地目录,直接下到那儿;
- 对应不上会弹文件夹选择框;
- 文件夹是递归下载的。
整个项目:侧边栏工具栏的下载图标,或命令面板
Remote File: 从远程下载整个项目(覆盖本地)。用于"服务器上那份才是新的,拉下来覆盖本地"。
- 动手前会确认一次,写清楚「哪个远程目录 → 哪个本地目录」
- 同名文件会被覆盖;本地多出来的文件不会被删(只下载,不做反向删除)
- 忽略规则同样生效:被
ignore的远程路径不下载
个别文件下不下来不会中断
服务器上总有那么几个下不来的文件(比如宝塔建站生成的 .user.ini,属主是 root,
服务器对它的 RETR 直接回 550)。这类文件只跳过并记下来,其余照常下载完:
下载完成:成功 3 个,失败 1 个,跳过 1 个
以下是没能下载的文件(其余都已下载成功):
/.user.ini —— 远程操作失败:... 550 Can't open .user.ini
失败清单在 Remote File: 显示运行日志 里,弹窗上也有「查看失败清单」按钮。
下载完不用再按保存"确认"一遍
下载成功后会同步登记到账本,所以紧接着按 Ctrl+S 不会把它原样传回服务器
(只有你真的改了才会传)。
删除同步
{ "deleteOnLocalDelete": true }
打开之后,你在 VSCode 里删掉一个本地文件(或整个目录),对应的远程文件也会被删掉。
⚠️ 这是不可撤销的。 切分支、
git checkout、git clean引起的文件消失同样会触发。 建议只在专门的部署目录上用,并且不要在没有备份的服务器上开。
界面上主动删除远程文件(右键 → 删除)是会先弹窗确认的,
确认开关是 VSCode 设置 remoteFile.confirmDelete。
FTP 主被动模式与 FTPS
被动 vs 主动
{ "protocol": "ftp", "ftpMode": "passive" } // 默认,绝大多数情况用这个
{ "protocol": "ftp", "ftpMode": "active" } // 服务器/网络要求主动模式时
- 被动模式:服务器开一个临时端口,客户端去连。方向和你平时上网一致,能穿 NAT,所以是默认值。
- 主动模式:客户端开一个临时端口,服务器反过来连你。在 NAT 后面经常连不上。
到底是谁决定用哪种模式?
每次传输都是客户端选的:客户端在传数据之前先发 PASV/EPSV(被动)或 PORT/EPRT(主动),
服务器照着配合。所以表面上看是"你的配置说了算"。
但服务器可以不配合,而且最终决定成败的往往是网络路径 —— 既不是你的配置,也不是服务器的偏好。 下表是各种失败现象分别该找谁:
| 日志里的现象 | 是谁的问题 | 改配置能解决吗 |
|---|---|---|
PASV / EPSV 回 5xx |
服务器禁用了被动模式 | 只能改主动 —— 但你在 NAT 后面的就用不了 |
PASV 返回一个内网/奇怪的 IP |
服务器 pasv_address 没配或配错 |
能。插件默认忽略服务器报的这个 IP,改用你实际连上的那个地址 |
| 连服务器被动数据端口超时 / 被拒 | 服务器防火墙或云安全组没放行被动端口范围 | 不能,得让服务器管理员放行(见下) |
主动模式:500 ... only to ... |
服务器的"回连地址必须和控制连接来源一致"安全检查 + 你在 NAT 后 | 能,改成 passive |
主动模式:服务器接受了 PORT,但数据连不回来 |
你这边的 NAT / 运营商网关 | 能,改成 passive |
| 主动模式:本机防火墙拦了入站 | 你的电脑 | 能:放行临时端口,或改 passive |
一句话:不是公网 IP 就别用主动模式;被动模式连不上就是服务器端的事。
⚠️ 主动模式在 NAT / 宽带后面会「时通时不通」
典型症状:根目录能列出来(快),点开下面的子目录就卡住、最后超时。 原因是第一次数据连接侥幸通了,后面的连不回来。
日志里会看到这样的痕迹:
> EPRT |1|192.168.2.6|51600|
< 500 I won't open a connection to 100.80.212.197 (only to 180.113.77.205)
服务器拒绝了回连地址(100.80.212.197 属于运营商级 NAT 的地址段),说明你不在公网。
这时候唯一可靠的办法是改用被动模式:
{ "ftpMode": "passive" } // 或者直接把 ftpMode / passive 这一行删掉,默认就是被动
被动模式是客户端主动去连服务器,方向和你上网的方向一致,所以能穿 NAT。
改成被动模式之后还是连不上?
那基本可以确定是服务器端没放行被动端口。日志里会直接写出来:
连不上服务器的被动数据端口 1.2.3.4:40021(connect ECONNREFUSED)。
被动模式是客户端主动连服务器,一般不会被你这边拦住,所以这通常是服务器端的问题:
操作系统防火墙或云服务器安全组没有放行被动端口范围。
要让服务器管理员:
- 确定被动端口范围(
vsftpd的pasv_min_port/pasv_max_port、pure-ftpd的-p、ProFTPD的PassivePorts、IIS 的「数据通道端口范围」) - 在操作系统防火墙和云服务器安全组里都放行这段端口
- 如果配了
pasv_address/pasv_addr_resolve,确认它写的是公网地址
这三步缺一个都会表现为"能登录、能列根目录、一进子目录就卡"。这不是客户端能绕过去的。
FTPS
{ "protocol": "ftps", "ftpsMode": "explicit" } // AUTH TLS,端口通常还是 21
{ "protocol": "ftps", "ftpsMode": "implicit", "port": 990 }
自签名证书加上:
{ "allowUnauthorizedTls": true }
控制通道和数据通道都会加密(PBSZ 0 + PROT P),并且数据连接会复用控制连接的
TLS 会话,兼容那些要求会话复用的服务器。
连接与保活
连接是按需建立、长期复用的:不是打开 VSCode 就连,而是第一次操作(展开目录、上传) 时才连;之后缓存在内存里反复使用。配置指纹(协议 / 主机 / 端口 / 用户名 / 密码 / 密钥 / 主被动模式 / 编码)没变就复用同一个连接。
侧边栏一露出来,插件会提前把连接建好,这样第一次展开目录不用再等握手。
心跳保活(防止服务器把空闲连接踢掉):
| 协议 | 机制 |
|---|---|
| SFTP | ssh2 每 20 秒发一次 SSH keepalive |
| FTP / FTPS | 每 30 秒检查一次,空闲超过 60 秒发 NOOP |
断线自动重连:遇到 ECONNRESET / EPIPE / ETIMEDOUT / 421 / "服务器关闭了连接"
这类错误且连接确实已断时,会自动重连并把刚才那个操作重试一次。日志里会写
连接似乎已断开,正在重连后重试:...。
什么时候会主动断开:点工具栏的「重新连接」按钮、改配置文件、改 VSCode 设置、切换服务器。
一个边界情况:如果服务器悄悄踢掉连接(没发 RST),要等下一次操作失败才会触发重连。 心跳就是为了把这种情况压到最低。
并发:不是连接数
插件始终只用一条连接。 能调的是"同一条连接上允许同时进行几个操作":
| 协议 | 默认并发 | 为什么 |
|---|---|---|
| SFTP | 4 | SSH 协议本身就支持一条连接上多路复用请求,串行化纯属浪费 |
| FTP / FTPS | 1 | FTP 的一条控制连接协议上就不允许并发传输,只能排队 |
调整方式(二选一,配置文件优先):
// .vscode/sftp.json
{ "concurrency": 8 }
或 VSCode 设置 remoteFile.concurrency(0 = 自动)。
想让 FTP 也快起来,唯一的办法是多开几条控制连接(每条都要登录一次)。 收益有限,而且不少 FTP 服务器限制单用户连接数,所以没有做。 如果你的服务器允许,可以提出来。
目录缓存
确认存在的远程目录会被记住。上传 20 个文件到同一个新目录时,不会对每个文件都把父目录
逐级探测一遍。(实测:连续 50 次 ensureDir 同一个目录,有缓存时总共 3ms。)
删除远程目录后缓存会立刻失效。
觉得慢?先看日志里的数字
日志里现在会打出每次列目录的耗时和项目数,以及连接握手的耗时:
连接就绪 sftp://1.2.3.4:22(握手 + 登录 186ms,之后复用)
列出 /var/www/html:42 项,212ms
有了这个数字才好判断是哪种情况。粗算的往返次数:
| 操作 | 往返次数 |
|---|---|
| SFTP 列一个目录 | ≈ 2 次 |
| FTP 列一个目录 | ≈ 3~4 次(EPSV + LIST + 数据 + 226) |
| 首次连接握手 | SFTP 约 3 次、FTP 约 3 次 |
所以如果一个目录要 200ms,而你的 ping 服务器是 50ms,那基本就是协议本身的开销,
不是插件的问题 —— 换 SFTP 会明显比 FTP 快(少一两次往返)。
常见问题
根目录能列出来,点开子目录就卡住超时
如果你用的是 FTP + 主动模式,几乎肯定是这个原因:主动模式要服务器反过来连你,
在 NAT / 运营商网关后面时通时不通。日志里会有
500 I won't open a connection to ... (only to ...)。
改成 "ftpMode": "passive" 即可 —— 详见上面「主动模式在 NAT 后面会时通时不通」。
顺便说一句:0.2.3 之前,这种情况会先等 MLSD 超时、再退回 LIST 再等一轮,一个目录要 白等两个超时周期;现在只会等一次,并且错误信息里直接告诉你怎么改。
连不上,日志里什么都没有
日志分两个频道:
Remote File: 显示运行日志—— 结果:传了哪些文件、成功/失败、失败原因、基线、连接就绪Remote File: 显示协议细节日志(排查传输问题)—— FTP/SFTP 的每一条命令和应答 (> USER root/< 331 ...),卡在哪一步一目了然
排查传输问题时看协议细节那个频道。
协议往来平时不写日志(VSCode 会把输出频道镜像到磁盘,一次「上传整个项目」上万行会写掉 好几百 KB)。它先待在内存里(最近 400 行),只在出错、告警、你主动执行那个命令时倒出来。 所以排查姿势是:复现一次,然后立刻执行「显示协议细节日志」。
SFTP 连接失败:认证失败
用户名/密码不对,或者服务器只允许密钥登录。用密钥的话填 privateKeyPath
(指向私钥本身,不是 .pub 公钥),私钥有密码就再填 passphrase。
服务器上的中文文件名显示成乱码
把 encoding 改成 gbk:
{ "encoding": "gbk" }
保存了但没上传
三个可能:
- 文件命中了
ignore规则 —— 日志里会写「跳过 xxx(命中忽略规则 "yyy")」; - 文件不在任何同步根里(
context和mappings之外)—— 日志里不会有这次上传记录; - 上传失败 —— 右下角会弹错误,日志里有堆栈。
VSCode 里删了文件,远程没删
deleteOnLocalDelete 默认是关的,要显式打开。
新建的文件没有自动上传
VSCode 的文件监听会跳过 files.watcherExclude 里排除的目录(默认包含
**/.git/objects/**、**/node_modules/** 等)。如果你要同步的目录正好被排除了,
在 VSCode 设置里把它从 files.watcherExclude 移除。
上传整个项目会不会把 node_modules 传上去
不会,默认忽略规则里就有。想确认的话看日志里的「跳过」记录。
加载列表很慢怎么办
先看日志里的实际数字:Remote File: 显示运行日志,会看到
连接就绪 sftp://1.2.3.4:22(握手 + 登录 186ms,之后复用)
列出 /var/www/html:42 项,212ms
- 如果是第一次展开慢:那是握手开销,现在已经提前建连了;重载窗口后第一次仍会有一次。
- 如果每次列目录都慢:SFTP 一个目录约 2 次往返、FTP 约 3~4 次,拿这个乘你的
ping延迟看看对不对得上。对得上就是协议开销,换 SFTP 会明显快一些。 - 如果是批量上传慢:把
concurrency调大(SFTP 有效,FTP 无效 —— 见上面「并发」一节)。 - 如果数字远大于延迟 × 往返次数:把日志贴给我,可能是服务器或网络的问题。
点远程文件后右边没有打开本地文件
两种情况:本地对应的文件不存在(只会打开远程那一份),或者
remoteFile.openMode 被设成了 remote。另外如果你在目录映射之外,
插件找不到本地对应文件,也会只打开远程。
配置文件能提交到 git 吗
里面写着密码的话不要。更稳妥的做法是用密钥登录 + 只在配置里写用户名。
已知限制
- 远程那一侧是只读预览(虚拟文档),在它里面改了不会回传。要改就先下载到本地, 或者直接改右边那一栏的本地文件。
- 本地那份文件没法在标签名上加前缀(VSCode 没有给真实文件重命名标签页的 API),
只能靠"它在右边那一栏"来区分。远程那份会显示成
(远程) 文件名。 - 上传不是原子的:远程文件在传输开始那一刻就已经存在了(内容是空的/不完整的), 传完才完整。如果服务器上还有别的程序在盯这个目录,可能读到半个文件。
- VSCode 外部改动的文件不会自动上传(原因见上面「哪些情况不会自动上传」)。
- 保存自动上传依赖 VSCode 的文件监听,超大项目(几万个文件)下建议把
context收窄到真正要部署的子目录,而不是整个工作区。 - 符号链接不会被跟随(避免目录成环)。
- 一个工作区同一时刻只操作一台服务器(用
selectServer切换,或展开另一台服务器)。 - 多根工作区(multi-root)只认第一个能找到配置文件的工作区文件夹。
给开发者
项目结构
src/
core/ 纯逻辑,不依赖 vscode,可以直接单测
posixPath.ts 远程路径运算(POSIX 风格)
ignore.ts glob 忽略规则
ignoreEdit.ts 往配置文件的 ignore 数组里增删规则(保留注释的文本手术)
config.ts 配置解析与校验、配置模板生成
pathMap.ts 本地路径 ↔ 远程路径映射
digestStore.ts 同步账本接口 + 内存实现
types.ts 公共类型与统一错误
net/ 协议实现
ftpClient.ts FTP/FTPS 客户端(自己实现,含主动模式)
sftpClient.ts SFTP 客户端(基于 ssh2-sftp-client)
index.ts 按协议挑实现
vscode/ VSCode 集成层
configStore.ts 配置读取、缓存、多服务器
configWizard.ts 配置向导(逐步问答生成配置)
connection.ts 连接复用、串行化、断线重连
workspaceDigestStore.ts 同步账本持久化(跨 VSCode 重启)
remoteTree.ts 侧边栏远程文件树
remoteContentProvider.ts 远程文件只读预览
watchers.ts 保存/新建/删除/重命名监听
sync.ts 上传 / 下载 / 增量同步的业务逻辑
ignoreCommands.ts 「忽略该目录/该文件/该类型」菜单的实现
ignoredPaths.ts 给菜单 when 条件喂「当前已忽略的路径」上下文键
logger.ts 输出面板日志(结果 / 协议细节两个频道)
extension.ts 入口:激活、命令注册、状态栏
test/ 单元测试 + 真实协议集成测试 + 端到端链路测试
为什么 FTP 客户端是自己写的
被动模式用现成的库很省事,但主动模式在 npm 上找不到还在维护的实现:
basic-ftp 只有被动模式,node-ftp 停更在 2015 年。所以 src/net/ftpClient.ts
按 RFC 959 / 2389 / 3659 / 4217 自己实现了控制通道、数据通道、MLSD/LIST 解析和
AUTH TLS 升级。好处是主被动和 FTPS 都能覆盖,缺点是协议边界情况得自己兜 ——
所以它的集成测试是拿真实的 FTP 服务器(ftp-srv)跑出来的,不是 mock。
常用命令
pnpm install # 装依赖(Node 22+,pnpm 10+)
pnpm run build # 打包成 dist/extension.js
pnpm run watch # 监听源码变化自动重新打包
pnpm run typecheck # 类型检查
pnpm test # 编译并运行全部测试
pnpm run test:serial # 同上,但串行跑(CI 用这个,见下)
pnpm run package # 生成 .vsix
本地调试
用 VSCode 打开本仓库,按 F5 —— 会启动一个装了本插件的「扩展开发宿主」窗口
(.vscode/launch.json 已经配好,会自动先构建)。在那个窗口里打开任意项目、
执行「配置向导」就能边改边试。
依赖都用
devDependencies放:因为 esbuild 会把js-yaml、ssh2-sftp-client这些运行时依赖全部打进dist/extension.js,打包时用vsce package --no-dependencies即可,不需要在发布时再装一遍 npm 依赖。
持续集成
.github/workflows/ci.yml 在 windows-latest 上跑
typecheck → build → test:serial → package。选 Windows 是因为测试会真的起本地
FTP / SFTP 服务器,和开发机环境一致最稳。
测试在 CI 里串行执行(test:serial):多个测试文件并行跑时,它们会同时起服务器、
同时往 .tmp-tests 里读写,实测会出现偶发假失败(同一处代码上一轮过、下一轮挂)。
本地用 pnpm test(并行,快)就够,CI 用串行换确定性。
测试覆盖了什么
pnpm test 会跑 159 个用例(其中 1 个是默认跳过的性能测量)。真正起服务器、走真实协议的不在少数:
端到端链路(VSCode 本身用替身,其余全是真的)
- 目录映射 + 忽略规则 + 上传整个目录,最后断言文件真的落在远程磁盘上、 该被忽略的确实没上去
- 保存自动上传、新建自动上传、重命名同步、删除同步,以及关掉开关后不删远程
- 保存自动上传后的定向刷新:断言
/site被重新列出、而/site/css没有(走缓存) - 并发:SFTP 默认 4 路并发且内容正确(含同一新目录下的 mkdir 竞争)、
把
concurrency设为 1 时恢复严格串行 - 目录缓存:第一次
ensureDir会探测、第二次一条命令都不发、删目录后缓存失效 - 侧边栏远程文件树:展开真实目录、列出子目录、节点类型与右键菜单上下文
- 递归下载远程目录到本地
- 异常路径:映射范围外的文件被明确跳过、列出不存在的远程目录报可读错误
协议层
- FTP 被动模式:连真实的
ftp-srv,跑完建目录 / 上传 / 列目录 / 下载 / 重命名 / 递归删除 - FTP 主动模式:同上,另一套数据连接流程
- FTPS 显式加密:自签证书 +
AUTH TLS+PROT P - SFTP:连本机
ssh2起的真实 SSH/SFTP 服务器,含 1.5 MB 大文件多轮读写、 认证失败、端口不通三条路径
其它
- 打包产物冒烟:把
dist/extension.js用真实require加载起来并调用activate(), 校验package.json里声明的每个命令都注册了、订阅都挂上了 - 预览 URI 往返(回归):各种远程路径经
uriFor → toString → parse → remotePathOf必须无损,且 URI 不能产生 authority(0.1.1 修的那个RETR /bug) - 预览标签:带
(远程)前缀、两台服务器的同名路径是两个不同文档 - 纯逻辑单测:路径运算、忽略规则、配置解析、目录映射、并发数默认值与夹取、 LIST/MLSD/DOS 列表解析
性能测量
REMOTE_FILE_BENCH=1 pnpm test
会打印本机回环上各操作的耗时(连接握手、列目录、目录缓存效果),默认跳过。 回环的 RTT 接近 0,真实服务器要按往返次数乘以 RTT 估算。
VSCode API 由 test/helpers/vscodeStub.ts 顶替,通过 esbuild 的
--alias:vscode=... 注入(见 esbuild.mjs)。所以测试里能直接构造
ConfigStore、SyncService、FileSyncWatcher,不需要真的开一个编辑器。