阅读视图

发现新文章,点击刷新页面。

VSCode debugger 调试指南

在以前的文章 深入前端调试原理,我们主要从原理的角度来看了如何调试。本篇文章则是从实践角度出发,看看如何在 vscode 中配置和使用各种调试功能。

本文涉及到代码均在仓库 vscode-debugger-dojo,全面的 VSCode Debugger 调试示例项目,涵盖了各种常见场景的调试配置。

VSCode Debugger 原理

在 VSCode 的项目中 .vscode/launch.json 中加入如下的配置即可调试:

SCode 并不是 JS 语言的专属编辑器,它可以用于多种语言的开发,自然不能对某一种语言的调试协议进行深度适配,所以它提供了 Debugger 适配层和适配器协议,不同的语言可以通过提供 Debugger 插件的形式,增加 VSCode 对不同语言的调试能力:

如此,VSCode Debugger 就能以唯一的 Debugger 前端调试各个不同的语言,插件市场中也提供了诸多不同语言的调试插件:

因此需要使用 type 字段来配置对应的 Debugger 适配器,在前端场景主要是 node 或者 chrome,分别对应 node 和浏览器环境。

调试模式

调试模式通过 request 的方式指定如何启动和链接调试器。

Attach 模式

调试的前后端之间是通过 websocket 进行通信的,所以确保前后端能正确的连接是调试成功的关键。

除了在需要调试的网页中直接打开 Devtools 的方式外,我们使用第三方前端工具进行调试时,都需要知道所需要调试的网页的后端 ws 地址才行。因此我们需要让 Chrome 以指定调试端口的形式跑起来,使用 --remote-debugging-port 参数指定调试端口:

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222

Chrome 运行参数非常多,可以通过 该文档 进行查看。

在 Chrome 运行起来后,随意的打开几个网页,然后访问 localhost:9222/json/list 网址,此时就能得到所有运行的网页后端 ws 地址:

在百度网页中同时打开了 Devtools, 可以看到连 Devtools 的调试信息都一起打印出来了,因为 Devtools 本质上也是一个网页程序,所以甚至可以做到对 Devtools Debugger 进行 Debug 这样的套娃操作。

有了 ws 信息,我们就可以使用其他 Debugger 进行连接了,比如使用下面的 VSCode Debug 配置:

Node.js 程序在运行时则是通过 --inspect 或者 --inspect-brk 参数开启调试模式:

Node.js 调试协议经过了漫长的迭代,最终也是以 CDP 协议进行调试,因此 Node.js 程序可以直接使用 Devtools 进行调试,在 Chrome 中访问 chrome://inspect/#devices 就可以看到调试目标了:

如果当前的 Node 项目没有被发现,可能是检测端口不是默认的 9229 ,可以通过 Configure 进行配置。

VSCode Debugger 同样能够连接上 Node.js 项目并进行调试,

Launch 模式

前面讲到的都是首先通过调试模式启动一个项目,然后再手动进行前端 ws 连接,最后再调试的模式。相对较繁琐,VSCode Debugger 提供了 launch 模式,它相当于是将上面的流程自动化了,以调试模式将 Chrome 或者 Node.js 程序运行起来,然后 Debugger 自动 attach 到调试端口,直接调试。

常见配置

launch.json 核心字段

每个调试配置都需要以下基础字段:

字段 说明 可选值 / 示例
type 调试器类型 nodechromelldb
request 调试模式 launch(启动)、attach(附加)
name 配置名称 任意描述性名称

常用配置项

Node.js 调试配置项

{
  "type": "node",
  "request": "launch",
  "name": "Node.js Debug",
  "program": "${workspaceFolder}/index.js",
  "args": ["--port", "3000"],
  "cwd": "${workspaceFolder}",
  "env": {
    "NODE_ENV": "development"
  },
  "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
  "console": "integratedTerminal",
  "runtimeExecutable": "node",
  "runtimeArgs": ["-r", "ts-node/register"]
}

配置说明:

  • program:程序入口文件
  • args:传递给程序的命令行参数
  • cwd:工作目录
  • env:环境变量
  • skipFiles:调试时跳过的文件(如 node 内部代码)
  • console:终端类型(integratedTerminalinternalConsoleexternalTerminal
  • runtimeExecutable:运行时可执行文件(如 nodenpmpnpm
  • runtimeArgs:传递给运行时的参数

Chrome 调试配置项

{
  "type": "chrome",
  "request": "launch",
  "name": "Chrome Debug",
  "url": "http://localhost:3000",
  "webRoot": "${workspaceFolder}",
  "userDataDir": "${workspaceFolder}/.chrome-data",
  "runtimeArgs": ["--auto-open-devtools-for-tabs"],
  "sourceMaps": true,
  "sourceMapPathOverrides": {
    "webpack:///./~/*": "${webRoot}/node_modules/*",
    "webpack:///./*": "${webRoot}/*"
  }
}

配置说明:

  • url:要打开的网页地址
  • webRoot:Web 根目录,用于源码映射,本质上就是对 sourcemapPathOverrides 的快捷配置方式
  • userDataDir:Chrome 用户数据目录,保存登录状态和扩展
  • runtimeArgs:传递给 Chrome 的启动参数
  • sourceMaps:是否启用 Source Map
  • sourceMapPathOverrides:源码路径映射覆盖规则

Compound 配置

使用 compounds 可以同时启动多个调试会话:

{
  "compounds": [
    {
      "name": "Full Stack Debug",
      "configurations": ["Server", "Client"],
      "stopAll": true
    }
  ]
}

VSCode 变量

在配置中可以使用以下变量:

变量 说明
${workspaceFolder} 工作区根目录
${file} 当前打开的文件完整路径
${relativeFile} 相对于工作区的文件路径
${fileBasename} 文件名(含扩展名)
${fileBasenameNoExtension} 文件名(不含扩展名)
${fileDirname} 文件所在目录
${cwd} 当前工作目录
${env:NAME} 环境变量 NAME 的值

可参考官方文档:Variables reference

输入变量

使用 inputs 可以在启动调试时要求用户输入:

{
  "configurations": [
    {
      "name": "Debug with args",
      "args": ["${input:testName}"]
    }
  ],
  "inputs": [
    {
      "id": "testName",
      "type": "promptString",
      "description": "输入测试名称",
      "default": "test_example"
    }
  ]
}

典型场景

Vite + React 项目

在常规的 React 项目中,通常可以通过先启动完 vite dev 项目后,再运行如下配置启动调试:

{
  "type": "chrome",
  "request": "launch",
  "name": "Vite React",
  "url": "http://localhost:5173",
  "webRoot": "${workspaceFolder}/packages/vite-react-demo",
  "runtimeArgs": ["--auto-open-devtools-for-tabs"]
}

保存登录状态

但如此一次,每次启动的 chrome 都是一个全新的实例,没有任何的插件或者登录信息。如果我们希望每次启动的 chrome 都是带上 react 插件,并且保留登录信息,可以通过 userDataDir 自定用户信息目录,然后在第一次安装插件后,后续就无需在处理了:

{
  "userDataDir": "${workspaceFolder}/packages/vite-react-demo/.chrome-data"
}

任务集成

如果不希望单独运行 vite dev,而是在每次启动调试时,自行运行 vite dev 后再开启调试,可以通过 preLaunchTaskpostDebugTask 可以在调试前后执行任务:

{
  "type": "chrome",
  "request": "launch",
  "name": "Vite React Debug",
  "url": "http://localhost:5173",
  "preLaunchTask": "vite-react: dev",
  "postDebugTask": "kill-vite-react-dev"
}

需要在 .vscode/tasks.json 中定义对应的任务:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "vite-react: dev",
      "type": "npm",
      "script": "dev",
      "isBackground": true,
      "problemMatcher": {
        "background": {
          "activeOnStart": true,
          "beginsPattern": ".",
          // 说明 task 准备完成
          "endsPattern": "ready in"
        }
      }
    }
  ]
}

Task 配置属于锦上添花,不是调试过程中的必须配置,更多信息可以参考官网 Integrate with External Tools via Tasks

Next.js 全栈调试

Next.js 同时包含客户端和服务端代码,所以我们的调试场景既包含了 node 端,也包含了 chrome 端。可以借助 serverReadyActioncompounds 两种方式同时连接 node ,web 端 debugger 调试器。

serverReadyAction 一个强大的自动化调试配置,它允许你在服务器启动后,自动触发一个额外的任务,比如针对 Chrome 浏览器的调试会话,从而实现 全栈调试

compounds 则是同时启动多个调试,但是不处理调试之间的依赖关系。

serverReadyAction:debugWithChrome

在启动完成 node 后端后,如果 node 控制台检测到输出了 pattern 内容,则开启一个 chrome 调试窗口:

{
  "type": "node",
  "request": "launch",
  "name": "Next.js - Full Stack",
  "runtimeExecutable": "pnpm",
  "runtimeArgs": ["next", "dev"],
  "serverReadyAction": {
    "pattern": "Local:\s+(https?://.+)",
    "action": "debugWithChrome",
    "uriFormat": "%s",
    "webRoot": "${workspaceFolder}"
  },
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

但是该模式下对 chrome 调试器的配置很有限,比如 userDataDir 都无法配置,适用于在开启后端调试后,同时兼顾简单的前端调试。

serverReadyAction:startDebugging

startDebugging 则更强大,可以在服务器启动后自动启动另一个客户端调试配置,这样客户端的配置就是完整的,可以完成使用各种标准配置

{
  "type": "node",
  "request": "launch",
  "name": "Next.js - Server",
  "runtimeExecutable": "pnpm",
  "runtimeArgs": ["next", "dev"],
  "serverReadyAction": {
    "pattern": "Local:\s+(https?://.+)",
    "action": "startDebugging",
    "name": "Next.js - Client"
  },
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

配合的客户端配置:

{
  "type": "chrome",
  "request": "launch",
  "name": "Next.js - Client",
  "url": "http://localhost:3000",
  "webRoot": "${workspaceFolder}"
}

Compounds

不使用 serverReadyAction 时,通常单独各个端的调试配置,然后通过 compounds 同时启动多个独立的调试会话:

{
  "compounds": [
    {
      "name": "Next.js - Full Stack",
      "configurations": ["Next.js - Server", "Next.js - Client"],
      "stopAll": true
    }
  ]
}

三种方式对比:

方式 优点 缺点 适用场景
debugWithChrome 配置简单,一键启动 调试会话合并,控制粒度较粗 日常开发
startDebugging 自动化程度高,会话独立 需要两个配置 需要独立控制前后端
Compound 完全手动控制,灵活性高 需要手动管理多个会话 复杂调试场景

serverReadyAction 还有一种 action openExternally,它的作用更为直接,会在 server 启动后,直接打开在 chrome 打开一个页面,但是不连接客户端调试,只是为了方便交互客户端后,调试服务端。

基本上涉及到多端调试的场景都可以用类似的方案,比如 electron, express 前后端 mono repo 项目等等。

Node 项目

TypeScript 项目

在开发 node ts 项目时,除了编译完成后在运行 js 程序,也可以采用 tsx 或者 ts-node 直接运行 ts 程序,它们会在项目运行前自动进行编译操作。

需要在 runtimeArgs 增加 -r 参数来使用,也可以直接将 runtimeExecutable 进行替换。

使用 tsx(推荐,启动快):

{
  "type": "node",
  "request": "launch",
  "name": "TypeScript - Current File (tsx)",
  "program": "${file}",
  // 或者
  // "runtimeExecutable": "tsx",
  "runtimeArgs": ["-r", "tsx/cjs"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

使用 ts-node(完整类型检查):

{
  "type": "node",
  "request": "launch",
  "name": "TypeScript - Current File (ts-node)",
  "program": "${file}",
  // 或者
  // "runtimeExecutable": "ts-node",
  "runtimeArgs": ["-r", "ts-node/register"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"],
  "env": {
    "TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.json"
  }
}

Pnpm 管理项目

在 npm/yarn 管理的 Node 项目中,通常可以直接通过 program 指定 node_modules/.bin 下的命令文件:

{
  "type": "node",
  "request": "launch",
  "name": "Jest",
  "program": "${workspaceFolder}/node_modules/.bin/jest"
}

但在 pnpm 项目中,这种方式会失败。

问题根源: pnpm 出于性能和磁盘空间考虑,使用了独特的依赖管理方式,将 .bin 目录下的文件转化为 shell 脚本:

当尝试用 node 直接执行这些文件时,会报错:

SyntaxError: Invalid or unexpected token

因为 Node.js 无法直接执行 shell 脚本文件。

方案 1:使用 pnpm 执行 - 将 runtimeExecutable 改为 pnpm,通过 runtimeArgs 传递命令:

{
  "type": "node",
  "request": "launch",
  "name": "Jest - pnpm",
  "runtimeExecutable": "pnpm",
  "runtimeArgs": ["jest", "--runInBand"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

方案 2:直接指向 JS 文件 - 绕过 shell 脚本,直接指向包内的 JavaScript 入口文件:

{
  "type": "node",
  "request": "launch",
  "name": "Jest - direct",
  "program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
  "args": ["--runInBand"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

方案 3:使用 sh 执行 - 让 sh 来执行 shell 脚本:

{
  "type": "node",
  "request": "launch",
  "name": "Jest - sh",
  "runtimeExecutable": "sh",
  "program": "${workspaceFolder}/node_modules/.bin/jest",
  "args": ["--runInBand"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

实际应用示例:

调试 Vitest 当前文件:

{
  "type": "node",
  "request": "launch",
  "name": "Vitest - Current File",
  "runtimeExecutable": "pnpm",
  "runtimeArgs": ["vitest", "run", "${relativeFile}"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

调试 ESLint:

{
  "type": "node",
  "request": "launch",
  "name": "ESLint - Current File",
  "runtimeExecutable": "pnpm",
  "runtimeArgs": ["eslint", "${file}", "--fix"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal"
}

使用直接路径调试 TypeScript:

{
  "type": "node",
  "request": "launch",
  "name": "TSC - Build",
  "program": "${workspaceFolder}/node_modules/typescript/bin/tsc",
  "args": ["--noEmit"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal"
}

单测

Jest 单元测试

在运行单侧时,通常有两种场景,运行所有单侧,和运行当前单侧文件的单测。

调试当前打开的测试文件:

{
  "type": "node",
  "request": "launch",
  "name": "Jest - Current File",
  "program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
  "args": [
    "${relativeFile}",
    "--config=${workspaceFolder}/jest.config.js",
    "--runInBand",
    "--no-coverage"
  ],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen"
}

关键参数:

  • --runInBand:串行运行测试(调试必需)
  • --no-coverage:禁用覆盖率收集,加快调试速度

调试所有测试:

{
  "type": "node",
  "request": "launch",
  "name": "Jest - All Tests",
  "program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
  "args": ["--runInBand", "--config=${workspaceFolder}/jest.config.js"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal"
}

Vitest 单元测试

调试当前测试文件:

{
  "type": "node",
  "request": "launch",
  "name": "Vitest - Current File",
  "runtimeExecutable": "pnpm",
  "runtimeArgs": ["vitest", "run", "${file}"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

监视模式:

{
  "type": "node",
  "request": "launch",
  "name": "Vitest - Watch Mode",
  "runtimeExecutable": "pnpm",
  "runtimeArgs": ["vitest"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

npm script 调试

通常,我们会将实际运行的程序写入到 npm scripts 中,所以也通过通过以下方式直接调试 script 脚本,就不需要在 launch.json 中重定义一套配置了:

方式一:使用 node 类型:

{
  "type": "node",
  "request": "launch",
  "name": "npm script - Start",
  "runtimeExecutable": "npm",
  "runtimeArgs": ["run-script", "start"],
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"]
}

方式二:使用 node-terminal 类型:

{
  "type": "node-terminal",
  "name": "npm script - Start (Terminal)",
  "request": "launch",
  "command": "npm start",
  "cwd": "${workspaceFolder}"
}

Puppeteer 自动化调试

有些时候,调试准备调试数据,尤其是表单类的应用,除了在代码中进行处理,也可以借助 puppeteer 自动化前置的流程,直达要调试的阶段。

通过脚本 启动 puppeteer ,并搭配 --remote-debugging-port=9222 开启调试端口,在启动 puppeteer 后,通过 attach 模式连接 chrome 就可以自动化前置内容填写和连接 chrome 调试器两个阶段:

调试 Puppeteer 脚本:

{
  "type": "node",
  "request": "launch",
  "name": "Puppeteer - Script",
  "program": "${workspaceFolder}/scripts/auto-fill-form.js",
  "cwd": "${workspaceFolder}",
  "console": "integratedTerminal",
  "skipFiles": ["<node_internals>/**"],
  "env": {
    "HEADLESS": "false"
  }
}

Chrome Attach 配置:

{
  "type": "chrome",
  "request": "attach",
  "name": "Puppeteer - Chrome Attach",
  "port": 9222,
  "webRoot": "${workspaceFolder}"
}

同时调试 Puppeteer 脚本和浏览器代码:

{
  "compounds": [
    {
      "name": "Puppeteer - Full Stack",
      "configurations": ["Puppeteer - Script", "Puppeteer - Chrome Attach"],
      "stopAll": true
    }
  ]
}

puppeteer 脚本可以直接借助 ai 来生成,编写成本也非常低

快捷调试方式

除了使用 launch.json 配置外,VSCode 也提供了很多便捷的开启调试的手段。相对于通过 ui 交互简化了配置过程。

Package.json 调试按钮

VSCode 会在 package.json 的 scripts 上方显示调试按钮,点击即可快速调试脚本。

使用方式:

  1. 打开 package.json 文件
  2. 在 scripts 字段上方会出现 "Debug" 按钮
  3. 点击按钮即可开始调试

Testing 面板

VSCode 提供了统一的测试面板,支持 Jest、Vitest、Mocha 等测试框架。

启用方式:

  1. 安装对应的测试扩展(如 Jest Runner、Vitest)
  2. 打开测试面板(左侧活动栏的烧杯图标)
  3. VSCode 会自动发现项目中的测试

可以非常便捷的在面板完成单侧的运行。

并且在单测文件中,每个单测也有便捷的调试按钮:

参考:VSCode Testing 文档

Auto Attach

Auto Attach 是 VSCode 提供的一个非常方便的功能,可以自动附加调试器到在集成终端中启动的 Node.js 进程,可以在配置中打开:

三种模式:

模式 说明 适用场景
Smart 自动检测常见脚本,如 npm scripts 推荐,日常使用
Always 附加到所有 Node.js 进程 调试所有 Node 程序
Only With Flag 仅附加到使用 --inspect 标志的进程 精确控制

使用示例:

启用 Auto Attach 后,在集成终端中直接运行:

node src/index.js
# 或
npm run dev
# 或
pnpm test

调试器会自动附加。

参考:auto-attach

经典问题

为什么断点不生效

最常见的是为什么明明设置了断点,调试器也连接了,但是就是没有停住。此时添加的断点是灰色的:

可以鼠标 hover 断点进行简单诊断:

常见原因:

launch.json 配置关闭了 sourcemap

sourceMaps 默认是 true, 如果改为了 false 就不会加载 sourcemap 了

{
  "type": "chrome",
  "request": "launch",
  "name": "Vite React - Launch (自定义用户信息)",
  "url": "http://localhost:5173",
  "runtimeArgs": ["--auto-open-devtools-for-tabs"],
  "sourceMaps": false
}

Source Map 配置错误

确保构建工具正确生成了 Source Map:

TypeScript (tsconfig.json):

{
  "compilerOptions": {
    "sourceMap": true,
    "inlineSourceMap": false,
    "sourceRoot": ""
  }
}

Vite (vite.config.ts):

export default {
  build: {
    sourcemap: true,
  },
};

Webpack (webpack.config.js):

module.exports = {
  devtool: "source-map", // 或 'eval-source-map' 用于开发
};

源码路径映射错误

即使所有的 sourcemap 配置都正常,也可能会出现问题,常见于 webpack。比如 webpack 生成的 sourcemap 是:

映射出的原始文件以 webpack:// 开头,比如 webpack://webpack-demo/./src/app.ts,而我们打的断点在 ${workspaceFolder}/packages/webpack-demo/src/app.ts。VSCode 不知道两者关系,此时需要 sourceMapPathOverrideswebRoot 修正 Source Map 路径与本地文件路径的映射。

配置说明:

  1. webRoot 的作用 - webRoot 指定 Web 应用的源码根目录,作为 Source Map 路径映射的基准路径。在 sourceMapPathOverrides 中可以通过 ${webRoot} 变量引用这个基准路径。
  2. sourceMapPathOverrides 配置 - 当 Source Map 中的路径与本地文件路径不匹配时,使用 sourceMapPathOverrides 修正路径映射:
{
  "type": "chrome",
  "request": "launch",
  "name": "Chrome Debug",
  "url": "http://localhost:3000",
  "webRoot": "${workspaceFolder}/packages/webpack-demo",
  // 相当于:
  "sourceMapPathOverrides": {
    "webpack://webpack-demo/./src/*": "${webRoot}/src/*",
    "webpack://webpack-demo/./*": "${webRoot}/*"
  }
}

默认配置:

如果不配置 sourceMapPathOverrides,VSCode 会使用以下默认规则 (适用于 Chrome/浏览器调试):

{
  "sourceMapPathOverrides": {
    "webpack:///./~/*": "${webRoot}/node_modules/*",
    "webpack:///./*": "${webRoot}/*",
    "webpack:///*": "*",
    "webpack:///src/*": "${webRoot}/*",
    "meteor://💻app/*": "${webRoot}/*"
  }
}

注意: 对于 Node.js 调试,默认规则中使用 ${workspaceFolder} 替代 ${webRoot}

配置说明:

  • 左侧是 Source Map 中的路径模式
  • 右侧是本地文件系统的实际路径
  • * 是通配符,匹配任意路径片段
  • 映射规则从上到下匹配,使用第一个匹配的规则

常见打包工具的配置示例:

  1. Webpack 项目:
{
  "sourceMapPathOverrides": {
    "webpack:///./*": "${webRoot}/*",
    "webpack:///src/*": "${webRoot}/src/*",
    "webpack:///*": "${webRoot}/*"
  }
}

2. Vite 项目:

ViteSource Map 路径通常已经是正确的,一般不需要额外配置。但如果遇到问题:
{
  "sourceMapPathOverrides": {
    "/@fs/*": "/*",
    "/src/*": "${webRoot}/src/*"
  }
}

3. Monorepo 项目:

{
  "webRoot": "${workspaceFolder}/packages/your-app",
  "sourceMapPathOverrides": {
    "webpack:///packages/your-app/*": "${webRoot}/*",
    "webpack:///../your-app/*": "${webRoot}/*"
  }
}

假设:

  • webRoot = "${workspaceFolder}/packages/app"
  • Source Map 路径: webpack://app/./src/index.ts

映射规则:

{
  "sourceMapPathOverrides": {
    "webpack://app/./*": "${webRoot}/*"
  }
}

映射过程:

  1. webpack://app/./src/index.ts 匹配规则 webpack://app/./*
  2. * 匹配到 src/index.ts
  3. 替换为 ${webRoot}/src/index.ts
  4. 展开 ${webRoot} 得到最终路径: ${workspaceFolder}/packages/app/src/index.ts

调试器启动慢

原因:

  • 项目文件过多
  • Source Map 文件过大
  • skipFiles 配置不当

优化方法:

  1. 跳过不必要的文件
{
  "skipFiles": [
    "<node_internals>/**",
    "**/node_modules/**",
    "${workspaceFolder}/dist/vendor.js"
  ]
}

2. 限制 Source Map 查找范围

{
  "outFiles": ["${workspaceFolder}/dist/**/*.js", "!**/node_modules/**"]
}

3. 使用更快的 Source Map 类型

开发环境使用 `eval-source-map`:
// webpack.config.js
devtool: "eval-source-map"; // 比 'source-map' 快但体积大

总结

vscode debugger 是非常强大的,可以极大提升调试效率。并且设置虽多,关键的就是那几个,配置也不麻烦。了解了 vscode debugger 能做哪些事情后,搭配 AI, 更是进一步简化了配置的难度。

欢迎关注笔者的个人公众号,共同学习,共同前进

image.png

Next.js第五章(动态路由)

动态路由 动态路由是指在路由中使用方括号[]来定义路由参数,例如/blog/[id],其中[id]就是动态路由参数,因为在某些需求下,我们需要根据不同的id来显示不同的页面内容,例如商品详情页,文章详

每日一题-是否所有 1 都至少相隔 k 个元素🟢

给你一个由若干 01 组成的数组 nums 以及整数 k。如果所有 1 都至少相隔 k 个元素,则返回 true ;否则,返回 false

 

示例 1:

输入:nums = [1,0,0,0,1,0,0,1], k = 2
输出:true
解释:每个 1 都至少相隔 2 个元素。

示例 2:

输入:nums = [1,0,0,1,0,1], k = 2
输出:false
解释:第二个 1 和第三个 1 之间只隔了 1 个元素。

 

提示:

  • 1 <= nums.length <= 105
  • 0 <= k <= nums.length
  • nums[i] 的值为 01

前端实时推送 & WebSocket 面试题(2026版)

一、历史背景 + 时间轴 网页一旦需要 “实时” ,麻烦就开始了:数据在不断变化,用户却只能等下一次刷新; 刷新解决不了的延迟,用短轮询凑数,又被无数空请求反噬; 再加长轮询,试图把“有了新数据再说”

简单题,简单做(Python/Java/C++/C/Go/JS/Rust)

如果所有相邻的 $1$ 都至少相隔 $k$ 个元素,那么所有 $1$ 都至少相隔 $k$ 个元素。

所以只需检查相邻的 $1$。

记录上一个 $1$ 的位置 $\textit{last}_1$。

如果 $\textit{nums}[i] = 1$ 且 $i - \textit{last}_1 - 1 < k$,即 $i - \textit{last}_1 \le k$,则不满足要求。

为了兼容 $\textit{nums}$ 的第一个 $1$,可以初始化 $\textit{last}_1 = -k-1$。

###py

class Solution:
    def kLengthApart(self, nums: List[int], k: int) -> bool:
        last1 = -inf
        for i, x in enumerate(nums):
            if x != 1:
                continue
            if i - last1 <= k:
                return False
            last1 = i
        return True

###java

class Solution {
    public boolean kLengthApart(int[] nums, int k) {
        int last1 = -k - 1;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] != 1) {
                continue;
            }
            if (i - last1 <= k) {
                return false;
            }
            last1 = i;
        }
        return true;
    }
}

###cpp

class Solution {
public:
    bool kLengthApart(vector<int>& nums, int k) {
        int last1 = -k - 1;
        for (int i = 0; i < nums.size(); i++) {
            if (nums[i] != 1) {
                continue;
            }
            if (i - last1 <= k) {
                return false;
            }
            last1 = i;
        }
        return true;
    }
};

###c

bool kLengthApart(int* nums, int numsSize, int k) {
    int last1 = -k - 1;
    for (int i = 0; i < numsSize; i++) {
        if (nums[i] != 1) {
            continue;
        }
        if (i - last1 <= k) {
            return false;
        }
        last1 = i;
    }
    return true;
}

###go

func kLengthApart(nums []int, k int) bool {
last1 := -k - 1
for i, x := range nums {
if x != 1 {
continue
}
if i-last1 <= k {
return false
}
last1 = i
}
return true
}

###js

var kLengthApart = function(nums, k) {
    let last1 = -Infinity;
    for (let i = 0; i < nums.length; i++) {
        if (nums[i] !== 1) {
            continue;
        }
        if (i - last1 <= k) {
            return false;
        }
        last1 = i;
    }
    return true;
};

###rust

impl Solution {
    pub fn k_length_apart(nums: Vec<i32>, k: i32) -> bool {
        let mut last1 = -k - 1;
        for (i, x) in nums.into_iter().enumerate() {
            if x != 1 {
                continue;
            }
            if i as i32 - last1 <= k {
                return false;
            }
            last1 = i as i32;
        }
        true
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

思路简单,性能高效

解题思路

此处撰写解题思路
image.png

代码

###python3

class Solution:
    def kLengthApart(self, nums: List[int], k: int) -> bool:
        if 1 not in nums:
            return True

        min_len = n = len(nums)
        start = nums.index(1)
        for idx in range(start+1,n):
            if nums[idx] == 1:
                min_len = min(min_len, idx - start - 1)
                start = idx
                if min_len < k:
                    return False
        return True



[Python3/Java/C++/Go/TypeScript] 一题一解:遍历计数(清晰题解)

方法一:遍历计数

我们遍历字符串 $s$,用一个变量 $\textit{cur}$ 记录当前连续的 1 的个数,用变量 $\textit{ans}$ 记录答案。当遍历到字符 $s[i]$ 时,如果 $s[i] = 0$,则 $\textit{cur}$ 置 0,否则 $\textit{cur}$ 自增 1,然后 $\textit{ans}$ 自增 $\textit{cur}$,并对 $10^9 + 7$ 取模。

遍历结束,返回 $\textit{ans}$ 即可。

###python

class Solution:
    def numSub(self, s: str) -> int:
        mod = 10**9 + 7
        ans = cur = 0
        for c in s:
            if c == "0":
                cur = 0
            else:
                cur += 1
                ans = (ans + cur) % mod
        return ans

###java

class Solution {
    public int numSub(String s) {
        final int mod = 1_000_000_007;
        int ans = 0, cur = 0;
        for (int i = 0; i < s.length(); ++i) {
            if (s.charAt(i) == '0') {
                cur = 0;
            } else {
                cur++;
                ans = (ans + cur) % mod;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int numSub(string s) {
        const int mod = 1e9 + 7;
        int ans = 0, cur = 0;
        for (char c : s) {
            if (c == '0') {
                cur = 0;
            } else {
                cur++;
                ans = (ans + cur) % mod;
            }
        }
        return ans;
    }
};

###go

func numSub(s string) (ans int) {
const mod int = 1e9 + 7
cur := 0
for _, c := range s {
if c == '0' {
cur = 0
} else {
cur++
ans = (ans + cur) % mod
}
}
return
}

###ts

function numSub(s: string): number {
    const mod = 1_000_000_007;
    let [ans, cur] = [0, 0];
    for (const c of s) {
        if (c === '0') {
            cur = 0;
        } else {
            cur++;
            ans = (ans + cur) % mod;
        }
    }
    return ans;
}

###rust

impl Solution {
    pub fn num_sub(s: String) -> i32 {
        const MOD: i32 = 1_000_000_007;
        let mut ans: i32 = 0;
        let mut cur: i32 = 0;
        for c in s.chars() {
            if c == '0' {
                cur = 0;
            } else {
                cur += 1;
                ans = (ans + cur) % MOD;
            }
        }
        ans
    }
}

时间复杂度 $O(n)$,其中 $n$ 为字符串 $s$ 的长度。空间复杂度 $O(1)$。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈 😄~

❌