Knip:JavaScript 与 TypeScript 项目终极死代码检测器

发布: (2025年12月24日 GMT+8 03:51)
16 min read
原文: Dev.to

Source: Dev.to

Knip:终极的 JavaScript/TypeScript 项目死代码检测工具

在现代前端和后端项目中,代码库的体积会随着时间的推移而不断增长。未使用的函数、未引用的文件以及多余的依赖会导致:

  • 构建时间变慢
  • 产出体积增大
  • 维护成本上升

虽然 ESLint、TSLint 等工具可以捕获一些未使用的变量,但它们并不能完整地检测整个项目层面的死代码。Knip 正是为了解决这个痛点而诞生的——它能够在项目级别发现未使用的文件、导出、依赖以及脚本。


目录

  1. 为什么需要死代码检测?
  2. Knip 的核心特性
  3. 快速上手
  4. 配置选项详解
  5. 与其他工具的对比
  6. 常见问题与最佳实践
  7. 结论

为什么需要死代码检测?

  • 提升性能:删除未使用的模块可以让打包工具(如 Webpack、Vite、Rollup)更好地进行 tree‑shaking,从而生成更小的 bundle。
  • 降低技术债务:随着团队成员的更迭,旧代码往往被遗忘。及时清理可以防止“代码腐烂”。
  • 安全性:未使用的依赖仍然会被安装到 node_modules,可能带来潜在的安全漏洞。

提示:即使是 devDependencies 中的未使用包,也会在 CI 环境中被安装,增加构建时间和攻击面。


Knip 的核心特性

特性描述
文件级别检测找出项目中未被任何入口文件引用的 .js/.ts 文件。
导出级别检测标记未被其他模块使用的命名导出(export const foo = …)。
未使用的依赖检测 package.json 中声明但未在代码中 import/require 的依赖。
未使用的脚本识别 package.json 中未被 npm run 调用的自定义脚本。
可配置的忽略列表通过 knip.jsonpackage.json 中的 knip 字段自定义排除规则。
快速且增量采用 TypeScript 编译器 API,支持增量分析,适合大型 monorepo。
CI 集成可在 GitHub Actions、GitLab CI 等环境中直接返回错误码,阻止死代码进入主分支。

快速上手

1. 安装

# 使用 npm
npm i -D knip

# 使用 Yarn
yarn add -D knip

# 使用 pnpm
pnpm add -D knip

2. 运行基本检测

npx knip

默认情况下,Knip 会读取项目根目录的 package.jsontsconfig.json(或 jsconfig.json)以及 knip.json(如果存在),并输出类似下面的报告:

✖ Unused files (3)
  src/utils/oldHelper.ts
  src/components/LegacyButton.tsx
  src/services/unusedService.ts

✖ Unused exports (5)
  src/constants/index.ts: export const DEPRECATED_FLAG
  src/hooks/useOldHook.ts: export function useOldHook()
  ...

✖ Unused dependencies (2)
  lodash
  moment

3. 在 CI 中使用

在 GitHub Actions 中加入一步:

- name: Run Knip
  run: npx knip --fail-on-unused

--fail-on-unused 参数会在检测到任何未使用的项目时返回非零退出码,从而让 CI 任务失败。


配置选项详解

Knip 支持两种配置方式:

  1. 独立的 knip.json
  2. package.json 中的 knip 字段

下面给出一个完整的 knip.json 示例(已翻译注释):

{
  // 项目根目录(相对路径或绝对路径)
  "projectRoot": ".",

  // 指定入口文件,Knip 将从这些文件开始递归分析
  "entry": [
    "src/index.ts",
    "src/server.ts"
  ],

  // 要忽略的文件或目录(支持 glob)
  "ignore": [
    "src/**/*.test.ts",
    "scripts/**",
    "dist/**"
  ],

  // 对特定文件的自定义规则
  "paths": {
    "src/generated/**": {
      // 对生成的代码不进行任何检测
      "ignore": true
    }
  },

  // 依赖检测的细粒度控制
  "dependencies": {
    // 将这些依赖视为“始终使用”,即使未在代码中出现也不报错
    "alwaysUsed": ["react", "react-dom"]
  },

  // 导出检测的排除列表
  "exports": {
    // 对某些文件的导出不进行检测
    "ignoreExportsFrom": ["src/types/**"]
  },

  // 脚本检测
  "scripts": {
    // 将这些脚本标记为“已使用”,即使未在 CI 中调用
    "alwaysUsed": ["build", "dev"]
  }
}

常用 CLI 参数

参数作用
--entry <paths>手动指定入口文件(覆盖配置文件中的 entry)。
--ignore <globs>在命令行层面添加忽略规则。
--fail-on-unused检测到未使用的项目时返回非零退出码。
--json以 JSON 格式输出报告,便于后续自定义处理。
--watch监听文件变化,实时重新分析(适合本地开发)。

与其他工具的对比

工具检测范围增量分析配置灵活性CI 集成
ESLint (no-unused-vars)仅变量/函数级别✅(依赖 ESLint 本身)中等
ts-prune未使用的导出❌(全量扫描)
depcheck未使用的依赖❌(全量扫描)中等
Knip文件、导出、依赖、脚本✅(增量)

结论:如果你需要一次性覆盖 所有层面的死代码(包括未引用的文件和脚本),Knip 是目前最完整的解决方案。


常见问题与最佳实践

Q1: 为什么某些文件仍然被标记为未使用?

  • 动态导入:Knip 只能静态分析 import/require。如果你使用 import() 动态加载模块,需要在 knip.json 中手动将这些文件加入 ignore
  • 插件/框架约定:例如 Next.js 会自动加载 pages 目录下的文件。可以在配置中添加 pathsignore 来排除这些约定目录。

Q2: 如何在 monorepo 中使用 Knip?

  1. 在根目录放置统一的 knip.json,使用 projectRoot 指向各子包。
  2. 为每个子包单独运行 knip,并通过 --entry 指定子包的入口文件。
  3. 使用 --json 输出统一报告,再通过自定义脚本聚合。

Q3: 是否可以自动删除检测到的死代码?

Knip 本身只负责检测并报告。不建议直接在 CI 中自动删除文件,因为这可能导致误删。推荐的流程:

  1. 生成报告(JSON)。
  2. 在 PR 中附加报告链接或自动生成的评论。
  3. 开发者手动审查并删除。

如果你确实想实现自动化,可以结合 git rm 与自定义脚本,在本地或专用的 “cleanup” 分支上执行。

最佳实践清单

  • 在 CI 中使用 --fail-on-unused,防止新死代码进入主分支。
  • 在本地开发时开启 --watch,实时获取未使用的导出提示。
  • 为动态导入的文件添加显式的 ignore,避免误报。
  • 定期(如每月一次)运行全量分析,清理长期未使用的依赖。

结论

Knip 通过 文件、导出、依赖以及脚本 四个维度提供了全方位的死代码检测能力,配合增量分析和灵活的配置选项,能够轻松融入现代 JavaScript/TypeScript 项目的开发与 CI 流程。无论是单体项目还是大型 monorepo,Knip 都是提升代码质量、降低构建体积、减少安全风险的实用工具。

立即在你的项目中尝试 Knip,告别冗余代码,让代码库保持轻盈与健康!

问题:死代码无处不在

每个成熟的代码库都有一个不为人知的脏秘密:死代码
有人写的“以防万一”的工具函数。
被废弃特性的组件。
为一次试验而安装的 npm 包,却从未真正使用。

ESLint 能捕获文件内部未使用的变量和导入,但以下情况怎么办:

  • 从未在任何地方被导入的文件?
  • 没有人使用的导出函数?
  • package.json 中忘记删除的依赖?
  • 定义了却从未引用的类型和接口?

这就是 Knip 的用武之地。

什么是 Knip?

Knip(荷兰语意为 “cut”)是一个强大的静态分析工具,能够在你的 JavaScript/TypeScript 项目中查找 未使用的文件、依赖和导出。可以把它想象成代码库的 Marie Kondo —— 如果代码不带来喜悦(或未被使用),就得删掉。

什么是 Knip 检测

类别描述
🗂️ 未使用的文件未在任何地方导入的源文件
📦 未使用的依赖package.json 中未使用的包
📤 未使用的导出已导出但从未被导入的函数、类、类型
🔧 未使用的开发依赖不需要的开发包
未列出的依赖代码中使用但在 package.json 中缺失的包
🔗 未解析的导入指向不存在模块的导入

Knip 报告示例

入门指南

安装

# npm
npm install -D knip

# yarn
yarn add -D knip

# pnpm
pnpm add -D knip

基本用法

npx knip

就这么简单!Knip 会分析你的项目并输出它发现的所有未使用的代码。

配置

对于大多数项目,Knip 开箱即用。对于复杂的设置(monorepo、自定义入口等),你需要一个配置文件。

在项目根目录创建 knip.json

{
  "$schema": "https://unpkg.com/knip@5/schema.json",
  "entry": ["src/index.ts", "src/App.tsx"],
  "project": ["src/**/*.{ts,tsx}"],
  "ignore": [
    "**/__tests__/**",
    "**/__mocks__/**",
    "**/node_modules/**"
  ],
  "ignoreDependencies": [
    "prettier",
    "husky"
  ],
  "ignoreExportsUsedInFile": true
}

关键配置选项

选项描述
entryKnip 开始追踪的入口文件
project需要分析的文件
ignore要跳过的文件/模式
ignoreDependencies要跳过的依赖(对仅作配置的包很有用)
ignoreExportsUsedInFile不报告仅在同一文件中使用的导出

React Native 示例

{
  "$schema": "https://unpkg.com/knip@5/schema.json",
  "entry": ["src/App.tsx", "index.js"],
  "project": ["src/**/*.{ts,tsx}"],
  "ignore": [
    "**/__tests__/**",
    "**/__mocks__/**",
    "android/**",
    "ios/**"
  ],
  "ignoreDependencies": [
    "@react-native/metro-config",
    "@react-native/typescript-config",
    "@react-native/babel-preset",
    "patch-package",
    "husky",
    "reactotron-react-native"
  ],
  "ignoreExportsUsedInFile": true
}

真实世界的结果

在生产环境的 React Native 应用上运行 Knip,得到以下结果:

Unused files (73)
src/components/map/index.tsx
src/components/views/NotificationMenuButton.tsx
src/models/requests/auth/authRequests.ts
src/utils/helpers/contactHelper.ts
... and 69 more

Unused dependencies (1)
@react-native-firebase/perf

Unused devDependencies (4)
@babel/preset-env
@testing-library/jest-native
@types/react-native-get-random-values
eslint-plugin-react-you-might-not-need-an-effect

Unlisted dependencies (2)
credit-card-type  src/features/paymentMethods/addNewCard/index.tsx

Unused exports (74)
translations        src/config/localization/languages.ts
defaultAddressForm src/features/addressBook/addressTypes.ts
... and more

73 个未使用的文件! 这可能是成千上万行的死代码,导致:

  • 增加包体积
  • 让新开发者感到困惑
  • 增加维护负担
  • 出现在搜索结果中,浪费时间

将其集成到您的工作流中

NPM 脚本

将以下内容添加到您的 package.json

{
  "scripts": {
    "knip:check": "knip",
    "knip:fix": "knip --fix"
  }
}

CI/CD 集成

# .github/workflows/code-quality.yml
name: Code Quality

on: [push, pull_request]

jobs:
  knip:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx knip

预提交钩子(使用 Husky)

# .husky/pre-commit
npx knip --no-exit-code

使用 --fix 自动修复

npx knip --fix

⚠️ 警告: 这会修改您的文件!请始终审查更改并准备好版本控制。

--fix 能做到的:

  • ✅ 删除未使用的导出
  • ✅ 从 package.json 中删除未使用的依赖
  • ❌ 不能删除未使用的文件(风险太大)

提示与最佳实践

  1. 使用 --include 开始聚焦 – 如果结果让你不知所措,先将分析限制在特定文件夹或类别。
  2. 定期运行 Knip(例如在 CI 中),防止死代码累积。
  3. 将 Knip 与代码审查清单配合使用:“没有新的未使用的导入/文件”。
  4. 修复后,提交更改并告知团队清理已完成。

通过采用 Knip,你将保持代码库精简,提升开发者体验,并发布更小、更快的包。祝清理愉快!

Knip 使用技巧

1. 包含特定检查

# Only check for unused files
npx knip --include files

# Only check dependencies
npx knip --include dependencies

# Check multiple categories
npx knip --include files,exports

2. 选择输出报告器

# JSON output (great for tooling)
npx knip --reporter json

# Markdown (for documentation)
npx knip --reporter markdown

3. 忽略已知的误报

有些代码是通过动态方式或约定使用的,Knip 无法检测到:

{
  "ignore": [
    "src/generated/**",
    "**/*.stories.tsx"
  ],
  "ignoreDependencies": [
    "tsconfig-paths"
  ]
}

4. 处理 Barrel 导出

如果你使用 barrel 文件(index.ts 重新导出所有内容),Knip 可能会报告误报。启用:

{
  "ignoreExportsUsedInFile": true
}

5. 框架特定插件

Knip 内置支持多种框架:

{
  "next": {
    "entry": ["pages/**/*.tsx", "app/**/*.tsx"]
  },
  "jest": {
    "config": ["jest.config.js"]
  },
  "storybook": {
    "entry": [".storybook/main.ts"]
  }
}

Knip 与其他工具对比

工具未使用的文件未使用的导出未使用的依赖自动修复
Knip
ESLint部分
ts‑prune
depcheck
unimported

Knip一站式解决方案,可替代多个工具。

常见问题与解决方案

“我得到太多误报!”

  • 验证代码是否是动态导入的。
  • 在配置中将模式添加到 ignore
  • 对仅用于配置的包使用 ignoreDependencies
  • 启用 ignoreExportsUsedInFile

“Knip 在我的大型 monorepo 中很慢”

{
  "ignore": [
    "**/dist/**",
    "**/build/**",
    "**/coverage/**"
  ]
}

“它没有找到我的入口点”

显式定义它们:

{
  "entry": [
    "src/index.ts",
    "src/cli.ts",
    "scripts/*.ts"
  ]
}

结论

死代码是会随时间累积的技术债务。每一个未使用的文件都是:

  • 发送给用户的字节
  • 开发者的认知负担
  • 潜在的安全漏洞
  • 浪费的 CI/CD 分钟

Knip 为您提供对代码库中实际使用情况的可视化。运行一次——您可能会对发现的内容感到震惊。定期运行,保持代码库精简且易于维护。

# Try it now
npx knip

资源

Knip 是否帮助你清理了代码库?在评论中分享你的结果吧!我很想知道你找到了多少死代码。 🔪

标签: javascript, typescript, webdev, productivity

Back to Blog

相关文章

阅读更多 »

Rust 把 'pub' 搞错了

!封面图片:Rust 把 “pub” 写错了 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s...